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.
Files changed (192) hide show
  1. package/AGENTS.md +147 -0
  2. package/LICENSE +22 -0
  3. package/README.md +129 -2
  4. package/distribution.json +197 -0
  5. package/docs/AI-WORKFLOW.md +517 -0
  6. package/docs/ARCHITECTURE.md +268 -0
  7. package/docs/CAPABILITIES.md +264 -0
  8. package/docs/DECISIONS.md +107 -0
  9. package/docs/M3-USAGE.md +192 -0
  10. package/docs/PELICAN-MIGRATION.md +38 -0
  11. package/docs/RENDER-BACKENDS.md +50 -0
  12. package/docs/ROADMAP.md +239 -0
  13. package/docs/THIRD-PARTY-NOTICES.md +151 -0
  14. package/docs/api/REFERENCE.md +1435 -0
  15. package/docs/api/index.json +1139 -0
  16. package/docs/npm/DISTRIBUTION.md +130 -0
  17. package/docs/npm/GAME-AUTHOR.md +147 -0
  18. package/engine.manifest.json +15 -0
  19. package/lib/assets/audio.js +67 -0
  20. package/lib/assets/errors.js +36 -0
  21. package/lib/assets/glb.js +277 -0
  22. package/lib/assets/index.js +53 -0
  23. package/lib/assets/jpeg.js +199 -0
  24. package/lib/assets/model.js +558 -0
  25. package/lib/assets/optimize.js +95 -0
  26. package/lib/assets/texture.js +187 -0
  27. package/lib/compiler/distribution.js +17 -0
  28. package/lib/compiler/extract.js +425 -0
  29. package/lib/compiler/graph.js +366 -0
  30. package/lib/compiler/index.js +574 -0
  31. package/lib/compiler/normalize.js +1194 -0
  32. package/lib/compiler/source-map.js +25 -0
  33. package/lib/compiler/types.js +2 -0
  34. package/lib/compiler/version.js +93 -0
  35. package/lib/experiments/simulation/index.js +2 -0
  36. package/lib/experiments/simulation/math.js +41 -0
  37. package/lib/experiments/simulation/probe.js +215 -0
  38. package/lib/experiments/simulation/scenes.js +192 -0
  39. package/lib/experiments/simulation/solver.js +346 -0
  40. package/lib/experiments/simulation/types.js +1 -0
  41. package/lib/experiments/world/adapters.js +30 -0
  42. package/lib/experiments/world/geometry.js +180 -0
  43. package/lib/experiments/world/index.js +2 -0
  44. package/lib/experiments/world/probe.js +288 -0
  45. package/lib/experiments/world/types.js +1 -0
  46. package/lib/experiments/world/world.js +599 -0
  47. package/lib/navigation/index.js +157 -0
  48. package/lib/physics/index.js +909 -0
  49. package/lib/protocol/index.js +92 -0
  50. package/lib/renderer/acceleration.js +609 -0
  51. package/lib/renderer/browser.js +2015 -0
  52. package/lib/renderer/bsdf.js +260 -0
  53. package/lib/renderer/checkpoint.js +88 -0
  54. package/lib/renderer/compute-executor.js +1034 -0
  55. package/lib/renderer/compute-texture-assets.js +49 -0
  56. package/lib/renderer/compute-view-contract.js +1 -0
  57. package/lib/renderer/denoiser.js +361 -0
  58. package/lib/renderer/dielectric.js +360 -0
  59. package/lib/renderer/embedded-image-pixels.js +368 -0
  60. package/lib/renderer/emissive-lights.js +67 -0
  61. package/lib/renderer/environment-importance.js +55 -0
  62. package/lib/renderer/environment.js +346 -0
  63. package/lib/renderer/gameplay.js +464 -0
  64. package/lib/renderer/geometry.js +103 -0
  65. package/lib/renderer/graph-expressions.js +485 -0
  66. package/lib/renderer/graph-tsl.js +141 -0
  67. package/lib/renderer/graph.js +214 -0
  68. package/lib/renderer/managed-texture-sampling.js +29 -0
  69. package/lib/renderer/material-graphs.js +297 -0
  70. package/lib/renderer/medium.js +348 -0
  71. package/lib/renderer/models.js +1053 -0
  72. package/lib/renderer/motion-shader.js +40 -0
  73. package/lib/renderer/path-shader.js +196 -0
  74. package/lib/renderer/pathtrace.js +324 -0
  75. package/lib/renderer/postprocess-contract.js +1 -0
  76. package/lib/renderer/postprocess.js +494 -0
  77. package/lib/renderer/render-buffers.js +132 -0
  78. package/lib/renderer/render-textures.js +155 -0
  79. package/lib/renderer/startup.js +50 -0
  80. package/lib/renderer/surface-capture.js +199 -0
  81. package/lib/renderer/surface-gpu.js +270 -0
  82. package/lib/renderer/surface-motion.js +84 -0
  83. package/lib/renderer/surface-ray-shader.js +91 -0
  84. package/lib/renderer/surface-shader.js +88 -0
  85. package/lib/renderer/surface-tracer.js +1720 -0
  86. package/lib/renderer/surface.js +1425 -0
  87. package/lib/renderer/temporal-filter.js +512 -0
  88. package/lib/renderer/texture-pixels.js +207 -0
  89. package/lib/renderer/traversal-nodes.js +54 -0
  90. package/lib/renderer/ui.js +446 -0
  91. package/lib/renderer/vertex-bounds.js +127 -0
  92. package/lib/renderer/vertex-contract.js +1 -0
  93. package/lib/renderer/vertex-graphs.js +197 -0
  94. package/lib/renderer/volume-shader.js +50 -0
  95. package/lib/runtime/compute-contract.js +1 -0
  96. package/lib/runtime/compute-service.js +674 -0
  97. package/lib/runtime/contracts.js +1 -0
  98. package/lib/runtime/error.js +37 -0
  99. package/lib/runtime/events.js +189 -0
  100. package/lib/runtime/game-services.js +772 -0
  101. package/lib/runtime/host.js +839 -0
  102. package/lib/runtime/material-parameters.js +145 -0
  103. package/lib/runtime/plugins.js +1 -0
  104. package/lib/runtime/postprocess-contract.js +1 -0
  105. package/lib/runtime/postprocess.js +189 -0
  106. package/lib/runtime/prefabs.js +13 -0
  107. package/lib/runtime/random.js +88 -0
  108. package/lib/runtime/render-buffer-contract.js +1 -0
  109. package/lib/runtime/render-buffers.js +243 -0
  110. package/lib/runtime/render-capabilities.js +17 -0
  111. package/lib/runtime/render-settings.js +249 -0
  112. package/lib/runtime/render-texture-contract.js +1 -0
  113. package/lib/runtime/render-textures.js +271 -0
  114. package/lib/runtime/resources.js +648 -0
  115. package/lib/runtime/scheduler.js +765 -0
  116. package/lib/runtime/storage.js +169 -0
  117. package/lib/runtime/world.js +594 -0
  118. package/lib/schema/index.js +24 -0
  119. package/lib/sdk/graph-values.js +16 -0
  120. package/lib/sdk/index.d.ts +800 -0
  121. package/lib/sdk/index.js +82 -0
  122. package/lib/tools/api-index.js +113 -0
  123. package/lib/tools/browser-session.js +89 -0
  124. package/lib/tools/cli.js +513 -0
  125. package/lib/tools/compress-dist.js +59 -0
  126. package/lib/tools/control-client.js +153 -0
  127. package/lib/tools/control-server.js +633 -0
  128. package/lib/tools/diagnostic-code.js +3 -0
  129. package/lib/tools/doctor.js +496 -0
  130. package/lib/tools/npm/browser-cli.js +6 -0
  131. package/lib/tools/npm/entry.js +40 -0
  132. package/lib/tools/npm/layout.js +195 -0
  133. package/lib/tools/setup.js +153 -0
  134. package/licenses/@dimforge/rapier2d-compat/LICENSE +201 -0
  135. package/licenses/@dimforge/rapier3d-compat/LICENSE +201 -0
  136. package/licenses/@gltf-transform/core/LICENSE.md +21 -0
  137. package/licenses/@gltf-transform/extensions/LICENSE.md +21 -0
  138. package/licenses/@gltf-transform/functions/LICENSE.md +21 -0
  139. package/licenses/@img/colour/LICENSE.md +82 -0
  140. package/licenses/@img/sharp-win32-x64/LICENSE +191 -0
  141. package/licenses/@oxc-project/types/LICENSE +22 -0
  142. package/licenses/@rolldown/pluginutils/LICENSE +21 -0
  143. package/licenses/@tweenjs/tween.js/LICENSE +23 -0
  144. package/licenses/@types/ndarray/LICENSE +21 -0
  145. package/licenses/@types/node/LICENSE +21 -0
  146. package/licenses/@types/stats.js/LICENSE +21 -0
  147. package/licenses/@types/three/@dimforge/rapier3d-compat/LICENSE +201 -0
  148. package/licenses/@types/three/LICENSE +21 -0
  149. package/licenses/@types/webxr/LICENSE +21 -0
  150. package/licenses/@webgpu/types/LICENSE +26 -0
  151. package/licenses/THIRD-PARTY-NOTICES.md +139 -0
  152. package/licenses/cwise-compiler/LICENSE +22 -0
  153. package/licenses/detect-libc/LICENSE +201 -0
  154. package/licenses/esbuild/LICENSE.md +21 -0
  155. package/licenses/fast-png/LICENSE +21 -0
  156. package/licenses/fdir/LICENSE +7 -0
  157. package/licenses/fflate/LICENSE +21 -0
  158. package/licenses/iobuffer/LICENSE +22 -0
  159. package/licenses/iota-array/LICENSE +22 -0
  160. package/licenses/is-buffer/LICENSE +21 -0
  161. package/licenses/ktx-parse/LICENSE +21 -0
  162. package/licenses/lightningcss/LICENSE +373 -0
  163. package/licenses/lightningcss-win32-x64-msvc/LICENSE +373 -0
  164. package/licenses/meshoptimizer/LICENSE.md +21 -0
  165. package/licenses/nanoid/LICENSE +20 -0
  166. package/licenses/ndarray/LICENSE +22 -0
  167. package/licenses/ndarray-lanczos/LICENSE +21 -0
  168. package/licenses/ndarray-ops/LICENSE +22 -0
  169. package/licenses/ndarray-pixels/LICENSE +21 -0
  170. package/licenses/ngraph.path/LICENSE +21 -0
  171. package/licenses/picocolors/LICENSE +15 -0
  172. package/licenses/picomatch/LICENSE +21 -0
  173. package/licenses/playwright/LICENSE +202 -0
  174. package/licenses/playwright/NOTICE +5 -0
  175. package/licenses/playwright-core/LICENSE +202 -0
  176. package/licenses/playwright-core/NOTICE +5 -0
  177. package/licenses/postcss/LICENSE +20 -0
  178. package/licenses/prettier/LICENSE +7 -0
  179. package/licenses/property-graph/LICENSE +21 -0
  180. package/licenses/rolldown/LICENSE +25 -0
  181. package/licenses/semver/LICENSE +15 -0
  182. package/licenses/sharp/LICENSE +191 -0
  183. package/licenses/source-map-js/LICENSE +28 -0
  184. package/licenses/three/LICENSE +21 -0
  185. package/licenses/tinyglobby/LICENSE +21 -0
  186. package/licenses/tsx/LICENSE +21 -0
  187. package/licenses/typescript/LICENSE.txt +55 -0
  188. package/licenses/undici-types/LICENSE +21 -0
  189. package/licenses/uniq/LICENSE +22 -0
  190. package/licenses/vite/LICENSE.md +2084 -0
  191. package/package.json +49 -4
  192. package/web/index.html +34 -0
@@ -0,0 +1,674 @@
1
+ import { graphValue, isGraphType } from "../sdk/graph-values.js";
2
+ let nextOwner = 1;
3
+ const remediation = "Use live handles from this owner, declared kernels/bindings, matching finite f32 values, and an available WebGPU executor; flush or cancel pending work before changing buffers.";
4
+ const fail = (code, at, detail) => {
5
+ throw Object.assign(new Error(`${code}: ${at}: ${detail} Repair: ${remediation}`), { code, remediation });
6
+ };
7
+ const plain = (value) => {
8
+ if (!value || typeof value !== "object" || Array.isArray(value))
9
+ return false;
10
+ const proto = Object.getPrototypeOf(value);
11
+ return (proto === Object.prototype || proto === null) && Reflect.ownKeys(value).every(key => {
12
+ const d = Object.getOwnPropertyDescriptor(value, key);
13
+ return typeof key === "string" && d.enumerable && "value" in d;
14
+ });
15
+ };
16
+ const exact = (value, keys) => plain(value) && Reflect.ownKeys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
17
+ const dense = (value) => {
18
+ if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype || Reflect.ownKeys(value).length !== value.length + 1)
19
+ return false;
20
+ for (let i = 0; i < value.length; i++) {
21
+ const d = Object.getOwnPropertyDescriptor(value, String(i));
22
+ if (!d || !d.enumerable || !("value" in d))
23
+ return false;
24
+ }
25
+ return true;
26
+ };
27
+ const countOK = (value) => Number.isSafeInteger(value) && value > 0 && value <= 0xffffffff;
28
+ const textureSize = (value, code, at) => {
29
+ if (!exact(value, ["width", "height"]) || !countOK(value.width) || !countOK(value.height) || value.width * value.height > 2 ** 24)
30
+ return fail(code, at, "Expected positive integer texture dimensions with at most 2^24 texels.");
31
+ return Object.freeze({ width: value.width, height: value.height });
32
+ };
33
+ const checked = (type, value, code, at) => {
34
+ if (Array.isArray(value) && !dense(value))
35
+ return fail(code, at, "Expected a dense plain vector.");
36
+ return graphValue(type, value) ?? fail(code, at, `Expected ${type} with finite f32 components.`);
37
+ };
38
+ const values = (type, data, code, at) => {
39
+ if (!dense(data))
40
+ return fail(code, at, "Expected a dense plain values array.");
41
+ return Object.freeze(data.map((value, i) => checked(type, value, code, `${at}[${i}]`)));
42
+ };
43
+ // Canonical plain data prevents key-order differences and snapshot aliases from affecting identity.
44
+ const canonical = (value, code, seen = new Set()) => {
45
+ if (value === null || typeof value === "string" || typeof value === "boolean")
46
+ return value;
47
+ if (typeof value === "number" && Number.isFinite(value))
48
+ return value;
49
+ if (typeof value !== "object" || !value || seen.has(value))
50
+ return fail(code, "definitions", "Expected acyclic finite plain data.");
51
+ seen.add(value);
52
+ let result;
53
+ if (dense(value))
54
+ result = value.map(v => canonical(v, code, seen));
55
+ else if (plain(value))
56
+ result = Object.fromEntries(Object.keys(value).sort().map(k => [k, canonical(value[k], code, seen)]));
57
+ else
58
+ return fail(code, "definitions", "Expected plain data without accessors or sparse arrays.");
59
+ seen.delete(value);
60
+ return result;
61
+ };
62
+ const freeze = (value) => {
63
+ if (value && typeof value === "object") {
64
+ for (const child of Object.values(value))
65
+ freeze(child);
66
+ Object.freeze(value);
67
+ }
68
+ return value;
69
+ };
70
+ const failure = (error, at) => {
71
+ const message = error instanceof Error ? error.message : typeof error === "string" ? error : "Executor failed.";
72
+ const code = /\bTC_COMPUTE_[A-Z0-9_]+\b/.exec(message)?.[0] ?? "TC_COMPUTE_EXECUTION";
73
+ return { code, message: `${code}: ${at}: ${message}`, remediation };
74
+ };
75
+ /** CPU mirror and lifecycle owner only. All computation is delegated to the private executor. */
76
+ export function createComputeDriver(options) {
77
+ try {
78
+ return createComputeOwner(options);
79
+ }
80
+ catch (error) {
81
+ // Construction never starts executor work. Unreturned CPU state is unreachable;
82
+ // release the transferred executor as well, without hiding the validation error.
83
+ try {
84
+ options.executor?.dispose();
85
+ }
86
+ catch { /* Preserve the construction failure. */ }
87
+ try {
88
+ options.textureAssets?.dispose();
89
+ }
90
+ catch { /* Preserve the construction failure. */ }
91
+ throw error;
92
+ }
93
+ }
94
+ function createComputeOwner(options) {
95
+ const kernels = new Map();
96
+ if (!dense(options.kernels))
97
+ fail("TC_COMPUTE_KERNEL", "kernels", "Expected a dense kernel definitions array.");
98
+ for (const source of options.kernels) {
99
+ const k = canonical(source, "TC_COMPUTE_KERNEL");
100
+ if (!plain(k) || typeof k.id !== "string" || !k.id.trim() || kernels.has(k.id) || !countOK(k.count) ||
101
+ !Number.isSafeInteger(k.workgroupSize) || k.workgroupSize <= 0 || !isGraphType(k.outputType) || !plain(k.graph) || k.graph.outputType !== k.outputType)
102
+ fail("TC_COMPUTE_KERNEL", "kernels", "Expected unique valid compiled kernel definitions.");
103
+ const bindings = k.buffers === undefined ? [] : k.buffers, parameters = k.graph.parameters === undefined ? [] : k.graph.parameters;
104
+ if (!dense(bindings) || !dense(parameters))
105
+ fail("TC_COMPUTE_KERNEL", k.id, "Invalid declarations.");
106
+ const seen = new Set();
107
+ for (const binding of bindings) {
108
+ if (!exact(binding, ["name", "type"]) || typeof binding.name !== "string" || !binding.name.trim() || !isGraphType(binding.type) || seen.has(binding.name))
109
+ fail("TC_COMPUTE_KERNEL", k.id, "Invalid or duplicate buffer declaration.");
110
+ seen.add(binding.name);
111
+ }
112
+ seen.clear();
113
+ if (k.textures !== undefined) {
114
+ if (!dense(k.textures))
115
+ fail("TC_COMPUTE_KERNEL", k.id, "Invalid texture declarations.");
116
+ k.textures = k.textures.map(binding => {
117
+ if (!plain(binding) || Object.keys(binding).some(key => !["name", "filter", "wrapU", "wrapV"].includes(key)) ||
118
+ typeof binding.name !== "string" || !binding.name.trim() || seen.has(binding.name) ||
119
+ (binding.filter !== undefined && !["nearest", "linear"].includes(binding.filter)) ||
120
+ (binding.wrapU !== undefined && !["clamp", "repeat", "mirror"].includes(binding.wrapU)) ||
121
+ (binding.wrapV !== undefined && !["clamp", "repeat", "mirror"].includes(binding.wrapV)))
122
+ return fail("TC_COMPUTE_KERNEL", k.id, "Invalid or duplicate texture declaration.");
123
+ seen.add(binding.name);
124
+ return { name: binding.name, filter: binding.filter ?? "linear", wrapU: binding.wrapU ?? "clamp", wrapV: binding.wrapV ?? "clamp" };
125
+ }).sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
126
+ }
127
+ seen.clear();
128
+ for (const parameter of parameters) {
129
+ if (!exact(parameter, ["name", "type", "default"]) || typeof parameter.name !== "string" || !parameter.name.trim() || !isGraphType(parameter.type) || seen.has(parameter.name))
130
+ fail("TC_COMPUTE_KERNEL", k.id, "Invalid or duplicate parameter declaration.");
131
+ checked(parameter.type, parameter.default, "TC_COMPUTE_KERNEL", `${k.id}:${parameter.name}`);
132
+ seen.add(parameter.name);
133
+ }
134
+ k.buffers = [...bindings].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
135
+ if (k.graph.parameters)
136
+ k.graph.parameters = [...parameters].sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
137
+ kernels.set(k.id, freeze(k));
138
+ }
139
+ const definitions = [...kernels.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map(({ source: _source, ...k }) => k);
140
+ const definitionText = JSON.stringify(canonical(definitions, "TC_COMPUTE_KERNEL"));
141
+ let owner = nextOwner++, epoch = 0, disposed = false, executor = options.executor;
142
+ let nextTask = 1, flushing, cleanupErrors = 0;
143
+ const buffers = new Map(), names = new Map(), tasks = new Map();
144
+ let renderPins = new Map();
145
+ const generations = [], free = [];
146
+ let queue = [];
147
+ const active = () => { if (disposed)
148
+ fail("TC_COMPUTE_DISPOSED", "owner", "Owner is disposed; create a new driver."); };
149
+ const cleanup = (fn) => { try {
150
+ fn();
151
+ }
152
+ catch {
153
+ cleanupErrors++;
154
+ } };
155
+ const resourceHandle = (handle, isTexture) => {
156
+ active();
157
+ const kind = isTexture ? "texture" : "buffer", code = isTexture ? "TC_COMPUTE_TEXTURE_HANDLE" : "TC_COMPUTE_BUFFER_HANDLE";
158
+ if (!exact(handle, isTexture ? ["owner", "index", "generation", "kind"] : ["owner", "index", "generation"]) ||
159
+ handle.owner !== owner || !Number.isSafeInteger(handle.index) || !Number.isSafeInteger(handle.generation) ||
160
+ (isTexture && handle.kind !== "texture"))
161
+ return fail(code, kind, "Invalid or foreign handle; resolve its name again after restore.");
162
+ const b = buffers.get(handle.index);
163
+ if (!b || b.handle.generation !== handle.generation || !!b.texture !== isTexture)
164
+ return fail(code, kind, "Stale handle or incorrect resource kind.");
165
+ return b;
166
+ };
167
+ const buffer = (handle) => resourceHandle(handle, false);
168
+ const texture = (handle) => resourceHandle(handle, true);
169
+ const textureHandle = (b) => Object.freeze({ ...b.handle, kind: "texture" });
170
+ const descriptor = (b) => Object.freeze({ key: b.key, type: b.type, revision: b.revision, values: b.values, ...(b.texture ? { texture: b.texture } : {}) });
171
+ const task = (handle) => {
172
+ active();
173
+ if (!exact(handle, ["owner", "id"]) || handle.owner !== owner || !Number.isSafeInteger(handle.id) || !tasks.has(handle.id))
174
+ return fail("TC_COMPUTE_TASK_HANDLE", "task", "Invalid, released or stale task handle.");
175
+ return tasks.get(handle.id);
176
+ };
177
+ const unfinished = (t) => t.state.status === "queued" || t.state.status === "running";
178
+ const live = (b) => buffers.get(b.handle.index) === b;
179
+ const collect = () => {
180
+ const retained = new Map();
181
+ const keep = ({ key, revision }) => retained.set(key + "/" + revision, { key, revision });
182
+ for (const b of buffers.values())
183
+ keep(b);
184
+ for (const t of tasks.values())
185
+ if (t.result && live(t.result.source))
186
+ keep(t.result.input);
187
+ cleanup(() => executor?.collect?.([...retained.values()]));
188
+ };
189
+ const collectIfIdle = () => {
190
+ if (!disposed && !flushing && ![...tasks.values()].some(t => t.state.status === "running"))
191
+ collect();
192
+ };
193
+ const dependency = (handle, at) => {
194
+ if (!exact(handle, ["owner", "id"]) || handle.owner !== owner || !Number.isSafeInteger(handle.id) ||
195
+ handle.id <= 0 || handle.id >= nextTask)
196
+ return fail("TC_COMPUTE_DEPENDENCY", at, "Expected an existing earlier task handle from this owner.");
197
+ return tasks.get(handle.id) ?? fail("TC_COMPUTE_DEPENDENCY", at, "Dependency task was released or invalidated.");
198
+ };
199
+ const finish = (t, state) => {
200
+ if (!unfinished(t))
201
+ return;
202
+ t.state = state;
203
+ for (const b of t.buffers)
204
+ b.busy--;
205
+ t.buffers.clear();
206
+ for (const predecessor of t.dependencies)
207
+ predecessor.consumers--;
208
+ t.dependencies.clear();
209
+ t.taskBindings.clear();
210
+ t.textureBindings.clear();
211
+ t.request = undefined;
212
+ t.output = undefined;
213
+ };
214
+ const cancel = (t) => {
215
+ if (!unfinished(t))
216
+ return;
217
+ finish(t, { status: "cancelled" });
218
+ t.controller.abort();
219
+ };
220
+ const idle = () => {
221
+ active();
222
+ if ([...tasks.values()].some(unfinished))
223
+ fail("TC_COMPUTE_BUSY", "snapshot", "Flush or cancel unfinished tasks before taking or restoring a snapshot.");
224
+ };
225
+ const addBuffer = (id, type, count, data, texture) => {
226
+ const index = free.pop() ?? generations.length;
227
+ const generation = (generations[index] ?? 0) + 1;
228
+ generations[index] = generation;
229
+ const handle = Object.freeze({ owner, index, generation });
230
+ const b = { handle, key: `compute:${owner}:${index}:${generation}`, id, type, count, values: data, ...(texture ? { texture } : {}), revision: 0, busy: 0 };
231
+ buffers.set(index, b);
232
+ names.set(id, b);
233
+ return b;
234
+ };
235
+ const release = (b) => {
236
+ if (renderPins.has(b.id))
237
+ fail("TC_COMPUTE_BUFFER_IN_USE", b.id, "Unbind all render references before releasing this resource.");
238
+ if (b.busy)
239
+ fail("TC_COMPUTE_BUSY", b.id, "Resource is used by unfinished tasks.");
240
+ buffers.delete(b.handle.index);
241
+ names.delete(b.id);
242
+ free.push(b.handle.index);
243
+ for (const t of tasks.values())
244
+ if (t.source === b) {
245
+ t.source = undefined;
246
+ t.result = undefined;
247
+ }
248
+ const current = executor;
249
+ if (current)
250
+ cleanup(() => current.release(b.key));
251
+ };
252
+ const service = {
253
+ createTexture(input) {
254
+ active();
255
+ if (plain(input) && Object.hasOwn(input, "asset")) {
256
+ if (!exact(input, ["id", "asset"]) || typeof input.id !== "string" || !input.id.trim())
257
+ return fail("TC_COMPUTE_TEXTURE_ASSET", "texture", "Asset import needs only a nonempty id and a declared texture asset.");
258
+ const asset = typeof input.asset === "string" ? input.asset : plain(input.asset) && input.asset.kind === "asset" ? input.asset.id : undefined;
259
+ if (typeof asset !== "string" || !asset.trim() || !options.textureAssets)
260
+ return fail("TC_COMPUTE_TEXTURE_ASSET", input.id, "No declared loaded texture asset provider is available.");
261
+ let resolved;
262
+ try {
263
+ resolved = options.textureAssets.resolve(asset);
264
+ }
265
+ catch (error) {
266
+ return fail("TC_COMPUTE_TEXTURE_ASSET", input.id + ":" + asset, String(error));
267
+ }
268
+ if (!exact(resolved, ["width", "height", "data"]))
269
+ return fail("TC_COMPUTE_TEXTURE_ASSET", input.id, "Asset provider returned an invalid pixel descriptor.");
270
+ const size = textureSize({ width: resolved.width, height: resolved.height }, "TC_COMPUTE_TEXTURE_ASSET", input.id);
271
+ return service.createTexture({ id: input.id, ...size, data: resolved.data });
272
+ }
273
+ if (!plain(input) || Object.keys(input).some(k => !["id", "width", "height", "data"].includes(k)) || typeof input.id !== "string" || !input.id.trim())
274
+ return fail("TC_COMPUTE_TEXTURE", "texture", "Expected a nonempty ID, positive dimensions and optional rgba32float data.");
275
+ const size = textureSize({ width: input.width, height: input.height }, "TC_COMPUTE_TEXTURE", input.id);
276
+ if (names.has(input.id))
277
+ return fail("TC_COMPUTE_TEXTURE_NAME", input.id, "Resource name already exists; release it or use a unique name.");
278
+ const count = size.width * size.height;
279
+ let data;
280
+ if (input.data !== undefined)
281
+ data = values("vec4", input.data, "TC_COMPUTE_VALUE", input.id);
282
+ else {
283
+ try {
284
+ data = Object.freeze(Array(count).fill(Object.freeze([0, 0, 0, 0])));
285
+ }
286
+ catch {
287
+ return fail("TC_COMPUTE_TEXTURE", input.id, "Texture allocation failed; reduce dimensions.");
288
+ }
289
+ }
290
+ if (data.length !== count)
291
+ return fail("TC_COMPUTE_COUNT", input.id, "Initial data count does not match texture dimensions.");
292
+ return textureHandle(addBuffer(input.id, "vec4", count, data, size));
293
+ },
294
+ resolveTexture(id) {
295
+ active();
296
+ const b = names.get(id);
297
+ return b?.texture ? textureHandle(b) : fail("TC_COMPUTE_TEXTURE_NAME", String(id), "Unknown texture name.");
298
+ },
299
+ describeTexture(handle) {
300
+ const b = texture(handle);
301
+ return Object.freeze({ id: b.id, ...b.texture, format: "rgba32float", revision: b.revision });
302
+ },
303
+ readTexture(handle) { return structuredClone(texture(handle).values); },
304
+ writeTexture(handle, data, region) {
305
+ const b = texture(handle), size = b.texture;
306
+ if (b.busy)
307
+ fail("TC_COMPUTE_BUSY", b.id, "Texture is used by unfinished tasks.");
308
+ const rect = region === undefined ? { x: 0, y: 0, ...size } : region;
309
+ if (!exact(rect, ["x", "y", "width", "height"]) || !Number.isSafeInteger(rect.x) || !Number.isSafeInteger(rect.y) || rect.x < 0 || rect.y < 0)
310
+ return fail("TC_COMPUTE_RANGE", b.id, "Expected an explicit nonnegative integer rectangle.");
311
+ const extent = textureSize({ width: rect.width, height: rect.height }, "TC_COMPUTE_RANGE", b.id);
312
+ if (rect.x > size.width - extent.width || rect.y > size.height - extent.height)
313
+ return fail("TC_COMPUTE_RANGE", b.id, "Write rectangle is outside the texture.");
314
+ const next = values("vec4", data, "TC_COMPUTE_VALUE", b.id);
315
+ if (next.length !== extent.width * extent.height)
316
+ return fail("TC_COMPUTE_COUNT", b.id, "Write data count does not match the rectangle.");
317
+ const staged = [...b.values];
318
+ for (let y = 0; y < extent.height; y++)
319
+ for (let x = 0; x < extent.width; x++)
320
+ staged[(rect.y + y) * size.width + rect.x + x] = next[y * extent.width + x];
321
+ b.values = Object.freeze(staged);
322
+ b.revision++;
323
+ },
324
+ releaseTexture(handle) { release(texture(handle)); },
325
+ createBuffer(input) {
326
+ active();
327
+ if (!plain(input) || Object.keys(input).some(k => !["id", "type", "count", "data"].includes(k)) || typeof input.id !== "string" || !input.id.trim() || !isGraphType(input.type) || !countOK(input.count))
328
+ return fail("TC_COMPUTE_BUFFER", "buffer", "Expected a nonempty ID, graph type and positive array count.");
329
+ if (names.has(input.id))
330
+ return fail("TC_COMPUTE_BUFFER_NAME", input.id, "Buffer name already exists; release it or use a unique name.");
331
+ let data;
332
+ if (input.data !== undefined)
333
+ data = values(input.type, input.data, "TC_COMPUTE_VALUE", input.id);
334
+ else {
335
+ const zero = input.type === "bool" ? false : input.type === "float" ? 0 : Object.freeze(Array(Number(input.type.slice(3))).fill(0));
336
+ try {
337
+ data = Object.freeze(Array(input.count).fill(zero));
338
+ }
339
+ catch {
340
+ return fail("TC_COMPUTE_BUFFER", input.id, "Buffer allocation failed; reduce count.");
341
+ }
342
+ }
343
+ if (data.length !== input.count)
344
+ return fail("TC_COMPUTE_COUNT", input.id, "Initial data count does not match buffer count.");
345
+ return addBuffer(input.id, input.type, input.count, data).handle;
346
+ },
347
+ resolve(id) {
348
+ active();
349
+ const b = names.get(id);
350
+ return b && !b.texture ? b.handle : fail("TC_COMPUTE_BUFFER_NAME", String(id), "Unknown buffer name.");
351
+ },
352
+ describeBuffer(handle) {
353
+ const b = buffer(handle);
354
+ return Object.freeze({ id: b.id, type: b.type, count: b.count, revision: b.revision });
355
+ },
356
+ readBuffer(handle) { return structuredClone(buffer(handle).values); },
357
+ writeBuffer(handle, data, offset = 0) {
358
+ const b = buffer(handle);
359
+ if (b.busy)
360
+ fail("TC_COMPUTE_BUSY", b.id, "Buffer is used by unfinished tasks.");
361
+ const next = values(b.type, data, "TC_COMPUTE_VALUE", b.id);
362
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > b.count || next.length > b.count - offset)
363
+ fail("TC_COMPUTE_RANGE", b.id, "Write offset/count is outside the buffer.");
364
+ if (!next.length)
365
+ return;
366
+ const staged = [...b.values];
367
+ for (let i = 0; i < next.length; i++)
368
+ staged[offset + i] = next[i];
369
+ b.values = Object.freeze(staged);
370
+ b.revision++;
371
+ },
372
+ releaseBuffer(handle) { release(buffer(handle)); },
373
+ dispatch(reference, input) {
374
+ active();
375
+ const id = typeof reference === "string" ? reference : plain(reference) && reference.kind === "compute" ? reference.id : undefined;
376
+ const kernel = typeof id === "string" ? kernels.get(id) : undefined;
377
+ if (!kernel)
378
+ return fail("TC_COMPUTE_KERNEL", String(id), "Kernel is not declared in this scene.");
379
+ if (!plain(input) || Object.keys(input).some(k => !["output", "bindings", "textures", "parameters", "time", "after"].includes(k)))
380
+ return fail("TC_COMPUTE_DISPATCH", kernel.id, "Expected dispatch options.");
381
+ const output = resourceHandle(input.output, plain(input.output) && input.output.kind === "texture");
382
+ if (output.type !== kernel.outputType)
383
+ fail("TC_COMPUTE_TYPE", output.id, "Output type differs from the kernel output type.");
384
+ if (output.count !== kernel.count)
385
+ fail("TC_COMPUTE_COUNT", output.id, "Output count differs from kernel count.");
386
+ const bindings = input.bindings === undefined ? {} : input.bindings;
387
+ if (!exact(bindings, (kernel.buffers ?? []).map(b => b.name)))
388
+ fail("TC_COMPUTE_BINDING", kernel.id, "Bindings must match all and only declared input names.");
389
+ const textureBindingsInput = input.textures === undefined ? {} : input.textures;
390
+ if (!exact(textureBindingsInput, (kernel.textures ?? []).map(b => b.name)))
391
+ fail("TC_COMPUTE_BINDING", kernel.id, "Textures must match all and only declared sampled texture names.");
392
+ const involved = new Set([output]);
393
+ const dependencies = new Set(), taskBindings = new Map(), textureBindings = new Map();
394
+ if (input.after !== undefined) {
395
+ if (!dense(input.after))
396
+ fail("TC_COMPUTE_DEPENDENCY", kernel.id + ":after", "Expected a dense array of earlier task handles.");
397
+ for (const [i, handle] of input.after.entries())
398
+ dependencies.add(dependency(handle, kernel.id + ":after[" + i + "]"));
399
+ }
400
+ const inputs = {}, textures = {};
401
+ const bind = (name, value, isTexture, type) => {
402
+ const at = kernel.id + ":" + name;
403
+ let b;
404
+ if (value && typeof value === "object" && (Object.hasOwn(value, "task") || Object.hasOwn(value, "id"))) {
405
+ if (!exact(value, ["task"]))
406
+ return fail("TC_COMPUTE_DEPENDENCY", at, "Expected only a task handle in a task-result binding.");
407
+ const predecessor = dependency(value.task, at);
408
+ b = predecessor.source ?? fail("TC_COMPUTE_DEPENDENCY_RESOURCE", at, "Task " + predecessor.handle.id + " output resource was released or invalidated.");
409
+ if (!live(b))
410
+ fail("TC_COMPUTE_DEPENDENCY_RESOURCE", at, "Task " + predecessor.handle.id + " output resource is no longer live.");
411
+ if (!!b.texture !== isTexture)
412
+ fail("TC_COMPUTE_TYPE", at, "Task result resource kind differs from its declaration.");
413
+ dependencies.add(predecessor);
414
+ (isTexture ? textureBindings : taskBindings).set(name, predecessor);
415
+ }
416
+ else {
417
+ b = resourceHandle(value, isTexture);
418
+ (isTexture ? textures : inputs)[name] = Object.freeze({ ...descriptor(b), captured: true });
419
+ }
420
+ if (b.type !== type)
421
+ fail("TC_COMPUTE_TYPE", at, "Input resource type differs from its declaration.");
422
+ involved.add(b);
423
+ };
424
+ for (const binding of kernel.buffers ?? [])
425
+ bind(binding.name, bindings[binding.name], false, binding.type);
426
+ for (const binding of kernel.textures ?? [])
427
+ bind(binding.name, textureBindingsInput[binding.name], true, "vec4");
428
+ const supplied = input.parameters === undefined ? {} : input.parameters, declared = kernel.graph.parameters ?? [];
429
+ if (!plain(supplied) || Object.keys(supplied).some(name => !declared.some(p => p.name === name)))
430
+ fail("TC_COMPUTE_PARAMETER", kernel.id, "Unknown parameters or invalid parameter record.");
431
+ const parameters = Object.fromEntries(declared.map(p => [p.name, checked(p.type, Object.hasOwn(supplied, p.name) ? supplied[p.name] : p.default, "TC_COMPUTE_PARAMETER", `${kernel.id}:${p.name}`)]));
432
+ let time, capabilities;
433
+ try {
434
+ time = input.time === undefined ? options.time ? options.time() : 0 : input.time;
435
+ }
436
+ catch {
437
+ return fail("TC_COMPUTE_TIME", kernel.id, "Simulation time provider failed.");
438
+ }
439
+ if (typeof time !== "number" || !Number.isFinite(time) || !Number.isFinite(Math.fround(time)))
440
+ return fail("TC_COMPUTE_TIME", kernel.id, "Expected finite f32 simulation time.");
441
+ try {
442
+ capabilities = options.capabilities();
443
+ }
444
+ catch {
445
+ return fail("TC_COMPUTE_CAPABILITY", kernel.id, "Capability provider failed.");
446
+ }
447
+ if (capabilities?.backend !== "webgpu" || !capabilities.compute || !executor)
448
+ fail("TC_COMPUTE_CAPABILITY", kernel.id, "Managed dispatch requires real WebGPU compute and an executor; no CPU fallback is available.");
449
+ const controller = new AbortController(), handle = Object.freeze({ owner, id: nextTask++ });
450
+ const request = Object.freeze({ kernel, inputs: Object.freeze(inputs), ...(kernel.textures !== undefined ? { textures: Object.freeze(textures) } : {}), output: Object.freeze({ key: output.key, type: output.type, count: output.count, ...(output.texture ? { texture: output.texture } : {}), ...(typeof executor?.commit === "function" ? { retain: true } : {}) }), parameters: Object.freeze(parameters), time, signal: controller.signal });
451
+ const t = { handle, state: { status: "queued" }, request, buffers: involved, output, source: output, dependencies, taskBindings, textureBindings, consumers: 0, controller, epoch };
452
+ for (const b of involved)
453
+ b.busy++;
454
+ for (const predecessor of dependencies)
455
+ predecessor.consumers++;
456
+ tasks.set(handle.id, t);
457
+ queue.push(t);
458
+ return handle;
459
+ },
460
+ task(handle) { return structuredClone(task(handle).state); },
461
+ tasks() {
462
+ active();
463
+ return [...tasks.values()].map(t => ({ handle: { ...t.handle }, state: structuredClone(t.state) }));
464
+ },
465
+ cancel(handle) { cancel(task(handle)); },
466
+ releaseTask(handle) {
467
+ const t = task(handle);
468
+ if (t.consumers)
469
+ fail("TC_COMPUTE_TASK_IN_USE", String(handle.id), "Unfinished consumers still reference this task.");
470
+ if (unfinished(t))
471
+ fail("TC_COMPUTE_BUSY", String(handle.id), "Only terminal tasks can be released; flush or cancel first.");
472
+ tasks.delete(handle.id);
473
+ queue = queue.filter(entry => entry !== t);
474
+ t.result = undefined;
475
+ t.source = undefined;
476
+ collectIfIdle();
477
+ },
478
+ };
479
+ const run = async (t) => {
480
+ if (!unfinished(t) || t.epoch !== epoch || disposed)
481
+ return;
482
+ t.state = { status: "running" };
483
+ const runner = executor, pending = t.request, output = t.output;
484
+ const current = () => !t.controller.signal.aborted && t.epoch === epoch && !disposed && unfinished(t);
485
+ const aborted = Symbol("cancelled");
486
+ let onAbort;
487
+ const cancellation = new Promise(resolve => { onAbort = () => resolve(aborted); });
488
+ t.controller.signal.addEventListener("abort", onAbort, { once: true });
489
+ let submitted = false;
490
+ try {
491
+ for (const predecessor of t.dependencies)
492
+ if (predecessor.state.status !== "succeeded")
493
+ fail("TC_COMPUTE_DEPENDENCY_FAILED", pending.kernel.id + ":task " + predecessor.handle.id, "Predecessor is " + predecessor.state.status + "; successful completion is required.");
494
+ const inputs = { ...pending.inputs }, textures = { ...pending.textures };
495
+ const resolveResults = (bindings, target) => {
496
+ for (const [name, predecessor] of bindings) {
497
+ const result = predecessor.result;
498
+ if (!result || !live(result.source))
499
+ return fail("TC_COMPUTE_DEPENDENCY_RESOURCE", pending.kernel.id + ":" + name + ":task " + predecessor.handle.id, "Predecessor output resource is no longer live.");
500
+ target[name] = result.input;
501
+ }
502
+ };
503
+ resolveResults(t.taskBindings, inputs);
504
+ resolveResults(t.textureBindings, textures);
505
+ const request = Object.freeze({ ...pending, inputs: Object.freeze(inputs), ...(pending.textures !== undefined ? { textures: Object.freeze(textures) } : {}) });
506
+ submitted = true;
507
+ // Racing cancellation also observes late rejections from executors that ignore abort.
508
+ const result = await Promise.race([runner.run(request), cancellation]);
509
+ if (result === aborted || !current())
510
+ return;
511
+ const staged = values(output.type, result, "TC_COMPUTE_RESULT", output.id);
512
+ if (staged.length !== output.count)
513
+ fail("TC_COMPUTE_RESULT", output.id, "Executor returned the wrong output count.");
514
+ if (!current())
515
+ return;
516
+ if (typeof runner.commit === "function")
517
+ runner.commit(output.key, output.revision + 1);
518
+ if (!current())
519
+ return;
520
+ output.values = staged;
521
+ output.revision++;
522
+ t.result = Object.freeze({ source: output, input: descriptor(output) });
523
+ finish(t, { status: "succeeded" });
524
+ }
525
+ catch (error) {
526
+ if (t.epoch === epoch && !disposed && unfinished(t))
527
+ finish(t, { status: "failed", error: failure(error, pending.kernel.id) });
528
+ }
529
+ finally {
530
+ t.controller.signal.removeEventListener("abort", onAbort);
531
+ if (submitted)
532
+ cleanup(() => runner.discard?.(output.key));
533
+ }
534
+ };
535
+ const flush = () => {
536
+ active();
537
+ if (flushing)
538
+ return flushing;
539
+ flushing = (async () => {
540
+ while (!disposed && queue.length)
541
+ await run(queue.shift());
542
+ if (!disposed)
543
+ collect();
544
+ })().finally(() => { flushing = undefined; });
545
+ return flushing;
546
+ };
547
+ const validateSnapshot = (value) => {
548
+ idle();
549
+ const code = "TC_COMPUTE_SNAPSHOT";
550
+ if (!exact(value, ["version", "kernels", "buffers"]) || value.version !== 1 || JSON.stringify(canonical(value.kernels, code)) !== definitionText || !dense(value.buffers))
551
+ return fail(code, "snapshot", "Incompatible snapshot or kernel definitions.");
552
+ const ids = new Set();
553
+ return value.buffers.map(entry => {
554
+ if (!exact(entry, plain(entry) && Object.hasOwn(entry, "texture") ? ["id", "type", "count", "data", "texture"] : ["id", "type", "count", "data"]) || typeof entry.id !== "string" || !entry.id.trim() || ids.has(entry.id) || !isGraphType(entry.type) || !countOK(entry.count))
555
+ return fail(code, "buffers", "Invalid buffer definition or duplicate name.");
556
+ ids.add(entry.id);
557
+ const texture = Object.hasOwn(entry, "texture") ? textureSize(entry.texture, code, entry.id) : undefined;
558
+ if (texture && (entry.type !== "vec4" || entry.count !== texture.width * texture.height))
559
+ return fail(code, entry.id, "Texture snapshots require vec4 data matching their dimensions.");
560
+ const data = values(entry.type, entry.data, code, entry.id);
561
+ if (data.length !== entry.count)
562
+ return fail(code, entry.id, "Buffer data count mismatch.");
563
+ return { id: entry.id, type: entry.type, count: entry.count, data, ...(texture ? { texture } : {}) };
564
+ });
565
+ };
566
+ const invalidate = () => {
567
+ epoch++;
568
+ for (const t of tasks.values()) {
569
+ cancel(t);
570
+ // Executor history and restored generations cannot back old task results.
571
+ t.result = undefined;
572
+ t.source = undefined;
573
+ }
574
+ queue = [];
575
+ };
576
+ const dispose = () => {
577
+ if (disposed)
578
+ return;
579
+ disposed = true;
580
+ invalidate();
581
+ buffers.clear();
582
+ names.clear();
583
+ tasks.clear();
584
+ renderPins.clear();
585
+ free.length = generations.length = 0;
586
+ const old = executor;
587
+ executor = undefined;
588
+ if (old)
589
+ cleanup(() => old.dispose());
590
+ cleanup(() => options.textureAssets?.dispose());
591
+ };
592
+ const inspect = () => ({
593
+ owner, disposed, buffers: buffers.size, tasks: tasks.size,
594
+ queued: [...tasks.values()].filter(t => t.state.status === "queued").length,
595
+ running: [...tasks.values()].filter(t => t.state.status === "running").length,
596
+ busyBuffers: [...buffers.values()].filter(b => b.busy > 0).length,
597
+ executor: !!executor, cleanupErrors,
598
+ });
599
+ const factory = () => ({
600
+ id: "$engine.compute", version: "1", services: { compute: service }, settle: flush,
601
+ snapshot() {
602
+ idle();
603
+ return { version: 1, kernels: JSON.parse(definitionText), buffers: [...names.values()].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0).map(b => ({ id: b.id, type: b.type, count: b.count, data: structuredClone(b.values), ...(b.texture ? { texture: { ...b.texture } } : {}) })) };
604
+ },
605
+ validateSnapshot(value) { validateSnapshot(value); },
606
+ restore(value) {
607
+ const staged = validateSnapshot(value);
608
+ invalidate();
609
+ const old = [...buffers.values()], current = executor;
610
+ buffers.clear();
611
+ names.clear();
612
+ tasks.clear();
613
+ free.length = generations.length = 0;
614
+ owner = nextOwner++;
615
+ for (const b of staged)
616
+ addBuffer(b.id, b.type, b.count, b.data, b.texture);
617
+ if (current)
618
+ for (const b of old)
619
+ cleanup(() => current.release(b.key));
620
+ },
621
+ dispose, inspect,
622
+ });
623
+ if (options.buffers !== undefined) {
624
+ if (!dense(options.buffers))
625
+ fail("TC_COMPUTE_BUFFER", "buffers", "Expected a dense initial buffer definitions array.");
626
+ for (const input of options.buffers)
627
+ service.createBuffer(input);
628
+ }
629
+ if (options.textures !== undefined) {
630
+ if (!dense(options.textures))
631
+ fail("TC_COMPUTE_TEXTURE", "textures", "Expected a dense initial texture definitions array.");
632
+ for (const input of options.textures)
633
+ service.createTexture(input);
634
+ }
635
+ return {
636
+ service, factory, flush, dispose, inspect,
637
+ resource(id) {
638
+ active();
639
+ const b = names.get(id) ?? fail("TC_COMPUTE_BUFFER_NAME", String(id), "Unknown buffer name.");
640
+ if (b.texture)
641
+ return fail("TC_COMPUTE_BUFFER_NAME", String(id), "Expected a buffer; this name belongs to a texture.");
642
+ return freeze({ ...descriptor(b), values: structuredClone(b.values) });
643
+ },
644
+ textureResource(id) {
645
+ active();
646
+ const b = names.get(id);
647
+ if (!b?.texture)
648
+ return fail("TC_COMPUTE_TEXTURE_NAME", String(id), "Unknown texture name.");
649
+ return freeze({ ...descriptor(b), values: structuredClone(b.values) });
650
+ },
651
+ setRenderPins(ids) {
652
+ if (!dense(ids) || ids.some(id => typeof id !== "string" || !id.trim()))
653
+ fail("TC_COMPUTE_BUFFER_NAME", "render pins", "Expected a dense array of nonempty logical buffer IDs.");
654
+ // Render restores first after Host preflight; the logical names may arrive later.
655
+ if (ids.length)
656
+ active();
657
+ const next = new Map();
658
+ for (const id of ids)
659
+ next.set(id, (next.get(id) ?? 0) + 1);
660
+ renderPins = next;
661
+ },
662
+ replaceExecutor(next) {
663
+ active();
664
+ invalidate();
665
+ const old = executor;
666
+ executor = next;
667
+ if (old && old !== next)
668
+ cleanup(() => old.dispose());
669
+ else if (old)
670
+ for (const b of buffers.values())
671
+ cleanup(() => old.release(b.key));
672
+ },
673
+ };
674
+ }