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,2015 @@
1
+ import * as THREE from "three/webgpu";
2
+ import { prepareGameplay } from "./gameplay.js";
3
+ import { GPUTracer } from "./pathtrace.js";
4
+ import { SurfaceTracer } from "./surface-tracer.js";
5
+ import { createRenderSettingsDriver, resolveRenderSettings, } from "../runtime/render-settings.js";
6
+ import { buildBVH, extractTriangles } from "./geometry.js";
7
+ import { evaluateGraph, runComputeGraph } from "./graph.js";
8
+ import { uniform } from "three/tsl";
9
+ import { typedGraphToTSL, materialGraphNormalToTSL } from "./graph-tsl.js";
10
+ import { startupPolicy, boundedOperation, runtimeStatus, inspectTraceCapabilities } from "./startup.js";
11
+ import { renderCapabilities, requireCompute, validateRenderCapabilities } from "../runtime/render-capabilities.js";
12
+ import { createComputeDriver } from "../runtime/compute-service.js";
13
+ import { createComputeExecutor } from "./compute-executor.js";
14
+ import { createPostProcessComposer } from "./postprocess.js";
15
+ import { vertexGraphsToTSL } from "./vertex-graphs.js";
16
+ import { createVertexBoundsOwner } from "./vertex-bounds.js";
17
+ import { createRenderBufferBridge } from "./render-buffers.js";
18
+ import { createRenderTextureBridge } from "./render-textures.js";
19
+ import { createComputeTextureAssets } from "./compute-texture-assets.js";
20
+ import { SDK_VERSION } from "../sdk/index.js";
21
+ import { RuntimeHost } from "../runtime/host.js";
22
+ import { resolveEntityDefinition } from "../runtime/prefabs.js";
23
+ import { ResourceManager, } from "../runtime/resources.js";
24
+ import { parseCommand, result as protocolResult, } from "../protocol/index.js";
25
+ import { validateBrowserCheckpoint, prepareCheckpointProbes, selectInspectRecords, } from "./checkpoint.js";
26
+ import { createSimulation } from "../experiments/simulation/index.js";
27
+ import { createWorldProbe } from "../experiments/world/index.js";
28
+ const width = Number(new URLSearchParams(location.search).get("width") ?? 960);
29
+ const height = Number(new URLSearchParams(location.search).get("height") ?? 540);
30
+ const status = document.querySelector("#status");
31
+ const errors = [];
32
+ const startup = startupPolicy(import.meta.env.DEV, location.search);
33
+ let adapterInfo;
34
+ let mode = "raster";
35
+ let tick = 0, playing = false, cameraPhase = 0, bvhMs = 0, uploadMs = 0, submitMs = 0;
36
+ let frameAccumulator = 0;
37
+ let lastStepCpuMs = 0;
38
+ let lastFrameTime;
39
+ let triangleCount = 0, nodeCount = 0, generation = 0;
40
+ let lastGeometryHash = 0;
41
+ const inputLog = [];
42
+ const topologyObjects = [];
43
+ const metrics = [];
44
+ const scene = new THREE.Scene();
45
+ const probeEnabled = new URLSearchParams(location.search).get("probe") === "m0";
46
+ const probeRoot = new THREE.Group();
47
+ probeRoot.visible = probeEnabled;
48
+ scene.add(probeRoot);
49
+ scene.background = new THREE.Color("#111d2a");
50
+ const camera = new THREE.PerspectiveCamera(48, width / height, 0.05, 100);
51
+ const rasterCanvas = document.createElement("canvas"), traceCanvas = document.createElement("canvas");
52
+ for (const canvas of [rasterCanvas, traceCanvas]) {
53
+ canvas.style.position = "absolute";
54
+ canvas.style.left = "0";
55
+ canvas.style.top = "0";
56
+ canvas.style.width = `${width}px`;
57
+ canvas.style.height = `${height}px`;
58
+ document.body.appendChild(canvas);
59
+ }
60
+ traceCanvas.style.pointerEvents = "none";
61
+ traceCanvas.style.display = "none";
62
+ const standard = (color, roughness = 0.75) => new THREE.MeshStandardMaterial({ color, roughness });
63
+ const ground = new THREE.Mesh(new THREE.BoxGeometry(14, 0.2, 14), standard(0x7c8797));
64
+ ground.position.y = -0.2;
65
+ ground.receiveShadow = true;
66
+ probeRoot.add(ground);
67
+ const back = new THREE.Mesh(new THREE.BoxGeometry(14, 5, 0.15), standard(0x243b50));
68
+ back.position.set(0, 2.2, -4);
69
+ probeRoot.add(back);
70
+ const box = new THREE.Mesh(new THREE.BoxGeometry(1, 1, 1), standard(0xe5923f, 0.25));
71
+ box.position.set(-1.6, 0.5, 0);
72
+ box.castShadow = true;
73
+ probeRoot.add(box);
74
+ const morphGeo = new THREE.SphereGeometry(0.7, 12, 8);
75
+ const original = morphGeo.getAttribute("position");
76
+ const morph = original.clone();
77
+ for (let i = 0; i < morph.count; i++)
78
+ morph.setXYZ(i, original.getX(i) * (1 + 0.4 * Math.sin(original.getY(i) * 5)), original.getY(i) * 1.5, original.getZ(i));
79
+ morphGeo.morphAttributes.position = [morph];
80
+ const morphMesh = new THREE.Mesh(morphGeo, standard(0x36a8a1, 0.4));
81
+ morphMesh.position.set(1.5, 0.8, -0.2);
82
+ morphMesh.castShadow = true;
83
+ probeRoot.add(morphMesh);
84
+ const skinGeo = new THREE.CylinderGeometry(0.24, 0.35, 1.8, 8, 6);
85
+ const positions = skinGeo.getAttribute("position");
86
+ const indices = [], weights = [];
87
+ for (let i = 0; i < positions.count; i++) {
88
+ const y = positions.getY(i);
89
+ const factor = Math.max(0, Math.min(1, (y + 0.3) / 1.2));
90
+ indices.push(0, 1, 0, 0);
91
+ weights.push(1 - factor, factor, 0, 0);
92
+ }
93
+ skinGeo.setAttribute("skinIndex", new THREE.Uint16BufferAttribute(indices, 4));
94
+ skinGeo.setAttribute("skinWeight", new THREE.Float32BufferAttribute(weights, 4));
95
+ const skinned = new THREE.SkinnedMesh(skinGeo, standard(0xc24f72));
96
+ const root = new THREE.Bone(), tip = new THREE.Bone();
97
+ root.position.y = -0.9;
98
+ tip.position.y = 0.9;
99
+ root.add(tip);
100
+ skinned.add(root);
101
+ skinned.bind(new THREE.Skeleton([root, tip]));
102
+ skinned.position.set(0, 1, -1.4);
103
+ probeRoot.add(skinned);
104
+ const lamp = new THREE.PointLight(0xffdfb0, 100, 30, 2);
105
+ lamp.position.set(-3, 6, 3);
106
+ lamp.castShadow = true;
107
+ lamp.shadow.mapSize.set(1024, 1024);
108
+ probeRoot.add(lamp, new THREE.HemisphereLight(0xa4c8f0, 0x26321d, 1));
109
+ let renderer, tracer, device;
110
+ let backendKind = "uninitialized";
111
+ let contextWasPlaying = false;
112
+ let waitingForWebglRestore = false;
113
+ let pendingSurface;
114
+ const lostDevices = new WeakSet();
115
+ const lostRenderers = new WeakSet();
116
+ let surfaceTracer;
117
+ let renderControl;
118
+ let computeControl;
119
+ let computeExecutor;
120
+ let renderBufferBridge;
121
+ let renderTextureBridge;
122
+ let postComposer;
123
+ let vertexBounds;
124
+ let legacyTracerDisposed = false;
125
+ let recoveringGpu = false;
126
+ let recoveryTask;
127
+ let recoveryCount = 0;
128
+ let drawTail = Promise.resolve();
129
+ let tickTail = Promise.resolve();
130
+ let nativeInputTail = Promise.resolve();
131
+ let inputDrawActive = false, inputDrawRequested = false;
132
+ let frameSerial = 0;
133
+ const recoveryEvents = [];
134
+ const gameObjects = new Map();
135
+ const gameRoot = new THREE.Group();
136
+ scene.add(gameRoot);
137
+ let authoredCamera = { position: [0, 3.4, 7], target: [0, 0.9, 0] };
138
+ let authoredLights = [];
139
+ let compiledIR = null;
140
+ let runtime;
141
+ const resourceManager = new ResourceManager();
142
+ resourceManager.registerLoader("json", {
143
+ async load(descriptor, signal) {
144
+ const response = await fetch(descriptor.url, { signal });
145
+ if (!response.ok)
146
+ throw new Error(`TC_RESOURCE_HTTP: ${response.status}`);
147
+ return response.json();
148
+ },
149
+ dispose() { },
150
+ });
151
+ resourceManager.registerLoader("module", {
152
+ async load(descriptor, signal) {
153
+ const module = await import(/* @vite-ignore */ descriptor.url);
154
+ if (signal.aborted)
155
+ throw new Error("TC_RESOURCE_CANCELLED");
156
+ if (typeof module.createGame !== "function")
157
+ throw new Error("TC_GAME_EXPORT");
158
+ return module;
159
+ },
160
+ dispose() { },
161
+ });
162
+ let gameplay;
163
+ let contentScope;
164
+ let contentGeneration = 0;
165
+ let loadingContent = false;
166
+ let disposed = false;
167
+ let terminalFault = false;
168
+ let restoring = false;
169
+ let frameRequest;
170
+ let disposal;
171
+ let gameMaterials = new Map();
172
+ const componentState = new Map();
173
+ const transforms = new Map();
174
+ const materialGraphs = [];
175
+ const graphTime = uniform(0);
176
+ function slotTypes(graphs) { return Object.assign({}, ...graphs.map(graph => graph.bufferTypes ?? {})); }
177
+ function materialSlotTypes(binding) { return slotTypes([...Object.values(binding.graphs), ...Object.values(binding.vertex ?? {})]); }
178
+ function textureSlotTypes(graphs) { return Object.assign({}, ...graphs.map(graph => graph.textureSlots ?? {})); }
179
+ function materialTextureTypes(binding) { return textureSlotTypes([...Object.values(binding.graphs), ...Object.values(binding.vertex ?? {})]); }
180
+ function usesRenderTextures(ir) { return ir.materials.some(d => Object.keys(textureSlotTypes([...(d.graph ? [d.graph] : []), ...Object.values(d.graphs ?? {}), ...Object.values(d.vertex ?? {})])).length) || ir.postProcesses?.some(d => Object.keys(d.graph.textureSlots ?? {}).length); }
181
+ function usesRenderSlots(ir) { return ir.materials.some(d => Object.keys(slotTypes([...(d.graph ? [d.graph] : []), ...Object.values(d.graphs ?? {}), ...Object.values(d.vertex ?? {})])).length) || ir.postProcesses?.some(d => Object.keys(d.graph.bufferTypes ?? {}).length); }
182
+ function slotResources(owner, id) { return id.startsWith("$post.") ? owner.effectBuffers.resources(id.slice(6)) : owner.buffers.resources(id); }
183
+ function textureResources(owner, id) { return id.startsWith("$post.") ? owner.effectTextures.resources(id.slice(6)) : owner.textures.resources(id); }
184
+ function applyMaterialGraphs(material, binding, uniforms, bridge, textureBridge) {
185
+ const slots = materialSlotTypes(binding);
186
+ if (Object.keys(slots).length) {
187
+ if (!bridge)
188
+ throw new Error("TC_RENDER_BUFFER_CAPABILITY: Material storage slots require a managed WebGPU bridge.");
189
+ bridge.prepare(material, slots);
190
+ }
191
+ const textureSlots = materialTextureTypes(binding);
192
+ if (Object.keys(textureSlots).length) {
193
+ if (!textureBridge)
194
+ throw new Error("TC_RENDER_TEXTURE_CAPABILITY: Material texture slots require a managed WebGPU bridge.");
195
+ textureBridge.prepare(material, textureSlots);
196
+ }
197
+ const sampleTextureSlot = (name, uv) => textureBridge.node(material, name, uv, textureSlots[name]);
198
+ for (const [channel, graph] of Object.entries(binding.graphs)) {
199
+ let node = typedGraphToTSL(graph, graphTime, uniforms, binding.textures, {
200
+ readBuffer: (name, index, type) => bridge.node(material, name, index, type, true),
201
+ sampleTextureSlot,
202
+ });
203
+ if (channel === "normalWorld")
204
+ node = materialGraphNormalToTSL(node);
205
+ else if (["roughness", "metalness", "opacity"].includes(channel))
206
+ node = node.clamp(0, 1);
207
+ const key = { baseColor: "colorNode", emissive: "emissiveNode", roughness: "roughnessNode", metalness: "metalnessNode", opacity: "opacityNode", normalWorld: "normalNode" }[channel];
208
+ material[key] = node;
209
+ }
210
+ if (binding.graphs.opacity) {
211
+ material.transparent = true;
212
+ material.alphaMap = null;
213
+ }
214
+ if (binding.vertex) {
215
+ material.positionNode = vertexGraphsToTSL(binding, graphTime, uniforms, binding.textures, (name, index, type) => bridge.node(material, name, index, type), sampleTextureSlot);
216
+ material.userData.pelicanVertexNormal = Boolean(binding.vertex.normal);
217
+ }
218
+ material.needsUpdate = true;
219
+ }
220
+ const componentsId = (component) => typeof component === "string" ? component : component.id;
221
+ const facade = {
222
+ getTransform(id) {
223
+ if (!runtime)
224
+ throw new Error("TC_RUNTIME_NOT_ATTACHED");
225
+ return runtime.facade.getTransform(id);
226
+ },
227
+ setTransform(id, patch) {
228
+ if (!runtime)
229
+ throw new Error("TC_RUNTIME_NOT_ATTACHED");
230
+ runtime.facade.setTransform(id, patch);
231
+ syncRuntime();
232
+ },
233
+ getComponent(id, component) {
234
+ if (!runtime)
235
+ throw new Error("TC_RUNTIME_NOT_ATTACHED");
236
+ return runtime.facade.getComponent(id, componentsId(component));
237
+ },
238
+ setComponent(id, component, value) {
239
+ if (!runtime)
240
+ throw new Error("TC_RUNTIME_NOT_ATTACHED");
241
+ runtime.facade.setComponent(id, componentsId(component), value);
242
+ syncRuntime();
243
+ },
244
+ query(component) {
245
+ return runtime?.world.query(componentsId(component)) ?? [];
246
+ },
247
+ };
248
+ function reportFailure(error) {
249
+ if (disposed)
250
+ return;
251
+ playing = false;
252
+ terminalFault = true;
253
+ lastFrameTime = undefined;
254
+ const message = String(error);
255
+ errors.push(message);
256
+ if (document.documentElement) {
257
+ document.documentElement.dataset.pelicanReady = "false";
258
+ document.documentElement.dataset.pelicanFailure = message;
259
+ }
260
+ status.hidden = false;
261
+ status.textContent = message;
262
+ console.error(error);
263
+ }
264
+ function requireRuntime() {
265
+ if (disposed)
266
+ throw new Error("TC_BROWSER_DISPOSED: Reload the page before issuing commands.");
267
+ if (loadingContent || restoring || recoveringGpu)
268
+ throw new Error("TC_CONTENT_BUSY: Wait for the current content or GPU recovery transaction.");
269
+ if (!runtime)
270
+ throw new Error("TC_RUNTIME_NOT_ATTACHED");
271
+ return runtime;
272
+ }
273
+ function releaseObject(root, releaseMaterials = false, ownedMaterials = []) {
274
+ const geometries = new Set();
275
+ const materials = new Set(ownedMaterials);
276
+ root.traverse((object) => {
277
+ if (object instanceof THREE.Mesh) {
278
+ geometries.add(object.geometry);
279
+ if (releaseMaterials)
280
+ for (const material of Array.isArray(object.material)
281
+ ? object.material
282
+ : [object.material])
283
+ materials.add(material);
284
+ }
285
+ if (object instanceof THREE.SkinnedMesh)
286
+ object.skeleton.dispose();
287
+ if (object instanceof THREE.Light && "shadow" in object)
288
+ object.shadow?.dispose();
289
+ });
290
+ for (const geometry of geometries)
291
+ geometry.dispose();
292
+ for (const material of materials)
293
+ material.dispose();
294
+ root.userData.lightTarget?.removeFromParent();
295
+ root.removeFromParent();
296
+ root.clear();
297
+ }
298
+ function createEntityView(entity, materials) {
299
+ const node = new THREE.Group();
300
+ node.name = entity.id;
301
+ if (entity.light) {
302
+ const light = entity.light.kind === "point"
303
+ ? new THREE.PointLight(entity.light.color ?? "#ffffff", entity.light.intensity ?? 1)
304
+ : new THREE.DirectionalLight(entity.light.color ?? "#ffffff", entity.light.intensity ?? 1);
305
+ light.castShadow = entity.light.castShadow ?? false;
306
+ if (light instanceof THREE.DirectionalLight) {
307
+ light.target.position.fromArray(entity.light.target ?? [0, 0, 0]);
308
+ // Targets use world coordinates and must remain outside the transformed light group.
309
+ node.userData.lightTarget = light.target;
310
+ }
311
+ if ("shadow" in light) {
312
+ const shadow = entity.light.shadow;
313
+ light.shadow.mapSize.set(shadow?.mapSize ?? 2048, shadow?.mapSize ?? 2048);
314
+ light.shadow.bias = shadow?.bias ?? -0.0001;
315
+ light.shadow.normalBias = shadow?.normalBias ?? 0.04;
316
+ light.shadow.camera.near = shadow?.near ?? 0.1;
317
+ light.shadow.camera.far = shadow?.far ?? 100;
318
+ if (light instanceof THREE.DirectionalLight) {
319
+ const extent = shadow?.extent ?? 20;
320
+ Object.assign(light.shadow.camera, {
321
+ left: -extent,
322
+ right: extent,
323
+ top: extent,
324
+ bottom: -extent,
325
+ });
326
+ }
327
+ light.shadow.camera.updateProjectionMatrix();
328
+ }
329
+ node.add(light);
330
+ }
331
+ if (entity.mesh) {
332
+ const material = materials.get(entity.mesh.material);
333
+ if (!material)
334
+ throw new Error(`TC_MATERIAL_MISSING: ${entity.mesh.material}`);
335
+ const g = entity.mesh.geometry;
336
+ const geometry = g.kind === "box"
337
+ ? new THREE.BoxGeometry(...(g.size ?? [1, 1, 1]))
338
+ : g.kind === "sphere"
339
+ ? new THREE.SphereGeometry(g.radius ?? 0.5, g.segments ?? 12, 8)
340
+ : new THREE.PlaneGeometry(...(g.size ?? [1, 1]));
341
+ for (const input of material.userData.pelicanGraphInputs ?? []) {
342
+ if ((input === "uv" || input === "uv1") && !geometry.hasAttribute(input)) {
343
+ geometry.dispose();
344
+ throw new Error(`TC_GRAPH_UV: Material ${entity.mesh.material} requires ${input} on entity ${entity.id}. Use geometry with that UV channel or change the graph.`);
345
+ }
346
+ }
347
+ const mesh = new THREE.Mesh(geometry, material);
348
+ mesh.castShadow = entity.mesh.castShadow;
349
+ mesh.receiveShadow = entity.mesh.receiveShadow;
350
+ node.add(mesh);
351
+ }
352
+ return node;
353
+ }
354
+ function syncRuntime() {
355
+ if (!runtime)
356
+ return;
357
+ transforms.clear();
358
+ componentState.clear();
359
+ const records = runtime.world.records();
360
+ const live = new Set(records.map((record) => record.id));
361
+ const modelRecords = compiledIR
362
+ ? records.filter((record) => resolveEntityDefinition(compiledIR, record)?.model)
363
+ : [];
364
+ gameplay?.models.reconcileBindings(modelRecords.map((record) => ({
365
+ entity: record.id,
366
+ definition: resolveEntityDefinition(compiledIR, record).model,
367
+ })));
368
+ // Each entity view owns its own visual subtree; detach tracked descendants before disposal.
369
+ const prefabById = new Map(records.map((record) => [record.id, record.prefab]));
370
+ const removed = new Set([...gameObjects]
371
+ .filter(([id, object]) => !live.has(id) || object.userData.pelicanPrefab !== prefabById.get(id))
372
+ .map(([, object]) => object));
373
+ for (const object of gameObjects.values())
374
+ for (let parent = object.parent; parent; parent = parent.parent)
375
+ if (removed.has(parent)) {
376
+ gameRoot.add(object);
377
+ break;
378
+ }
379
+ for (const [id, object] of gameObjects)
380
+ if (!live.has(id)) {
381
+ gameplay?.models.remove(id);
382
+ releaseObject(object);
383
+ gameObjects.delete(id);
384
+ }
385
+ for (const record of records) {
386
+ transforms.set(record.id, structuredClone(record.transform));
387
+ componentState.set(record.id, structuredClone(record.components));
388
+ let object = gameObjects.get(record.id);
389
+ if (object && object.userData.pelicanPrefab !== record.prefab) {
390
+ gameplay?.models.get(record.id)?.node.removeFromParent();
391
+ releaseObject(object);
392
+ gameObjects.delete(record.id);
393
+ object = undefined;
394
+ }
395
+ if (!object) {
396
+ const authored = compiledIR
397
+ ? resolveEntityDefinition(compiledIR, record)
398
+ : undefined;
399
+ object = authored
400
+ ? createEntityView(authored, gameMaterials)
401
+ : new THREE.Group();
402
+ object.name = record.id;
403
+ object.userData.pelicanPrefab = record.prefab;
404
+ gameRoot.add(object);
405
+ if (object.userData.lightTarget)
406
+ gameRoot.add(object.userData.lightTarget);
407
+ gameObjects.set(record.id, object);
408
+ }
409
+ if (modelRecords.some((model) => model.id === record.id))
410
+ gameplay?.models.attach(record.id, object);
411
+ object.position.fromArray(record.transform.position);
412
+ object.rotation.set(...record.transform.rotation);
413
+ object.scale.fromArray(record.transform.scale);
414
+ }
415
+ for (const record of records) {
416
+ const object = gameObjects.get(record.id);
417
+ const parent = record.parent ? gameObjects.get(record.parent) : gameRoot;
418
+ if (parent && object.parent !== parent)
419
+ parent.add(object);
420
+ }
421
+ gameRoot.updateMatrixWorld(true);
422
+ authoredLights = [];
423
+ for (const entity of compiledIR?.scene.entities ?? [])
424
+ if (entity.light) {
425
+ const light = gameObjects
426
+ .get(entity.id)
427
+ ?.children.find((child) => child instanceof THREE.Light);
428
+ if (light)
429
+ authoredLights.push({ definition: entity.light, object: light });
430
+ }
431
+ }
432
+ let simulation;
433
+ const simulationGroup = new THREE.Group();
434
+ scene.add(simulationGroup);
435
+ let world;
436
+ const worldGroup = new THREE.Group();
437
+ scene.add(worldGroup);
438
+ function syncWorld() {
439
+ for (const child of [...worldGroup.children]) {
440
+ worldGroup.remove(child);
441
+ if (child instanceof THREE.Mesh) {
442
+ child.geometry.dispose();
443
+ child.material.dispose();
444
+ }
445
+ }
446
+ if (!world)
447
+ return;
448
+ for (const chunk of world.getRenderState().chunks) {
449
+ const geometry = new THREE.BufferGeometry();
450
+ geometry.setAttribute("position", new THREE.Float32BufferAttribute(chunk.positions, 3));
451
+ geometry.setIndex(chunk.indices);
452
+ geometry.computeVertexNormals();
453
+ const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({
454
+ color: 0x61935c,
455
+ roughness: 0.9,
456
+ side: THREE.DoubleSide,
457
+ }));
458
+ mesh.position.set(0, 0.15, -1);
459
+ worldGroup.add(mesh);
460
+ }
461
+ }
462
+ function syncSimulation() {
463
+ // Rebuild the deliberately tiny probe representation; production pooling is a later milestone.
464
+ for (const child of [...simulationGroup.children]) {
465
+ simulationGroup.remove(child);
466
+ if (child instanceof THREE.Mesh) {
467
+ child.geometry.dispose();
468
+ child.material.dispose();
469
+ }
470
+ }
471
+ if (!simulation)
472
+ return;
473
+ const state = simulation.getRenderState();
474
+ const offset = new THREE.Vector3(0, 0.3, 1.4);
475
+ for (const [list, color] of [
476
+ [state.bodies, 0xffb54a],
477
+ [state.particles, 0x4ea9d9],
478
+ ])
479
+ for (const particle of list) {
480
+ const mesh = new THREE.Mesh(new THREE.IcosahedronGeometry(particle.radius, 0), standard(color, 0.5));
481
+ mesh.position.fromArray(particle.position).add(offset);
482
+ simulationGroup.add(mesh);
483
+ }
484
+ if (state.triangles.length) {
485
+ const vertices = [];
486
+ for (const triangle of state.triangles)
487
+ for (const index of triangle)
488
+ vertices.push(...new THREE.Vector3()
489
+ .fromArray(state.particles[index].position)
490
+ .add(offset)
491
+ .toArray());
492
+ const geometry = new THREE.BufferGeometry();
493
+ geometry.setAttribute("position", new THREE.Float32BufferAttribute(vertices, 3));
494
+ geometry.computeVertexNormals();
495
+ const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({
496
+ color: 0x7bd1cd,
497
+ side: THREE.DoubleSide,
498
+ roughness: 0.7,
499
+ }));
500
+ simulationGroup.add(mesh);
501
+ }
502
+ }
503
+ async function loadGame(sceneId, saved, loadSlot) {
504
+ if (disposed)
505
+ throw new Error("TC_BROWSER_DISPOSED");
506
+ if (loadingContent || restoring)
507
+ throw new Error("TC_CONTENT_BUSY: Wait for the current content transaction.");
508
+ loadingContent = true;
509
+ let nextScope;
510
+ let nextGameplay;
511
+ let nextRender;
512
+ let nextCompute;
513
+ let nextPostComposer;
514
+ let nextVertexBounds;
515
+ let nextComputeExecutor;
516
+ let nextRenderBufferBridge;
517
+ let nextRenderTextureBridge;
518
+ let candidate;
519
+ let candidateGame;
520
+ let attached = false;
521
+ let committed = false;
522
+ const nextRoot = new THREE.Group();
523
+ const nextMaterials = new Map();
524
+ const nextObjects = new Map();
525
+ const nextGraphs = [];
526
+ try {
527
+ nextScope = resourceManager.createScope();
528
+ const generation = ++contentGeneration;
529
+ const sceneURL = new URL(`${import.meta.env.BASE_URL}generated/scene.ir.json`, document.baseURI);
530
+ sceneURL.searchParams.set("v", String(generation));
531
+ const sceneLease = await resourceManager.acquire("json", `compiled-scene-${generation}`, { url: sceneURL.href }, nextScope);
532
+ const ir = structuredClone(resourceManager.get(sceneLease.handle));
533
+ if (!ir || ir.format !== "pelican.scene" || ir.version !== 1)
534
+ throw new Error("TC_IR_VERSION");
535
+ if (sceneId && sceneId !== ir.scene.id) {
536
+ const selected = ir.scenes?.find((entry) => entry.id === sceneId);
537
+ if (!selected)
538
+ throw new Error("TC_SCENE_MISSING: " + sceneId);
539
+ ir.scenes = [
540
+ ir.scene,
541
+ ...(ir.scenes ?? []).filter((entry) => entry.id !== sceneId),
542
+ ];
543
+ ir.scene = selected;
544
+ }
545
+ if (saved &&
546
+ (saved.format !== "pelican.save" ||
547
+ saved.version !== 1 ||
548
+ saved.engineVersion !== SDK_VERSION ||
549
+ saved.gameId !== ir.gameId ||
550
+ JSON.stringify(saved.assets) !==
551
+ JSON.stringify(Object.fromEntries(ir.assets.map((a) => [a.id, a.hash])))))
552
+ throw new Error("TC_SAVE_VERSION: Save does not match this game version and asset set.");
553
+ // Normal games have SDK services even when their content is only primitives.
554
+ if (!probeEnabled ||
555
+ ir.input ||
556
+ ir.ui ||
557
+ ir.sounds ||
558
+ ir.scene.follow ||
559
+ ir.scene.environment.lighting === "studio" ||
560
+ [...ir.scene.entities, ...ir.prefabs].some((entity) => entity.model || entity.rigidBody || entity.character) ||
561
+ ir.materials.some((material) => material.texture))
562
+ nextGameplay = await prepareGameplay(ir, new URL(import.meta.env.BASE_URL + "generated/", document.baseURI).href, renderer, nextMaterials);
563
+ nextComputeExecutor = backendKind === "webgpu" && (ir.computes.length || ir.computeBuffers?.length || ir.computeTextures?.length) ? createComputeExecutor(device) : undefined;
564
+ nextCompute = createComputeDriver({
565
+ kernels: ir.computes,
566
+ buffers: ir.computeBuffers,
567
+ textures: ir.computeTextures,
568
+ textureAssets: createComputeTextureAssets(nextGameplay?.textures ?? new Map()),
569
+ capabilities: () => renderCapabilities(backendKind),
570
+ time: () => candidate?.facade.time.elapsed ?? 0,
571
+ executor: nextComputeExecutor,
572
+ });
573
+ nextRender = createRenderSettingsDriver(ir.render ?? {}, ir.materials, () => renderCapabilities(backendKind), ir.postProcesses, {
574
+ describe: handle => { const descriptor = nextCompute.service.describeBuffer(handle); return { id: descriptor.id, type: descriptor.type, count: descriptor.count }; },
575
+ resource: id => nextCompute.resource(id),
576
+ setPins: ids => nextCompute.setRenderPins(ids),
577
+ }, {
578
+ describe: handle => { const { id, width, height, format } = nextCompute.service.describeTexture(handle); return { id, width, height, format }; },
579
+ resource: id => nextCompute.textureResource(id),
580
+ setPins: ids => nextCompute.setRenderPins(ids),
581
+ });
582
+ if (usesRenderSlots(ir))
583
+ nextRenderBufferBridge = createRenderBufferBridge(renderer, () => computeExecutor, id => slotResources(nextRender, id));
584
+ if (usesRenderTextures(ir))
585
+ nextRenderTextureBridge = createRenderTextureBridge(renderer, () => computeExecutor, id => textureResources(nextRender, id));
586
+ candidate = new RuntimeHost(ir, {
587
+ plugins: [nextRender.factory, nextCompute.factory, ...(nextGameplay?.plugins ?? [])],
588
+ });
589
+ const gameURL = new URL(`${import.meta.env.BASE_URL}generated/game.js`, document.baseURI);
590
+ gameURL.searchParams.set("v", String(generation));
591
+ const moduleLease = await resourceManager.acquire("module", `compiled-game-${generation}`, { url: gameURL.href }, nextScope);
592
+ const module = resourceManager.get(moduleLease.handle);
593
+ candidateGame = module.createGame(candidate.facade);
594
+ candidate.attach(candidateGame);
595
+ attached = true;
596
+ if (ir.postProcesses?.length)
597
+ nextPostComposer = createPostProcessComposer(renderer, width, height, ir.postProcesses, nextGameplay?.textures ?? new Map(), nextRenderBufferBridge, nextRenderTextureBridge);
598
+ const nextCamera = {
599
+ position: [...ir.scene.camera.position],
600
+ target: [...ir.scene.camera.target],
601
+ };
602
+ const nextBackground = new THREE.Color(ir.scene.environment.background);
603
+ const ambient = new THREE.HemisphereLight(0xa4c8f0, 0x26321d, ir.scene.environment.ambientIntensity);
604
+ nextRoot.add(ambient);
605
+ for (const definition of ir.materials) {
606
+ const Material = (definition.transmission ?? 0) > 0 || definition.ior !== undefined || definition.specularIntensity !== undefined || definition.specularColor !== undefined
607
+ ? THREE.MeshPhysicalNodeMaterial
608
+ : THREE.MeshStandardNodeMaterial;
609
+ const material = new Material({
610
+ color: definition.color,
611
+ roughness: definition.roughness,
612
+ metalness: definition.metalness,
613
+ emissive: definition.emissive ?? "#000000",
614
+ emissiveIntensity: definition.emissiveIntensity ?? 1,
615
+ opacity: definition.opacity ?? 1,
616
+ transparent: (definition.opacity ?? 1) < 1,
617
+ alphaTest: definition.alphaTest ?? 0,
618
+ side: definition.side === "double"
619
+ ? THREE.DoubleSide
620
+ : definition.side === "back"
621
+ ? THREE.BackSide
622
+ : THREE.FrontSide,
623
+ });
624
+ if (material instanceof THREE.MeshPhysicalNodeMaterial) {
625
+ material.transmission = definition.transmission ?? 0;
626
+ material.ior = definition.ior ?? 1.5;
627
+ material.specularIntensity = definition.specularIntensity ?? 1;
628
+ material.specularColor.set(definition.specularColor ?? "#ffffff");
629
+ material.attenuationColor.set(definition.attenuationColor ?? "#ffffff");
630
+ material.attenuationDistance =
631
+ definition.attenuationDistance ?? Infinity;
632
+ }
633
+ material.name = definition.id;
634
+ if (definition.texture)
635
+ material.map = nextGameplay.textures.get(definition.texture);
636
+ nextMaterials.set(definition.id, material);
637
+ if (definition.graph || definition.graphs || definition.vertex) {
638
+ const graphs = { ...(definition.graph ? { baseColor: definition.graph } : {}), ...definition.graphs };
639
+ const uniforms = new Map();
640
+ for (const p of definition.parameters ?? []) {
641
+ const value = Array.isArray(p.default) ? p.default.length === 2 ? new THREE.Vector2(...p.default) : p.default.length === 3 ? new THREE.Vector3(...p.default) : new THREE.Vector4(...p.default) : p.default;
642
+ uniforms.set(p.name, uniform(value, p.type));
643
+ }
644
+ const textures = nextGameplay?.textures ?? new Map();
645
+ const binding = { id: definition.id, graphs, ...(definition.vertex ? { vertex: definition.vertex } : {}), time: 0, parameters: nextRender.service.materialParameters(definition.id), textures };
646
+ if (Object.keys(materialSlotTypes(binding)).length)
647
+ binding.buffers = nextRender.buffers.resources(definition.id);
648
+ if (Object.keys(materialTextureTypes(binding)).length)
649
+ binding.managedTextures = nextRender.textures.resources(definition.id);
650
+ applyMaterialGraphs(material, binding, uniforms, nextRenderBufferBridge, nextRenderTextureBridge);
651
+ material.userData.pelicanGraphInputs = [...new Set([...Object.values(graphs), ...Object.values(definition.vertex ?? {})].flatMap(graph => graph.requiredInputs ?? []))];
652
+ nextGraphs.push({ material, graph: definition.graph, binding, uniforms });
653
+ }
654
+ }
655
+ for (const entity of ir.scene.entities) {
656
+ const node = createEntityView(entity, nextMaterials);
657
+ node.userData.pelicanPrefab = entity.prefab;
658
+ if (entity.model)
659
+ nextGameplay.models.instantiate(entity.id, entity.model, node);
660
+ node.position.fromArray(entity.transform.position);
661
+ node.rotation.set(...entity.transform.rotation);
662
+ node.scale.fromArray(entity.transform.scale);
663
+ nextRoot.add(node);
664
+ if (node.userData.lightTarget)
665
+ nextRoot.add(node.userData.lightTarget);
666
+ nextObjects.set(entity.id, node);
667
+ }
668
+ for (const entity of ir.scene.entities)
669
+ if (entity.parent)
670
+ nextObjects.get(entity.parent).add(nextObjects.get(entity.id));
671
+ nextRoot.traverse(object => {
672
+ if (!(object instanceof THREE.Mesh))
673
+ return;
674
+ for (const material of Array.isArray(object.material) ? object.material : [object.material]) {
675
+ for (const input of material.userData.pelicanGraphInputs ?? [])
676
+ if ((input === "uv" || input === "uv1") && !object.geometry.hasAttribute(input))
677
+ throw new Error(`TC_GRAPH_UV: Material ${material.name} requires ${input} on object ${object.name || object.uuid}.`);
678
+ if (material.userData.pelicanVertexNormal && !object.geometry.hasAttribute("normal"))
679
+ throw new Error("TC_VERTEX_NORMAL_ATTRIBUTE: Vertex normal graphs require a normal attribute.");
680
+ }
681
+ });
682
+ if (saved) {
683
+ if (loadSlot)
684
+ nextGameplay?.services.adoptStorageRequest("load", loadSlot);
685
+ candidate.restore(saved.runtime);
686
+ }
687
+ if (nextGraphs.some(item => item.binding.vertex)) {
688
+ nextVertexBounds = createVertexBoundsOwner();
689
+ for (const item of nextGraphs) {
690
+ item.binding.time = candidate.inspect().scheduler.tick / 60;
691
+ item.binding.parameters = nextRender.service.materialParameters(item.binding.id);
692
+ if (item.binding.buffers)
693
+ item.binding.buffers = nextRender.buffers.resources(item.binding.id);
694
+ if (item.binding.managedTextures)
695
+ item.binding.managedTextures = nextRender.textures.resources(item.binding.id);
696
+ }
697
+ const boundsScene = new THREE.Scene();
698
+ boundsScene.add(nextRoot);
699
+ nextVertexBounds.update(boundsScene, new Map(nextGraphs.map(item => [item.material, item.binding])));
700
+ nextRoot.removeFromParent();
701
+ }
702
+ if (disposed)
703
+ throw new Error("TC_BROWSER_DISPOSED");
704
+ // No await in commit: a complete host/view/scope replaces the previous content together.
705
+ const previous = runtime, previousScope = contentScope, previousMaterials = gameMaterials, previousGameplay = gameplay, previousPostComposer = postComposer, previousVertexBounds = vertexBounds, previousRenderBufferBridge = renderBufferBridge, previousRenderTextureBridge = renderTextureBridge;
706
+ const previousRoot = new THREE.Group();
707
+ if (gameRoot.children.length)
708
+ previousRoot.add(...gameRoot.children);
709
+ compiledIR = ir;
710
+ runtime = candidate;
711
+ renderControl = nextRender;
712
+ computeControl = nextCompute;
713
+ computeExecutor = nextComputeExecutor;
714
+ renderBufferBridge = nextRenderBufferBridge;
715
+ renderTextureBridge = nextRenderTextureBridge;
716
+ postComposer = nextPostComposer;
717
+ vertexBounds = nextVertexBounds;
718
+ surfaceTracer?.clearContent();
719
+ gameplay = nextGameplay;
720
+ contentScope = nextScope;
721
+ gameMaterials = nextMaterials;
722
+ committed = true;
723
+ if (nextRoot.children.length)
724
+ gameRoot.add(...nextRoot.children);
725
+ gameObjects.clear();
726
+ for (const [id, node] of nextObjects)
727
+ gameObjects.set(id, node);
728
+ materialGraphs.splice(0, materialGraphs.length, ...nextGraphs);
729
+ authoredCamera = nextCamera;
730
+ scene.background = nextBackground;
731
+ scene.environment = nextGameplay?.environment ?? null;
732
+ camera.fov = ir.scene.camera.fov;
733
+ camera.near = ir.scene.camera.near;
734
+ camera.far = ir.scene.camera.far;
735
+ camera.updateProjectionMatrix();
736
+ tick = candidate.inspect().scheduler.tick;
737
+ frameAccumulator = 0;
738
+ lastFrameTime = undefined;
739
+ inputLog.length = 0;
740
+ terminalFault = false;
741
+ syncRuntime();
742
+ nextGameplay?.mount((key, pressed) => {
743
+ if (!loadingContent && !restoring && !disposed)
744
+ runtime?.input(key, pressed);
745
+ }, (type, payload) => {
746
+ if (!loadingContent && !restoring && !disposed)
747
+ runtime?.emitExternal(type, payload);
748
+ });
749
+ try {
750
+ previous?.dispose();
751
+ }
752
+ catch (error) {
753
+ errors.push(`TC_CONTENT_CLEANUP: ${String(error)}`);
754
+ }
755
+ await previousGameplay?.dispose();
756
+ previousPostComposer?.dispose();
757
+ previousVertexBounds?.dispose();
758
+ previousRenderBufferBridge?.dispose();
759
+ previousRenderTextureBridge?.dispose();
760
+ releaseObject(previousRoot);
761
+ for (const material of previousMaterials.values())
762
+ material.dispose();
763
+ await previousScope?.release();
764
+ return ir;
765
+ }
766
+ catch (error) {
767
+ if (!committed) {
768
+ try {
769
+ if (attached)
770
+ candidate?.dispose();
771
+ else {
772
+ try {
773
+ candidateGame?.dispose();
774
+ }
775
+ finally {
776
+ candidate?.dispose();
777
+ }
778
+ }
779
+ }
780
+ catch (cleanupError) {
781
+ errors.push(`TC_CONTENT_CLEANUP: ${String(cleanupError)}`);
782
+ }
783
+ await nextGameplay?.dispose();
784
+ nextRender?.dispose();
785
+ nextCompute?.dispose();
786
+ nextPostComposer?.dispose();
787
+ nextVertexBounds?.dispose();
788
+ nextRenderBufferBridge?.dispose();
789
+ nextRenderTextureBridge?.dispose();
790
+ releaseObject(nextRoot);
791
+ for (const material of nextMaterials.values())
792
+ material.dispose();
793
+ await nextScope?.release();
794
+ }
795
+ throw error;
796
+ }
797
+ finally {
798
+ loadingContent = false;
799
+ }
800
+ }
801
+ /** Only completed fixed ticks may request browser persistence or scene transactions. */
802
+ async function processGameRequests() {
803
+ await computeControl?.flush();
804
+ if (!gameplay || !compiledIR || !runtime)
805
+ return;
806
+ const requests = gameplay.services.takeRequests();
807
+ for (const request of requests) {
808
+ if (request.type === "scene") {
809
+ await loadGame(request.id);
810
+ continue;
811
+ }
812
+ const slot = request.id;
813
+ const key = "pelican:" + compiledIR.gameId + ":" + slot;
814
+ try {
815
+ if (request.type === "save") {
816
+ syncRuntime();
817
+ const saved = {
818
+ format: "pelican.save",
819
+ version: 1,
820
+ engineVersion: SDK_VERSION,
821
+ gameId: compiledIR.gameId,
822
+ sceneId: compiledIR.scene.id,
823
+ assets: Object.fromEntries(compiledIR.assets.map((a) => [a.id, a.hash])),
824
+ runtime: runtime.snapshot(),
825
+ };
826
+ localStorage.setItem(key, JSON.stringify(saved));
827
+ gameplay.services.storageComplete("save", slot);
828
+ }
829
+ else {
830
+ const text = localStorage.getItem(key);
831
+ if (text === null)
832
+ throw new Error("TC_SAVE_MISSING: " + slot);
833
+ const saved = JSON.parse(text);
834
+ if (!saved || typeof saved.sceneId !== "string")
835
+ throw new Error("TC_SAVE_FORMAT");
836
+ await loadGame(saved.sceneId, saved, slot);
837
+ gameplay.services.storageComplete("load", slot);
838
+ }
839
+ }
840
+ catch (error) {
841
+ gameplay?.services.storageComplete(request.type, slot, String(error));
842
+ }
843
+ }
844
+ }
845
+ function animate() {
846
+ const time = tick / 60;
847
+ graphTime.value = time;
848
+ for (const item of materialGraphs) {
849
+ item.binding.time = time;
850
+ item.binding.parameters = renderControl.service.materialParameters(item.binding.id);
851
+ if (item.binding.buffers)
852
+ item.binding.buffers = renderControl.buffers.resources(item.binding.id);
853
+ if (item.binding.managedTextures)
854
+ item.binding.managedTextures = renderControl.textures.resources(item.binding.id);
855
+ for (const [name, parameter] of item.uniforms) {
856
+ const value = item.binding.parameters[name];
857
+ if (Array.isArray(value))
858
+ parameter.value.fromArray(value);
859
+ else
860
+ parameter.value = value;
861
+ }
862
+ if (probeEnabled && item.graph) {
863
+ const value = evaluateGraph(item.graph, time);
864
+ if (typeof value === "boolean")
865
+ throw Error("TC_GRAPH_TYPE: Legacy color graph must return vec3.");
866
+ item.material.color.fromArray(typeof value === "number" ? [value, value, value] : [...value]);
867
+ }
868
+ }
869
+ box.position.x = -1.6 + Math.sin(time) * 0.65;
870
+ box.rotation.y = time * 0.5;
871
+ morphMesh.morphTargetInfluences[0] = (Math.sin(time * 1.3) + 1) * 0.5;
872
+ tip.rotation.z = Math.sin(time * 1.8) * 0.5;
873
+ const base = new THREE.Vector3().fromArray(authoredCamera.position), target = new THREE.Vector3().fromArray(authoredCamera.target);
874
+ const offset = base
875
+ .sub(target)
876
+ .applyAxisAngle(new THREE.Vector3(0, 1, 0), cameraPhase);
877
+ if (!gameplay?.services.inspect().camera) {
878
+ camera.position.copy(target).add(offset);
879
+ camera.lookAt(target);
880
+ }
881
+ camera.updateMatrixWorld();
882
+ vertexBounds?.update(scene, new Map(materialGraphs.map(item => [item.material, item.binding])));
883
+ gameplay?.present(camera, gameObjects, playing ? 1 / 60 : 0, tick);
884
+ }
885
+ function updateTraceGeometry() {
886
+ const start = performance.now();
887
+ const geometry = buildBVH(extractTriangles(scene));
888
+ bvhMs = performance.now() - start;
889
+ triangleCount = geometry.triangleCount;
890
+ nodeCount = geometry.nodeCount;
891
+ let hash = 2166136261;
892
+ for (let i = 0; i < geometry.triangles.length; i += 7)
893
+ hash = Math.imul(hash ^ Math.round(geometry.triangles[i] * 10000), 16777619);
894
+ lastGeometryHash = hash >>> 0;
895
+ const uploadStart = performance.now();
896
+ tracer.upload(geometry);
897
+ uploadMs = performance.now() - uploadStart;
898
+ generation++;
899
+ }
900
+ function cameraFrame() {
901
+ const forward = new THREE.Vector3();
902
+ camera.getWorldDirection(forward);
903
+ const right = new THREE.Vector3()
904
+ .setFromMatrixColumn(camera.matrixWorld, 0)
905
+ .multiplyScalar(Math.tan(THREE.MathUtils.degToRad(camera.fov / 2)) * camera.aspect);
906
+ const up = new THREE.Vector3()
907
+ .setFromMatrixColumn(camera.matrixWorld, 1)
908
+ .multiplyScalar(Math.tan(THREE.MathUtils.degToRad(camera.fov / 2)));
909
+ const light = authoredLights[0];
910
+ const position = light
911
+ ? light.object.getWorldPosition(new THREE.Vector3()).toArray()
912
+ : [-3, 6, 3];
913
+ const definition = light?.definition;
914
+ const color = new THREE.Color(definition?.color ?? 0xffdfb0);
915
+ return {
916
+ origin: camera.position.toArray(),
917
+ forward: forward.toArray(),
918
+ right: right.toArray(),
919
+ up: up.toArray(),
920
+ near: camera.near,
921
+ far: camera.far,
922
+ light: {
923
+ position,
924
+ color: color.toArray(),
925
+ intensity: definition?.intensity ?? 55,
926
+ directional: definition?.kind === "directional",
927
+ },
928
+ environment: scene.background.toArray(),
929
+ ambient: compiledIR?.scene.environment.ambientIntensity ?? 1,
930
+ };
931
+ }
932
+ function draw(accumulate = false, changed = true) {
933
+ const operation = drawTail.then(async () => {
934
+ document.documentElement.dataset.pelicanFrameState = "drawing";
935
+ try {
936
+ await drawFrame(accumulate, changed);
937
+ frameSerial++;
938
+ document.documentElement.dataset.pelicanFrameSerial =
939
+ String(frameSerial);
940
+ }
941
+ finally {
942
+ document.documentElement.dataset.pelicanFrameState = "idle";
943
+ }
944
+ });
945
+ drawTail = operation.catch(() => { });
946
+ return operation;
947
+ }
948
+ async function drawFrame(accumulate = false, changed = true) {
949
+ if (disposed)
950
+ throw new Error("TC_BROWSER_DISPOSED");
951
+ await computeControl?.flush();
952
+ const start = performance.now();
953
+ syncRuntime();
954
+ animate();
955
+ renderBufferBridge?.update();
956
+ renderTextureBridge?.update();
957
+ const settings = renderControl?.service.state() ?? resolveRenderSettings({ mode });
958
+ const effects = postComposer && renderControl?.postProcesses.states().some(effect => effect.enabled) ? postComposer : undefined;
959
+ validateRenderCapabilities(settings, renderCapabilities(backendKind));
960
+ if (mode !== settings.mode)
961
+ setMode(settings.mode);
962
+ if (probeEnabled && vertexBounds)
963
+ throw new Error("TC_VERTEX_PROBE_UNSUPPORTED: Vertex graphs require a normal game scene, not the historical tracing sidecar.");
964
+ rasterCanvas.style.display = mode === "raster" || effects ? "block" : "none";
965
+ traceCanvas.style.display = mode === "raster" || effects ? "none" : "block";
966
+ if (effects && probeEnabled)
967
+ throw new Error("TC_POSTPROCESS_PROBE_UNSUPPORTED: Controlled effects require a normal game scene, not the historical M0 tracing sidecar.");
968
+ if (mode !== "raster" && !probeEnabled) {
969
+ if (!surfaceTracer) {
970
+ if (!legacyTracerDisposed) {
971
+ tracer.dispose();
972
+ legacyTracerDisposed = true;
973
+ }
974
+ const controller = new AbortController();
975
+ const ownedDevice = device;
976
+ pendingSurface = controller;
977
+ const retain = (candidate) => {
978
+ if (controller.signal.aborted || disposed || device !== ownedDevice) {
979
+ candidate.dispose();
980
+ throw new Error("TC_TRACE_CANCELLED: Surface initialization owner changed.");
981
+ }
982
+ return candidate;
983
+ };
984
+ try {
985
+ const candidate = await boundedOperation(SurfaceTracer.create(device, traceCanvas, width, height, renderer, reportFailure, import.meta.env.DEV, controller.signal).then(retain), "surface renderer initialization");
986
+ surfaceTracer = retain(candidate);
987
+ }
988
+ finally {
989
+ controller.abort();
990
+ if (pendingSurface === controller)
991
+ pendingSurface = undefined;
992
+ }
993
+ changed = true;
994
+ }
995
+ const surfaceState = surfaceTracer.inspect();
996
+ const needsMotion = settings.denoise && settings.debug === "beauty";
997
+ if (changed ||
998
+ surfaceState.triangleCount === 0 ||
999
+ surfaceState.motion.enabled !== needsMotion)
1000
+ await boundedOperation(surfaceTracer.update(scene, new Set(materialGraphs.map((item) => item.material)), needsMotion, new Map(materialGraphs.map(item => [item.material, item.binding])), computeExecutor), "surface content capture");
1001
+ if (effects)
1002
+ await surfaceTracer.prepareLinearPresentation();
1003
+ submitMs = surfaceTracer.render(camera, settings, effects?.traceTargets());
1004
+ if (effects)
1005
+ effects.present(renderControl.postProcesses, tick / 60, settings);
1006
+ const report = surfaceTracer.inspect();
1007
+ triangleCount = report.triangleCount;
1008
+ nodeCount = report.nodeCount;
1009
+ bvhMs =
1010
+ (report.acceleration?.metrics.refitMs ?? 0) +
1011
+ (report.acceleration?.metrics.buildMs ?? 0);
1012
+ uploadMs = report.uploadMs;
1013
+ metrics.push({
1014
+ frame: tick,
1015
+ bvhMs,
1016
+ uploadMs,
1017
+ submitMs,
1018
+ wallMs: performance.now() - start,
1019
+ simulationMs: lastStepCpuMs,
1020
+ });
1021
+ if (metrics.length > 2000)
1022
+ metrics.shift();
1023
+ status.hidden = false;
1024
+ status.textContent = runtimeStatus({
1025
+ backend: backendKind,
1026
+ mode,
1027
+ playing,
1028
+ automation: startup.automation,
1029
+ tick,
1030
+ triangles: triangleCount,
1031
+ faulted: terminalFault,
1032
+ });
1033
+ return;
1034
+ }
1035
+ if (changed) {
1036
+ if ((gameplay || legacyTracerDisposed || backendKind === "webgl2") && mode === "raster") {
1037
+ // Raster does not consume a CPU BVH. Count authored triangles without rebuilding/uploading tracing buffers.
1038
+ triangleCount = 0;
1039
+ scene.traverseVisible((object) => {
1040
+ if (object instanceof THREE.Mesh && !object.userData.noTrace)
1041
+ triangleCount +=
1042
+ (object.geometry.index?.count ??
1043
+ object.geometry.getAttribute("position").count) / 3;
1044
+ });
1045
+ nodeCount = 0;
1046
+ bvhMs = 0;
1047
+ uploadMs = 0;
1048
+ }
1049
+ else
1050
+ updateTraceGeometry();
1051
+ }
1052
+ renderer.toneMappingExposure = settings.exposure;
1053
+ if (mode !== "path") {
1054
+ if (effects) {
1055
+ effects.captureRaster(scene, camera);
1056
+ effects.present(renderControl.postProcesses, tick / 60, settings);
1057
+ }
1058
+ else
1059
+ renderer.render(scene, camera);
1060
+ }
1061
+ if (mode !== "raster")
1062
+ submitMs = tracer.renderFrame(cameraFrame(), mode, accumulate);
1063
+ else
1064
+ submitMs = 0;
1065
+ metrics.push({
1066
+ frame: tick,
1067
+ bvhMs: changed ? bvhMs : 0,
1068
+ uploadMs: changed ? uploadMs : 0,
1069
+ submitMs,
1070
+ wallMs: performance.now() - start,
1071
+ simulationMs: lastStepCpuMs,
1072
+ });
1073
+ if (metrics.length > 2000)
1074
+ metrics.shift();
1075
+ status.hidden = false;
1076
+ status.textContent = runtimeStatus({
1077
+ backend: backendKind,
1078
+ mode,
1079
+ playing,
1080
+ automation: startup.automation,
1081
+ tick,
1082
+ triangles: triangleCount,
1083
+ faulted: terminalFault,
1084
+ });
1085
+ }
1086
+ async function applyInput(key) {
1087
+ if (!probeEnabled) {
1088
+ const host = requireRuntime();
1089
+ host.input(key, true);
1090
+ if (!playing)
1091
+ await stepRuntime();
1092
+ syncRuntime();
1093
+ tick = host.inspect().scheduler.tick;
1094
+ return;
1095
+ }
1096
+ const id = compiledIR?.scene.entities.find((entity) => entity.mesh)?.id;
1097
+ if (!id)
1098
+ return;
1099
+ const t = facade.getTransform(id);
1100
+ if (key === "ArrowRight" || key === "ArrowLeft")
1101
+ facade.setTransform(id, {
1102
+ position: [
1103
+ t.position[0] + (key === "ArrowRight" ? 0.25 : -0.25),
1104
+ t.position[1],
1105
+ t.position[2],
1106
+ ],
1107
+ });
1108
+ }
1109
+ function onKeyDown(event) {
1110
+ if (gameplay?.consumesKeyboard(event))
1111
+ return;
1112
+ if (disposed || terminalFault || loadingContent || restoring || !runtime)
1113
+ return;
1114
+ try {
1115
+ if (compiledIR?.input &&
1116
+ Object.values(compiledIR.input).some((keys) => keys.includes(event.key)))
1117
+ event.preventDefault();
1118
+ gameplay?.unlockAudio();
1119
+ queueNativeInput(async () => {
1120
+ inputLog.push({ tick, key: event.key });
1121
+ await applyInput(event.key);
1122
+ if (!playing)
1123
+ requestInputDraw();
1124
+ });
1125
+ }
1126
+ catch (error) {
1127
+ reportFailure(error);
1128
+ }
1129
+ }
1130
+ function requestInputDraw() {
1131
+ inputDrawRequested = true;
1132
+ if (inputDrawActive)
1133
+ return;
1134
+ inputDrawActive = true;
1135
+ void (async () => {
1136
+ try {
1137
+ while (inputDrawRequested && !playing && !disposed) {
1138
+ inputDrawRequested = false;
1139
+ await draw(false, true);
1140
+ }
1141
+ }
1142
+ catch (error) {
1143
+ if (!disposed)
1144
+ reportFailure(error);
1145
+ }
1146
+ finally {
1147
+ inputDrawActive = false;
1148
+ }
1149
+ })();
1150
+ }
1151
+ function queueNativeInput(operation) {
1152
+ nativeInputTail = nativeInputTail.then(async () => {
1153
+ await tickTail;
1154
+ if (disposed || terminalFault || loadingContent || restoring || !runtime)
1155
+ return;
1156
+ await operation();
1157
+ }).catch(error => { if (!disposed)
1158
+ reportFailure(error); });
1159
+ }
1160
+ function onKeyUp(event) {
1161
+ if (gameplay?.consumesKeyboard(event))
1162
+ return;
1163
+ if (disposed ||
1164
+ terminalFault ||
1165
+ loadingContent ||
1166
+ restoring ||
1167
+ !runtime ||
1168
+ probeEnabled)
1169
+ return;
1170
+ queueNativeInput(() => { requireRuntime().input(event.key, false); });
1171
+ }
1172
+ window.addEventListener("keydown", onKeyDown);
1173
+ window.addEventListener("keyup", onKeyUp);
1174
+ async function stepRuntime() {
1175
+ const started = performance.now();
1176
+ const host = requireRuntime();
1177
+ if (terminalFault)
1178
+ throw new Error("TC_BROWSER_FAULTED: Restore a valid checkpoint or reload content before stepping.");
1179
+ try {
1180
+ const operation = tickTail.then(async () => {
1181
+ if (disposed || host !== runtime)
1182
+ throw new Error("TC_BROWSER_DISPOSED: Tick owner changed.");
1183
+ await host.stepAsync(1);
1184
+ });
1185
+ tickTail = operation.catch(() => { });
1186
+ await operation;
1187
+ tick = host.inspect().scheduler.tick;
1188
+ simulation?.step();
1189
+ }
1190
+ catch (error) {
1191
+ tick = host.inspect().scheduler.tick;
1192
+ reportFailure(error);
1193
+ throw error;
1194
+ }
1195
+ finally {
1196
+ lastStepCpuMs = performance.now() - started;
1197
+ }
1198
+ }
1199
+ function setMode(next) {
1200
+ if (next !== "raster")
1201
+ requireCompute(renderCapabilities(backendKind), next);
1202
+ if (next !== mode) {
1203
+ surfaceTracer?.invalidateTemporal();
1204
+ postComposer?.resetHistory("mode");
1205
+ }
1206
+ mode = next;
1207
+ document.documentElement.dataset.pelicanMode = next;
1208
+ rasterCanvas.style.display = next === "raster" ? "block" : "none";
1209
+ traceCanvas.style.display = next === "raster" ? "none" : "block";
1210
+ traceCanvas.style.opacity = "1";
1211
+ }
1212
+ function inspect(options) {
1213
+ const bounded = options &&
1214
+ (options.entity !== undefined ||
1215
+ options.offset !== undefined ||
1216
+ options.limit !== undefined);
1217
+ syncRuntime();
1218
+ const runtimeState = runtime?.inspect();
1219
+ const selected = bounded
1220
+ ? selectInspectRecords(runtimeState?.records ?? [], options)
1221
+ : undefined;
1222
+ return {
1223
+ ready: !disposed && !terminalFault && !recoveringGpu,
1224
+ playback: {
1225
+ playing,
1226
+ automation: startup.automation,
1227
+ state: terminalFault ? "faulted" : playing ? "running" : "paused",
1228
+ },
1229
+ backend: backendKind === "webgpu" ? "WebGPUBackend" : backendKind === "webgl2" ? "WebGLBackend" : "uninitialized",
1230
+ adapter: adapterInfo,
1231
+ mode,
1232
+ rendering: {
1233
+ postProcesses: postComposer?.inspect(),
1234
+ vertex: vertexBounds?.inspect(),
1235
+ bufferBindings: renderBufferBridge?.inspect(),
1236
+ textureBindings: renderTextureBridge?.inspect(),
1237
+ computeGpu: computeExecutor?.inspect(),
1238
+ effectStates: renderControl?.postProcesses.states(),
1239
+ compute: computeControl?.inspect(),
1240
+ backendDiagnostic: backendKind === "webgl2" ? { code: "TC_RENDER_WEBGL2_FALLBACK", severity: "info", message: "WebGPU initialization unavailable; using Three.js built-in WebGL2 fallback. Available modes: raster." } : null,
1241
+ settings: renderControl?.service.state(),
1242
+ surface: surfaceTracer?.inspect(),
1243
+ gpuRecovery: {
1244
+ active: recoveringGpu,
1245
+ count: recoveryCount,
1246
+ events: [...recoveryEvents],
1247
+ },
1248
+ },
1249
+ tick,
1250
+ width,
1251
+ height,
1252
+ inputLog: [...inputLog],
1253
+ triangleCount,
1254
+ nodeCount,
1255
+ generation,
1256
+ geometryHash: lastGeometryHash,
1257
+ objects: {
1258
+ box: box.position.toArray(),
1259
+ morph: morphMesh.morphTargetInfluences[0],
1260
+ bone: tip.rotation.z,
1261
+ camera: camera.position.toArray(),
1262
+ },
1263
+ errors: [...errors],
1264
+ ...(bounded ? {} : { compiledIR }),
1265
+ runtime: runtimeState && {
1266
+ ...runtimeState,
1267
+ records: selected?.records ?? runtimeState.records,
1268
+ },
1269
+ ...(selected ? { paging: selected.paging } : {}),
1270
+ frameClock: {
1271
+ fixedDt: 1 / 60,
1272
+ accumulator: frameAccumulator,
1273
+ backlogTicks: Math.floor(frameAccumulator * 60),
1274
+ maxStepsPerFrame: 8,
1275
+ },
1276
+ resources: resourceManager.inspect(),
1277
+ gameplay: gameplay?.inspect(),
1278
+ game: {
1279
+ transforms: selected
1280
+ ? Object.fromEntries(selected.records.map((record) => [record.id, record.transform]))
1281
+ : Object.fromEntries(transforms),
1282
+ components: selected
1283
+ ? Object.fromEntries(selected.records.map((record) => [record.id, record.components]))
1284
+ : Object.fromEntries(componentState),
1285
+ },
1286
+ simulation: simulation?.inspect(),
1287
+ world: world?.inspect(),
1288
+ metrics: [...metrics],
1289
+ capabilities: {
1290
+ ...renderCapabilities(backendKind),
1291
+ ...inspectTraceCapabilities({ backend: backendKind, legacyProbe: probeEnabled, surfaceInitialized: !!surfaceTracer, gameplay: !!gameplay, maxBounces: renderControl?.service.state().maxBounces ?? 5 }),
1292
+ },
1293
+ };
1294
+ }
1295
+ async function disposeBrowser() {
1296
+ if (disposal)
1297
+ return disposal;
1298
+ disposed = true;
1299
+ playing = false;
1300
+ lastFrameTime = undefined;
1301
+ if (frameRequest !== undefined)
1302
+ cancelAnimationFrame(frameRequest);
1303
+ frameRequest = undefined;
1304
+ document.documentElement.dataset.pelicanReady = "false";
1305
+ window.removeEventListener("keydown", onKeyDown);
1306
+ window.removeEventListener("keyup", onKeyUp);
1307
+ rasterCanvas.removeEventListener("webglcontextrestored", onContextRestored);
1308
+ window.removeEventListener("pagehide", onPageHide);
1309
+ const pending = [];
1310
+ const clean = (action) => {
1311
+ try {
1312
+ const result = action();
1313
+ if (result && typeof result.then === "function")
1314
+ pending.push(Promise.resolve(result));
1315
+ }
1316
+ catch (error) {
1317
+ errors.push(`TC_BROWSER_CLEANUP: ${String(error)}`);
1318
+ }
1319
+ };
1320
+ clean(() => runtime?.dispose());
1321
+ clean(() => computeControl?.dispose());
1322
+ clean(() => postComposer?.dispose());
1323
+ clean(() => vertexBounds?.dispose());
1324
+ clean(() => renderBufferBridge?.dispose());
1325
+ clean(() => renderTextureBridge?.dispose());
1326
+ clean(() => gameplay?.dispose());
1327
+ clean(() => simulation?.dispose());
1328
+ clean(() => world?.dispose());
1329
+ clean(() => resourceManager.dispose());
1330
+ clean(() => releaseObject(scene, true, gameMaterials.values()));
1331
+ clean(() => pendingSurface?.abort());
1332
+ clean(() => tracer?.dispose());
1333
+ clean(() => surfaceTracer?.dispose());
1334
+ clean(async () => {
1335
+ const gl = backendKind === "webgl2" ? rasterCanvas.getContext("webgl2") : null;
1336
+ try {
1337
+ await renderer?.dispose();
1338
+ }
1339
+ finally {
1340
+ gl?.getExtension("WEBGL_lose_context")?.loseContext();
1341
+ }
1342
+ });
1343
+ clean(() => device?.destroy());
1344
+ gameObjects.clear();
1345
+ gameMaterials.clear();
1346
+ transforms.clear();
1347
+ componentState.clear();
1348
+ materialGraphs.length = 0;
1349
+ authoredLights = [];
1350
+ topologyObjects.length = 0;
1351
+ compiledIR = null;
1352
+ runtime = undefined;
1353
+ renderControl = undefined;
1354
+ computeControl = undefined;
1355
+ postComposer = undefined;
1356
+ vertexBounds = undefined;
1357
+ computeExecutor = undefined;
1358
+ renderBufferBridge = undefined;
1359
+ renderTextureBridge = undefined;
1360
+ gameplay = undefined;
1361
+ simulation = undefined;
1362
+ world = undefined;
1363
+ contentScope = undefined;
1364
+ rasterCanvas.remove();
1365
+ traceCanvas.remove();
1366
+ disposal = Promise.allSettled(pending).then((results) => {
1367
+ for (const result of results)
1368
+ if (result.status === "rejected")
1369
+ errors.push(`TC_BROWSER_CLEANUP: ${String(result.reason)}`);
1370
+ });
1371
+ return disposal;
1372
+ }
1373
+ function onPageHide() {
1374
+ void disposeBrowser();
1375
+ }
1376
+ window.addEventListener("pagehide", onPageHide);
1377
+ async function initializeGpu() {
1378
+ let abandoned = false;
1379
+ const candidate = new THREE.WebGPURenderer({ canvas: rasterCanvas, antialias: true, powerPreference: "high-performance" });
1380
+ renderer = candidate;
1381
+ renderer.setPixelRatio(1);
1382
+ renderer.setSize(width, height);
1383
+ renderer.shadowMap.enabled = true;
1384
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
1385
+ renderer.onDeviceLost = (info) => {
1386
+ if (disposed || renderer !== candidate)
1387
+ return;
1388
+ if (info.api === "WebGL") {
1389
+ lostRenderers.add(candidate);
1390
+ if (recoveringGpu)
1391
+ return;
1392
+ contextWasPlaying = playing;
1393
+ playing = false;
1394
+ recoveringGpu = true;
1395
+ waitingForWebglRestore = true;
1396
+ document.documentElement.dataset.pelicanReady = "false";
1397
+ recoveryEvents.push({ phase: "lost", reason: "TC_RENDER_CONTEXT_LOST: WebGL2 context lost; waiting for browser restoration.", tick });
1398
+ status.hidden = false;
1399
+ status.textContent = "WebGL2 · TC_RENDER_CONTEXT_LOST";
1400
+ }
1401
+ };
1402
+ try {
1403
+ await boundedOperation(candidate.init().then(async () => {
1404
+ if (abandoned || disposed || renderer !== candidate)
1405
+ await candidate.dispose();
1406
+ }), "renderer initialization");
1407
+ }
1408
+ catch (error) {
1409
+ abandoned = true;
1410
+ throw new Error("TC_RENDER_BACKEND_UNAVAILABLE: WebGPU and built-in WebGL2 fallback could not initialize. Enable a supported graphics backend and reload. " + String(error));
1411
+ }
1412
+ if (disposed)
1413
+ return;
1414
+ // Backend objects stay private; only plain capability data crosses the SDK boundary.
1415
+ const backend = candidate.backend;
1416
+ backendKind = backend.isWebGLBackend ? "webgl2" : "webgpu";
1417
+ document.documentElement.dataset.pelicanBackend = backendKind;
1418
+ adapterInfo = undefined;
1419
+ legacyTracerDisposed = backendKind !== "webgpu";
1420
+ if (backendKind === "webgl2")
1421
+ return;
1422
+ if (!backend.device)
1423
+ throw new Error("TC_RENDER_BACKEND_UNAVAILABLE: Initialized backend has no compute device.");
1424
+ device = backend.device;
1425
+ const identity = device.adapterInfo;
1426
+ adapterInfo = {
1427
+ vendor: identity.vendor,
1428
+ architecture: identity.architecture,
1429
+ isFallbackAdapter: identity.isFallbackAdapter,
1430
+ features: [...device.features],
1431
+ };
1432
+ const ownedDevice = device;
1433
+ device.addEventListener("uncapturederror", (event) => {
1434
+ if (!disposed && device === ownedDevice && !recoveringGpu)
1435
+ reportFailure(new Error("TC_GPU_VALIDATION: " + event.error.message));
1436
+ });
1437
+ void device.lost.then((info) => {
1438
+ lostDevices.add(ownedDevice);
1439
+ if (disposed || device !== ownedDevice || recoveringGpu)
1440
+ return;
1441
+ recoveryTask = recoverGpu(info);
1442
+ void recoveryTask.catch((error) => {
1443
+ reportFailure(error);
1444
+ });
1445
+ });
1446
+ status.textContent = "GPU tracer";
1447
+ tracer = new GPUTracer(device, traceCanvas, width, height);
1448
+ legacyTracerDisposed = false;
1449
+ }
1450
+ async function submittedWork(label = "GPU submission") {
1451
+ if (backendKind === "webgpu")
1452
+ await boundedOperation(device.queue.onSubmittedWorkDone(), label);
1453
+ }
1454
+ function onContextRestored() {
1455
+ if (disposed || backendKind !== "webgl2" || !waitingForWebglRestore)
1456
+ return;
1457
+ waitingForWebglRestore = false;
1458
+ recoveringGpu = false;
1459
+ playing = contextWasPlaying;
1460
+ recoveryTask = recoverGpu({ reason: "unknown", message: "WebGL2 context restored" });
1461
+ void recoveryTask.catch(reportFailure);
1462
+ }
1463
+ rasterCanvas.addEventListener("webglcontextrestored", onContextRestored);
1464
+ async function recoverGpu(info) {
1465
+ const wasPlaying = playing;
1466
+ recoveringGpu = true;
1467
+ playing = false;
1468
+ lastFrameTime = undefined;
1469
+ document.documentElement.dataset.pelicanReady = "false";
1470
+ status.hidden = false;
1471
+ status.textContent = `Rebuilding ${backendKind} resources`;
1472
+ recoveryEvents.push({
1473
+ phase: "lost",
1474
+ reason: info.reason + ": " + info.message,
1475
+ tick,
1476
+ });
1477
+ try {
1478
+ pendingSurface?.abort();
1479
+ renderBufferBridge?.dispose();
1480
+ renderBufferBridge = undefined;
1481
+ renderTextureBridge?.dispose();
1482
+ renderTextureBridge = undefined;
1483
+ computeControl?.replaceExecutor(undefined);
1484
+ postComposer?.dispose();
1485
+ postComposer = undefined;
1486
+ surfaceTracer?.dispose();
1487
+ surfaceTracer = undefined;
1488
+ tracer?.dispose();
1489
+ await renderer?.dispose();
1490
+ await initializeGpu();
1491
+ if (disposed)
1492
+ return;
1493
+ computeExecutor = backendKind === "webgpu" && (compiledIR?.computes.length || compiledIR?.computeBuffers?.length || compiledIR?.computeTextures?.length) ? createComputeExecutor(device) : undefined;
1494
+ computeControl?.replaceExecutor(computeExecutor);
1495
+ if (compiledIR && usesRenderSlots(compiledIR)) {
1496
+ renderBufferBridge = createRenderBufferBridge(renderer, () => computeExecutor, id => slotResources(renderControl, id));
1497
+ }
1498
+ if (compiledIR && usesRenderTextures(compiledIR))
1499
+ renderTextureBridge = createRenderTextureBridge(renderer, () => computeExecutor, id => textureResources(renderControl, id));
1500
+ for (const item of materialGraphs)
1501
+ applyMaterialGraphs(item.material, item.binding, item.uniforms, renderBufferBridge, renderTextureBridge);
1502
+ if (gameplay)
1503
+ scene.environment =
1504
+ (await gameplay.rebuildGpuResources(renderer)) ?? null;
1505
+ if (compiledIR?.postProcesses?.length)
1506
+ postComposer = createPostProcessComposer(renderer, width, height, compiledIR.postProcesses, gameplay?.textures ?? new Map(), renderBufferBridge, renderTextureBridge);
1507
+ await draw(false, true);
1508
+ if (disposed)
1509
+ return;
1510
+ if (lostRenderers.has(renderer) || (backendKind === "webgpu" && lostDevices.has(device)))
1511
+ throw new Error("TC_GPU_RECOVERY_DEVICE_LOST: Replacement backend was lost during reconstruction. Reload after restoring graphics availability.");
1512
+ terminalFault = false;
1513
+ delete document.documentElement.dataset.pelicanFailure;
1514
+ document.documentElement.dataset.pelicanReady = "true";
1515
+ recoveryCount++;
1516
+ recoveryEvents.push({
1517
+ phase: "restored",
1518
+ reason: backendKind === "webgpu" ? "new WebGPU device" : "new WebGL2 renderer",
1519
+ tick,
1520
+ });
1521
+ playing = wasPlaying;
1522
+ lastFrameTime = undefined;
1523
+ }
1524
+ catch (error) {
1525
+ playing = false;
1526
+ recoveryEvents.push({ phase: "failed", reason: String(error), tick });
1527
+ throw new Error("TC_GPU_RECOVERY_FAILED: " + String(error));
1528
+ }
1529
+ finally {
1530
+ recoveringGpu = false;
1531
+ }
1532
+ }
1533
+ async function boot() {
1534
+ await initializeGpu();
1535
+ status.textContent = "Loading compiled game";
1536
+ await boundedOperation(loadGame(), "compiled content initialization");
1537
+ status.textContent = "Drawing compiled game";
1538
+ setMode("raster");
1539
+ await draw();
1540
+ const api = {
1541
+ inspect,
1542
+ configureRendering: async (settings) => {
1543
+ requireRuntime().facade.render.configure(settings);
1544
+ await draw(false, true);
1545
+ await submittedWork("render settings frame");
1546
+ return inspect();
1547
+ },
1548
+ exerciseDeviceLoss: async () => {
1549
+ requireRuntime();
1550
+ requireCompute(renderCapabilities(backendKind), "Device loss probe");
1551
+ const lost = device;
1552
+ lost.destroy();
1553
+ await boundedOperation(lost.lost, "injected device loss");
1554
+ await Promise.resolve();
1555
+ if (!recoveryTask)
1556
+ throw new Error("TC_GPU_RECOVERY_NOT_STARTED");
1557
+ await boundedOperation(recoveryTask, "device recovery", 60000);
1558
+ if (device === lost)
1559
+ throw new Error("TC_GPU_RECOVERY_DEVICE_UNCHANGED");
1560
+ return inspect();
1561
+ },
1562
+ exerciseContextLoss: async () => {
1563
+ requireRuntime();
1564
+ if (backendKind !== "webgl2")
1565
+ throw new Error("TC_RENDER_CONTEXT_PROBE_BACKEND: This probe requires WebGL2.");
1566
+ const gl = rasterCanvas.getContext("webgl2");
1567
+ const extension = gl.getExtension("WEBGL_lose_context");
1568
+ if (!extension)
1569
+ throw new Error("TC_RENDER_CONTEXT_PROBE_UNAVAILABLE: Browser does not expose WEBGL_lose_context.");
1570
+ const lost = new Promise((resolve) => rasterCanvas.addEventListener("webglcontextlost", () => resolve(), { once: true }));
1571
+ const restored = new Promise((resolve) => rasterCanvas.addEventListener("webglcontextrestored", () => resolve(), { once: true }));
1572
+ extension.loseContext();
1573
+ await boundedOperation(lost, "WebGL2 injected context loss");
1574
+ await new Promise((resolve) => setTimeout(resolve, 100));
1575
+ extension.restoreContext();
1576
+ await boundedOperation(restored, "WebGL2 context restoration");
1577
+ await boundedOperation(recoveryTask, "WebGL2 resource recovery");
1578
+ return inspect();
1579
+ },
1580
+ mode: async (next) => {
1581
+ requireRuntime();
1582
+ const previous = mode;
1583
+ try {
1584
+ renderControl?.service.configure({ mode: next });
1585
+ setMode(next);
1586
+ await draw(false, true);
1587
+ }
1588
+ catch (error) {
1589
+ renderControl?.service.configure({ mode: previous });
1590
+ setMode(previous);
1591
+ await draw(false, true);
1592
+ throw error;
1593
+ }
1594
+ await submittedWork("GPU submission");
1595
+ return inspect();
1596
+ },
1597
+ step: async (count = 1) => {
1598
+ requireRuntime();
1599
+ if (!Number.isInteger(count) || count < 1 || count > 10000)
1600
+ throw new Error("TC_PROTOCOL_STEPS: count must be in [1,10000].");
1601
+ for (let i = 0; i < count; i++) {
1602
+ await stepRuntime();
1603
+ await processGameRequests();
1604
+ syncSimulation();
1605
+ await draw(false, true);
1606
+ }
1607
+ await submittedWork("GPU submission");
1608
+ return inspect();
1609
+ },
1610
+ camera: async (phase) => {
1611
+ cameraPhase = phase;
1612
+ await draw(false, false);
1613
+ await submittedWork("GPU submission");
1614
+ return inspect();
1615
+ },
1616
+ accumulate: async (count = 16) => {
1617
+ if (mode === "raster")
1618
+ throw new Error("TC_TRACE_MODE: Accumulation is available in path or hybrid mode.");
1619
+ if (!Number.isSafeInteger(count) || count < 1 || count > 4096)
1620
+ throw new Error("TC_TRACE_SAMPLES: Development accumulation count must be an integer in [1,4096].");
1621
+ for (let i = 0; i < count; i++)
1622
+ await draw(i > 0, false);
1623
+ await submittedWork("GPU submission");
1624
+ return surfaceTracer ? surfaceTracer.pixels() : tracer.pixelStats();
1625
+ },
1626
+ pixels: () => surfaceTracer && mode !== "raster"
1627
+ ? surfaceTracer.pixels()
1628
+ : legacyTracerDisposed
1629
+ ? Promise.resolve({
1630
+ available: false,
1631
+ mode: "raster",
1632
+ reason: "Surface statistics are not a raster framebuffer readback; use the external current-page capture.",
1633
+ })
1634
+ : tracer.pixelStats(),
1635
+ linearPixels: (filtered = true) => {
1636
+ if (!surfaceTracer)
1637
+ throw new Error("TC_TRACE_NOT_ACTIVE");
1638
+ return surfaceTracer.readRadiance(filtered);
1639
+ },
1640
+ motionPixels: () => {
1641
+ if (!surfaceTracer)
1642
+ throw new Error("TC_TRACE_NOT_ACTIVE");
1643
+ return surfaceTracer.readMotion();
1644
+ },
1645
+ traceFaultTarget: (pixel, sample = 0) => {
1646
+ if (!surfaceTracer)
1647
+ throw new Error("TC_TRACE_NOT_ACTIVE");
1648
+ surfaceTracer.setFaultTarget(pixel, sample);
1649
+ },
1650
+ temporalPixels: () => {
1651
+ if (!surfaceTracer)
1652
+ throw new Error("TC_TRACE_NOT_ACTIVE");
1653
+ return surfaceTracer.readTemporal();
1654
+ },
1655
+ primaryStats: () => {
1656
+ if (!surfaceTracer || mode !== "hybrid")
1657
+ throw new Error("TC_TRACE_PRIMARY_NOT_ACTIVE");
1658
+ return surfaceTracer.readPrimaryStats();
1659
+ },
1660
+ guidePixels: () => {
1661
+ if (!surfaceTracer)
1662
+ throw new Error("TC_TRACE_NOT_ACTIVE");
1663
+ return surfaceTracer.readGuides();
1664
+ },
1665
+ gpuTimeMs: () => backendKind !== "webgpu" ? Promise.resolve(null) : surfaceTracer ? surfaceTracer.gpuTimeMs() : tracer.gpuTimeMs(),
1666
+ vertexRay: (origin, direction) => {
1667
+ requireRuntime();
1668
+ if (!vertexBounds)
1669
+ throw new Error("TC_VERTEX_NOT_ACTIVE: This scene has no controlled vertex graphs.");
1670
+ return vertexBounds.intersect(origin, direction);
1671
+ },
1672
+ ray: (origin, direction) => {
1673
+ requireCompute(renderCapabilities(backendKind), "Traced ray query");
1674
+ return surfaceTracer
1675
+ ? surfaceTracer.queryRay(origin, direction)
1676
+ : tracer.queryRay(origin, direction);
1677
+ },
1678
+ compute: async (id, time) => {
1679
+ requireCompute(renderCapabilities(backendKind), "Compute graph");
1680
+ const definition = compiledIR?.computes.find((c) => c.id === id);
1681
+ if (!definition)
1682
+ throw new Error(`TC_COMPUTE_MISSING:${id}`);
1683
+ return runComputeGraph(device, definition.graph, definition.count, definition.workgroupSize, time);
1684
+ },
1685
+ simulation: async (kind) => {
1686
+ simulation?.dispose();
1687
+ simulation = createSimulation(kind);
1688
+ syncSimulation();
1689
+ await draw();
1690
+ await submittedWork("GPU submission");
1691
+ return inspect();
1692
+ },
1693
+ snapshot: () => {
1694
+ const host = requireRuntime();
1695
+ syncRuntime();
1696
+ return structuredClone({
1697
+ format: "pelican.browser",
1698
+ version: 1,
1699
+ engineVersion: SDK_VERSION,
1700
+ tick,
1701
+ cameraPhase,
1702
+ transforms: Object.fromEntries(transforms),
1703
+ components: Object.fromEntries(componentState),
1704
+ inputLog,
1705
+ simulation: simulation?.snapshot(),
1706
+ world: world?.snapshot(),
1707
+ runtime: host.snapshot(),
1708
+ frameAccumulator,
1709
+ resources: resourceManager.snapshot(contentScope),
1710
+ topology: topologyObjects.map((object) => object.position.toArray()),
1711
+ });
1712
+ },
1713
+ restore: async (value) => {
1714
+ const host = requireRuntime();
1715
+ const saved = validateBrowserCheckpoint(value, resourceManager.snapshot(contentScope));
1716
+ restoring = true;
1717
+ let prepared;
1718
+ const nextTopology = [];
1719
+ let committed = false;
1720
+ try {
1721
+ prepared = await prepareCheckpointProbes(saved);
1722
+ for (const position of saved.topology) {
1723
+ const mesh = new THREE.Mesh(new THREE.IcosahedronGeometry(0.35, 0), standard(0x98dd66));
1724
+ mesh.position.fromArray(position);
1725
+ nextTopology.push(mesh);
1726
+ }
1727
+ if (disposed || runtime !== host)
1728
+ throw new Error("TC_BROWSER_DISPOSED: Checkpoint owner changed.");
1729
+ // Host preflights scheduler/world atomically and preserves generational invalidation.
1730
+ // Invalid sidecars have already failed on isolated candidates, without touching this host.
1731
+ host.restore(saved.runtime);
1732
+ postComposer?.resetHistory("restore");
1733
+ surfaceTracer?.invalidateTemporal("restore");
1734
+ const previousSimulation = simulation, previousWorld = world;
1735
+ simulation = prepared.simulation;
1736
+ world = prepared.world;
1737
+ frameAccumulator = saved.frameAccumulator;
1738
+ lastFrameTime = undefined;
1739
+ tick = saved.tick;
1740
+ cameraPhase = saved.cameraPhase;
1741
+ terminalFault = false;
1742
+ inputLog.length = 0;
1743
+ inputLog.push(...saved.inputLog);
1744
+ for (const object of topologyObjects)
1745
+ releaseObject(object, true);
1746
+ topologyObjects.splice(0, topologyObjects.length, ...nextTopology);
1747
+ for (const mesh of nextTopology)
1748
+ scene.add(mesh);
1749
+ committed = true;
1750
+ previousSimulation?.dispose();
1751
+ previousWorld?.dispose();
1752
+ syncRuntime();
1753
+ syncSimulation();
1754
+ syncWorld();
1755
+ }
1756
+ finally {
1757
+ if (!committed) {
1758
+ prepared?.dispose();
1759
+ for (const object of nextTopology)
1760
+ releaseObject(object, true);
1761
+ }
1762
+ restoring = false;
1763
+ }
1764
+ await draw();
1765
+ await submittedWork("GPU submission");
1766
+ return inspect();
1767
+ },
1768
+ worldLoad: async () => {
1769
+ world?.dispose();
1770
+ world = createWorldProbe({ chunkSize: 2, cellsPerChunk: 4 });
1771
+ await Promise.all([world.loadChunk([0, 0]), world.loadChunk([1, 0])]);
1772
+ syncWorld();
1773
+ await draw();
1774
+ await submittedWork("GPU submission");
1775
+ return inspect();
1776
+ },
1777
+ worldEdit: async () => {
1778
+ if (!world)
1779
+ throw new Error("TC_WORLD_NOT_LOADED");
1780
+ await world.applyEdits([{ chunk: [0, 0], x: 2, z: 2, height: 1.8 }]);
1781
+ syncWorld();
1782
+ await draw();
1783
+ await submittedWork("GPU submission");
1784
+ return {
1785
+ state: inspect(),
1786
+ collision: world.sampleHeight({ chunk: [0, 0], local: [1, 0, 1] }),
1787
+ navigation: world.getNavigationState(),
1788
+ };
1789
+ },
1790
+ worldUnload: async () => {
1791
+ await world?.unloadChunk([0, 0]);
1792
+ syncWorld();
1793
+ await draw();
1794
+ await submittedWork("GPU submission");
1795
+ return inspect();
1796
+ },
1797
+ worldReload: async () => {
1798
+ await world?.loadChunk([0, 0]);
1799
+ syncWorld();
1800
+ await draw();
1801
+ await submittedWork("GPU submission");
1802
+ return inspect();
1803
+ },
1804
+ topology: async () => {
1805
+ const extra = new THREE.Mesh(new THREE.IcosahedronGeometry(0.35, 0), standard(0x98dd66));
1806
+ extra.position.set(2, 0.5, 1.5);
1807
+ scene.add(extra);
1808
+ topologyObjects.push(extra);
1809
+ await draw();
1810
+ await submittedWork("GPU submission");
1811
+ return inspect();
1812
+ },
1813
+ play: () => {
1814
+ requireRuntime();
1815
+ if (terminalFault)
1816
+ throw new Error("TC_BROWSER_FAULTED: Restore a valid checkpoint or reload content before resuming.");
1817
+ playing = true;
1818
+ lastFrameTime = undefined;
1819
+ status.textContent = runtimeStatus({
1820
+ backend: backendKind,
1821
+ mode,
1822
+ playing,
1823
+ automation: startup.automation,
1824
+ tick,
1825
+ triangles: triangleCount,
1826
+ });
1827
+ },
1828
+ pause: () => {
1829
+ playing = false;
1830
+ lastFrameTime = undefined;
1831
+ status.textContent = runtimeStatus({
1832
+ backend: backendKind,
1833
+ mode,
1834
+ playing,
1835
+ automation: startup.automation,
1836
+ tick,
1837
+ triangles: triangleCount,
1838
+ });
1839
+ },
1840
+ get adapter() {
1841
+ return adapterInfo;
1842
+ },
1843
+ loadGame,
1844
+ setComponent: (id, component, value) => facade.setComponent(id, component, value),
1845
+ worldTransforms: () => {
1846
+ scene.updateMatrixWorld(true);
1847
+ return Object.fromEntries([...gameObjects].map(([id, object]) => [
1848
+ id,
1849
+ object.getWorldPosition(new THREE.Vector3()).toArray(),
1850
+ ]));
1851
+ },
1852
+ geometryProbe: (id, value) => {
1853
+ if (id === "morph")
1854
+ morphMesh.morphTargetInfluences[0] = value;
1855
+ else
1856
+ tip.rotation.z = value;
1857
+ const object = id === "morph" ? morphMesh : skinned;
1858
+ scene.updateMatrixWorld(true);
1859
+ if (object instanceof THREE.SkinnedMesh)
1860
+ object.skeleton.update();
1861
+ const vertices = [];
1862
+ for (let i = 0; i < object.geometry.getAttribute("position").count; i++)
1863
+ vertices.push(object
1864
+ .getVertexPosition(i, new THREE.Vector3())
1865
+ .applyMatrix4(object.matrixWorld)
1866
+ .toArray());
1867
+ updateTraceGeometry();
1868
+ return { vertices, hash: lastGeometryHash };
1869
+ },
1870
+ cameraDirection: () => cameraFrame(),
1871
+ command: async (input) => {
1872
+ const taskId = `runtime-${Date.now()}`;
1873
+ try {
1874
+ const command = parseCommand(input);
1875
+ await nativeInputTail;
1876
+ await tickTail;
1877
+ if (command.type !== "inspect")
1878
+ requireRuntime();
1879
+ let data;
1880
+ switch (command.type) {
1881
+ case "inspect":
1882
+ data = inspect(command);
1883
+ break;
1884
+ case "capture":
1885
+ throw new Error("TC_CONTROL_TOOL_ONLY: Use capture --live through development bridge.");
1886
+ case "step":
1887
+ data = await api.step(command.count);
1888
+ break;
1889
+ case "pause":
1890
+ api.pause();
1891
+ data = inspect();
1892
+ break;
1893
+ case "resume":
1894
+ api.play();
1895
+ data = inspect();
1896
+ break;
1897
+ case "snapshot":
1898
+ data = api.snapshot();
1899
+ break;
1900
+ case "recordStart":
1901
+ requireRuntime().startRecording();
1902
+ data = inspect();
1903
+ break;
1904
+ case "recordStop":
1905
+ data = requireRuntime().stopRecording();
1906
+ break;
1907
+ case "recordCancel":
1908
+ requireRuntime().cancelRecording();
1909
+ data = inspect();
1910
+ break;
1911
+ case "replay": {
1912
+ const host = requireRuntime();
1913
+ const replay = await host.replayAsync(command.trace);
1914
+ tick = host.inspect().scheduler.tick;
1915
+ if (replay.status !== "passed") {
1916
+ const error = new Error(`TC_REPLAY_DIVERGENCE: ${replay.fault?.message ?? "Replay did not complete."}`);
1917
+ reportFailure(error);
1918
+ throw error;
1919
+ }
1920
+ data = replay;
1921
+ syncRuntime();
1922
+ await draw();
1923
+ break;
1924
+ }
1925
+ case "restore":
1926
+ data = await api.restore(command.snapshot);
1927
+ break;
1928
+ case "input":
1929
+ requireRuntime().input(command.key, command.pressed);
1930
+ data = inspect();
1931
+ break;
1932
+ case "setComponent":
1933
+ requireRuntime().facade.setComponent(command.entity, command.component, command.value);
1934
+ syncRuntime();
1935
+ await draw();
1936
+ data = inspect();
1937
+ break;
1938
+ case "mode":
1939
+ data = await api.mode(command.mode);
1940
+ break;
1941
+ }
1942
+ if (data === undefined)
1943
+ throw new Error("TC_CONTROL_NO_RESULT: Command did not produce a result.");
1944
+ return protocolResult(taskId, "succeeded", data);
1945
+ }
1946
+ catch (error) {
1947
+ const message = error instanceof Error ? error.message : String(error);
1948
+ if (message.includes("TC_GPU_TIMEOUT"))
1949
+ reportFailure(error);
1950
+ return protocolResult(taskId, "failed", null, [
1951
+ {
1952
+ code: message.match(/TC_[A-Z0-9_]+/)?.[0] ?? "TC_CONTROL_FAILURE",
1953
+ severity: "error",
1954
+ message,
1955
+ remediation: "Inspect the runtime state and correct the command or source before retrying.",
1956
+ },
1957
+ ]);
1958
+ }
1959
+ },
1960
+ };
1961
+ if (import.meta.env.DEV)
1962
+ window.__pelican = api;
1963
+ async function loop(now) {
1964
+ if (disposed)
1965
+ return;
1966
+ try {
1967
+ if (playing &&
1968
+ !terminalFault &&
1969
+ !loadingContent &&
1970
+ !restoring &&
1971
+ !recoveringGpu) {
1972
+ const elapsed = lastFrameTime === undefined ? 0 : (now - lastFrameTime) / 1000;
1973
+ lastFrameTime = now;
1974
+ frameAccumulator += Math.max(0, elapsed);
1975
+ let steps = 0;
1976
+ while (frameAccumulator >= 1 / 60 && steps < 8) {
1977
+ await stepRuntime();
1978
+ frameAccumulator -= 1 / 60;
1979
+ await processGameRequests();
1980
+ steps++;
1981
+ }
1982
+ if (steps)
1983
+ syncSimulation();
1984
+ await draw(false, steps > 0);
1985
+ }
1986
+ else {
1987
+ lastFrameTime = undefined;
1988
+ }
1989
+ }
1990
+ catch (error) {
1991
+ if (!terminalFault)
1992
+ reportFailure(error);
1993
+ }
1994
+ if (!disposed)
1995
+ frameRequest = requestAnimationFrame(loop);
1996
+ }
1997
+ frameRequest = requestAnimationFrame(loop);
1998
+ playing = startup.playing;
1999
+ document.documentElement.dataset.pelicanReady = "true";
2000
+ status.textContent = runtimeStatus({
2001
+ backend: backendKind,
2002
+ mode,
2003
+ playing,
2004
+ automation: startup.automation,
2005
+ tick,
2006
+ triangles: triangleCount,
2007
+ });
2008
+ }
2009
+ boot().catch((error) => {
2010
+ reportFailure(error);
2011
+ if (import.meta.env.DEV)
2012
+ window.__pelicanFailure =
2013
+ String(error);
2014
+ void disposeBrowser();
2015
+ });