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,271 @@
1
+ import { isGraphType } from "../sdk/graph-values.js";
2
+ const remediation = "Use declared material/pass and texture slot names, live managed rgba32float texture handles with matching dimensions, and matching render/compute snapshots; bind required slots before rendering and create a new owner after disposal.";
3
+ const fail = (code, at, detail) => {
4
+ throw Object.assign(new Error(`${code}: ${at}: ${detail} Repair: ${remediation}`), { code, remediation });
5
+ };
6
+ const plain = (value) => {
7
+ if (!value || typeof value !== "object" || Array.isArray(value))
8
+ return false;
9
+ const proto = Object.getPrototypeOf(value);
10
+ return (proto === Object.prototype || proto === null) && Reflect.ownKeys(value).every(key => {
11
+ const d = Object.getOwnPropertyDescriptor(value, key);
12
+ return typeof key === "string" && d.enumerable && "value" in d;
13
+ });
14
+ };
15
+ const exact = (value, keys) => plain(value) && Reflect.ownKeys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
16
+ const dense = (value) => {
17
+ if (!Array.isArray(value) || Object.getPrototypeOf(value) !== Array.prototype || Reflect.ownKeys(value).length !== value.length + 1)
18
+ return false;
19
+ for (let i = 0; i < value.length; i++) {
20
+ const d = Object.getOwnPropertyDescriptor(value, String(i));
21
+ if (!d || !d.enumerable || !("value" in d))
22
+ return false;
23
+ }
24
+ return true;
25
+ };
26
+ const nameOK = (value) => typeof value === "string" && !!value.trim();
27
+ const countOK = (value) => Number.isSafeInteger(value) && value > 0 && value <= 0xffffffff;
28
+ // Match the compute owner's texture extent limit, including constraints on a single axis.
29
+ const dimensionOK = (value) => countOK(value) && value <= 2 ** 24;
30
+ const sizeOK = (value) => exact(value, ["width", "height"]) && dimensionOK(value.width) && dimensionOK(value.height) && value.width * value.height <= 2 ** 24;
31
+ const texelsOK = (value, count) => dense(value) && value.length === count && value.every(texel => dense(texel) && texel.length === 4 &&
32
+ texel.every(component => typeof component === "number" && Number.isFinite(component) && Number.isFinite(Math.fround(component))));
33
+ const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
34
+ const copyReferences = (entries) => Object.fromEntries(Object.entries(entries).sort(([a], [b]) => compare(a, b)).map(([name, ref]) => [name, { ...ref }]));
35
+ const pins = (bindings) => [...new Set(Object.values(bindings).flatMap(slots => Object.values(slots).map(ref => ref.id)))].sort(compare);
36
+ const same = (a, b) => Object.keys(a).every(id => {
37
+ const left = a[id], right = b[id];
38
+ return Object.keys(left).length === Object.keys(right).length && Object.entries(left).every(([name, ref]) => Object.hasOwn(right, name) && ref.id === right[name].id && ref.width === right[name].width && ref.height === right[name].height && ref.format === right[name].format);
39
+ });
40
+ /** CPU slot owner. Snapshots and pins contain logical IDs, never native textures. */
41
+ export function createRenderTextureOwner(materials = [], provider) {
42
+ const definitions = new Map();
43
+ const initial = [];
44
+ if (!dense(materials))
45
+ fail("TC_RENDER_TEXTURE_DEFINITION", "*:*", "Expected a dense materials/passes array.");
46
+ for (const material of materials) {
47
+ if (!plain(material) || typeof material.id !== "string" || !/^[a-zA-Z][a-zA-Z0-9_.-]*$/.test(material.id) || definitions.has(material.id))
48
+ fail("TC_RENDER_TEXTURE_DEFINITION", "*:*", "Expected a unique valid material/pass ID.");
49
+ const id = material.id, slots = [], names = new Set();
50
+ const source = material.textures === undefined ? [] : material.textures;
51
+ if (!dense(source))
52
+ fail("TC_RENDER_TEXTURE_DEFINITION", `${id}:*`, "Expected a dense texture slots array.");
53
+ for (const entry of source) {
54
+ const at = `${id}:*`;
55
+ if (!plain(entry) || Object.keys(entry).some(key => !["name", "filter", "wrapU", "wrapV", "texture", "width", "height"].includes(key)) ||
56
+ !nameOK(entry.name) || names.has(entry.name) || (entry.texture !== undefined && !nameOK(entry.texture)) ||
57
+ (entry.filter !== undefined && entry.filter !== "nearest" && entry.filter !== "linear") ||
58
+ (entry.wrapU !== undefined && entry.wrapU !== "clamp" && entry.wrapU !== "repeat" && entry.wrapU !== "mirror") ||
59
+ (entry.wrapV !== undefined && entry.wrapV !== "clamp" && entry.wrapV !== "repeat" && entry.wrapV !== "mirror") ||
60
+ (entry.width !== undefined && !dimensionOK(entry.width)) || (entry.height !== undefined && !dimensionOK(entry.height)) ||
61
+ (entry.width !== undefined && entry.height !== undefined && entry.width * entry.height > 2 ** 24))
62
+ fail("TC_RENDER_TEXTURE_DEFINITION", at, "Expected unique texture slots, valid samplers, optional logical IDs and positive integer dimensions with at most 2^24 texels.");
63
+ names.add(entry.name);
64
+ slots.push({ name: entry.name, filter: entry.filter ?? "linear", wrapU: entry.wrapU ?? "clamp", wrapV: entry.wrapV ?? "clamp",
65
+ ...(entry.width === undefined ? {} : { width: entry.width }), ...(entry.height === undefined ? {} : { height: entry.height }) });
66
+ if (entry.texture !== undefined)
67
+ initial.push({ material: id, name: entry.name, id: entry.texture });
68
+ }
69
+ definitions.set(id, slots.sort((a, b) => compare(a.name, b.name)));
70
+ }
71
+ const canonical = [...definitions].filter(([, slots]) => slots.length).sort(([a], [b]) => compare(a, b))
72
+ .map(([id, textures]) => ({ id, textures }));
73
+ let bindings = Object.fromEntries(canonical.map(d => [d.id, {}]));
74
+ let disposed = false, revision = 0;
75
+ const active = () => {
76
+ if (disposed)
77
+ fail("TC_RENDER_TEXTURE_DISPOSED", "*:*", "Render texture owner is disposed.");
78
+ };
79
+ const material = (id) => {
80
+ active();
81
+ return definitions.get(id) ?? fail("TC_RENDER_TEXTURE_MATERIAL", `${id}:*`, "Unknown material/pass.");
82
+ };
83
+ const slot = (id, name) => material(id).find(s => s.name === name) ??
84
+ fail("TC_RENDER_TEXTURE_SLOT", `${id}:${name}`, "Unknown texture slot.");
85
+ const withProvider = (at, action) => {
86
+ if (!provider)
87
+ return fail("TC_RENDER_TEXTURE_PROVIDER", at, "A compute texture provider is required for binding resources.");
88
+ try {
89
+ return action(provider);
90
+ }
91
+ catch (error) {
92
+ return fail("TC_RENDER_TEXTURE_PROVIDER", at, error instanceof Error ? error.message : "Compute provider rejected the operation.");
93
+ }
94
+ };
95
+ const reference = (value, expected, at, code) => {
96
+ const keys = ["id", "width", "height", "format"];
97
+ // describeTexture may additionally report its revision; it is never part of a logical reference.
98
+ if (!exact(value, code === undefined && plain(value) && Object.hasOwn(value, "revision") ? [...keys, "revision"] : keys) ||
99
+ !nameOK(value.id) || value.format !== "rgba32float" || !sizeOK({ width: value.width, height: value.height }) ||
100
+ (Object.hasOwn(value, "revision") && (!Number.isSafeInteger(value.revision) || value.revision < 0)))
101
+ return fail(code ?? "TC_RENDER_TEXTURE_REFERENCE", at, "Expected a logical rgba32float texture ID and valid width/height.");
102
+ if ((expected.width !== undefined && value.width !== expected.width) || (expected.height !== undefined && value.height !== expected.height))
103
+ fail(code ?? "TC_RENDER_TEXTURE_SIZE", at, `Texture ${value.id} does not match the slot's required width/height.`);
104
+ return { id: value.id, width: value.width, height: value.height, format: "rgba32float" };
105
+ };
106
+ const resource = (id, expected, at) => {
107
+ const result = withProvider(at, p => p.resource(id));
108
+ if (!exact(result, ["key", "type", "revision", "values", "texture"]) || !nameOK(result.key) || result.type !== "vec4" ||
109
+ !Number.isSafeInteger(result.revision) || result.revision < 0 || !sizeOK(result.texture) ||
110
+ !texelsOK(result.values, result.texture.width * result.texture.height))
111
+ return fail("TC_RENDER_TEXTURE_RESOURCE", at, `Expected a managed vec4 texture descriptor with finite f32 texels matching its dimensions for ${id}.`);
112
+ reference({ id, ...result.texture, format: "rgba32float" }, expected, at);
113
+ return result;
114
+ };
115
+ const publishPins = (next) => {
116
+ const previous = pins(bindings);
117
+ if (provider)
118
+ withProvider("pins", p => {
119
+ try {
120
+ p.setPins(next);
121
+ }
122
+ catch (error) {
123
+ // Restore providers that changed their pin set before rejecting publication.
124
+ try {
125
+ p.setPins(previous);
126
+ }
127
+ catch { /* Preserve the original provider failure. */ }
128
+ throw error;
129
+ }
130
+ });
131
+ else if (next.length)
132
+ fail("TC_RENDER_TEXTURE_PROVIDER", "pins", "A compute texture provider is required for bound slots.");
133
+ };
134
+ const publish = (next) => {
135
+ if (same(bindings, next))
136
+ return;
137
+ publishPins(pins(next));
138
+ bindings = next;
139
+ revision++;
140
+ };
141
+ // Resolve every initial descriptor and its data before publishing even the first pin.
142
+ const initialBindings = Object.fromEntries(canonical.map(d => [d.id, {}]));
143
+ for (const entry of initial) {
144
+ const expected = slot(entry.material, entry.name), at = `${entry.material}:${entry.name}`;
145
+ const found = resource(entry.id, expected, at);
146
+ Object.defineProperty(initialBindings[entry.material], entry.name, {
147
+ value: { id: entry.id, ...found.texture, format: "rgba32float" }, enumerable: true, writable: true, configurable: true,
148
+ });
149
+ }
150
+ publish(initialBindings);
151
+ revision = 0;
152
+ const validate = (value) => {
153
+ active();
154
+ const code = "TC_RENDER_TEXTURE_SNAPSHOT";
155
+ const bad = (at = "*:*", detail = "Incompatible render texture snapshot.") => fail(code, at, detail);
156
+ if (!canonical.length && value === undefined)
157
+ return {};
158
+ if (!canonical.length || !exact(value, ["version", "definitions", "bindings"]) || value.version !== 1 || !dense(value.definitions) ||
159
+ value.definitions.length !== canonical.length || !exact(value.bindings, canonical.map(d => d.id)))
160
+ return bad();
161
+ const staged = {};
162
+ for (let i = 0; i < canonical.length; i++) {
163
+ const expected = canonical[i], actual = value.definitions[i], at = `${expected.id}:*`;
164
+ if (!exact(actual, ["id", "textures"]) || actual.id !== expected.id || !dense(actual.textures) || actual.textures.length !== expected.textures.length)
165
+ return bad(at);
166
+ const entries = value.bindings[expected.id];
167
+ if (!plain(entries) || Object.keys(entries).some(name => !expected.textures.some(s => s.name === name)))
168
+ return bad(at);
169
+ const checked = [];
170
+ for (let j = 0; j < expected.textures.length; j++) {
171
+ const s = expected.textures[j], declaration = actual.textures[j], location = `${expected.id}:${s.name}`;
172
+ const keys = Object.keys(s);
173
+ if (!exact(declaration, keys) || keys.some(key => declaration[key] !== s[key]))
174
+ return bad(location);
175
+ if (Object.hasOwn(entries, s.name))
176
+ checked.push([s.name, reference(entries[s.name], s, location, code)]);
177
+ }
178
+ Object.defineProperty(staged, expected.id, { value: Object.fromEntries(checked), enumerable: true, writable: true, configurable: true });
179
+ }
180
+ return staged;
181
+ };
182
+ const service = {
183
+ bind(id, name, handle) {
184
+ const expected = slot(id, name), at = `${id}:${name}`;
185
+ if (!exact(handle, ["owner", "index", "generation", "kind"]) || handle.kind !== "texture" ||
186
+ !Number.isSafeInteger(handle.owner) || handle.owner <= 0 || !Number.isSafeInteger(handle.index) || handle.index < 0 ||
187
+ !Number.isSafeInteger(handle.generation) || handle.generation <= 0)
188
+ fail("TC_RENDER_TEXTURE_HANDLE", at, "Expected a managed texture handle; ordinary buffers cannot bind to texture slots.");
189
+ const ref = reference(withProvider(at, p => p.describe(handle)), expected, at);
190
+ publish({ ...bindings, [id]: { ...bindings[id], [name]: ref } });
191
+ },
192
+ unbind(id, name) {
193
+ slot(id, name);
194
+ const next = copyReferences(bindings[id]);
195
+ delete next[name];
196
+ publish({ ...bindings, [id]: next });
197
+ },
198
+ state(id) {
199
+ material(id);
200
+ return Object.freeze(Object.fromEntries(Object.entries(bindings[id] ?? {}).map(([name, ref]) => [name, Object.freeze({ ...ref })])));
201
+ },
202
+ };
203
+ return {
204
+ service,
205
+ resources(id) {
206
+ const slots = material(id);
207
+ // Missing slots take precedence over temporarily absent compute resources during restore.
208
+ for (const s of slots)
209
+ if (!Object.hasOwn(bindings[id], s.name))
210
+ fail("TC_RENDER_TEXTURE_UNBOUND", `${id}:${s.name}`, "Bind this declared slot before rendering the material/pass.");
211
+ return Object.freeze(Object.fromEntries(slots.map(s => {
212
+ const ref = bindings[id][s.name], at = `${id}:${s.name}`;
213
+ const current = resource(ref.id, ref, at);
214
+ return [s.name, Object.freeze({ key: current.key, type: current.type, revision: current.revision,
215
+ texture: Object.freeze({ ...current.texture }),
216
+ values: Object.freeze(current.values.map(value => Object.freeze([...value]))) })];
217
+ })));
218
+ },
219
+ snapshot() {
220
+ active();
221
+ if (!canonical.length)
222
+ return undefined;
223
+ return { version: 1, definitions: structuredClone(canonical), bindings: Object.fromEntries(canonical.map(d => [d.id, copyReferences(bindings[d.id])])) };
224
+ },
225
+ validateSnapshot(value) { validate(value); },
226
+ validatePeerSnapshot(value, peers) {
227
+ const staged = validate(value);
228
+ if (!Object.values(staged).some(entries => Object.keys(entries).length))
229
+ return;
230
+ const code = "TC_RENDER_TEXTURE_PEER_SNAPSHOT";
231
+ const bad = (at, detail) => fail(code, at, detail);
232
+ if (!plain(peers) || !Object.hasOwn(peers, "$engine.compute"))
233
+ return bad("$engine.compute", "Missing compute snapshot for bound texture slots.");
234
+ const peer = peers["$engine.compute"];
235
+ if (!plain(peer) || !plain(peer.payload) || !dense(peer.payload.buffers))
236
+ return bad("$engine.compute", "Expected compute snapshot buffers with texture metadata.");
237
+ const available = new Map(), names = new Set();
238
+ for (const entry of peer.payload.buffers) {
239
+ const isTexture = plain(entry) && Object.hasOwn(entry, "texture");
240
+ if (!exact(entry, isTexture ? ["id", "type", "count", "data", "texture"] : ["id", "type", "count", "data"]) ||
241
+ !nameOK(entry.id) || !isGraphType(entry.type) || !countOK(entry.count) || names.has(entry.id) || !dense(entry.data) || entry.data.length !== entry.count)
242
+ return bad("$engine.compute", "Invalid or duplicate compute resource descriptor.");
243
+ names.add(entry.id);
244
+ if (isTexture) {
245
+ if (entry.type !== "vec4" || !sizeOK(entry.texture) || entry.count !== entry.texture.width * entry.texture.height || !texelsOK(entry.data, entry.count))
246
+ return bad(entry.id, "Texture snapshots require vec4 finite f32 texels matching width, height and count.");
247
+ available.set(entry.id, { id: entry.id, ...entry.texture, format: "rgba32float" });
248
+ }
249
+ }
250
+ for (const [id, entries] of Object.entries(staged))
251
+ for (const [name, ref] of Object.entries(entries)) {
252
+ const peerRef = available.get(ref.id);
253
+ if (!peerRef || peerRef.width !== ref.width || peerRef.height !== ref.height || peerRef.format !== ref.format)
254
+ return bad(`${id}:${name}`, `Compute snapshot must contain managed rgba32float texture ${ref.id} with dimensions ${ref.width}x${ref.height}.`);
255
+ }
256
+ },
257
+ restore(value) { publish(validate(value)); },
258
+ dispose() {
259
+ if (disposed)
260
+ return;
261
+ publishPins([]);
262
+ bindings = {};
263
+ disposed = true;
264
+ },
265
+ inspect() {
266
+ active();
267
+ return { materials: definitions.size, slots: canonical.reduce((n, d) => n + d.textures.length, 0),
268
+ bound: Object.values(bindings).reduce((n, entries) => n + Object.keys(entries).length, 0), pins: pins(bindings).length, revision };
269
+ },
270
+ };
271
+ }