@forgeax/engine-app 0.1.2

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 (354) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +418 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/animation-asset-lookup.unit.test.d.ts +2 -0
  5. package/dist/__tests__/animation-asset-lookup.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/app-rebuild.test.d.ts +2 -0
  7. package/dist/__tests__/app-rebuild.test.d.ts.map +1 -0
  8. package/dist/__tests__/asset-registry-types.test-d.d.ts +2 -0
  9. package/dist/__tests__/asset-registry-types.test-d.d.ts.map +1 -0
  10. package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts +2 -0
  11. package/dist/__tests__/assets-runtime-assembly.integration.test.d.ts.map +1 -0
  12. package/dist/__tests__/callback-deletion.unit.test.d.ts +2 -0
  13. package/dist/__tests__/callback-deletion.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/callback-inference.test-d.d.ts +2 -0
  15. package/dist/__tests__/callback-inference.test-d.d.ts.map +1 -0
  16. package/dist/__tests__/component-introspection.unit.test.d.ts +2 -0
  17. package/dist/__tests__/component-introspection.unit.test.d.ts.map +1 -0
  18. package/dist/__tests__/consumer-migration.unit.test.d.ts +2 -0
  19. package/dist/__tests__/consumer-migration.unit.test.d.ts.map +1 -0
  20. package/dist/__tests__/create-app-features.test.d.ts +2 -0
  21. package/dist/__tests__/create-app-features.test.d.ts.map +1 -0
  22. package/dist/__tests__/create-app-listener-sync.test.d.ts +2 -0
  23. package/dist/__tests__/create-app-listener-sync.test.d.ts.map +1 -0
  24. package/dist/__tests__/create-app-lock-provider.test.d.ts +2 -0
  25. package/dist/__tests__/create-app-lock-provider.test.d.ts.map +1 -0
  26. package/dist/__tests__/create-app-plugin.test.d.ts +2 -0
  27. package/dist/__tests__/create-app-plugin.test.d.ts.map +1 -0
  28. package/dist/__tests__/create-app-stop.test.d.ts +2 -0
  29. package/dist/__tests__/create-app-stop.test.d.ts.map +1 -0
  30. package/dist/__tests__/create-app-time.unit.test.d.ts +2 -0
  31. package/dist/__tests__/create-app-time.unit.test.d.ts.map +1 -0
  32. package/dist/__tests__/create-app.test-d.d.ts +2 -0
  33. package/dist/__tests__/create-app.test-d.d.ts.map +1 -0
  34. package/dist/__tests__/create-app.test.d.ts +2 -0
  35. package/dist/__tests__/create-app.test.d.ts.map +1 -0
  36. package/dist/__tests__/create-renderer-error.integration.test.d.ts +2 -0
  37. package/dist/__tests__/create-renderer-error.integration.test.d.ts.map +1 -0
  38. package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts +2 -0
  39. package/dist/__tests__/create-renderer-lifecycle.integration.test.d.ts.map +1 -0
  40. package/dist/__tests__/docs-gate-green.unit.test.d.ts +2 -0
  41. package/dist/__tests__/docs-gate-green.unit.test.d.ts.map +1 -0
  42. package/dist/__tests__/draw-source-headless-full-chain.test.d.ts +2 -0
  43. package/dist/__tests__/draw-source-headless-full-chain.test.d.ts.map +1 -0
  44. package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts +2 -0
  45. package/dist/__tests__/draw-source-injected-world-update-visibility.test.d.ts.map +1 -0
  46. package/dist/__tests__/draw-source-single-world-regression.test.d.ts +2 -0
  47. package/dist/__tests__/draw-source-single-world-regression.test.d.ts.map +1 -0
  48. package/dist/__tests__/ecs-import.unit.test.d.ts +2 -0
  49. package/dist/__tests__/ecs-import.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts +2 -0
  51. package/dist/__tests__/engine-worker-bootstrap-channel.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts +2 -0
  53. package/dist/__tests__/engine-worker-world-swap.unit.test.d.ts.map +1 -0
  54. package/dist/__tests__/errors-pointer-lock-failed.test.d.ts +2 -0
  55. package/dist/__tests__/errors-pointer-lock-failed.test.d.ts.map +1 -0
  56. package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts +2 -0
  57. package/dist/__tests__/execution-bootstrap-isolation.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/execution-error-detail.test-d.d.ts +2 -0
  59. package/dist/__tests__/execution-error-detail.test-d.d.ts.map +1 -0
  60. package/dist/__tests__/execution-error-detail.unit.test.d.ts +2 -0
  61. package/dist/__tests__/execution-error-detail.unit.test.d.ts.map +1 -0
  62. package/dist/__tests__/execution-frame-order.unit.test.d.ts +2 -0
  63. package/dist/__tests__/execution-frame-order.unit.test.d.ts.map +1 -0
  64. package/dist/__tests__/execution-frame-protocol.unit.test.d.ts +2 -0
  65. package/dist/__tests__/execution-frame-protocol.unit.test.d.ts.map +1 -0
  66. package/dist/__tests__/execution-input-sample.contract.test.d.ts +2 -0
  67. package/dist/__tests__/execution-input-sample.contract.test.d.ts.map +1 -0
  68. package/dist/__tests__/execution-measurement.unit.test.d.ts +2 -0
  69. package/dist/__tests__/execution-measurement.unit.test.d.ts.map +1 -0
  70. package/dist/__tests__/execution-public-api.test-d.d.ts +2 -0
  71. package/dist/__tests__/execution-public-api.test-d.d.ts.map +1 -0
  72. package/dist/__tests__/execution-public-api.unit.test.d.ts +2 -0
  73. package/dist/__tests__/execution-public-api.unit.test.d.ts.map +1 -0
  74. package/dist/__tests__/execution-rebuild.contract.test.d.ts +2 -0
  75. package/dist/__tests__/execution-rebuild.contract.test.d.ts.map +1 -0
  76. package/dist/__tests__/execution-report.contract.test.d.ts +2 -0
  77. package/dist/__tests__/execution-report.contract.test.d.ts.map +1 -0
  78. package/dist/__tests__/execution-report.types.test-d.d.ts +2 -0
  79. package/dist/__tests__/execution-report.types.test-d.d.ts.map +1 -0
  80. package/dist/__tests__/execution-schema-freshness.unit.test.d.ts +2 -0
  81. package/dist/__tests__/execution-schema-freshness.unit.test.d.ts.map +1 -0
  82. package/dist/__tests__/execution-selector.unit.test.d.ts +2 -0
  83. package/dist/__tests__/execution-selector.unit.test.d.ts.map +1 -0
  84. package/dist/__tests__/execution-shared-kernel.unit.test.d.ts +2 -0
  85. package/dist/__tests__/execution-shared-kernel.unit.test.d.ts.map +1 -0
  86. package/dist/__tests__/execution-world-health-owner.test-d.d.ts +2 -0
  87. package/dist/__tests__/execution-world-health-owner.test-d.d.ts.map +1 -0
  88. package/dist/__tests__/fallback-camera.unit.test.d.ts +2 -0
  89. package/dist/__tests__/fallback-camera.unit.test.d.ts.map +1 -0
  90. package/dist/__tests__/frame-loop-time-elapsed.test.d.ts +2 -0
  91. package/dist/__tests__/frame-loop-time-elapsed.test.d.ts.map +1 -0
  92. package/dist/__tests__/frame-loop-time.unit.test.d.ts +2 -0
  93. package/dist/__tests__/frame-loop-time.unit.test.d.ts.map +1 -0
  94. package/dist/__tests__/frame-loop-world-array.test.d.ts +2 -0
  95. package/dist/__tests__/frame-loop-world-array.test.d.ts.map +1 -0
  96. package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts +2 -0
  97. package/dist/__tests__/kernel-worker-runtime.unit.test.d.ts.map +1 -0
  98. package/dist/__tests__/migration-surface-inventory.unit.test.d.ts +2 -0
  99. package/dist/__tests__/migration-surface-inventory.unit.test.d.ts.map +1 -0
  100. package/dist/__tests__/plugin-policy.integration.test.d.ts +2 -0
  101. package/dist/__tests__/plugin-policy.integration.test.d.ts.map +1 -0
  102. package/dist/__tests__/plugin-profiles.unit.test.d.ts +2 -0
  103. package/dist/__tests__/plugin-profiles.unit.test.d.ts.map +1 -0
  104. package/dist/__tests__/profiler-default-off.test.d.ts +2 -0
  105. package/dist/__tests__/profiler-default-off.test.d.ts.map +1 -0
  106. package/dist/__tests__/profiler-frame-token.test.d.ts +2 -0
  107. package/dist/__tests__/profiler-frame-token.test.d.ts.map +1 -0
  108. package/dist/__tests__/profiler-phase-catalog.test.d.ts +2 -0
  109. package/dist/__tests__/profiler-phase-catalog.test.d.ts.map +1 -0
  110. package/dist/__tests__/profiler-phase-integration.test.d.ts +2 -0
  111. package/dist/__tests__/profiler-phase-integration.test.d.ts.map +1 -0
  112. package/dist/__tests__/profiler-remote-bridge.test.d.ts +2 -0
  113. package/dist/__tests__/profiler-remote-bridge.test.d.ts.map +1 -0
  114. package/dist/__tests__/remote-integration.test.d.ts +2 -0
  115. package/dist/__tests__/remote-integration.test.d.ts.map +1 -0
  116. package/dist/__tests__/remote-serve-flag.test.d.ts +2 -0
  117. package/dist/__tests__/remote-serve-flag.test.d.ts.map +1 -0
  118. package/dist/__tests__/rhi-capture-capability.test-d.d.ts +2 -0
  119. package/dist/__tests__/rhi-capture-capability.test-d.d.ts.map +1 -0
  120. package/dist/__tests__/rhi-capture-host.integration.test.d.ts +2 -0
  121. package/dist/__tests__/rhi-capture-host.integration.test.d.ts.map +1 -0
  122. package/dist/__tests__/rhi-capture.browser.test.d.ts +2 -0
  123. package/dist/__tests__/rhi-capture.browser.test.d.ts.map +1 -0
  124. package/dist/__tests__/surface-handoff.test.d.ts +2 -0
  125. package/dist/__tests__/surface-handoff.test.d.ts.map +1 -0
  126. package/dist/__tests__/types-m2.test-d.d.ts +2 -0
  127. package/dist/__tests__/types-m2.test-d.d.ts.map +1 -0
  128. package/dist/__tests__/visibility-remote.integration.test.d.ts +2 -0
  129. package/dist/__tests__/visibility-remote.integration.test.d.ts.map +1 -0
  130. package/dist/__tests__/worker-execution-stop.test.d.ts +2 -0
  131. package/dist/__tests__/worker-execution-stop.test.d.ts.map +1 -0
  132. package/dist/animation-asset-lookup.d.ts +8 -0
  133. package/dist/animation-asset-lookup.d.ts.map +1 -0
  134. package/dist/assets-runtime-assembly.d.ts +19 -0
  135. package/dist/assets-runtime-assembly.d.ts.map +1 -0
  136. package/dist/canvas-policy.d.ts +4 -0
  137. package/dist/canvas-policy.d.ts.map +1 -0
  138. package/dist/create-app.d.ts +57 -0
  139. package/dist/create-app.d.ts.map +1 -0
  140. package/dist/engine-worker-runtime.mjs +884 -0
  141. package/dist/engine-worker-runtime.mjs.map +1 -0
  142. package/dist/errors.d.ts +263 -0
  143. package/dist/errors.d.ts.map +1 -0
  144. package/dist/execution/attached-world-swap.d.ts +21 -0
  145. package/dist/execution/attached-world-swap.d.ts.map +1 -0
  146. package/dist/execution/bootstrap-entry.d.ts +30 -0
  147. package/dist/execution/bootstrap-entry.d.ts.map +1 -0
  148. package/dist/execution/capabilities.d.ts +5 -0
  149. package/dist/execution/capabilities.d.ts.map +1 -0
  150. package/dist/execution/control.d.ts +14 -0
  151. package/dist/execution/control.d.ts.map +1 -0
  152. package/dist/execution/engine-worker-runtime.d.ts +2 -0
  153. package/dist/execution/engine-worker-runtime.d.ts.map +1 -0
  154. package/dist/execution/engine-worker.d.ts +23 -0
  155. package/dist/execution/engine-worker.d.ts.map +1 -0
  156. package/dist/execution/host-controller.d.ts +16 -0
  157. package/dist/execution/host-controller.d.ts.map +1 -0
  158. package/dist/execution/index.d.ts +10 -0
  159. package/dist/execution/index.d.ts.map +1 -0
  160. package/dist/execution/kernel-pool.d.ts +14 -0
  161. package/dist/execution/kernel-pool.d.ts.map +1 -0
  162. package/dist/execution/kernel-worker-runtime.d.ts +2 -0
  163. package/dist/execution/kernel-worker-runtime.d.ts.map +1 -0
  164. package/dist/execution/measurement.d.ts +7 -0
  165. package/dist/execution/measurement.d.ts.map +1 -0
  166. package/dist/execution/protocol.d.ts +84 -0
  167. package/dist/execution/protocol.d.ts.map +1 -0
  168. package/dist/execution/report.d.ts +5 -0
  169. package/dist/execution/report.d.ts.map +1 -0
  170. package/dist/execution/schema.d.ts +4 -0
  171. package/dist/execution/schema.d.ts.map +1 -0
  172. package/dist/execution/selector.d.ts +10 -0
  173. package/dist/execution/selector.d.ts.map +1 -0
  174. package/dist/execution/types.d.ts +107 -0
  175. package/dist/execution/types.d.ts.map +1 -0
  176. package/dist/fallback-camera.d.ts +8 -0
  177. package/dist/fallback-camera.d.ts.map +1 -0
  178. package/dist/game-context.d.ts +98 -0
  179. package/dist/game-context.d.ts.map +1 -0
  180. package/dist/index.d.ts +24 -0
  181. package/dist/index.d.ts.map +1 -0
  182. package/dist/index.mjs +3985 -0
  183. package/dist/index.mjs.map +1 -0
  184. package/dist/input-map-plugin.d.ts +5 -0
  185. package/dist/input-map-plugin.d.ts.map +1 -0
  186. package/dist/input-plugin.d.ts +4 -0
  187. package/dist/input-plugin.d.ts.map +1 -0
  188. package/dist/internal/assembled-engine-profile.d.ts +5 -0
  189. package/dist/internal/assembled-engine-profile.d.ts.map +1 -0
  190. package/dist/internal/assets-world-plugin.d.ts +21 -0
  191. package/dist/internal/assets-world-plugin.d.ts.map +1 -0
  192. package/dist/internal/browser-remote-bridge.d.ts +25 -0
  193. package/dist/internal/browser-remote-bridge.d.ts.map +1 -0
  194. package/dist/internal/browser-rhi-debug-runtime.d.ts +18 -0
  195. package/dist/internal/browser-rhi-debug-runtime.d.ts.map +1 -0
  196. package/dist/internal/component-introspection.d.ts +24 -0
  197. package/dist/internal/component-introspection.d.ts.map +1 -0
  198. package/dist/internal/ecs-import.d.ts +6 -0
  199. package/dist/internal/ecs-import.d.ts.map +1 -0
  200. package/dist/internal/engine-profile-common.d.ts +9 -0
  201. package/dist/internal/engine-profile-common.d.ts.map +1 -0
  202. package/dist/internal/error-fanout.d.ts +66 -0
  203. package/dist/internal/error-fanout.d.ts.map +1 -0
  204. package/dist/internal/frame-loop.d.ts +35 -0
  205. package/dist/internal/frame-loop.d.ts.map +1 -0
  206. package/dist/internal/input-attach.d.ts +44 -0
  207. package/dist/internal/input-attach.d.ts.map +1 -0
  208. package/dist/internal/main-engine-profile.d.ts +15 -0
  209. package/dist/internal/main-engine-profile.d.ts.map +1 -0
  210. package/dist/internal/remote-serve-flag.d.ts +23 -0
  211. package/dist/internal/remote-serve-flag.d.ts.map +1 -0
  212. package/dist/internal/remote-server-plugin.d.ts +8 -0
  213. package/dist/internal/remote-server-plugin.d.ts.map +1 -0
  214. package/dist/internal/rhi-capture.d.ts +19 -0
  215. package/dist/internal/rhi-capture.d.ts.map +1 -0
  216. package/dist/internal/rhi-debug-flag.d.ts +18 -0
  217. package/dist/internal/rhi-debug-flag.d.ts.map +1 -0
  218. package/dist/internal/worker-engine-profile.d.ts +13 -0
  219. package/dist/internal/worker-engine-profile.d.ts.map +1 -0
  220. package/dist/kernel-worker-runtime.mjs +69 -0
  221. package/dist/kernel-worker-runtime.mjs.map +1 -0
  222. package/dist/load-game-errors.d.ts +131 -0
  223. package/dist/load-game-errors.d.ts.map +1 -0
  224. package/dist/load-game.d.ts +34 -0
  225. package/dist/load-game.d.ts.map +1 -0
  226. package/dist/plugin-factories.d.ts +3 -0
  227. package/dist/plugin-factories.d.ts.map +1 -0
  228. package/dist/tool-preview/bootstrap.d.ts +113 -0
  229. package/dist/tool-preview/bootstrap.d.ts.map +1 -0
  230. package/dist/tool-preview/evidence.d.ts +20 -0
  231. package/dist/tool-preview/evidence.d.ts.map +1 -0
  232. package/dist/tool-preview/framing.d.ts +13 -0
  233. package/dist/tool-preview/framing.d.ts.map +1 -0
  234. package/dist/tool-preview/recipe.d.ts +54 -0
  235. package/dist/tool-preview/recipe.d.ts.map +1 -0
  236. package/dist/types.d.ts +444 -0
  237. package/dist/types.d.ts.map +1 -0
  238. package/package.json +86 -0
  239. package/schema/execution-report.schema.json +168 -0
  240. package/src/__tests__/animation-asset-lookup.unit.test.ts +20 -0
  241. package/src/__tests__/app-rebuild.test.ts +31 -0
  242. package/src/__tests__/asset-registry-types.test-d.ts +16 -0
  243. package/src/__tests__/assets-runtime-assembly.integration.test.ts +68 -0
  244. package/src/__tests__/callback-deletion.unit.test.ts +14 -0
  245. package/src/__tests__/callback-inference.test-d.ts +32 -0
  246. package/src/__tests__/component-introspection.unit.test.ts +56 -0
  247. package/src/__tests__/consumer-migration.unit.test.ts +21 -0
  248. package/src/__tests__/create-app-features.test.ts +40 -0
  249. package/src/__tests__/create-app-listener-sync.test.ts +377 -0
  250. package/src/__tests__/create-app-lock-provider.test.ts +212 -0
  251. package/src/__tests__/create-app-plugin.test.ts +217 -0
  252. package/src/__tests__/create-app-stop.test.ts +617 -0
  253. package/src/__tests__/create-app-time.unit.test.ts +26 -0
  254. package/src/__tests__/create-app.test-d.ts +82 -0
  255. package/src/__tests__/create-app.test.ts +346 -0
  256. package/src/__tests__/create-renderer-error.integration.test.ts +24 -0
  257. package/src/__tests__/create-renderer-lifecycle.integration.test.ts +19 -0
  258. package/src/__tests__/docs-gate-green.unit.test.ts +61 -0
  259. package/src/__tests__/draw-source-headless-full-chain.test.ts +226 -0
  260. package/src/__tests__/draw-source-injected-world-update-visibility.test.ts +182 -0
  261. package/src/__tests__/draw-source-single-world-regression.test.ts +185 -0
  262. package/src/__tests__/ecs-import.unit.test.ts +40 -0
  263. package/src/__tests__/engine-worker-bootstrap-channel.unit.test.ts +74 -0
  264. package/src/__tests__/engine-worker-world-swap.unit.test.ts +76 -0
  265. package/src/__tests__/errors-pointer-lock-failed.test.ts +268 -0
  266. package/src/__tests__/execution-bootstrap-isolation.unit.test.ts +80 -0
  267. package/src/__tests__/execution-error-detail.test-d.ts +13 -0
  268. package/src/__tests__/execution-error-detail.unit.test.ts +35 -0
  269. package/src/__tests__/execution-frame-order.unit.test.ts +38 -0
  270. package/src/__tests__/execution-frame-protocol.unit.test.ts +61 -0
  271. package/src/__tests__/execution-input-sample.contract.test.ts +23 -0
  272. package/src/__tests__/execution-measurement.unit.test.ts +20 -0
  273. package/src/__tests__/execution-public-api.test-d.ts +10 -0
  274. package/src/__tests__/execution-public-api.unit.test.ts +9 -0
  275. package/src/__tests__/execution-rebuild.contract.test.ts +36 -0
  276. package/src/__tests__/execution-report.contract.test.ts +50 -0
  277. package/src/__tests__/execution-report.types.test-d.ts +16 -0
  278. package/src/__tests__/execution-schema-freshness.unit.test.ts +20 -0
  279. package/src/__tests__/execution-selector.unit.test.ts +72 -0
  280. package/src/__tests__/execution-shared-kernel.unit.test.ts +180 -0
  281. package/src/__tests__/execution-world-health-owner.test-d.ts +9 -0
  282. package/src/__tests__/fallback-camera.unit.test.ts +32 -0
  283. package/src/__tests__/frame-loop-time-elapsed.test.ts +115 -0
  284. package/src/__tests__/frame-loop-time.unit.test.ts +61 -0
  285. package/src/__tests__/frame-loop-world-array.test.ts +441 -0
  286. package/src/__tests__/kernel-worker-runtime.unit.test.ts +50 -0
  287. package/src/__tests__/migration-surface-inventory.unit.test.ts +13 -0
  288. package/src/__tests__/plugin-policy.integration.test.ts +15 -0
  289. package/src/__tests__/plugin-profiles.unit.test.ts +190 -0
  290. package/src/__tests__/profiler-default-off.test.ts +53 -0
  291. package/src/__tests__/profiler-frame-token.test.ts +248 -0
  292. package/src/__tests__/profiler-phase-catalog.test.ts +30 -0
  293. package/src/__tests__/profiler-phase-integration.test.ts +129 -0
  294. package/src/__tests__/profiler-remote-bridge.test.ts +31 -0
  295. package/src/__tests__/remote-integration.test.ts +224 -0
  296. package/src/__tests__/remote-serve-flag.test.ts +24 -0
  297. package/src/__tests__/rhi-capture-capability.test-d.ts +22 -0
  298. package/src/__tests__/rhi-capture-host.integration.test.ts +157 -0
  299. package/src/__tests__/rhi-capture.browser.test.ts +51 -0
  300. package/src/__tests__/surface-handoff.test.ts +92 -0
  301. package/src/__tests__/types-m2.test-d.ts +81 -0
  302. package/src/__tests__/visibility-remote.integration.test.ts +119 -0
  303. package/src/__tests__/worker-execution-stop.test.ts +116 -0
  304. package/src/animation-asset-lookup.ts +13 -0
  305. package/src/assets-runtime-assembly.ts +70 -0
  306. package/src/canvas-policy.ts +16 -0
  307. package/src/create-app.ts +1351 -0
  308. package/src/errors.ts +454 -0
  309. package/src/execution/attached-world-swap.ts +67 -0
  310. package/src/execution/bootstrap-entry.ts +122 -0
  311. package/src/execution/capabilities.ts +102 -0
  312. package/src/execution/control.ts +52 -0
  313. package/src/execution/engine-worker-runtime.ts +400 -0
  314. package/src/execution/engine-worker.ts +161 -0
  315. package/src/execution/host-controller.ts +454 -0
  316. package/src/execution/index.ts +43 -0
  317. package/src/execution/kernel-pool.ts +236 -0
  318. package/src/execution/kernel-worker-runtime.ts +103 -0
  319. package/src/execution/measurement.ts +39 -0
  320. package/src/execution/protocol.ts +131 -0
  321. package/src/execution/report.ts +66 -0
  322. package/src/execution/schema.ts +168 -0
  323. package/src/execution/selector.ts +98 -0
  324. package/src/execution/types.ts +146 -0
  325. package/src/fallback-camera.ts +20 -0
  326. package/src/game-context.ts +126 -0
  327. package/src/index.ts +176 -0
  328. package/src/input-map-plugin.ts +21 -0
  329. package/src/input-plugin.ts +26 -0
  330. package/src/internal/assembled-engine-profile.ts +22 -0
  331. package/src/internal/assets-world-plugin.ts +79 -0
  332. package/src/internal/browser-remote-bridge.ts +267 -0
  333. package/src/internal/browser-rhi-debug-runtime.ts +67 -0
  334. package/src/internal/component-introspection.ts +116 -0
  335. package/src/internal/ecs-import.ts +41 -0
  336. package/src/internal/engine-profile-common.ts +10 -0
  337. package/src/internal/error-fanout.ts +129 -0
  338. package/src/internal/frame-loop.ts +525 -0
  339. package/src/internal/input-attach.ts +104 -0
  340. package/src/internal/main-engine-profile.ts +80 -0
  341. package/src/internal/remote-serve-flag.ts +50 -0
  342. package/src/internal/remote-server-plugin.ts +16 -0
  343. package/src/internal/rhi-capture.ts +211 -0
  344. package/src/internal/rhi-debug-flag.ts +40 -0
  345. package/src/internal/worker-engine-profile.ts +47 -0
  346. package/src/load-game-errors.ts +211 -0
  347. package/src/load-game.ts +115 -0
  348. package/src/plugin-factories.ts +2 -0
  349. package/src/tool-preview/bootstrap.ts +848 -0
  350. package/src/tool-preview/evidence.ts +59 -0
  351. package/src/tool-preview/framing.ts +52 -0
  352. package/src/tool-preview/recipe.ts +137 -0
  353. package/src/types.ts +505 -0
  354. package/src/vite-env.d.ts +12 -0
package/dist/index.mjs ADDED
@@ -0,0 +1,3985 @@
1
+ import { Update, World, createWorldContext, componentDefinition } from '@forgeax/engine-ecs';
2
+ export { Context } from '@forgeax/engine-plugin';
3
+ import * as engineRuntimeModule from '@forgeax/engine-runtime';
4
+ import { defaultAssetDecoderContributions, createRenderer, EngineEnvironmentError } from '@forgeax/engine-runtime';
5
+ import { err, ok } from '@forgeax/engine-types';
6
+ import { createAssetRegistry, createCatalogSource } from '@forgeax/engine-assets-runtime';
7
+ import { getAssetRegistryResolver } from '@forgeax/engine-assets-runtime/internal';
8
+ import { loadRhiPack, createDebugDrawOnReady, releaseDebugDraw } from '@forgeax/engine-render/internal';
9
+ import { RhiError } from '@forgeax/engine-rhi';
10
+ import { Camera, perspective, ownedRendererPlugin, renderComponentsPlugin, rendererPlugin, CAMERA_PROJECTION_PERSPECTIVE } from '@forgeax/engine-render';
11
+ import { createHostAudioConsumer } from '@forgeax/engine-audio-webaudio';
12
+ import { INPUT_BACKEND_KEY, InputSet, InputFrameStartScan, attachBrowserInputBackend, inputBackendPlugin, ownedInputBackendPlugin, INPUT_MAP_KEY } from '@forgeax/engine-input';
13
+ import { componentSchema } from '@forgeax/engine-ecs/internal';
14
+ import { animationPayloadsPlugin, animationRuntimePlugin } from '@forgeax/engine-animation';
15
+ import { Transform, scenePlugin } from '@forgeax/engine-scene';
16
+ import { statePlugin } from '@forgeax/engine-state';
17
+ import { decodeTape, openReplay, decodeToRgba8, attachRecorder, buildTapeIndex, createRhiDebugError } from '@forgeax/engine-rhi-debug';
18
+ import { createProfiler } from '@forgeax/engine-profiler';
19
+ import { isSerializableValue, createPreviewArtifactManifest, validatePreviewArtifactManifest, createArtifactRef, snapshotStaleError } from '@forgeax/engine-tool-runtime';
20
+
21
+ var __defProp = Object.defineProperty;
22
+ var __getOwnPropNames = Object.getOwnPropertyNames;
23
+ var __esm = (fn, res) => function __init() {
24
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
25
+ };
26
+ var __export = (target, all) => {
27
+ for (var name in all)
28
+ __defProp(target, name, { get: all[name], enumerable: true });
29
+ };
30
+
31
+ // src/internal/ecs-import.ts
32
+ function isRecord2(value) {
33
+ return value !== null && typeof value === "object";
34
+ }
35
+ function projectEcsPublicModule(value) {
36
+ if (!isRecord2(value)) return {};
37
+ const projected = {};
38
+ for (const symbol of ECS_PUBLIC_SYMBOLS) {
39
+ if (symbol in value) projected[symbol] = value[symbol];
40
+ }
41
+ return projected;
42
+ }
43
+ function createEcsImportModule(importModule) {
44
+ return async (specifier) => {
45
+ const moduleValue = await importModule(specifier);
46
+ return specifier === ECS_MODULE_SPECIFIER ? projectEcsPublicModule(moduleValue) : moduleValue;
47
+ };
48
+ }
49
+ var ECS_MODULE_SPECIFIER, ECS_PUBLIC_SYMBOLS;
50
+ var init_ecs_import = __esm({
51
+ "src/internal/ecs-import.ts"() {
52
+ ECS_MODULE_SPECIFIER = "@forgeax/engine-ecs";
53
+ ECS_PUBLIC_SYMBOLS = Object.freeze([
54
+ "World",
55
+ "Entity",
56
+ "Update",
57
+ "FixedUpdate",
58
+ "Time",
59
+ "FixedTime"
60
+ ]);
61
+ }
62
+ });
63
+
64
+ // src/internal/browser-remote-bridge.ts
65
+ var browser_remote_bridge_exports = {};
66
+ __export(browser_remote_bridge_exports, {
67
+ installBrowserRemoteBridge: () => installBrowserRemoteBridge
68
+ });
69
+ function canonicalRuntimeModule(moduleValue, world) {
70
+ if (moduleValue === null || typeof moduleValue !== "object") return moduleValue;
71
+ const componentsByName = /* @__PURE__ */ new Map();
72
+ for (const [name, component] of world.components.entries()) {
73
+ if (!componentsByName.has(name)) componentsByName.set(name, component);
74
+ }
75
+ const projected = { ...moduleValue };
76
+ for (const [key, value] of Object.entries(projected)) {
77
+ if (value === null || typeof value !== "object" || !("name" in value)) continue;
78
+ const canonical = componentsByName.get(value.name);
79
+ if (canonical !== void 0) projected[key] = canonical;
80
+ }
81
+ return projected;
82
+ }
83
+ function serializeError(error) {
84
+ if (error !== null && typeof error === "object") {
85
+ const e = error;
86
+ const out = {
87
+ code: typeof e.code === "string" ? e.code : "script-runtime-error"
88
+ };
89
+ if (typeof e.expected === "string") out.expected = e.expected;
90
+ if (typeof e.hint === "string") out.hint = e.hint;
91
+ if (e.detail !== void 0) out.detail = e.detail;
92
+ if (out.hint === void 0 && typeof e.message === "string") out.hint = e.message;
93
+ return out;
94
+ }
95
+ return { code: "script-runtime-error", hint: String(error) };
96
+ }
97
+ async function installBrowserRemoteBridge(deps) {
98
+ const { world, renderer, assets, rhiCapture, profiler, execution, port } = deps;
99
+ const mod = await import(
100
+ /* @vite-ignore */
101
+ '@forgeax/engine-remote/execute'
102
+ );
103
+ const executeScript = mod.executeScript;
104
+ const importModule = createEcsImportModule((specifier) => {
105
+ if (specifier === "@forgeax/engine-runtime")
106
+ return Promise.resolve(canonicalRuntimeModule(deps.runtimeModule, world));
107
+ const browserSpecifier = specifier.startsWith("@") ? `/@id/${specifier}` : specifier;
108
+ return import(
109
+ /* @vite-ignore */
110
+ browserSpecifier
111
+ ).then(
112
+ (moduleValue) => canonicalRuntimeModule(moduleValue, world)
113
+ );
114
+ });
115
+ let ws = null;
116
+ let backoff = 1e3;
117
+ let stopped = false;
118
+ const evalQueue = [];
119
+ const drainEvalQueue = () => {
120
+ if (evalQueue.length === 0) return;
121
+ const jobs = evalQueue.splice(0, evalQueue.length);
122
+ for (const job of jobs) {
123
+ const reply = (payload) => {
124
+ try {
125
+ ws?.send(JSON.stringify({ type: "result", id: job.id, payload }));
126
+ } catch {
127
+ }
128
+ };
129
+ void (async () => {
130
+ let res;
131
+ try {
132
+ res = await executeScript(job.code, {
133
+ world,
134
+ renderer,
135
+ assets,
136
+ rhiCapture,
137
+ profiler,
138
+ execution,
139
+ importModule
140
+ });
141
+ } catch (e) {
142
+ reply({
143
+ ok: false,
144
+ error: { code: "BRIDGE_EVAL_THREW", hint: String(e?.message ?? e) }
145
+ });
146
+ return;
147
+ }
148
+ const envelope = res.ok ? { ok: true, value: res.value } : { ok: false, error: serializeError(res.error) };
149
+ try {
150
+ JSON.stringify(envelope);
151
+ reply(envelope);
152
+ } catch {
153
+ reply({ ok: true, value: "[unserializable value \u2014 inspect in the live window]" });
154
+ }
155
+ })();
156
+ }
157
+ };
158
+ world.addSystem(Update, {
159
+ name: "browser-remote-bridge-drain-eval-queue",
160
+ queries: [],
161
+ fn: drainEvalQueue
162
+ }).unwrap();
163
+ const connect = () => {
164
+ if (stopped) return;
165
+ try {
166
+ ws = new WebSocket(`ws://127.0.0.1:${port}/bridge`);
167
+ } catch {
168
+ return;
169
+ }
170
+ ws.addEventListener("open", () => {
171
+ backoff = 1e3;
172
+ });
173
+ ws.addEventListener("message", (ev) => {
174
+ let msg;
175
+ try {
176
+ msg = JSON.parse(typeof ev.data === "string" ? ev.data : "");
177
+ } catch {
178
+ return;
179
+ }
180
+ if (msg?.type !== "eval" || typeof msg.id !== "number" || typeof msg.code !== "string")
181
+ return;
182
+ evalQueue.push({ id: msg.id, code: msg.code });
183
+ });
184
+ const retry = () => {
185
+ ws = null;
186
+ if (stopped) return;
187
+ setTimeout(connect, backoff);
188
+ backoff = Math.min(backoff * 2, 15e3);
189
+ };
190
+ ws.addEventListener("close", retry);
191
+ ws.addEventListener("error", () => {
192
+ try {
193
+ ws?.close();
194
+ } catch {
195
+ }
196
+ });
197
+ };
198
+ connect();
199
+ const teardown = () => {
200
+ if (stopped) return;
201
+ stopped = true;
202
+ world.removeSystem(Update, "browser-remote-bridge-drain-eval-queue");
203
+ const s = ws;
204
+ ws = null;
205
+ if (s) {
206
+ try {
207
+ s.onclose = null;
208
+ s.close();
209
+ } catch {
210
+ }
211
+ }
212
+ };
213
+ const hot = import.meta.hot;
214
+ if (hot) hot.dispose(teardown);
215
+ return teardown;
216
+ }
217
+ var init_browser_remote_bridge = __esm({
218
+ "src/internal/browser-remote-bridge.ts"() {
219
+ init_ecs_import();
220
+ }
221
+ });
222
+ function assembleAssetRuntime(registry, contributions) {
223
+ const leases = [];
224
+ const overrides = new Set(contributions.map((contribution) => contribution.kind.kind));
225
+ const defaults = defaultAssetDecoderContributions ?? [];
226
+ const assembledContributions = [
227
+ ...defaults.filter((contribution) => !overrides.has(contribution.kind.kind)),
228
+ ...contributions
229
+ ];
230
+ try {
231
+ const installedKinds = /* @__PURE__ */ new Set();
232
+ for (const contribution of assembledContributions) {
233
+ if (installedKinds.has(contribution.kind.kind)) {
234
+ throw new TypeError(`duplicate decoder kind "${contribution.kind.kind}"`);
235
+ }
236
+ installedKinds.add(contribution.kind.kind);
237
+ leases.push(
238
+ registry.installDecoder(
239
+ contribution.kind,
240
+ contribution.decoder
241
+ )
242
+ );
243
+ }
244
+ } catch (cause) {
245
+ for (const lease of leases) lease.dispose();
246
+ const failed = assembledContributions[leases.length];
247
+ return err({
248
+ code: "asset-assembly-failed",
249
+ expected: "each owner decoder contribution to install exactly once in the realm registry",
250
+ hint: "remove the conflicting owner contribution and assemble the current realm again",
251
+ detail: { kind: failed?.kind.kind ?? "unknown", cause }
252
+ });
253
+ }
254
+ let disposed = false;
255
+ return ok({
256
+ registry,
257
+ leases: Object.freeze([...leases]),
258
+ dispose() {
259
+ if (disposed) return;
260
+ disposed = true;
261
+ for (const lease of leases) lease.dispose();
262
+ }
263
+ });
264
+ }
265
+
266
+ // src/animation-asset-lookup.ts
267
+ function createAnimationPayloadLookup(source) {
268
+ return (guid) => source?.lookup(guid);
269
+ }
270
+ function syncCameraAspect(world, canvasW, canvasH) {
271
+ if (canvasW <= 0 || canvasH <= 0) return;
272
+ const aspect = canvasW / canvasH;
273
+ const query = world.query({ with: [Camera] }).unwrap();
274
+ for (const row of query) {
275
+ const camera = world.get(row.entity, Camera);
276
+ if (!camera.ok) continue;
277
+ if (camera.value.autoAspect !== true) continue;
278
+ if (camera.value.projection !== CAMERA_PROJECTION_PERSPECTIVE) continue;
279
+ world.set(row.entity, Camera, { aspect });
280
+ }
281
+ }
282
+
283
+ // src/errors.ts
284
+ function summarizeCause(cause) {
285
+ if (cause === null || cause === void 0) return String(cause);
286
+ if (typeof cause !== "object") return String(cause);
287
+ const r = cause;
288
+ const name = typeof r.name === "string" && r.name.length > 0 ? r.name : "Error";
289
+ const code = typeof r.code === "string" && r.code.length > 0 ? r.code : "";
290
+ const message = typeof r.message === "string" ? r.message : "";
291
+ const head = code !== "" ? `${name} ${code}` : name;
292
+ const body = message !== "" ? `: ${message}` : "";
293
+ return `${head}${body}`.replace(/\s*\n+\s*/g, " / ");
294
+ }
295
+ var AppErrorClass = class extends Error {
296
+ code;
297
+ expected;
298
+ hint;
299
+ detail;
300
+ constructor(args) {
301
+ let causeSuffix = "";
302
+ if (args.code === "app-system-update-failed") {
303
+ const d = args.detail;
304
+ const sys = typeof d.systemName === "string" && d.systemName.length > 0 ? ` (system=${d.systemName})` : "";
305
+ causeSuffix = `; cause: ${summarizeCause(d.cause)}${sys}`;
306
+ } else if (args.code === "app-pointer-lock-failed") {
307
+ const d = args.detail;
308
+ causeSuffix = `; path: ${d.path}; cause: ${summarizeCause(d.cause)}`;
309
+ } else if (args.code === "app-plugin-activation-failed") {
310
+ const d = args.detail;
311
+ causeSuffix = `; cause: ${summarizeCause(d.cause)}`;
312
+ } else if (args.code === "app-execution-bootstrap-failed") {
313
+ const d = args.detail;
314
+ causeSuffix = `; phase: ${d.phase}; cause: ${summarizeCause(d.cause)}`;
315
+ } else if (args.code === "app-execution-kernel-failed") {
316
+ const d = args.detail;
317
+ causeSuffix = `; kernel: ${d.kernelName}; cause: ${summarizeCause(d.cause)}`;
318
+ } else if (args.code === "app-execution-rebuild-failed") {
319
+ const d = args.detail;
320
+ causeSuffix = `; cause: ${summarizeCause(d.cause)}`;
321
+ }
322
+ super(`[AppError ${args.code}] expected: ${args.expected}; hint: ${args.hint}${causeSuffix}`);
323
+ this.name = "AppError";
324
+ this.code = args.code;
325
+ this.expected = args.expected;
326
+ this.hint = args.hint;
327
+ this.detail = args.detail;
328
+ }
329
+ };
330
+ var AppError = AppErrorClass;
331
+ var appErrorPolicy = {
332
+ "app-not-started": {
333
+ expected: 'state must be "running" or "paused" to accept stop/pause/resume; "idle" / "stopped" terminal sinks reject',
334
+ hint: "check getState() before calling stop/pause/resume; rebuild the handle via createApp({...}) when the previous one terminated on device-lost"
335
+ },
336
+ "app-already-running": {
337
+ expected: 'state must be "idle" or "paused" to start; "running" handles ignore subsequent start() calls',
338
+ hint: "call stop() first or audit start() call sites; the second start() is a no-op so state is preserved"
339
+ },
340
+ "app-canvas-detached": {
341
+ expected: "canvas.isConnected === true at createApp(canvas) entry",
342
+ hint: "append the canvas to the document tree before calling createApp(canvas), or use the assemble entry createApp({ renderer, world }) when the host already manages canvas lifetime"
343
+ },
344
+ "app-invalid-canvas-pixel-ratio": {
345
+ expected: "maxCanvasPixelRatio is finite and greater than 0",
346
+ hint: "omit maxCanvasPixelRatio to keep the device pixel ratio, or pass a finite value > 0 from the Host policy"
347
+ },
348
+ "app-frame-step-invalid": {
349
+ expected: "stepFrame(deltaSeconds) runs only while the App is paused and deltaSeconds is finite and non-negative",
350
+ hint: "pause the App before deterministic stepping and pass an explicit finite delta; resume after the bounded step sequence completes"
351
+ },
352
+ "app-system-update-failed": {
353
+ expected: "world.update(world), renderer.draw(world), and frame-loop callbacks complete without failure",
354
+ hint: "inspect detail.cause for the original thrown value (EcsError / RhiError / host system bug); detail.systemName names the offending system when the call site can supply it"
355
+ },
356
+ "app-pointer-lock-failed": {
357
+ expected: "pointer-lock request (W3C requestPointerLock or host lockProvider.requestLock) to succeed; failure signals the browser rejected the lock or the host provider threw",
358
+ hint: 'remain in unlocked state; the next trusted click will automatically retry the lock request. inspect detail.path ("w3c" or "provider") and detail.cause to determine the root cause'
359
+ },
360
+ "app-plugin-activation-failed": {
361
+ expected: "every requested Cordis plugin fiber reaches a stable active or pending state",
362
+ hint: "inspect detail.cause and the plugin fiber effects; repair the failing activation before creating the App again"
363
+ },
364
+ "app-execution-tier-unavailable": {
365
+ expected: "the explicitly requested execution tier has every required observed capability and the shipped shared evidence gate",
366
+ hint: 'inspect detail.missingCapabilities and detail.sharedEvidencePassed; use tier="auto" only when an observed fallback is acceptable'
367
+ },
368
+ "app-execution-bootstrap-failed": {
369
+ expected: "the bootstrap URL imports a module whose default export completes as an ExecutionBootstrapEntry in the selected Engine Realm",
370
+ hint: "inspect detail.phase, moduleUrl and cause; export one default ExecutionBootstrapEntry that creates only realm-local engine state"
371
+ },
372
+ "app-execution-deadline-exceeded": {
373
+ expected: "the execution startup, handshake or frame completes within its configured bounded deadline",
374
+ hint: "inspect detail.phase and timeoutMs; the timed-out Worker has been terminated, so fix startup or frame work before creating a new App"
375
+ },
376
+ "app-execution-kernel-failed": {
377
+ expected: "a shared kernel completes every dispatched shard without leaving a possibly partial World write",
378
+ hint: "do not retry or draw the poisoned World; inspect detail.kernelName and cause, then call app.execution.rebuild()"
379
+ },
380
+ "app-execution-stale-world": {
381
+ expected: "every execution message targets the currently active World identity before it can write",
382
+ hint: "discard the late message and keep the current World; inspect expectedIdentity, receivedIdentity and messageKind"
383
+ },
384
+ "app-execution-rebuild-failed": {
385
+ expected: "explicit rebuild disposes the poisoned World and bootstraps a fresh World identity in the surviving Engine Realm",
386
+ hint: "inspect detail.cause; this App remains stopped, so fix the bootstrap failure or create a new App explicitly"
387
+ }
388
+ };
389
+ var APP_EXPECTED = Object.fromEntries(
390
+ Object.entries(appErrorPolicy).map(([code, policy]) => [code, policy.expected])
391
+ );
392
+ var APP_ERROR_HINTS = Object.fromEntries(
393
+ Object.entries(appErrorPolicy).map(([code, policy]) => [code, policy.hint])
394
+ );
395
+ function isAppError(err13) {
396
+ return err13 instanceof AppErrorClass;
397
+ }
398
+ function executionBootstrapHostPlugin(host) {
399
+ return {
400
+ name: "execution-bootstrap-host",
401
+ provide: "executionBootstrapHost",
402
+ apply(ctx) {
403
+ ctx.provide("executionBootstrapHost", host);
404
+ if (host.port !== void 0) {
405
+ ctx.effect(() => () => host.port?.close(), "execution/bootstrap-port");
406
+ }
407
+ }
408
+ };
409
+ }
410
+ function bootstrapError(phase, moduleUrl, cause) {
411
+ return new AppError({
412
+ code: "app-execution-bootstrap-failed",
413
+ expected: APP_EXPECTED["app-execution-bootstrap-failed"],
414
+ hint: APP_ERROR_HINTS["app-execution-bootstrap-failed"],
415
+ detail: { phase, moduleUrl, cause }
416
+ });
417
+ }
418
+ function validateExecutionBootstrapData(data, moduleUrl) {
419
+ if (data === void 0) return ok(void 0);
420
+ try {
421
+ structuredClone(data);
422
+ return ok(void 0);
423
+ } catch (cause) {
424
+ return err(bootstrapError("data", moduleUrl, cause));
425
+ }
426
+ }
427
+ async function loadBootstrapEntry(moduleUrl) {
428
+ let loaded;
429
+ try {
430
+ loaded = await import(
431
+ /* @vite-ignore */
432
+ moduleUrl
433
+ );
434
+ } catch (cause) {
435
+ return err(bootstrapError("import", moduleUrl, cause));
436
+ }
437
+ const entry = loaded.default;
438
+ if (typeof entry !== "function") {
439
+ return err(
440
+ bootstrapError(
441
+ "export",
442
+ moduleUrl,
443
+ new TypeError("default export is not an ExecutionBootstrapEntry function")
444
+ )
445
+ );
446
+ }
447
+ return ok(entry);
448
+ }
449
+ async function prepareBootstrapEntry(moduleUrl, data) {
450
+ const valid = validateExecutionBootstrapData(data, moduleUrl);
451
+ if (!valid.ok) return valid;
452
+ const loaded = await loadBootstrapEntry(moduleUrl);
453
+ if (!loaded.ok) return loaded;
454
+ try {
455
+ const prepared = await loaded.value(data);
456
+ if (typeof prepared !== "object" || prepared === null || prepared.features !== void 0 && !Array.isArray(prepared.features) || prepared.plugins !== void 0 && !Array.isArray(prepared.plugins)) {
457
+ return err(
458
+ bootstrapError(
459
+ "prepare",
460
+ moduleUrl,
461
+ new TypeError("execution bootstrap must return an object with feature and plugin arrays")
462
+ )
463
+ );
464
+ }
465
+ return ok(prepared);
466
+ } catch (cause) {
467
+ return err(bootstrapError("prepare", moduleUrl, cause));
468
+ }
469
+ }
470
+
471
+ // src/execution/types.ts
472
+ var EXECUTION_TIERS = ["main-serial", "engine-worker", "shared"];
473
+ var EXECUTION_REQUESTED_TIERS = ["auto", ...EXECUTION_TIERS];
474
+ var EXECUTION_CAPABILITY_NAMES = [
475
+ "worker",
476
+ "offscreenCanvas",
477
+ "workerAnimationFrame",
478
+ "workerWebGpu",
479
+ "crossOriginIsolated",
480
+ "sharedArrayBuffer",
481
+ "atomicsWait"
482
+ ];
483
+
484
+ // src/execution/capabilities.ts
485
+ function unavailableExecutionCapabilities(reason) {
486
+ return Object.fromEntries(
487
+ EXECUTION_CAPABILITY_NAMES.map((name) => [name, { available: false, reason }])
488
+ );
489
+ }
490
+ function missingExecutionCapabilities(capabilities, required) {
491
+ return required.filter((name) => !capabilities[name].available);
492
+ }
493
+ function probeWorker(timeoutMs) {
494
+ return new Promise((resolve) => {
495
+ const source = `postMessage({workerAnimationFrame:typeof requestAnimationFrame==='function',workerWebGpu:typeof navigator==='object'&&navigator.gpu!==undefined,atomicsWait:typeof Atomics==='object'&&typeof Atomics.wait==='function'})`;
496
+ const url = URL.createObjectURL(new Blob([source], { type: "text/javascript" }));
497
+ const worker = new Worker(url);
498
+ const finish = (result) => {
499
+ worker.terminate();
500
+ URL.revokeObjectURL(url);
501
+ resolve(result);
502
+ };
503
+ const timeout = setTimeout(
504
+ () => finish({ workerAnimationFrame: false, workerWebGpu: false, atomicsWait: false }),
505
+ timeoutMs
506
+ );
507
+ worker.onmessage = (event) => {
508
+ clearTimeout(timeout);
509
+ finish(event.data);
510
+ };
511
+ worker.onerror = () => {
512
+ clearTimeout(timeout);
513
+ finish({ workerAnimationFrame: false, workerWebGpu: false, atomicsWait: false });
514
+ };
515
+ });
516
+ }
517
+ async function probeExecutionCapabilities(canvas, timeoutMs = 2e3) {
518
+ const workerAvailable = typeof Worker === "function";
519
+ const offscreenAvailable = typeof canvas.transferControlToOffscreen === "function";
520
+ const isolated = globalThis.crossOriginIsolated === true;
521
+ const sabAvailable = typeof SharedArrayBuffer === "function";
522
+ const worker = workerAvailable ? await probeWorker(timeoutMs) : { workerAnimationFrame: false, workerWebGpu: false, atomicsWait: false };
523
+ return {
524
+ worker: {
525
+ available: workerAvailable,
526
+ reason: workerAvailable ? "Worker constructor observed" : "Worker constructor unavailable"
527
+ },
528
+ offscreenCanvas: {
529
+ available: offscreenAvailable,
530
+ reason: offscreenAvailable ? "canvas transferControlToOffscreen observed" : "canvas cannot transfer to OffscreenCanvas"
531
+ },
532
+ workerAnimationFrame: {
533
+ available: worker.workerAnimationFrame,
534
+ reason: worker.workerAnimationFrame ? "requestAnimationFrame observed in DedicatedWorker" : "requestAnimationFrame unavailable in DedicatedWorker"
535
+ },
536
+ workerWebGpu: {
537
+ available: worker.workerWebGpu,
538
+ reason: worker.workerWebGpu ? "navigator.gpu observed in DedicatedWorker" : "navigator.gpu unavailable in DedicatedWorker"
539
+ },
540
+ crossOriginIsolated: {
541
+ available: isolated,
542
+ reason: isolated ? "crossOriginIsolated is true" : "crossOriginIsolated is false"
543
+ },
544
+ sharedArrayBuffer: {
545
+ available: sabAvailable,
546
+ reason: sabAvailable ? "SharedArrayBuffer constructor observed" : "SharedArrayBuffer constructor unavailable"
547
+ },
548
+ atomicsWait: {
549
+ available: worker.atomicsWait,
550
+ reason: worker.atomicsWait ? "Atomics.wait observed in DedicatedWorker" : "Atomics.wait unavailable in DedicatedWorker"
551
+ }
552
+ };
553
+ }
554
+
555
+ // src/execution/schema.ts
556
+ var EXECUTION_REPORT_SCHEMA_VERSION = 1;
557
+ function isRecord(value) {
558
+ return value !== null && typeof value === "object" && !Array.isArray(value);
559
+ }
560
+ function hasExactKeys(value, keys) {
561
+ const actual = Object.keys(value);
562
+ return actual.length === keys.length && keys.every((key) => key in value);
563
+ }
564
+ function isMeasurement(value) {
565
+ if (!isRecord(value) || !hasExactKeys(value, ["samples", "p50", "p95", "p99", "jitter"])) {
566
+ return false;
567
+ }
568
+ return Number.isInteger(value.samples) && value.samples > 0 && ["p50", "p95", "p99", "jitter"].every(
569
+ (key) => typeof value[key] === "number" && Number.isFinite(value[key]) && value[key] >= 0
570
+ );
571
+ }
572
+ function isExecutionReport(value) {
573
+ if (!isRecord(value)) return false;
574
+ if (!hasExactKeys(value, [
575
+ "schemaVersion",
576
+ "requestedTier",
577
+ "actualTier",
578
+ "selectionReason",
579
+ "sharedEvidencePassed",
580
+ "capabilities",
581
+ "engine",
582
+ "world",
583
+ "kernelDispatch",
584
+ "performance",
585
+ "audio",
586
+ "fault"
587
+ ])) {
588
+ return false;
589
+ }
590
+ const report = value;
591
+ if (report.schemaVersion !== EXECUTION_REPORT_SCHEMA_VERSION) return false;
592
+ if (!EXECUTION_REQUESTED_TIERS.includes(report.requestedTier)) return false;
593
+ if (report.actualTier !== null && !EXECUTION_TIERS.includes(report.actualTier)) {
594
+ return false;
595
+ }
596
+ if (typeof report.sharedEvidencePassed !== "boolean" || report.capabilities === void 0) {
597
+ return false;
598
+ }
599
+ if (!EXECUTION_CAPABILITY_NAMES.every((name) => {
600
+ const fact = report.capabilities?.[name];
601
+ return typeof fact?.available === "boolean" && typeof fact.reason === "string";
602
+ }))
603
+ return false;
604
+ const engine = report.engine;
605
+ const world = report.world;
606
+ const dispatch = report.kernelDispatch;
607
+ const performance2 = report.performance;
608
+ const audio = report.audio;
609
+ if (!isRecord(engine) || !hasExactKeys(engine, ["realm", "health"])) return false;
610
+ if (!["host", "worker"].includes(engine.realm)) return false;
611
+ if (!["idle", "starting", "running", "stopped", "faulted"].includes(engine.health)) {
612
+ return false;
613
+ }
614
+ if (!isRecord(world) || !hasExactKeys(world, ["identity", "health", "partialWrite", "retryable"])) {
615
+ return false;
616
+ }
617
+ if (world.identity !== null && typeof world.identity !== "string") return false;
618
+ if (!["healthy", "poisoned"].includes(world.health)) return false;
619
+ if (typeof world.partialWrite !== "boolean" || typeof world.retryable !== "boolean") return false;
620
+ if (!isRecord(dispatch) || !hasExactKeys(dispatch, ["eligible", "usedShared", "reason", "dispatched", "completed"])) {
621
+ return false;
622
+ }
623
+ if (typeof dispatch.eligible !== "boolean" || typeof dispatch.usedShared !== "boolean")
624
+ return false;
625
+ if (![
626
+ "no-eligible-kernel",
627
+ "zero-work",
628
+ "small-span",
629
+ "forced-inline",
630
+ "shared",
631
+ "poisoned"
632
+ ].includes(dispatch.reason))
633
+ return false;
634
+ if (!Number.isInteger(dispatch.dispatched) || !Number.isInteger(dispatch.completed)) return false;
635
+ if (!isRecord(performance2) || !hasExactKeys(performance2, ["hostFrameMs", "engineUpdateMs", "kernelWaitMs", "hostAudioMs"]))
636
+ return false;
637
+ for (const key of ["hostFrameMs", "engineUpdateMs", "kernelWaitMs", "hostAudioMs"]) {
638
+ const measurement = performance2[key];
639
+ if (measurement !== null && !isMeasurement(measurement)) return false;
640
+ }
641
+ if (!isRecord(audio) || !hasExactKeys(audio, ["owner", "contextState", "activeSourceCount", "lastError"]))
642
+ return false;
643
+ if (audio.owner !== "host") return false;
644
+ if (!["running", "suspended", "closed"].includes(audio.contextState)) return false;
645
+ if (!Number.isInteger(audio.activeSourceCount) || audio.activeSourceCount < 0)
646
+ return false;
647
+ if (audio.lastError !== null) {
648
+ if (!isRecord(audio.lastError)) return false;
649
+ if (!hasExactKeys(audio.lastError, ["code", "expected", "hint", "detail"])) return false;
650
+ if (typeof audio.lastError.code !== "string" || typeof audio.lastError.expected !== "string" || typeof audio.lastError.hint !== "string")
651
+ return false;
652
+ }
653
+ if (report.fault !== null) {
654
+ if (!isRecord(report.fault)) return false;
655
+ if (!hasExactKeys(report.fault, [
656
+ "source",
657
+ "code",
658
+ "expected",
659
+ "hint",
660
+ "detail",
661
+ "partialWrite",
662
+ "retryable"
663
+ ]))
664
+ return false;
665
+ if (!["bootstrap", "handshake", "runtime", "kernel", "world", "rebuild"].includes(
666
+ report.fault.source
667
+ ))
668
+ return false;
669
+ if (typeof report.fault.code !== "string" || typeof report.fault.expected !== "string" || typeof report.fault.hint !== "string")
670
+ return false;
671
+ if (typeof report.fault.partialWrite !== "boolean" || typeof report.fault.retryable !== "boolean")
672
+ return false;
673
+ }
674
+ return true;
675
+ }
676
+
677
+ // src/execution/report.ts
678
+ function executionAudioReport(state) {
679
+ const error = state?.lastError ?? null;
680
+ return {
681
+ owner: "host",
682
+ contextState: state?.contextState ?? "suspended",
683
+ activeSourceCount: state?.activeSourceCount ?? 0,
684
+ lastError: error === null ? null : {
685
+ code: error.code,
686
+ expected: error.expected,
687
+ hint: error.hint,
688
+ detail: error.detail
689
+ }
690
+ };
691
+ }
692
+ function createExecutionReport(requestedTier, capabilities, selection) {
693
+ return {
694
+ schemaVersion: EXECUTION_REPORT_SCHEMA_VERSION,
695
+ requestedTier,
696
+ actualTier: selection?.actualTier ?? null,
697
+ selectionReason: selection?.selectionReason ?? null,
698
+ sharedEvidencePassed: selection?.sharedEvidencePassed ?? false,
699
+ capabilities,
700
+ engine: {
701
+ realm: selection?.actualTier === "main-serial" ? "host" : "worker",
702
+ health: "idle"
703
+ },
704
+ world: {
705
+ identity: null,
706
+ health: "healthy",
707
+ partialWrite: false,
708
+ retryable: true
709
+ },
710
+ kernelDispatch: {
711
+ eligible: false,
712
+ usedShared: false,
713
+ reason: "no-eligible-kernel",
714
+ dispatched: 0,
715
+ completed: 0
716
+ },
717
+ performance: {
718
+ hostFrameMs: null,
719
+ engineUpdateMs: null,
720
+ kernelWaitMs: null,
721
+ hostAudioMs: null
722
+ },
723
+ audio: executionAudioReport(),
724
+ fault: null
725
+ };
726
+ }
727
+
728
+ // src/execution/control.ts
729
+ function cloneExecutionReport(report) {
730
+ return structuredClone(report);
731
+ }
732
+ function createLocalExecutionControl(initial, providers = {}) {
733
+ let current = cloneExecutionReport(initial);
734
+ return {
735
+ setEngineHealth: (health) => {
736
+ current = { ...current, engine: { ...current.engine, health } };
737
+ },
738
+ report: () => cloneExecutionReport({
739
+ ...current,
740
+ world: providers.world?.() ?? current.world,
741
+ audio: executionAudioReport(providers.audio?.())
742
+ }),
743
+ rebuild: async () => {
744
+ if (providers.rebuild !== void 0) return providers.rebuild();
745
+ return err(
746
+ new AppError({
747
+ code: "app-execution-rebuild-failed",
748
+ expected: APP_EXPECTED["app-execution-rebuild-failed"],
749
+ hint: APP_ERROR_HINTS["app-execution-rebuild-failed"],
750
+ detail: {
751
+ worldIdentity: current.world.identity,
752
+ cause: new Error("This local assembly has no bootstrap module.")
753
+ }
754
+ })
755
+ );
756
+ }
757
+ };
758
+ }
759
+ var ENGINE_WORKER_CAPABILITIES = [
760
+ "worker",
761
+ "offscreenCanvas",
762
+ "workerWebGpu"
763
+ ];
764
+ var SHARED_CAPABILITIES = [
765
+ ...ENGINE_WORKER_CAPABILITIES,
766
+ "crossOriginIsolated",
767
+ "sharedArrayBuffer",
768
+ "atomicsWait"
769
+ ];
770
+ function requiredCapabilities(tier) {
771
+ if (tier === "main-serial") return [];
772
+ if (tier === "engine-worker") return ENGINE_WORKER_CAPABILITIES;
773
+ return SHARED_CAPABILITIES;
774
+ }
775
+ function missingFor(tier, capabilities) {
776
+ return missingExecutionCapabilities(capabilities, requiredCapabilities(tier));
777
+ }
778
+ function selectExecutionTier(input) {
779
+ if (input.requestedTier !== "auto") {
780
+ const missing = missingFor(input.requestedTier, input.capabilities);
781
+ if (missing.length > 0 || input.requestedTier === "shared" && !input.sharedEvidencePassed) {
782
+ return err(
783
+ new AppError({
784
+ code: "app-execution-tier-unavailable",
785
+ expected: APP_EXPECTED["app-execution-tier-unavailable"],
786
+ hint: APP_ERROR_HINTS["app-execution-tier-unavailable"],
787
+ detail: {
788
+ requestedTier: input.requestedTier,
789
+ missingCapabilities: missing,
790
+ sharedEvidencePassed: input.sharedEvidencePassed
791
+ }
792
+ })
793
+ );
794
+ }
795
+ return ok({
796
+ requestedTier: input.requestedTier,
797
+ actualTier: input.requestedTier,
798
+ selectionReason: "explicit-request",
799
+ missingCapabilities: [],
800
+ sharedEvidencePassed: input.sharedEvidencePassed
801
+ });
802
+ }
803
+ const sharedMissing = missingFor("shared", input.capabilities);
804
+ if (sharedMissing.length === 0 && input.sharedEvidencePassed) {
805
+ return ok({
806
+ requestedTier: "auto",
807
+ actualTier: "shared",
808
+ selectionReason: "auto-shared",
809
+ missingCapabilities: [],
810
+ sharedEvidencePassed: true
811
+ });
812
+ }
813
+ const workerMissing = missingFor("engine-worker", input.capabilities);
814
+ if (workerMissing.length === 0) {
815
+ return ok({
816
+ requestedTier: "auto",
817
+ actualTier: "engine-worker",
818
+ selectionReason: "auto-engine-worker",
819
+ missingCapabilities: sharedMissing,
820
+ sharedEvidencePassed: input.sharedEvidencePassed
821
+ });
822
+ }
823
+ return ok({
824
+ requestedTier: "auto",
825
+ actualTier: "main-serial",
826
+ selectionReason: "auto-main-serial",
827
+ missingCapabilities: workerMissing,
828
+ sharedEvidencePassed: input.sharedEvidencePassed
829
+ });
830
+ }
831
+
832
+ // src/internal/error-fanout.ts
833
+ var ErrorFanoutRegistry = class {
834
+ listeners = /* @__PURE__ */ new Set();
835
+ silenceUnhandledErrors;
836
+ constructor(opts = {}) {
837
+ this.silenceUnhandledErrors = opts.silenceUnhandledErrors === true;
838
+ }
839
+ /**
840
+ * Register a listener. Returns an unsubscribe function. Calling
841
+ * unsubscribe more than once is a safe no-op (Set.delete returns
842
+ * false on the second call but does not throw). Re-registering the
843
+ * same listener function is a no-op (Set semantics).
844
+ */
845
+ add(cb) {
846
+ this.listeners.add(cb);
847
+ return () => {
848
+ this.listeners.delete(cb);
849
+ };
850
+ }
851
+ /**
852
+ * Iterate over a snapshot of the listener set so a listener that
853
+ * mutates the set mid-fire does not perturb the iteration. When the
854
+ * set is empty AND silenceUnhandledErrors !== true, call
855
+ * console.error(err) so the unhandled error is visible in devtools
856
+ * (charter P3 explicit failure: silent drop is a footgun).
857
+ */
858
+ fire(err13) {
859
+ if (this.listeners.size === 0) {
860
+ if (!this.silenceUnhandledErrors) {
861
+ console.error(err13);
862
+ }
863
+ return;
864
+ }
865
+ const snapshot = Array.from(this.listeners);
866
+ for (const cb of snapshot) {
867
+ cb(err13);
868
+ }
869
+ }
870
+ /**
871
+ * Number of currently registered listeners. Reserved for diagnostics +
872
+ * test fixtures (e.g. asserting cleanup paths reach the registry); not
873
+ * part of the public app-shell surface.
874
+ *
875
+ * @internal
876
+ */
877
+ _size() {
878
+ return this.listeners.size;
879
+ }
880
+ };
881
+
882
+ // src/types.ts
883
+ var APP_PHASE_CATALOG = [
884
+ "frame-total",
885
+ "draw-source",
886
+ "world-update-primary",
887
+ "world-update-injected",
888
+ "renderer-draw",
889
+ "host-frame",
890
+ "engine-update",
891
+ "kernel-wait",
892
+ "host-audio"
893
+ ];
894
+ function deadlineError(timeoutMs) {
895
+ return new AppError({
896
+ code: "app-execution-deadline-exceeded",
897
+ expected: APP_EXPECTED["app-execution-deadline-exceeded"],
898
+ hint: APP_ERROR_HINTS["app-execution-deadline-exceeded"],
899
+ detail: { phase: "handshake", timeoutMs }
900
+ });
901
+ }
902
+ function startupFault(message, moduleUrl) {
903
+ return new AppError({
904
+ code: "app-execution-bootstrap-failed",
905
+ expected: APP_EXPECTED["app-execution-bootstrap-failed"],
906
+ hint: APP_ERROR_HINTS["app-execution-bootstrap-failed"],
907
+ detail: { phase: "bootstrap", moduleUrl, cause: message.detail }
908
+ });
909
+ }
910
+ async function startEngineWorker(options) {
911
+ const validData = validateExecutionBootstrapData(options.bootstrapData, options.bootstrapUrl);
912
+ if (!validData.ok) return validData;
913
+ const worker = options.workerFactory?.() ?? new Worker(new URL("./engine-worker-runtime.mjs", import.meta.url), {
914
+ type: "module",
915
+ name: "forgeax-engine"
916
+ });
917
+ const listeners = /* @__PURE__ */ new Set();
918
+ let settled = false;
919
+ let resolveReady = () => {
920
+ };
921
+ const readyPromise = new Promise((resolve) => {
922
+ resolveReady = resolve;
923
+ });
924
+ const timeout = setTimeout(() => {
925
+ if (settled) return;
926
+ settled = true;
927
+ worker.terminate();
928
+ resolveReady(err(deadlineError(options.timeoutMs)));
929
+ }, options.timeoutMs);
930
+ worker.onmessage = (event) => {
931
+ const message = event.data;
932
+ if (!settled && message.kind === "ready") {
933
+ settled = true;
934
+ clearTimeout(timeout);
935
+ resolveReady(ok(message));
936
+ } else if (!settled && message.kind === "fault") {
937
+ settled = true;
938
+ clearTimeout(timeout);
939
+ worker.terminate();
940
+ resolveReady(err(startupFault(message, options.bootstrapUrl)));
941
+ }
942
+ for (const listener of listeners) listener(message);
943
+ };
944
+ worker.onerror = (event) => {
945
+ if (settled) {
946
+ const crash = {
947
+ kind: "fault",
948
+ worldIdentity: null,
949
+ source: "runtime",
950
+ code: "worker-crashed",
951
+ expected: "Engine Worker remains alive while the App is running",
952
+ hint: "inspect the Worker error and create a new App; transferred canvas ownership is terminal",
953
+ detail: { message: event.message },
954
+ partialWrite: false,
955
+ retryable: false
956
+ };
957
+ for (const listener of listeners) listener(crash);
958
+ return;
959
+ }
960
+ settled = true;
961
+ clearTimeout(timeout);
962
+ worker.terminate();
963
+ resolveReady(
964
+ err(
965
+ startupFault(
966
+ {
967
+ detail: { message: event.message }},
968
+ options.bootstrapUrl
969
+ )
970
+ )
971
+ );
972
+ };
973
+ const offscreen = options.canvas.transferControlToOffscreen();
974
+ const init = {
975
+ kind: "init",
976
+ canvas: offscreen,
977
+ bootstrapUrl: options.bootstrapUrl,
978
+ ...options.bootstrapData === void 0 ? {} : { bootstrapData: options.bootstrapData },
979
+ ...options.bootstrapPort === void 0 ? {} : { bootstrapPort: options.bootstrapPort },
980
+ ...options.shaderManifestUrl !== void 0 ? { shaderManifestUrl: options.shaderManifestUrl } : {},
981
+ ...options.time !== void 0 ? { time: options.time } : {},
982
+ tier: options.tier
983
+ };
984
+ worker.postMessage(init, [
985
+ offscreen,
986
+ ...options.bootstrapPort === void 0 ? [] : [options.bootstrapPort]
987
+ ]);
988
+ const ready = await readyPromise;
989
+ if (!ready.ok) return ready;
990
+ return ok({
991
+ worker,
992
+ ready: ready.value,
993
+ post(message, transfer = []) {
994
+ worker.postMessage(message, transfer);
995
+ },
996
+ listen(listener) {
997
+ listeners.add(listener);
998
+ return () => listeners.delete(listener);
999
+ },
1000
+ dispose() {
1001
+ worker.postMessage({ kind: "dispose" });
1002
+ worker.terminate();
1003
+ listeners.clear();
1004
+ }
1005
+ });
1006
+ }
1007
+
1008
+ // src/execution/measurement.ts
1009
+ function percentile(sorted, fraction) {
1010
+ const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * fraction) - 1));
1011
+ return sorted[index] ?? 0;
1012
+ }
1013
+ function createMeasurementSeries(capacity = 240) {
1014
+ const values = [];
1015
+ let samples = 0;
1016
+ return {
1017
+ add(value) {
1018
+ samples += 1;
1019
+ values.push(value);
1020
+ if (values.length > capacity) values.shift();
1021
+ const sorted = [...values].sort((a, b) => a - b);
1022
+ let jitter = 0;
1023
+ for (let index = 1; index < values.length; index += 1) {
1024
+ jitter += Math.abs((values[index] ?? 0) - (values[index - 1] ?? 0));
1025
+ }
1026
+ return {
1027
+ samples,
1028
+ p50: percentile(sorted, 0.5),
1029
+ p95: percentile(sorted, 0.95),
1030
+ p99: percentile(sorted, 0.99),
1031
+ jitter: values.length < 2 ? 0 : jitter / (values.length - 1)
1032
+ };
1033
+ },
1034
+ clear() {
1035
+ values.length = 0;
1036
+ samples = 0;
1037
+ }
1038
+ };
1039
+ }
1040
+
1041
+ // src/execution/protocol.ts
1042
+ var FrameCreditLedger = class {
1043
+ constructor(worldIdentity) {
1044
+ this.worldIdentity = worldIdentity;
1045
+ }
1046
+ worldIdentity;
1047
+ nextFrameId = 1;
1048
+ inFlight = null;
1049
+ completed = 0;
1050
+ issue(deltaSeconds, sampleInput, canvasSize) {
1051
+ if (this.inFlight !== null) return void 0;
1052
+ const frameId = this.nextFrameId;
1053
+ this.nextFrameId += 1;
1054
+ this.inFlight = frameId;
1055
+ return {
1056
+ kind: "frame",
1057
+ worldIdentity: this.worldIdentity,
1058
+ frameId,
1059
+ deltaSeconds,
1060
+ inputSample: sampleInput(),
1061
+ canvasWidth: canvasSize.width,
1062
+ canvasHeight: canvasSize.height
1063
+ };
1064
+ }
1065
+ complete(message) {
1066
+ if (message.worldIdentity !== this.worldIdentity) return "stale-world";
1067
+ if (message.frameId <= this.completed) return "duplicate";
1068
+ if (message.frameId !== this.inFlight) return "late";
1069
+ this.completed = message.frameId;
1070
+ this.inFlight = null;
1071
+ return "accepted";
1072
+ }
1073
+ get hasCreditInFlight() {
1074
+ return this.inFlight !== null;
1075
+ }
1076
+ };
1077
+
1078
+ // src/execution/host-controller.ts
1079
+ function lifecycleError(code) {
1080
+ return new AppError({
1081
+ code,
1082
+ expected: APP_EXPECTED[code],
1083
+ hint: APP_ERROR_HINTS[code],
1084
+ detail: {}
1085
+ });
1086
+ }
1087
+ function runtimeError(message) {
1088
+ if (message.code === "shared-kernel-failed" && message.worldIdentity !== null) {
1089
+ return new AppError({
1090
+ code: "app-execution-kernel-failed",
1091
+ expected: APP_EXPECTED["app-execution-kernel-failed"],
1092
+ hint: APP_ERROR_HINTS["app-execution-kernel-failed"],
1093
+ detail: {
1094
+ kernelName: message.detail?.kernelName ?? "unknown",
1095
+ worldIdentity: message.worldIdentity,
1096
+ cause: message.detail,
1097
+ partialWrite: true,
1098
+ retryable: false
1099
+ }
1100
+ });
1101
+ }
1102
+ return new AppError({
1103
+ code: "app-system-update-failed",
1104
+ expected: APP_EXPECTED["app-system-update-failed"],
1105
+ hint: APP_ERROR_HINTS["app-system-update-failed"],
1106
+ detail: { cause: message.detail }
1107
+ });
1108
+ }
1109
+ async function createWorkerExecutionApp(options) {
1110
+ const executionOptions = options.appOptions.execution;
1111
+ if (executionOptions === void 0) throw new Error("execution options are required");
1112
+ const syncCanvas = options.syncCanvas;
1113
+ const bootstrapUrl = new URL(executionOptions.bootstrap, globalThis.location?.href).href;
1114
+ const startupTimeoutMs = executionOptions.startupTimeoutMs ?? 1e4;
1115
+ const frameTimeoutMs = executionOptions.frameTimeoutMs ?? 2e3;
1116
+ const started = await startEngineWorker({
1117
+ canvas: options.canvas,
1118
+ bootstrapUrl,
1119
+ ...executionOptions.bootstrapData === void 0 ? {} : { bootstrapData: executionOptions.bootstrapData },
1120
+ ...executionOptions.bootstrapPort === void 0 ? {} : { bootstrapPort: executionOptions.bootstrapPort },
1121
+ ...options.bundler?.shaderManifestUrl !== void 0 ? { shaderManifestUrl: options.bundler.shaderManifestUrl } : {},
1122
+ ...options.appOptions.time !== void 0 ? { time: options.appOptions.time } : {},
1123
+ timeoutMs: startupTimeoutMs,
1124
+ tier: options.selection.actualTier
1125
+ });
1126
+ if (!started.ok) return started;
1127
+ const session = started.value;
1128
+ const profiler = options.appOptions.profiler;
1129
+ const phaseCatalogRegistration = profiler?.registerPhaseCatalog("app", APP_PHASE_CATALOG);
1130
+ let releasePhaseCatalog = phaseCatalogRegistration?.ok === true ? phaseCatalogRegistration.value : void 0;
1131
+ const fanout = new ErrorFanoutRegistry(
1132
+ options.appOptions.silenceUnhandledErrors === void 0 ? {} : { silenceUnhandledErrors: options.appOptions.silenceUnhandledErrors }
1133
+ );
1134
+ const inputHandle = options.appOptions.input === void 0 ? attachBrowserInputBackend(options.canvas, {
1135
+ ...options.appOptions.uiRoot !== void 0 ? { uiRoot: options.appOptions.uiRoot } : {},
1136
+ ...options.appOptions.pointerLockAllowed !== void 0 ? { pointerLockAllowed: options.appOptions.pointerLockAllowed } : {},
1137
+ ...options.appOptions.virtualJoysticks !== void 0 ? { virtualJoysticks: options.appOptions.virtualJoysticks } : {},
1138
+ ...options.appOptions.lockProvider !== void 0 ? { lockProvider: options.appOptions.lockProvider } : {},
1139
+ onLockError: (detail) => fanout.fire(
1140
+ new AppError({
1141
+ code: "app-pointer-lock-failed",
1142
+ expected: APP_EXPECTED["app-pointer-lock-failed"],
1143
+ hint: APP_ERROR_HINTS["app-pointer-lock-failed"],
1144
+ detail
1145
+ })
1146
+ )
1147
+ }) : void 0;
1148
+ const input = options.appOptions.input ?? inputHandle?.backend ?? {
1149
+ sample: () => ({
1150
+ downKeys: /* @__PURE__ */ new Set(),
1151
+ upKeys: /* @__PURE__ */ new Set(),
1152
+ buttons: [false, false, false],
1153
+ movementX: 0,
1154
+ movementY: 0,
1155
+ wheelDelta: 0,
1156
+ focused: true,
1157
+ pointerLocked: false
1158
+ }),
1159
+ detach: () => {
1160
+ }
1161
+ };
1162
+ let report = {
1163
+ ...createExecutionReport(
1164
+ executionOptions.tier ?? "auto",
1165
+ options.capabilities,
1166
+ options.selection
1167
+ ),
1168
+ engine: { realm: "worker", health: "idle" },
1169
+ world: {
1170
+ identity: session.ready.worldIdentity,
1171
+ health: "healthy",
1172
+ partialWrite: false,
1173
+ retryable: true
1174
+ }
1175
+ };
1176
+ let state = "idle";
1177
+ let ledger = new FrameCreditLedger(session.ready.worldIdentity);
1178
+ let rafId = 0;
1179
+ let lastTimestamp = 0;
1180
+ let frameDeadline;
1181
+ let lastError;
1182
+ let rebuildResolve;
1183
+ let rebuildInFlight;
1184
+ const hostFrameMeasurements = createMeasurementSeries();
1185
+ const engineMeasurements = createMeasurementSeries();
1186
+ const kernelMeasurements = createMeasurementSeries();
1187
+ const audioMeasurements = createMeasurementSeries();
1188
+ let audio = createHostAudioConsumer();
1189
+ let frameSentAt = 0;
1190
+ let profilerCaptureId;
1191
+ let profilerFrameId = 0;
1192
+ let frameProfile = profiler?.activeSession();
1193
+ let profileFrameOpen = false;
1194
+ let hostFrameOpen = false;
1195
+ const setEngineHealth = (health) => {
1196
+ report = { ...report, engine: { ...report.engine, health } };
1197
+ };
1198
+ const finishProfile = () => {
1199
+ if (hostFrameOpen) frameProfile?.endPhase();
1200
+ if (profileFrameOpen) frameProfile?.endFrame();
1201
+ frameProfile = void 0;
1202
+ profileFrameOpen = false;
1203
+ hostFrameOpen = false;
1204
+ try {
1205
+ profiler?.activeSession()?.finish();
1206
+ } catch {
1207
+ }
1208
+ releasePhaseCatalog?.();
1209
+ releasePhaseCatalog = void 0;
1210
+ };
1211
+ const terminalFault = (message) => {
1212
+ if (frameDeadline !== void 0) {
1213
+ clearTimeout(frameDeadline);
1214
+ frameDeadline = void 0;
1215
+ }
1216
+ const error = runtimeError(message);
1217
+ lastError = error;
1218
+ state = message.partialWrite ? "faulted" : "stopped";
1219
+ report = {
1220
+ ...report,
1221
+ engine: { ...report.engine, health: "faulted" },
1222
+ world: {
1223
+ identity: message.worldIdentity,
1224
+ health: message.partialWrite ? "poisoned" : report.world.health,
1225
+ partialWrite: message.partialWrite,
1226
+ retryable: message.retryable
1227
+ },
1228
+ kernelDispatch: {
1229
+ ...report.kernelDispatch,
1230
+ reason: message.partialWrite ? "poisoned" : report.kernelDispatch.reason
1231
+ },
1232
+ fault: {
1233
+ source: message.source,
1234
+ code: message.code,
1235
+ expected: message.expected,
1236
+ hint: message.hint,
1237
+ detail: message.detail,
1238
+ partialWrite: message.partialWrite,
1239
+ retryable: message.retryable
1240
+ }
1241
+ };
1242
+ fanout.fire(error);
1243
+ if (!message.partialWrite) {
1244
+ audio.dispose();
1245
+ session.dispose();
1246
+ }
1247
+ finishProfile();
1248
+ };
1249
+ const scheduleFrame = () => {
1250
+ rafId = requestAnimationFrame((timestamp) => {
1251
+ if (state !== "running") return;
1252
+ const deltaSeconds = lastTimestamp === 0 ? 0 : Math.max(0, (timestamp - lastTimestamp) / 1e3);
1253
+ lastTimestamp = timestamp;
1254
+ const canvasSize = syncCanvas?.() ?? {
1255
+ width: options.canvas.width,
1256
+ height: options.canvas.height
1257
+ };
1258
+ const frame = ledger.issue(deltaSeconds, () => input.sample(), canvasSize);
1259
+ if (frame === void 0) return;
1260
+ frameProfile = profiler?.activeSession();
1261
+ if (frameProfile !== void 0 && profilerCaptureId !== frameProfile.captureId) {
1262
+ profilerCaptureId = frameProfile.captureId;
1263
+ profilerFrameId = 0;
1264
+ }
1265
+ profileFrameOpen = frameProfile?.beginFrame(++profilerFrameId).ok ?? false;
1266
+ hostFrameOpen = profileFrameOpen ? frameProfile?.beginPhase("app", "host-frame").ok ?? false : false;
1267
+ session.post(frame);
1268
+ frameSentAt = performance.now();
1269
+ frameDeadline = setTimeout(() => {
1270
+ if (state !== "running") return;
1271
+ terminalFault({
1272
+ worldIdentity: report.world.identity,
1273
+ source: "runtime",
1274
+ code: "app-execution-deadline-exceeded",
1275
+ expected: APP_EXPECTED["app-execution-deadline-exceeded"],
1276
+ hint: APP_ERROR_HINTS["app-execution-deadline-exceeded"],
1277
+ detail: { phase: "frame", timeoutMs: frameTimeoutMs },
1278
+ partialWrite: false,
1279
+ retryable: false
1280
+ });
1281
+ }, frameTimeoutMs);
1282
+ });
1283
+ };
1284
+ session.listen((message) => {
1285
+ if (message.kind === "frame-complete") {
1286
+ if (ledger.complete(message) !== "accepted") return;
1287
+ if (frameDeadline !== void 0) clearTimeout(frameDeadline);
1288
+ if (hostFrameOpen) frameProfile?.endPhase();
1289
+ if (profileFrameOpen) {
1290
+ frameProfile?.recordSkip({
1291
+ source: "app",
1292
+ phase: "engine-update",
1293
+ reason: `worker-report:${message.engineUpdateMs.toFixed(3)}ms`
1294
+ });
1295
+ frameProfile?.recordSkip({
1296
+ source: "app",
1297
+ phase: "kernel-wait",
1298
+ reason: `worker-report:${message.kernelWaitMs.toFixed(3)}ms`
1299
+ });
1300
+ }
1301
+ const audioStarted = performance.now();
1302
+ const audioProfileOpen = profileFrameOpen ? frameProfile?.beginPhase("app", "host-audio").ok ?? false : false;
1303
+ for (const intent of message.audioIntents ?? []) audio.consume(intent);
1304
+ if (audioProfileOpen) frameProfile?.endPhase();
1305
+ const audioMs = performance.now() - audioStarted;
1306
+ report = {
1307
+ ...report,
1308
+ performance: {
1309
+ ...report.performance,
1310
+ hostFrameMs: hostFrameMeasurements.add(performance.now() - frameSentAt),
1311
+ engineUpdateMs: engineMeasurements.add(message.engineUpdateMs),
1312
+ kernelWaitMs: kernelMeasurements.add(message.kernelWaitMs),
1313
+ hostAudioMs: audioMeasurements.add(audioMs)
1314
+ }
1315
+ };
1316
+ if (message.kernelDispatch !== void 0) {
1317
+ report = { ...report, kernelDispatch: message.kernelDispatch };
1318
+ }
1319
+ if (profileFrameOpen) frameProfile?.endFrame();
1320
+ frameProfile = void 0;
1321
+ profileFrameOpen = false;
1322
+ hostFrameOpen = false;
1323
+ if (state === "running") scheduleFrame();
1324
+ } else if (message.kind === "fault") {
1325
+ terminalFault(message);
1326
+ } else if (message.kind === "rebuilt") {
1327
+ audio.dispose();
1328
+ audio = createHostAudioConsumer();
1329
+ ledger = new FrameCreditLedger(message.worldIdentity);
1330
+ hostFrameMeasurements.clear();
1331
+ engineMeasurements.clear();
1332
+ kernelMeasurements.clear();
1333
+ audioMeasurements.clear();
1334
+ state = "idle";
1335
+ report = {
1336
+ ...report,
1337
+ engine: { ...report.engine, health: "idle" },
1338
+ world: {
1339
+ identity: message.worldIdentity,
1340
+ health: "healthy",
1341
+ partialWrite: false,
1342
+ retryable: true
1343
+ },
1344
+ kernelDispatch: {
1345
+ eligible: false,
1346
+ usedShared: false,
1347
+ reason: "no-eligible-kernel",
1348
+ dispatched: 0,
1349
+ completed: 0
1350
+ },
1351
+ fault: null,
1352
+ performance: {
1353
+ hostFrameMs: null,
1354
+ engineUpdateMs: null,
1355
+ kernelWaitMs: null,
1356
+ hostAudioMs: null
1357
+ }
1358
+ };
1359
+ rebuildResolve?.(ok(cloneExecutionReport(report)));
1360
+ rebuildResolve = void 0;
1361
+ rebuildInFlight = void 0;
1362
+ } else if (message.kind === "host-control") {
1363
+ if (message.command === "set-pointer-lock-allowed") {
1364
+ input.setPointerLockAllowed?.(message.allowed);
1365
+ }
1366
+ }
1367
+ });
1368
+ const execution = {
1369
+ report: () => cloneExecutionReport({
1370
+ ...report,
1371
+ audio: executionAudioReport(audio.state())
1372
+ }),
1373
+ rebuild: () => {
1374
+ if (rebuildInFlight !== void 0) return rebuildInFlight;
1375
+ if (state !== "faulted" || report.world.identity === null) {
1376
+ return Promise.resolve(
1377
+ err(
1378
+ new AppError({
1379
+ code: "app-execution-rebuild-failed",
1380
+ expected: APP_EXPECTED["app-execution-rebuild-failed"],
1381
+ hint: APP_ERROR_HINTS["app-execution-rebuild-failed"],
1382
+ detail: {
1383
+ worldIdentity: report.world.identity,
1384
+ cause: new Error("World is not in a rebuildable poisoned state.")
1385
+ }
1386
+ })
1387
+ )
1388
+ );
1389
+ }
1390
+ rebuildInFlight = new Promise((resolve) => {
1391
+ rebuildResolve = resolve;
1392
+ session.post({ kind: "rebuild", worldIdentity: report.world.identity });
1393
+ setTimeout(() => {
1394
+ if (rebuildResolve !== resolve) return;
1395
+ rebuildResolve = void 0;
1396
+ rebuildInFlight = void 0;
1397
+ resolve(
1398
+ err(
1399
+ new AppError({
1400
+ code: "app-execution-deadline-exceeded",
1401
+ expected: APP_EXPECTED["app-execution-deadline-exceeded"],
1402
+ hint: APP_ERROR_HINTS["app-execution-deadline-exceeded"],
1403
+ detail: { phase: "handshake", timeoutMs: startupTimeoutMs }
1404
+ })
1405
+ )
1406
+ );
1407
+ }, startupTimeoutMs);
1408
+ });
1409
+ return rebuildInFlight;
1410
+ }
1411
+ };
1412
+ const app = {
1413
+ execution,
1414
+ input,
1415
+ start: () => {
1416
+ if (state === "running") return err(lifecycleError("app-already-running"));
1417
+ if (state === "stopped" || state === "faulted") return err(lifecycleError("app-not-started"));
1418
+ state = "running";
1419
+ setEngineHealth("running");
1420
+ lastTimestamp = 0;
1421
+ scheduleFrame();
1422
+ return ok(void 0);
1423
+ },
1424
+ stop: () => {
1425
+ if (state !== "running" && state !== "paused") return err(lifecycleError("app-not-started"));
1426
+ if (state === "running") cancelAnimationFrame(rafId);
1427
+ state = "stopped";
1428
+ inputHandle?.();
1429
+ audio.dispose();
1430
+ session.dispose();
1431
+ setEngineHealth("stopped");
1432
+ finishProfile();
1433
+ return ok(void 0);
1434
+ },
1435
+ pause: () => {
1436
+ if (state !== "running") return err(lifecycleError("app-not-started"));
1437
+ state = "paused";
1438
+ cancelAnimationFrame(rafId);
1439
+ return ok(void 0);
1440
+ },
1441
+ resume: () => {
1442
+ if (state !== "paused") return err(lifecycleError("app-not-started"));
1443
+ state = "running";
1444
+ lastTimestamp = 0;
1445
+ scheduleFrame();
1446
+ return ok(void 0);
1447
+ },
1448
+ onError: (listener) => fanout.add(listener),
1449
+ get lastError() {
1450
+ return lastError;
1451
+ }
1452
+ };
1453
+ return ok(app);
1454
+ }
1455
+
1456
+ // src/internal/assets-world-plugin.ts
1457
+ var RENDER_ASSET_PORT_RESOURCE_KEY = "RenderAssetPort";
1458
+ var ASSET_REGISTRY_RESOURCE_KEY = "AssetRegistry";
1459
+ function rendererAssetsPlugin() {
1460
+ return {
1461
+ name: "renderer-assets",
1462
+ inject: ["renderer"],
1463
+ provide: "assets",
1464
+ apply(ctx) {
1465
+ if (ctx.renderer === void 0) throw new Error("assets require the renderer service");
1466
+ ctx.provide("assets", ctx.renderer.assets);
1467
+ }
1468
+ };
1469
+ }
1470
+ function assetRegistryPlugin(assembly) {
1471
+ return {
1472
+ name: "asset-registry",
1473
+ provide: "assetRegistry",
1474
+ apply(ctx) {
1475
+ ctx.provide("assetRegistry", assembly.registry);
1476
+ ctx.effect(
1477
+ () => () => {
1478
+ assembly.dispose();
1479
+ assembly.registry.dispose();
1480
+ },
1481
+ "assets/registry"
1482
+ );
1483
+ }
1484
+ };
1485
+ }
1486
+ function assetsWorldPlugin() {
1487
+ return {
1488
+ name: "assets-world",
1489
+ inject: ["world", "assets"],
1490
+ apply(ctx) {
1491
+ ctx.effect(() => {
1492
+ ctx.world.insertResource(RENDER_ASSET_PORT_RESOURCE_KEY, ctx.assets);
1493
+ return () => {
1494
+ ctx.world.removeResource(RENDER_ASSET_PORT_RESOURCE_KEY);
1495
+ };
1496
+ }, "assets/world-render-projection");
1497
+ }
1498
+ };
1499
+ }
1500
+ function assetRegistryWorldPlugin() {
1501
+ return {
1502
+ name: "asset-registry-world",
1503
+ inject: ["world", "assetRegistry"],
1504
+ apply(ctx) {
1505
+ ctx.effect(() => {
1506
+ ctx.world.insertResource(ASSET_REGISTRY_RESOURCE_KEY, ctx.assetRegistry);
1507
+ return () => {
1508
+ ctx.world.removeResource(ASSET_REGISTRY_RESOURCE_KEY);
1509
+ };
1510
+ }, "assets/world-registry");
1511
+ }
1512
+ };
1513
+ }
1514
+
1515
+ // src/internal/assembled-engine-profile.ts
1516
+ function assembledEngineProfile(options) {
1517
+ return [
1518
+ rendererPlugin(options.renderer),
1519
+ rendererAssetsPlugin(),
1520
+ ...options.assetAssembly === void 0 ? [] : [assetRegistryPlugin(options.assetAssembly), assetRegistryWorldPlugin()],
1521
+ assetsWorldPlugin(),
1522
+ ...options.extensions ?? []
1523
+ ];
1524
+ }
1525
+ function projectJson(value, seen) {
1526
+ if (value === null) return null;
1527
+ if (typeof value === "boolean" || typeof value === "string") return value;
1528
+ if (typeof value === "number") return Number.isFinite(value) ? value : void 0;
1529
+ if (typeof value !== "object") return void 0;
1530
+ if (seen.has(value)) return void 0;
1531
+ seen.add(value);
1532
+ if (ArrayBuffer.isView(value)) {
1533
+ const values = Array.from(value);
1534
+ const projected = values.map((entry) => projectJson(entry, seen));
1535
+ seen.delete(value);
1536
+ return projected.every((entry) => entry !== void 0) ? projected : void 0;
1537
+ }
1538
+ if (Array.isArray(value)) {
1539
+ const projected = value.map((entry) => projectJson(entry, seen));
1540
+ seen.delete(value);
1541
+ return projected.every((entry) => entry !== void 0) ? projected : void 0;
1542
+ }
1543
+ const object = {};
1544
+ for (const [key, entry] of Object.entries(value)) {
1545
+ const projected = projectJson(entry, seen);
1546
+ if (projected !== void 0) object[key] = projected;
1547
+ }
1548
+ seen.delete(value);
1549
+ return object;
1550
+ }
1551
+ function isJsonRecord(value) {
1552
+ return value !== void 0 && value !== null && typeof value === "object" && !Array.isArray(value);
1553
+ }
1554
+ function projectMeta(meta) {
1555
+ const projected = projectJson(meta, /* @__PURE__ */ new Set());
1556
+ return isJsonRecord(projected) ? projected : {};
1557
+ }
1558
+ function projectField(reflection) {
1559
+ const field = { type: reflection.type };
1560
+ const defaultValue = projectJson(reflection.default, /* @__PURE__ */ new Set());
1561
+ if (defaultValue !== void 0) Object.assign(field, { default: defaultValue });
1562
+ if (reflection.shape !== void 0) Object.assign(field, { shape: reflection.shape });
1563
+ if (reflection.transient !== void 0) Object.assign(field, { transient: reflection.transient });
1564
+ if (reflection.arrayMeta !== void 0) {
1565
+ Object.assign(field, {
1566
+ arrayMeta: {
1567
+ elementType: reflection.arrayMeta.elementType,
1568
+ ...reflection.arrayMeta.length !== void 0 ? { length: reflection.arrayMeta.length } : {}
1569
+ }
1570
+ });
1571
+ }
1572
+ if (reflection.labels !== void 0) Object.assign(field, { labels: { ...reflection.labels } });
1573
+ return Object.freeze(field);
1574
+ }
1575
+ function projectComponent(component) {
1576
+ const definition = componentDefinition(component);
1577
+ const fields = {};
1578
+ for (const [name, reflection] of Object.entries(definition.fields)) {
1579
+ fields[name] = projectField(reflection);
1580
+ }
1581
+ return Object.freeze({
1582
+ name: component.name,
1583
+ schema: Object.freeze({ ...componentSchema(component) }),
1584
+ fields: Object.freeze(fields),
1585
+ meta: Object.freeze(projectMeta(definition.policy.meta))
1586
+ });
1587
+ }
1588
+ function projectComponentIntrospection(components) {
1589
+ return Object.freeze(
1590
+ [...components.values()].sort((left, right) => left.name.localeCompare(right.name)).map(projectComponent)
1591
+ );
1592
+ }
1593
+ function beginFrame(session, frameId) {
1594
+ if (session === void 0) return false;
1595
+ try {
1596
+ return session.beginFrame(frameId).ok;
1597
+ } catch {
1598
+ return false;
1599
+ }
1600
+ }
1601
+ function beginPhase(session, phase) {
1602
+ if (session === void 0) return false;
1603
+ try {
1604
+ return session.beginPhase("app", phase).ok;
1605
+ } catch {
1606
+ return false;
1607
+ }
1608
+ }
1609
+ function endPhase(session) {
1610
+ if (session === void 0) return;
1611
+ try {
1612
+ session.endPhase();
1613
+ } catch {
1614
+ }
1615
+ }
1616
+ function endFrame(session) {
1617
+ if (session === void 0) return;
1618
+ try {
1619
+ session.endFrame();
1620
+ } catch {
1621
+ }
1622
+ }
1623
+ function finishProfilerCapture(profiler) {
1624
+ const session = profiler?.activeSession();
1625
+ if (session === void 0) return;
1626
+ try {
1627
+ session.finish();
1628
+ } catch {
1629
+ }
1630
+ }
1631
+ function makeAppError(code, expected, hint, detail) {
1632
+ return new AppError({ code, expected, hint, detail });
1633
+ }
1634
+ function makeWorldUpdateError(cause) {
1635
+ return makeAppError(
1636
+ "app-system-update-failed",
1637
+ "world.update(deltaSeconds) completes successfully",
1638
+ "check detail.cause for the original structured ECS error",
1639
+ { cause }
1640
+ );
1641
+ }
1642
+ function fireWorldUpdateResult(result, fireError) {
1643
+ if (!result.ok && fireError !== void 0) {
1644
+ fireError(makeWorldUpdateError(result.error));
1645
+ }
1646
+ return result.ok;
1647
+ }
1648
+ function updateInjectedWorlds(worlds, ownWorld, deltaSeconds, fireError, attachedWorlds, updatedWorlds) {
1649
+ for (const injectedWorld of worlds) {
1650
+ if (injectedWorld === ownWorld || updatedWorlds.has(injectedWorld) || !attachedWorlds.has(injectedWorld)) {
1651
+ continue;
1652
+ }
1653
+ try {
1654
+ if (fireWorldUpdateResult(injectedWorld.update(deltaSeconds), fireError)) {
1655
+ updatedWorlds.add(injectedWorld);
1656
+ }
1657
+ } catch (cause) {
1658
+ if (fireError !== void 0) fireError(makeWorldUpdateError(cause));
1659
+ }
1660
+ }
1661
+ }
1662
+ function resolveNow(opts) {
1663
+ if (opts.now !== void 0) return opts.now;
1664
+ return () => {
1665
+ const perf = globalThis.performance;
1666
+ const fn = perf?.now;
1667
+ return typeof fn === "function" ? fn.call(perf) : Date.now();
1668
+ };
1669
+ }
1670
+ function resolveRaf(opts) {
1671
+ if (opts.raf !== void 0) return opts.raf;
1672
+ const g = globalThis;
1673
+ return typeof g.requestAnimationFrame === "function" ? g.requestAnimationFrame.bind(globalThis) : () => 0;
1674
+ }
1675
+ function resolveCaf(opts) {
1676
+ if (opts.caf !== void 0) return opts.caf;
1677
+ const g = globalThis;
1678
+ return typeof g.cancelAnimationFrame === "function" ? g.cancelAnimationFrame.bind(globalThis) : () => {
1679
+ };
1680
+ }
1681
+ function createFrameLoop(opts) {
1682
+ const { world, renderer } = opts;
1683
+ const primaryDrawWorlds = [world];
1684
+ const phaseCatalogRegistration = opts.profiler?.registerPhaseCatalog("app", APP_PHASE_CATALOG);
1685
+ let releasePhaseCatalog = phaseCatalogRegistration?.ok === true ? phaseCatalogRegistration.value : void 0;
1686
+ let drawSource = opts.drawSource;
1687
+ const now = resolveNow(opts);
1688
+ const raf = resolveRaf(opts);
1689
+ const caf = resolveCaf(opts);
1690
+ let state = "idle";
1691
+ let lastTimestamp = 0;
1692
+ let pendingFrameId = 0;
1693
+ let profilerFrameId = 0;
1694
+ let profilerCaptureId;
1695
+ let primaryAttached = false;
1696
+ let activeInjectedWorlds;
1697
+ let nextInjectedWorlds;
1698
+ function attachPrimary(fireError) {
1699
+ if (primaryAttached) return;
1700
+ try {
1701
+ const result = renderer.attachWorld(world);
1702
+ primaryAttached = result.ok;
1703
+ if (!result.ok) fireError(result.error);
1704
+ } catch (cause) {
1705
+ fireError(makeWorldUpdateError(cause));
1706
+ }
1707
+ }
1708
+ function syncInjectedAttachments(worlds, fireError) {
1709
+ if (worlds === void 0) {
1710
+ if (activeInjectedWorlds !== void 0) {
1711
+ for (const attached of activeInjectedWorlds) renderer.detachWorld(attached);
1712
+ activeInjectedWorlds.clear();
1713
+ }
1714
+ return void 0;
1715
+ }
1716
+ const next = nextInjectedWorlds ?? /* @__PURE__ */ new Set();
1717
+ nextInjectedWorlds = next;
1718
+ next.clear();
1719
+ const active = activeInjectedWorlds;
1720
+ for (const candidate of worlds) {
1721
+ if (candidate === world || next.has(candidate)) continue;
1722
+ if (active?.has(candidate) === true) {
1723
+ next.add(candidate);
1724
+ continue;
1725
+ }
1726
+ try {
1727
+ const result = renderer.attachWorld(candidate);
1728
+ if (result.ok) next.add(candidate);
1729
+ else fireError?.(result.error);
1730
+ } catch (cause) {
1731
+ fireError?.(makeWorldUpdateError(cause));
1732
+ }
1733
+ }
1734
+ if (active !== void 0) {
1735
+ for (const attached of active) {
1736
+ if (!next.has(attached)) renderer.detachWorld(attached);
1737
+ }
1738
+ active.clear();
1739
+ }
1740
+ activeInjectedWorlds = next;
1741
+ nextInjectedWorlds = active;
1742
+ return next;
1743
+ }
1744
+ function releaseInjectedAttachments() {
1745
+ syncInjectedAttachments(void 0);
1746
+ }
1747
+ function releaseAttachments() {
1748
+ releaseInjectedAttachments();
1749
+ if (!primaryAttached) return;
1750
+ renderer.detachWorld(world);
1751
+ primaryAttached = false;
1752
+ }
1753
+ function runProfiledPhase(session, phase, action) {
1754
+ const opened = beginPhase(session, phase);
1755
+ try {
1756
+ return action();
1757
+ } finally {
1758
+ if (opened) endPhase(session);
1759
+ }
1760
+ }
1761
+ function runFrame(deltaSeconds) {
1762
+ const session = opts.profiler?.activeSession();
1763
+ let profileFrame;
1764
+ let frameError;
1765
+ let primaryUpdated = false;
1766
+ const reportError = (error) => {
1767
+ frameError ??= error;
1768
+ opts.onError?.(error);
1769
+ };
1770
+ if (session !== void 0) {
1771
+ if (profilerCaptureId !== session.captureId) {
1772
+ profilerCaptureId = session.captureId;
1773
+ profilerFrameId = 0;
1774
+ }
1775
+ const frameId = ++profilerFrameId;
1776
+ if (beginFrame(session, frameId)) {
1777
+ profileFrame = { captureId: session.captureId, frameId };
1778
+ }
1779
+ }
1780
+ runProfiledPhase(session, "frame-total", () => {
1781
+ attachPrimary(reportError);
1782
+ runProfiledPhase(session, "world-update-primary", () => {
1783
+ try {
1784
+ if (fireWorldUpdateResult(world.update(deltaSeconds), reportError)) {
1785
+ primaryUpdated = true;
1786
+ }
1787
+ } catch (cause) {
1788
+ reportError(makeWorldUpdateError(cause));
1789
+ }
1790
+ });
1791
+ let injected;
1792
+ runProfiledPhase(session, "draw-source", () => {
1793
+ if (drawSource === void 0) return;
1794
+ try {
1795
+ injected = drawSource();
1796
+ } catch (cause) {
1797
+ reportError(makeWorldUpdateError(cause));
1798
+ }
1799
+ });
1800
+ const attachedInjectedWorlds = syncInjectedAttachments(injected?.worlds, reportError);
1801
+ const updatedWorlds = injected === void 0 ? void 0 : /* @__PURE__ */ new Set();
1802
+ if (updatedWorlds !== void 0 && primaryUpdated && primaryAttached) {
1803
+ updatedWorlds.add(world);
1804
+ }
1805
+ runProfiledPhase(session, "world-update-injected", () => {
1806
+ if (injected !== void 0 && attachedInjectedWorlds !== void 0 && updatedWorlds !== void 0) {
1807
+ updateInjectedWorlds(
1808
+ injected.worlds,
1809
+ world,
1810
+ deltaSeconds,
1811
+ reportError,
1812
+ attachedInjectedWorlds,
1813
+ updatedWorlds
1814
+ );
1815
+ }
1816
+ });
1817
+ runProfiledPhase(session, "renderer-draw", () => {
1818
+ try {
1819
+ const profileOptions = profileFrame === void 0 ? {} : { profileFrame };
1820
+ let drawResult;
1821
+ if (injected === void 0) {
1822
+ if (!primaryUpdated || !primaryAttached) return;
1823
+ drawResult = renderer.draw(primaryDrawWorlds, {
1824
+ cameraOwner: 0,
1825
+ resourceOwner: 0,
1826
+ ...profileOptions
1827
+ });
1828
+ } else {
1829
+ if (updatedWorlds === void 0) return;
1830
+ const readyWorlds = injected.worlds.filter((candidate) => updatedWorlds.has(candidate));
1831
+ const cameraWorld = injected.worlds[injected.cameraOwner];
1832
+ const resourceWorld = injected.worlds[injected.resourceOwner];
1833
+ const cameraOwner = cameraWorld === void 0 ? -1 : readyWorlds.indexOf(cameraWorld);
1834
+ const resourceOwner = resourceWorld === void 0 ? -1 : readyWorlds.indexOf(resourceWorld);
1835
+ if (readyWorlds.length === 0 || cameraOwner < 0 || resourceOwner < 0) return;
1836
+ drawResult = renderer.draw(readyWorlds, {
1837
+ cameraOwner,
1838
+ resourceOwner,
1839
+ ...profileOptions
1840
+ });
1841
+ }
1842
+ if (drawResult !== void 0) {
1843
+ const result = drawResult;
1844
+ if (!result.ok && result.error !== void 0) {
1845
+ reportError(result.error);
1846
+ }
1847
+ }
1848
+ } catch (cause) {
1849
+ reportError(makeWorldUpdateError(cause));
1850
+ }
1851
+ });
1852
+ });
1853
+ endFrame(session);
1854
+ return frameError === void 0 ? ok(void 0) : err(frameError);
1855
+ }
1856
+ function tick() {
1857
+ if (state !== "running") return;
1858
+ if (renderer.health?.().reason === "device-lost") {
1859
+ pendingFrameId = raf(tick);
1860
+ return;
1861
+ }
1862
+ const timestamp = now();
1863
+ const deltaSeconds = (timestamp - lastTimestamp) / 1e3;
1864
+ lastTimestamp = timestamp;
1865
+ runFrame(deltaSeconds);
1866
+ pendingFrameId = raf(tick);
1867
+ }
1868
+ function releaseProfiler() {
1869
+ finishProfilerCapture(opts.profiler);
1870
+ releasePhaseCatalog?.();
1871
+ releasePhaseCatalog = void 0;
1872
+ }
1873
+ return {
1874
+ setDrawSource(nextDrawSource) {
1875
+ if (drawSource !== nextDrawSource) syncInjectedAttachments(void 0);
1876
+ drawSource = nextDrawSource;
1877
+ },
1878
+ stepFrame(deltaSeconds) {
1879
+ const reason = state !== "paused" ? "state" : !Number.isFinite(deltaSeconds) || deltaSeconds < 0 ? "delta" : void 0;
1880
+ if (reason !== void 0) {
1881
+ return err(
1882
+ makeAppError(
1883
+ "app-frame-step-invalid",
1884
+ 'state is "paused" and deltaSeconds is finite and non-negative',
1885
+ "pause the App and pass a finite non-negative delta before retrying",
1886
+ { state, deltaSeconds, reason }
1887
+ )
1888
+ );
1889
+ }
1890
+ return runFrame(deltaSeconds);
1891
+ },
1892
+ start() {
1893
+ if (state === "running") {
1894
+ return err(
1895
+ makeAppError(
1896
+ "app-already-running",
1897
+ 'state must be "idle" or "paused" to start',
1898
+ "call stop() first or check getState() before retrying",
1899
+ {}
1900
+ )
1901
+ );
1902
+ }
1903
+ if (state === "stopped") {
1904
+ return err(
1905
+ makeAppError(
1906
+ "app-not-started",
1907
+ 'frame-loop is in terminal "stopped" state',
1908
+ "create a new App via createApp({...}); the existing handle is dead",
1909
+ {}
1910
+ )
1911
+ );
1912
+ }
1913
+ lastTimestamp = now();
1914
+ state = "running";
1915
+ pendingFrameId = raf(tick);
1916
+ return ok(void 0);
1917
+ },
1918
+ stop() {
1919
+ if (state === "idle") {
1920
+ return err(
1921
+ makeAppError(
1922
+ "app-not-started",
1923
+ 'state must be "running" to stop',
1924
+ "check getState() before calling stop(); idle handles cannot stop",
1925
+ {}
1926
+ )
1927
+ );
1928
+ }
1929
+ if (state === "stopped") {
1930
+ return err(
1931
+ makeAppError(
1932
+ "app-not-started",
1933
+ 'frame-loop is in terminal "stopped" state',
1934
+ "discard this handle and create a new App",
1935
+ {}
1936
+ )
1937
+ );
1938
+ }
1939
+ caf(pendingFrameId);
1940
+ pendingFrameId = 0;
1941
+ state = "stopped";
1942
+ releaseAttachments();
1943
+ releaseProfiler();
1944
+ return ok(void 0);
1945
+ },
1946
+ pause() {
1947
+ if (state === "paused") return ok(void 0);
1948
+ if (state !== "running") {
1949
+ return err(
1950
+ makeAppError(
1951
+ "app-not-started",
1952
+ 'state must be "running" or "paused" to pause',
1953
+ "call start() first; idle handles cannot pause",
1954
+ {}
1955
+ )
1956
+ );
1957
+ }
1958
+ caf(pendingFrameId);
1959
+ pendingFrameId = 0;
1960
+ state = "paused";
1961
+ return ok(void 0);
1962
+ },
1963
+ resume() {
1964
+ if (state === "idle" || state === "stopped") {
1965
+ return err(
1966
+ makeAppError(
1967
+ "app-not-started",
1968
+ 'state must be "paused" to resume',
1969
+ "call start() first to leave idle; resume() expects an active handle",
1970
+ {}
1971
+ )
1972
+ );
1973
+ }
1974
+ if (state === "running") return ok(void 0);
1975
+ lastTimestamp = now();
1976
+ state = "running";
1977
+ pendingFrameId = raf(tick);
1978
+ return ok(void 0);
1979
+ },
1980
+ getState() {
1981
+ return state;
1982
+ },
1983
+ setStopped() {
1984
+ if (pendingFrameId !== 0) {
1985
+ caf(pendingFrameId);
1986
+ pendingFrameId = 0;
1987
+ }
1988
+ releaseAttachments();
1989
+ releaseProfiler();
1990
+ state = "stopped";
1991
+ }
1992
+ };
1993
+ }
1994
+ function attachInputAuto(canvas, options = {}) {
1995
+ let onLockErrorDispatch;
1996
+ const backendOpts = {
1997
+ ...options.uiRoot ? { uiRoot: options.uiRoot } : {},
1998
+ ...options.pointerLockAllowed ? { pointerLockAllowed: options.pointerLockAllowed } : {},
1999
+ ...options.virtualJoysticks ? { virtualJoysticks: options.virtualJoysticks } : {},
2000
+ ...options.lockProvider ? { lockProvider: options.lockProvider } : {},
2001
+ onLockError: (detail) => {
2002
+ if (onLockErrorDispatch) {
2003
+ const err13 = new AppError({
2004
+ code: "app-pointer-lock-failed",
2005
+ expected: APP_EXPECTED["app-pointer-lock-failed"],
2006
+ hint: APP_ERROR_HINTS["app-pointer-lock-failed"],
2007
+ detail
2008
+ });
2009
+ onLockErrorDispatch(err13);
2010
+ }
2011
+ }
2012
+ };
2013
+ const detach = attachBrowserInputBackend(canvas, backendOpts);
2014
+ const backend = detach.backend;
2015
+ let cleanedUp = false;
2016
+ return {
2017
+ backend,
2018
+ setOnErrorDispatch(fn) {
2019
+ onLockErrorDispatch = fn;
2020
+ },
2021
+ cleanup() {
2022
+ if (cleanedUp) {
2023
+ return;
2024
+ }
2025
+ cleanedUp = true;
2026
+ detach();
2027
+ }
2028
+ };
2029
+ }
2030
+ function inputMapPlugin(configs) {
2031
+ const deduped = /* @__PURE__ */ new Map();
2032
+ for (const config of configs) {
2033
+ if (config.action.length > 0) deduped.set(config.action, config);
2034
+ }
2035
+ const inputMap = Object.freeze([...deduped.values()]);
2036
+ return {
2037
+ name: "input-map",
2038
+ inject: ["world"],
2039
+ apply(ctx) {
2040
+ ctx.effect(() => {
2041
+ ctx.world.insertResource(INPUT_MAP_KEY, inputMap);
2042
+ return () => ctx.world.removeResource(INPUT_MAP_KEY);
2043
+ }, "input/action-map");
2044
+ }
2045
+ };
2046
+ }
2047
+ function inputPlugin() {
2048
+ return {
2049
+ name: "input",
2050
+ inject: ["world", "input"],
2051
+ apply(ctx) {
2052
+ const world = ctx.world;
2053
+ const input = ctx.input;
2054
+ if (input === void 0) throw new Error("Cordis activated input without its provider");
2055
+ ctx.effect(() => {
2056
+ world.insertResource(INPUT_BACKEND_KEY, input);
2057
+ return () => {
2058
+ world.removeResource(INPUT_BACKEND_KEY);
2059
+ };
2060
+ }, "input/resource");
2061
+ ctx.effect(() => {
2062
+ world.addSystems(Update, InputSet, [InputFrameStartScan]).unwrap();
2063
+ return () => world.removeSystem(Update, InputFrameStartScan.name);
2064
+ }, "input/frame-start-scan");
2065
+ }
2066
+ };
2067
+ }
2068
+
2069
+ // src/internal/main-engine-profile.ts
2070
+ function debugDrawPlugin(onReady) {
2071
+ return {
2072
+ name: "debug-draw",
2073
+ inject: ["renderer"],
2074
+ async apply(ctx) {
2075
+ if (ctx.renderer === void 0) throw new Error("debug draw requires the renderer service");
2076
+ try {
2077
+ const debugDraw = await createDebugDrawOnReady(ctx.renderer);
2078
+ onReady(debugDraw);
2079
+ ctx.effect(() => () => releaseDebugDraw(debugDraw), "render/debug-draw");
2080
+ } catch {
2081
+ }
2082
+ }
2083
+ };
2084
+ }
2085
+ function mainEngineProfile(options) {
2086
+ return [
2087
+ ownedRendererPlugin(options.renderer),
2088
+ renderComponentsPlugin(),
2089
+ rendererAssetsPlugin(),
2090
+ ...options.assetAssembly === void 0 ? [] : [assetRegistryPlugin(options.assetAssembly), assetRegistryWorldPlugin()],
2091
+ assetsWorldPlugin(),
2092
+ ...options.input === void 0 ? [] : [
2093
+ options.inputDispose === void 0 ? inputBackendPlugin(options.input) : ownedInputBackendPlugin(options.input, options.inputDispose)
2094
+ ],
2095
+ scenePlugin(),
2096
+ animationPayloadsPlugin(options.animationPayloads),
2097
+ animationRuntimePlugin(),
2098
+ statePlugin(),
2099
+ ...options.onDebugDrawReady === void 0 ? [] : [debugDrawPlugin(options.onDebugDrawReady)],
2100
+ ...options.input === void 0 ? [] : [inputPlugin()],
2101
+ ...options.inputMap === void 0 ? [] : [inputMapPlugin(options.inputMap)],
2102
+ ...options.extensions ?? []
2103
+ ];
2104
+ }
2105
+
2106
+ // src/internal/remote-serve-flag.ts
2107
+ function resolveRemoteServeFlag(isDev, processEnv, isBrowser = false) {
2108
+ if (isBrowser) return false;
2109
+ if (isDev === true) return true;
2110
+ if (processEnv?.FORGEAX_ENGINE_REMOTE_SERVE === "1") return true;
2111
+ return false;
2112
+ }
2113
+
2114
+ // src/internal/remote-server-plugin.ts
2115
+ function remoteServerPlugin(handle) {
2116
+ return {
2117
+ name: "remote-server",
2118
+ apply(ctx) {
2119
+ ctx.effect(() => async () => handle.close(), "remote/server");
2120
+ }
2121
+ };
2122
+ }
2123
+ var captureDrivers = /* @__PURE__ */ new WeakMap();
2124
+ function createRhiCapture(attachment) {
2125
+ let activeCapture;
2126
+ const capture = {
2127
+ captureFrame(options) {
2128
+ const driver = captureDrivers.get(capture);
2129
+ if (driver === void 0) return captureAttachment(attachment, options);
2130
+ if (activeCapture !== void 0) {
2131
+ return Promise.resolve(
2132
+ err(
2133
+ createRhiDebugError("capture-busy", {
2134
+ stage: "capture",
2135
+ cause: "another App capture transaction is active"
2136
+ })
2137
+ )
2138
+ );
2139
+ }
2140
+ const request = captureWithAppFrame(attachment, driver, options);
2141
+ activeCapture = request;
2142
+ void request.then(
2143
+ () => {
2144
+ if (activeCapture === request) activeCapture = void 0;
2145
+ },
2146
+ () => {
2147
+ if (activeCapture === request) activeCapture = void 0;
2148
+ }
2149
+ );
2150
+ return request;
2151
+ }
2152
+ };
2153
+ return capture;
2154
+ }
2155
+ function bindRhiCaptureFrameDriver(capture, driver) {
2156
+ captureDrivers.set(capture, driver);
2157
+ }
2158
+ async function captureAttachment(attachment, options) {
2159
+ const result = await attachment.captureFrame(options);
2160
+ if (!result.ok) return result;
2161
+ return ok(toArtifact(result.value));
2162
+ }
2163
+ async function captureWithAppFrame(attachment, driver, options) {
2164
+ const state = driver.getState();
2165
+ if (state !== "running" && state !== "paused") {
2166
+ return err(
2167
+ createRhiDebugError("capture-unavailable", {
2168
+ stage: "capture",
2169
+ cause: `App capture requires a running or paused frame loop, received '${state}'`
2170
+ })
2171
+ );
2172
+ }
2173
+ if (options?.signal?.aborted) return captureAttachment(attachment, options);
2174
+ const resumeAfter = state === "running";
2175
+ if (resumeAfter) {
2176
+ const paused = driver.pause();
2177
+ if (!paused.ok) {
2178
+ return err(
2179
+ createRhiDebugError("capture-unavailable", {
2180
+ stage: "capture",
2181
+ cause: `App capture could not pause the frame loop: ${describeFailure(paused.error)}`
2182
+ })
2183
+ );
2184
+ }
2185
+ }
2186
+ const controller = new AbortController();
2187
+ const abortFromUser = () => controller.abort();
2188
+ options?.signal?.addEventListener("abort", abortFromUser, { once: true });
2189
+ let captureResult;
2190
+ try {
2191
+ captureResult = attachment.captureFrame({
2192
+ ...options ?? {},
2193
+ signal: controller.signal
2194
+ });
2195
+ } catch (cause) {
2196
+ options?.signal?.removeEventListener("abort", abortFromUser);
2197
+ if (resumeAfter && driver.getState() === "paused") driver.resume();
2198
+ return err(
2199
+ createRhiDebugError("capture-unavailable", {
2200
+ stage: "capture",
2201
+ cause: `App capture could not arm the recorder: ${describeFailure(cause)}`
2202
+ })
2203
+ );
2204
+ }
2205
+ let result;
2206
+ let transactionError;
2207
+ try {
2208
+ const snapshot = await attachment.frameBoundary();
2209
+ if (!snapshot.ok) {
2210
+ result = await captureResult;
2211
+ } else if (options?.signal?.aborted) {
2212
+ controller.abort();
2213
+ result = await captureResult;
2214
+ } else {
2215
+ const stepped = driver.stepFrame(0);
2216
+ if (!stepped.ok) {
2217
+ transactionError = createRhiDebugError("capture-unavailable", {
2218
+ stage: "capture",
2219
+ cause: `App capture frame failed: ${describeFailure(stepped.error)}`
2220
+ });
2221
+ controller.abort();
2222
+ result = await captureResult;
2223
+ } else {
2224
+ result = await captureResult;
2225
+ }
2226
+ }
2227
+ } catch (cause) {
2228
+ transactionError = createRhiDebugError("capture-unavailable", {
2229
+ stage: "capture",
2230
+ cause: `App capture transaction failed: ${describeFailure(cause)}`
2231
+ });
2232
+ controller.abort();
2233
+ result = await captureResult;
2234
+ } finally {
2235
+ options?.signal?.removeEventListener("abort", abortFromUser);
2236
+ if (resumeAfter && driver.getState() === "paused") {
2237
+ const resumed = driver.resume();
2238
+ if (!resumed.ok && transactionError === void 0) {
2239
+ transactionError = createRhiDebugError("capture-unavailable", {
2240
+ stage: "capture",
2241
+ cause: `App capture could not resume the frame loop: ${describeFailure(resumed.error)}`
2242
+ });
2243
+ }
2244
+ }
2245
+ }
2246
+ if (transactionError !== void 0) return err(transactionError);
2247
+ if (result === void 0) result = await captureResult;
2248
+ if (!result.ok) return result;
2249
+ return ok(toArtifact(result.value));
2250
+ }
2251
+ function describeFailure(cause) {
2252
+ if (typeof cause === "string") return cause;
2253
+ if (cause instanceof Error) return cause.message;
2254
+ if (typeof cause === "object" && cause !== null && "hint" in cause) {
2255
+ const hint = cause.hint;
2256
+ if (typeof hint === "string") return hint;
2257
+ }
2258
+ return String(cause);
2259
+ }
2260
+ function createRhiInstrumentation(attachment) {
2261
+ return {
2262
+ resolveSurfaceDevice(device) {
2263
+ const resolved = attachment.backend.unwrapDeviceForSurface(device);
2264
+ if (resolved.ok) return resolved;
2265
+ return err(
2266
+ new RhiError({
2267
+ code: "rhi-not-available",
2268
+ expected: "the recorder can resolve the wrapped surface device",
2269
+ hint: resolved.error.hint
2270
+ })
2271
+ );
2272
+ },
2273
+ onFrameBoundary() {
2274
+ void attachment.frameBoundary();
2275
+ },
2276
+ onDeviceLost() {
2277
+ attachment.deviceLost();
2278
+ }
2279
+ };
2280
+ }
2281
+ function toArtifact(encoded) {
2282
+ return {
2283
+ kind: "rhi-tape",
2284
+ digest: encoded.digest,
2285
+ bytes: encoded.bytes
2286
+ };
2287
+ }
2288
+
2289
+ // src/internal/rhi-debug-flag.ts
2290
+ function resolveRhiDebugFlag(importMetaEnv, processEnv) {
2291
+ return importMetaEnv?.FORGEAX_ENGINE_RHI_DEBUG ?? processEnv?.FORGEAX_ENGINE_RHI_DEBUG;
2292
+ }
2293
+
2294
+ // src/create-app.ts
2295
+ var DEFAULT_ASSET_CATALOG_PATH = "pack-index.json";
2296
+ function defaultAssetCatalogUrl() {
2297
+ const base = typeof document !== "undefined" ? document.baseURI : globalThis.location?.href;
2298
+ return base === void 0 ? DEFAULT_ASSET_CATALOG_PATH : new URL(DEFAULT_ASSET_CATALOG_PATH, base).href;
2299
+ }
2300
+ function assembleAppAssetRuntime(catalog, contributions) {
2301
+ const registry = createAssetRegistry({
2302
+ catalog: catalog ?? createCatalogSource({ url: defaultAssetCatalogUrl() })
2303
+ });
2304
+ const assembled = assembleAssetRuntime(registry, contributions ?? []);
2305
+ if (!assembled.ok) registry.dispose();
2306
+ return assembled;
2307
+ }
2308
+ function makeAppError2(code, expected, hint, detail) {
2309
+ return new AppError({ code, expected, hint, detail });
2310
+ }
2311
+ function normalizeExecutionBootstrapUrl(bootstrap) {
2312
+ const moduleUrl = typeof bootstrap === "string" ? bootstrap : bootstrap.href;
2313
+ try {
2314
+ return ok(new URL(bootstrap, globalThis.location?.href).href);
2315
+ } catch (cause) {
2316
+ return err(
2317
+ makeAppError2(
2318
+ "app-execution-bootstrap-failed",
2319
+ APP_EXPECTED["app-execution-bootstrap-failed"],
2320
+ APP_ERROR_HINTS["app-execution-bootstrap-failed"],
2321
+ { phase: "prepare", moduleUrl, cause }
2322
+ )
2323
+ );
2324
+ }
2325
+ }
2326
+ function canvasAspectPlugin(canvas, maxCanvasPixelRatio) {
2327
+ return {
2328
+ name: "canvas-aspect",
2329
+ inject: ["world"],
2330
+ apply(ctx) {
2331
+ ctx.effect(() => {
2332
+ const system = {
2333
+ name: "app-sync-camera-aspect",
2334
+ queries: [],
2335
+ fn: () => {
2336
+ syncCanvasDrawingBuffer(canvas, maxCanvasPixelRatio);
2337
+ syncCameraAspect(ctx.world, canvas.width, canvas.height);
2338
+ }
2339
+ };
2340
+ ctx.world.addSystem(Update, system).unwrap();
2341
+ return () => ctx.world.removeSystem(Update, system.name).unwrap();
2342
+ }, "app/canvas-aspect");
2343
+ }
2344
+ };
2345
+ }
2346
+ function rhiDebugHostPlugin(dispose) {
2347
+ return {
2348
+ name: "rhi-debug-host",
2349
+ apply(ctx) {
2350
+ ctx.effect(() => dispose, "rhi-debug/host");
2351
+ }
2352
+ };
2353
+ }
2354
+ function createApp(arg, opts, bundler) {
2355
+ if ("tagName" in arg) {
2356
+ return createAppFromCanvas(arg, opts, bundler);
2357
+ }
2358
+ return createAppFromAssemble(arg);
2359
+ }
2360
+ async function createAppFromCanvas(canvas, opts, bundler) {
2361
+ const maxCanvasPixelRatio = opts?.maxCanvasPixelRatio;
2362
+ if (maxCanvasPixelRatio !== void 0 && (!Number.isFinite(maxCanvasPixelRatio) || maxCanvasPixelRatio <= 0)) {
2363
+ return err(
2364
+ makeAppError2(
2365
+ "app-invalid-canvas-pixel-ratio",
2366
+ APP_EXPECTED["app-invalid-canvas-pixel-ratio"],
2367
+ APP_ERROR_HINTS["app-invalid-canvas-pixel-ratio"],
2368
+ { value: maxCanvasPixelRatio }
2369
+ )
2370
+ );
2371
+ }
2372
+ if (!canvas.isConnected) {
2373
+ return err(
2374
+ makeAppError2(
2375
+ "app-canvas-detached",
2376
+ "canvas.isConnected === true at createApp(canvas) entry",
2377
+ "append the canvas to the document tree before calling createApp; or use the assemble entry createApp({ renderer, world }) when the host already manages canvas lifetime",
2378
+ {}
2379
+ )
2380
+ );
2381
+ }
2382
+ const executionOptions = opts?.execution;
2383
+ const normalizedExecutionBootstrap = executionOptions === void 0 ? void 0 : normalizeExecutionBootstrapUrl(executionOptions.bootstrap);
2384
+ if (normalizedExecutionBootstrap?.ok === false) {
2385
+ return err(normalizedExecutionBootstrap.error);
2386
+ }
2387
+ syncCanvasDrawingBuffer(canvas, maxCanvasPixelRatio);
2388
+ if (opts?.execution !== void 0 && normalizedExecutionBootstrap?.ok === true) {
2389
+ const realmBoundOption = [
2390
+ ["features", opts.features],
2391
+ ["plugins", opts.plugins],
2392
+ ["rhi", opts.rhi],
2393
+ ["rawDeviceForContextConfigure", opts.rawDeviceForContextConfigure],
2394
+ ["drawSource", opts.drawSource],
2395
+ ["membershipTiming", opts.membershipTiming],
2396
+ ["assetCatalog", opts.assetCatalog],
2397
+ ["assetDecoders", opts.assetDecoders],
2398
+ ["bundler.importTransport", bundler?.importTransport]
2399
+ ].find(([, value]) => value !== void 0)?.[0];
2400
+ if (realmBoundOption !== void 0) {
2401
+ return err(
2402
+ makeAppError2(
2403
+ "app-execution-bootstrap-failed",
2404
+ APP_EXPECTED["app-execution-bootstrap-failed"],
2405
+ APP_ERROR_HINTS["app-execution-bootstrap-failed"],
2406
+ {
2407
+ phase: "prepare",
2408
+ moduleUrl: normalizedExecutionBootstrap.value,
2409
+ cause: new TypeError(
2410
+ `${realmBoundOption} must be constructed by the execution bootstrap module`
2411
+ )
2412
+ }
2413
+ )
2414
+ );
2415
+ }
2416
+ }
2417
+ let executionContext;
2418
+ let preparedExecutionBootstrap;
2419
+ if (opts?.execution !== void 0 && normalizedExecutionBootstrap?.ok === true) {
2420
+ const executionOptions2 = opts.execution;
2421
+ const capabilities = await probeExecutionCapabilities(canvas);
2422
+ const selected = selectExecutionTier({
2423
+ requestedTier: executionOptions2.tier ?? "auto",
2424
+ capabilities,
2425
+ sharedEvidencePassed: true
2426
+ });
2427
+ if (!selected.ok) return err(selected.error);
2428
+ executionContext = { capabilities, selection: selected.value };
2429
+ if (selected.value.actualTier !== "main-serial") {
2430
+ return createWorkerExecutionApp({
2431
+ canvas,
2432
+ appOptions: opts,
2433
+ syncCanvas: () => measureCanvasDrawingBuffer(canvas, maxCanvasPixelRatio),
2434
+ ...bundler !== void 0 ? { bundler } : {},
2435
+ capabilities,
2436
+ selection: selected.value
2437
+ });
2438
+ }
2439
+ const prepared = await prepareBootstrapEntry(
2440
+ normalizedExecutionBootstrap.value,
2441
+ executionOptions2.bootstrapData
2442
+ );
2443
+ if (!prepared.ok) return err(prepared.error);
2444
+ preparedExecutionBootstrap = prepared.value;
2445
+ }
2446
+ const rendererOpts = {};
2447
+ if (opts?.rhi !== void 0) {
2448
+ Object.assign(rendererOpts, { rhi: opts.rhi });
2449
+ }
2450
+ if (opts?.rhiInstrumentation !== void 0) {
2451
+ Object.assign(rendererOpts, { rhiInstrumentation: opts.rhiInstrumentation });
2452
+ }
2453
+ if (opts?.rawDeviceForContextConfigure !== void 0) {
2454
+ Object.assign(rendererOpts, {
2455
+ rawDeviceForContextConfigure: opts.rawDeviceForContextConfigure
2456
+ });
2457
+ }
2458
+ if (opts?.profiler !== void 0) {
2459
+ Object.assign(rendererOpts, { profiler: opts.profiler });
2460
+ }
2461
+ if (opts?.membershipTiming !== void 0) {
2462
+ Object.assign(rendererOpts, { membershipTiming: opts.membershipTiming });
2463
+ }
2464
+ const rendererFeatures = preparedExecutionBootstrap?.features ?? opts?.features;
2465
+ if (rendererFeatures !== void 0) {
2466
+ Object.assign(rendererOpts, { features: rendererFeatures });
2467
+ }
2468
+ let rhiAttachment;
2469
+ let rhiCapture;
2470
+ let rhiDebugGlobal;
2471
+ const cleanupRhiDebugHost = () => {
2472
+ const host = globalThis;
2473
+ if (host.__forgeax === rhiDebugGlobal) delete host.__forgeax;
2474
+ rhiDebugGlobal = void 0;
2475
+ const attachment = rhiAttachment;
2476
+ rhiAttachment = void 0;
2477
+ if (attachment !== void 0) void attachment.dispose();
2478
+ };
2479
+ const importMetaEnv = typeof import.meta !== "undefined" ? { FORGEAX_ENGINE_RHI_DEBUG: import.meta.env?.FORGEAX_ENGINE_RHI_DEBUG } : void 0;
2480
+ const browserBuildRhiDebugFlag = typeof import.meta !== "undefined" ? import.meta.env?.FORGEAX_ENGINE_RHI_DEBUG : void 0;
2481
+ const processEnv = globalThis.process?.env;
2482
+ const rhiDebugFlag = resolveRhiDebugFlag(importMetaEnv, processEnv);
2483
+ const nav = typeof globalThis !== "undefined" ? globalThis.navigator : void 0;
2484
+ const hasWebGPU = nav !== void 0 && "gpu" in nav && nav.gpu !== void 0;
2485
+ if (opts?.rhi === void 0 && (browserBuildRhiDebugFlag === void 0 || browserBuildRhiDebugFlag === "1") && rhiDebugFlag === "1") {
2486
+ const backend = hasWebGPU ? await import('@forgeax/engine-rhi-webgpu') : await import('@forgeax/engine-rhi-wgpu');
2487
+ if (!hasWebGPU && "ensureReady" in backend) await backend.ensureReady();
2488
+ const pack = loadRhiPack(backend);
2489
+ const debugModule = await import(
2490
+ /* @vite-ignore */
2491
+ '@forgeax/engine-rhi-debug'
2492
+ );
2493
+ if (pack.createShaderModule === void 0) {
2494
+ throw new Error("RHI debug capture requires the backend shader-module entry");
2495
+ }
2496
+ const createShaderModule = pack.createShaderModule;
2497
+ const attached = debugModule.attachRecorder({
2498
+ rhi: pack.rhi,
2499
+ createShaderModule
2500
+ });
2501
+ if (!attached.ok) throw new Error(attached.error.hint);
2502
+ const attachment = attached.value;
2503
+ rhiAttachment = attachment;
2504
+ const capture = createRhiCapture(attachment);
2505
+ rhiCapture = capture;
2506
+ Object.assign(rendererOpts, {
2507
+ rhi: attachment.backend.rhi,
2508
+ rhiInstrumentation: createRhiInstrumentation(attachment)
2509
+ });
2510
+ rhiDebugGlobal = {
2511
+ captureFrame: (options) => capture.captureFrame(options)
2512
+ };
2513
+ globalThis.__forgeax = rhiDebugGlobal;
2514
+ }
2515
+ const assetAssemblyResult = assembleAppAssetRuntime(opts?.assetCatalog, opts?.assetDecoders);
2516
+ if (!assetAssemblyResult.ok) {
2517
+ cleanupRhiDebugHost();
2518
+ return err(assetAssemblyResult.error);
2519
+ }
2520
+ const assetAssembly = assetAssemblyResult.value;
2521
+ let renderer;
2522
+ try {
2523
+ renderer = await createRenderer(canvas, rendererOpts, {
2524
+ ...bundler,
2525
+ assetResolver: getAssetRegistryResolver(assetAssembly.registry)
2526
+ });
2527
+ } catch (e) {
2528
+ cleanupRhiDebugHost();
2529
+ assetAssembly.dispose();
2530
+ assetAssembly.registry.dispose();
2531
+ if (e instanceof EngineEnvironmentError) {
2532
+ return err(e);
2533
+ }
2534
+ throw e;
2535
+ }
2536
+ const shouldStartRemote = resolveRemoteServeFlag(
2537
+ typeof import.meta !== "undefined" ? import.meta.env?.DEV : void 0,
2538
+ globalThis.process?.env,
2539
+ typeof window !== "undefined"
2540
+ );
2541
+ let debugDraw;
2542
+ const world = new World(opts?.time !== void 0 ? { time: opts.time } : {});
2543
+ const inputHandle = opts?.input === void 0 ? attachInputAuto(canvas, {
2544
+ ...opts?.uiRoot ? { uiRoot: opts.uiRoot } : {},
2545
+ ...opts?.pointerLockAllowed ? { pointerLockAllowed: opts.pointerLockAllowed } : {},
2546
+ ...opts?.virtualJoysticks ? { virtualJoysticks: opts.virtualJoysticks } : {},
2547
+ ...opts?.lockProvider ? { lockProvider: opts.lockProvider } : {}
2548
+ }) : void 0;
2549
+ const inputBackend = opts?.input ?? inputHandle?.backend;
2550
+ const userPlugins = preparedExecutionBootstrap === void 0 ? opts?.plugins ?? [] : [
2551
+ executionBootstrapHostPlugin({
2552
+ ...opts?.execution?.bootstrapPort === void 0 ? {} : { port: opts.execution.bootstrapPort },
2553
+ setPointerLockAllowed: (allowed) => inputBackend?.setPointerLockAllowed?.(allowed)
2554
+ }),
2555
+ ...preparedExecutionBootstrap.plugins ?? []
2556
+ ];
2557
+ let pluginContext;
2558
+ try {
2559
+ pluginContext = await createWorldContext(
2560
+ world,
2561
+ mainEngineProfile({
2562
+ renderer,
2563
+ assetAssembly,
2564
+ animationPayloads: createAnimationPayloadLookup(renderer.assets),
2565
+ ...inputBackend === void 0 ? {} : { input: inputBackend },
2566
+ ...inputHandle === void 0 ? {} : { inputDispose: inputHandle.cleanup },
2567
+ ...opts?.inputMap === void 0 ? {} : { inputMap: opts.inputMap },
2568
+ onDebugDrawReady: (value) => {
2569
+ debugDraw = value;
2570
+ },
2571
+ extensions: [
2572
+ rhiDebugHostPlugin(cleanupRhiDebugHost),
2573
+ ...userPlugins,
2574
+ canvasAspectPlugin(canvas, maxCanvasPixelRatio)
2575
+ ]
2576
+ })
2577
+ );
2578
+ } catch (cause) {
2579
+ cleanupRhiDebugHost();
2580
+ assetAssembly.dispose();
2581
+ assetAssembly.registry.dispose();
2582
+ return err(
2583
+ makeAppError2(
2584
+ "app-plugin-activation-failed",
2585
+ APP_EXPECTED["app-plugin-activation-failed"],
2586
+ APP_ERROR_HINTS["app-plugin-activation-failed"],
2587
+ { cause }
2588
+ )
2589
+ );
2590
+ }
2591
+ const audioBackend = pluginContext.audio;
2592
+ const executionControl = createLocalExecutionControl(
2593
+ executionContext === void 0 ? {
2594
+ ...createExecutionReport("main-serial", unavailableExecutionCapabilities("not required")),
2595
+ actualTier: "main-serial",
2596
+ selectionReason: "explicit-request",
2597
+ sharedEvidencePassed: true,
2598
+ engine: { realm: "host", health: "idle" },
2599
+ world: {
2600
+ identity: world.identity,
2601
+ health: world.execution.health,
2602
+ partialWrite: false,
2603
+ retryable: true
2604
+ }
2605
+ } : {
2606
+ ...createExecutionReport(
2607
+ opts?.execution?.tier ?? "auto",
2608
+ executionContext.capabilities,
2609
+ executionContext.selection
2610
+ ),
2611
+ engine: { realm: "host", health: "idle" },
2612
+ world: {
2613
+ identity: world.identity,
2614
+ health: world.execution.health,
2615
+ partialWrite: false,
2616
+ retryable: true
2617
+ }
2618
+ },
2619
+ {
2620
+ ...audioBackend === void 0 ? {} : { audio: () => audioBackend.getState() },
2621
+ world: () => ({
2622
+ identity: world.identity,
2623
+ health: world.execution.health,
2624
+ partialWrite: world.execution.fault?.partialWrite ?? false,
2625
+ retryable: world.execution.fault?.retryable ?? true
2626
+ })
2627
+ }
2628
+ );
2629
+ const remoteHandle = shouldStartRemote ? await startRemoteServer(
2630
+ world,
2631
+ renderer,
2632
+ assetAssembly.registry,
2633
+ rhiCapture,
2634
+ opts?.profiler,
2635
+ executionControl
2636
+ ) : void 0;
2637
+ if (remoteHandle !== void 0) await pluginContext.plugin(remoteServerPlugin(remoteHandle));
2638
+ const buildArgs = {
2639
+ renderer,
2640
+ assets: assetAssembly.registry,
2641
+ world,
2642
+ pluginContext,
2643
+ executionControl,
2644
+ ...inputBackend !== void 0 ? { inputBackend } : {},
2645
+ ...inputHandle === void 0 ? {} : {
2646
+ wireOnLockErrorDispatch: (dispatch) => {
2647
+ inputHandle.setOnErrorDispatch(dispatch);
2648
+ }
2649
+ }
2650
+ };
2651
+ if (audioBackend !== void 0) {
2652
+ Object.assign(buildArgs, {
2653
+ audioBackend
2654
+ });
2655
+ }
2656
+ if (opts?.silenceUnhandledErrors !== void 0) {
2657
+ Object.assign(buildArgs, { silenceUnhandledErrors: opts.silenceUnhandledErrors });
2658
+ }
2659
+ if (opts?.drawSource !== void 0) {
2660
+ Object.assign(buildArgs, { drawSource: opts.drawSource });
2661
+ }
2662
+ if (opts?.profiler !== void 0) {
2663
+ Object.assign(buildArgs, { profiler: opts.profiler });
2664
+ }
2665
+ if (rhiCapture !== void 0) {
2666
+ Object.assign(buildArgs, {
2667
+ rhiCapture,
2668
+ ...rhiAttachment === void 0 ? {} : { rhiAttachment }
2669
+ });
2670
+ }
2671
+ if (debugDraw !== void 0) {
2672
+ Object.assign(buildArgs, { debugDraw });
2673
+ }
2674
+ if (remoteHandle !== void 0) {
2675
+ Object.assign(buildArgs, { remoteHandle });
2676
+ }
2677
+ const built = await buildApp(buildArgs);
2678
+ if (!built.ok) {
2679
+ await pluginContext.fiber.dispose();
2680
+ return built;
2681
+ }
2682
+ if (built.ok) {
2683
+ if (typeof import.meta !== "undefined" && import.meta.env?.DEV === true && import.meta.env?.VITE_FORGEAX_ENGINE_BRIDGE === "1") {
2684
+ const bridgePort = import.meta.env?.VITE_FORGEAX_ENGINE_BRIDGE_PORT ?? "5733";
2685
+ await pluginContext.plugin({
2686
+ name: "browser-remote-bridge",
2687
+ inject: ["world", "renderer", "assetRegistry"],
2688
+ async apply(ctx) {
2689
+ if (ctx.renderer === void 0 || ctx.assetRegistry === void 0) {
2690
+ throw new Error("browser remote bridge requires renderer and assetRegistry services");
2691
+ }
2692
+ try {
2693
+ const bridge = await Promise.resolve().then(() => (init_browser_remote_bridge(), browser_remote_bridge_exports));
2694
+ const teardown = await bridge.installBrowserRemoteBridge({
2695
+ world: ctx.world,
2696
+ renderer: ctx.renderer,
2697
+ assets: ctx.assetRegistry,
2698
+ runtimeModule: engineRuntimeModule,
2699
+ ...rhiCapture !== void 0 ? { rhiCapture } : {},
2700
+ ...opts?.profiler !== void 0 ? { profiler: opts.profiler } : {},
2701
+ execution: built.value.execution,
2702
+ port: bridgePort
2703
+ });
2704
+ ctx.effect(() => teardown, "remote/browser-bridge");
2705
+ } catch {
2706
+ }
2707
+ }
2708
+ });
2709
+ }
2710
+ }
2711
+ return built;
2712
+ }
2713
+ function syncCanvasDrawingBuffer(canvas, maxCanvasPixelRatio) {
2714
+ const size = resolveCanvasDrawingBufferSize(canvas, maxCanvasPixelRatio);
2715
+ if (size === void 0) return { width: canvas.width, height: canvas.height };
2716
+ if (canvas.width !== size.width) canvas.width = size.width;
2717
+ if (canvas.height !== size.height) canvas.height = size.height;
2718
+ return size;
2719
+ }
2720
+ function measureCanvasDrawingBuffer(canvas, maxCanvasPixelRatio) {
2721
+ return resolveCanvasDrawingBufferSize(canvas, maxCanvasPixelRatio) ?? {
2722
+ width: canvas.width,
2723
+ height: canvas.height
2724
+ };
2725
+ }
2726
+ function resolveCanvasDrawingBufferSize(canvas, maxCanvasPixelRatio) {
2727
+ if (!Number.isFinite(canvas.clientWidth) || !Number.isFinite(canvas.clientHeight) || canvas.clientWidth <= 0 || canvas.clientHeight <= 0)
2728
+ return void 0;
2729
+ const hasExplicitCssSize = (canvas.style?.width ?? "") !== "" || (canvas.style?.height ?? "") !== "";
2730
+ if (!hasExplicitCssSize && canvas.clientWidth === canvas.width && canvas.clientHeight === canvas.height) {
2731
+ return { width: canvas.width, height: canvas.height };
2732
+ }
2733
+ const devicePixelRatio = Math.max(1, globalThis.devicePixelRatio || 1);
2734
+ const dpr = maxCanvasPixelRatio === void 0 || !Number.isFinite(maxCanvasPixelRatio) || maxCanvasPixelRatio <= 0 ? devicePixelRatio : Math.min(devicePixelRatio, maxCanvasPixelRatio);
2735
+ const previous = syncedCanvasSizes.get(canvas);
2736
+ const drawingBufferIsTheLayoutMeasurement = previous !== void 0 && canvas.width === previous.drawingWidth && canvas.height === previous.drawingHeight && canvas.clientWidth === previous.drawingWidth && canvas.clientHeight === previous.drawingHeight;
2737
+ const cssWidth = drawingBufferIsTheLayoutMeasurement ? previous.cssWidth : canvas.clientWidth;
2738
+ const cssHeight = drawingBufferIsTheLayoutMeasurement ? previous.cssHeight : canvas.clientHeight;
2739
+ const width = Math.max(1, Math.round(cssWidth * dpr));
2740
+ const height = Math.max(1, Math.round(cssHeight * dpr));
2741
+ syncedCanvasSizes.set(canvas, {
2742
+ cssWidth,
2743
+ cssHeight,
2744
+ drawingWidth: width,
2745
+ drawingHeight: height
2746
+ });
2747
+ return { width, height };
2748
+ }
2749
+ var syncedCanvasSizes = /* @__PURE__ */ new WeakMap();
2750
+ async function startRemoteServer(world, renderer, assets, rhiCapture, profiler, execution) {
2751
+ try {
2752
+ const remoteServerMod = await import(
2753
+ /* @vite-ignore */
2754
+ '@forgeax/engine-remote/server'
2755
+ );
2756
+ const serverResult = await remoteServerMod.startServer({
2757
+ port: 0,
2758
+ host: "127.0.0.1",
2759
+ world,
2760
+ renderer,
2761
+ assets,
2762
+ introspection: projectComponentIntrospection(world.components.entries()),
2763
+ ...rhiCapture !== void 0 ? { rhiCapture } : {},
2764
+ ...profiler !== void 0 ? { profiler } : {},
2765
+ execution
2766
+ });
2767
+ if (serverResult.ok && serverResult.value !== void 0) {
2768
+ return { port: serverResult.value.port, close: serverResult.value.close };
2769
+ }
2770
+ } catch (_error) {
2771
+ }
2772
+ return void 0;
2773
+ }
2774
+ async function createAppFromAssemble(args) {
2775
+ const assetAssemblyResult = assembleAppAssetRuntime(args.assetCatalog, args.assetDecoders);
2776
+ if (!assetAssemblyResult.ok) return err(assetAssemblyResult.error);
2777
+ const assetAssembly = assetAssemblyResult.value;
2778
+ let pluginContext;
2779
+ try {
2780
+ pluginContext = await createWorldContext(
2781
+ args.world,
2782
+ assembledEngineProfile({
2783
+ renderer: args.renderer,
2784
+ assetAssembly,
2785
+ extensions: args.plugins ?? []
2786
+ })
2787
+ );
2788
+ } catch (cause) {
2789
+ assetAssembly.dispose();
2790
+ assetAssembly.registry.dispose();
2791
+ return err(
2792
+ makeAppError2(
2793
+ "app-plugin-activation-failed",
2794
+ APP_EXPECTED["app-plugin-activation-failed"],
2795
+ APP_ERROR_HINTS["app-plugin-activation-failed"],
2796
+ { cause }
2797
+ )
2798
+ );
2799
+ }
2800
+ const shouldStartRemote = resolveRemoteServeFlag(
2801
+ void 0,
2802
+ globalThis.process?.env
2803
+ );
2804
+ const assembledAudioBackend = pluginContext.audio;
2805
+ const executionControl = createLocalExecutionControl(
2806
+ {
2807
+ ...createExecutionReport("main-serial", unavailableExecutionCapabilities("not required")),
2808
+ actualTier: "main-serial",
2809
+ selectionReason: "explicit-request",
2810
+ sharedEvidencePassed: true,
2811
+ engine: { realm: "host", health: "idle" },
2812
+ world: {
2813
+ identity: args.world.identity,
2814
+ health: args.world.execution.health,
2815
+ partialWrite: false,
2816
+ retryable: true
2817
+ }
2818
+ },
2819
+ {
2820
+ ...assembledAudioBackend === void 0 ? {} : { audio: () => assembledAudioBackend.getState() },
2821
+ world: () => ({
2822
+ identity: args.world.identity,
2823
+ health: args.world.execution.health,
2824
+ partialWrite: args.world.execution.fault?.partialWrite ?? false,
2825
+ retryable: args.world.execution.fault?.retryable ?? true
2826
+ })
2827
+ }
2828
+ );
2829
+ const remoteHandle = shouldStartRemote ? await startRemoteServer(
2830
+ args.world,
2831
+ args.renderer,
2832
+ assetAssembly.registry,
2833
+ void 0,
2834
+ args.profiler,
2835
+ executionControl
2836
+ ) : void 0;
2837
+ if (remoteHandle !== void 0) await pluginContext.plugin(remoteServerPlugin(remoteHandle));
2838
+ const buildArgs = {
2839
+ renderer: args.renderer,
2840
+ assets: assetAssembly.registry,
2841
+ world: args.world,
2842
+ pluginContext,
2843
+ executionControl,
2844
+ ...remoteHandle !== void 0 ? { remoteHandle } : {}
2845
+ };
2846
+ const assembledInputBackend = pluginContext.input;
2847
+ if (assembledInputBackend !== void 0) {
2848
+ Object.assign(buildArgs, {
2849
+ inputBackend: assembledInputBackend
2850
+ });
2851
+ }
2852
+ if (assembledAudioBackend !== void 0) {
2853
+ Object.assign(buildArgs, {
2854
+ audioBackend: assembledAudioBackend
2855
+ });
2856
+ }
2857
+ if (args.silenceUnhandledErrors !== void 0) {
2858
+ Object.assign(buildArgs, { silenceUnhandledErrors: args.silenceUnhandledErrors });
2859
+ }
2860
+ if (args.drawSource !== void 0) {
2861
+ Object.assign(buildArgs, { drawSource: args.drawSource });
2862
+ }
2863
+ if (args.profiler !== void 0) {
2864
+ Object.assign(buildArgs, { profiler: args.profiler });
2865
+ }
2866
+ const built = await buildApp(buildArgs);
2867
+ if (!built.ok) await pluginContext.fiber.dispose();
2868
+ return built;
2869
+ }
2870
+ async function buildApp(args) {
2871
+ const {
2872
+ renderer,
2873
+ assets,
2874
+ world,
2875
+ pluginContext,
2876
+ inputBackend,
2877
+ audioBackend,
2878
+ silenceUnhandledErrors,
2879
+ rhiCapture,
2880
+ rhiAttachment,
2881
+ debugDraw,
2882
+ remoteHandle,
2883
+ profiler
2884
+ } = args;
2885
+ function readPhysicsWorld() {
2886
+ return pluginContext.physics;
2887
+ }
2888
+ const fanout = new ErrorFanoutRegistry(
2889
+ silenceUnhandledErrors !== void 0 ? { silenceUnhandledErrors } : {}
2890
+ );
2891
+ const execution = args.executionControl ?? createLocalExecutionControl(
2892
+ {
2893
+ ...createExecutionReport("main-serial", unavailableExecutionCapabilities("not required")),
2894
+ actualTier: "main-serial",
2895
+ selectionReason: "explicit-request",
2896
+ sharedEvidencePassed: true,
2897
+ engine: { realm: "host", health: "idle" },
2898
+ world: {
2899
+ identity: world.identity,
2900
+ health: world.execution.health,
2901
+ partialWrite: world.execution.fault?.partialWrite ?? false,
2902
+ retryable: world.execution.fault?.retryable ?? true
2903
+ }
2904
+ },
2905
+ {
2906
+ ...audioBackend === void 0 ? {} : { audio: () => audioBackend.getState() },
2907
+ world: () => ({
2908
+ identity: world.identity,
2909
+ health: world.execution.health,
2910
+ partialWrite: world.execution.fault?.partialWrite ?? false,
2911
+ retryable: world.execution.fault?.retryable ?? true
2912
+ })
2913
+ }
2914
+ );
2915
+ function dispatch(e) {
2916
+ fanout.fire(e);
2917
+ }
2918
+ if (args.wireOnLockErrorDispatch) {
2919
+ args.wireOnLockErrorDispatch(dispatch);
2920
+ }
2921
+ const loopOpts = {
2922
+ world,
2923
+ renderer,
2924
+ onError: dispatch
2925
+ };
2926
+ if (args.drawSource !== void 0) {
2927
+ Object.assign(loopOpts, { drawSource: args.drawSource });
2928
+ }
2929
+ if (profiler !== void 0) {
2930
+ Object.assign(loopOpts, { profiler });
2931
+ }
2932
+ const loop = createFrameLoop(loopOpts);
2933
+ if (rhiCapture !== void 0 && rhiAttachment !== void 0) {
2934
+ bindRhiCaptureFrameDriver(rhiCapture, {
2935
+ getState: () => loop.getState(),
2936
+ pause: () => {
2937
+ const result = loop.pause();
2938
+ if (result.ok) execution.setEngineHealth("idle");
2939
+ return result;
2940
+ },
2941
+ resume: () => {
2942
+ const result = loop.resume();
2943
+ if (result.ok) execution.setEngineHealth("running");
2944
+ return result;
2945
+ },
2946
+ stepFrame: (deltaSeconds) => loop.stepFrame(deltaSeconds)
2947
+ });
2948
+ }
2949
+ let lastError;
2950
+ let rendererUnsubscribe;
2951
+ let resumeAfterSurfaceRestore = false;
2952
+ function subscribeRendererErrors() {
2953
+ if (rendererUnsubscribe !== void 0) {
2954
+ return;
2955
+ }
2956
+ rendererUnsubscribe = renderer.onError((e) => {
2957
+ if (e?.code === "device-lost") {
2958
+ lastError = e;
2959
+ }
2960
+ dispatch(e);
2961
+ });
2962
+ }
2963
+ function unsubscribeRendererErrors() {
2964
+ if (rendererUnsubscribe !== void 0) {
2965
+ rendererUnsubscribe();
2966
+ rendererUnsubscribe = void 0;
2967
+ }
2968
+ }
2969
+ const stub = {
2970
+ renderer,
2971
+ assets,
2972
+ world,
2973
+ execution,
2974
+ async releaseSurfacePreserveWorld() {
2975
+ if (loop.getState() === "running") {
2976
+ const paused = loop.pause();
2977
+ if (!paused.ok) {
2978
+ return err(
2979
+ new RhiError({
2980
+ code: "rhi-not-available",
2981
+ expected: "running App pauses before releasing its presentation surface",
2982
+ hint: paused.error.hint
2983
+ })
2984
+ );
2985
+ }
2986
+ resumeAfterSurfaceRestore = true;
2987
+ execution.setEngineHealth("idle");
2988
+ }
2989
+ await Promise.resolve();
2990
+ const released = renderer.releaseSurface();
2991
+ if (!released.ok && resumeAfterSurfaceRestore) {
2992
+ loop.resume();
2993
+ resumeAfterSurfaceRestore = false;
2994
+ execution.setEngineHealth("running");
2995
+ }
2996
+ return released.ok ? ok(void 0) : err(released.error);
2997
+ },
2998
+ async restoreSurface() {
2999
+ const restored = renderer.restoreSurface();
3000
+ if (!restored.ok) return err(restored.error);
3001
+ if (resumeAfterSurfaceRestore) {
3002
+ const resumed = loop.resume();
3003
+ if (!resumed.ok) {
3004
+ return err(
3005
+ new RhiError({
3006
+ code: "rhi-not-available",
3007
+ expected: "paused App resumes after restoring its presentation surface",
3008
+ hint: resumed.error.hint
3009
+ })
3010
+ );
3011
+ }
3012
+ resumeAfterSurfaceRestore = false;
3013
+ execution.setEngineHealth("running");
3014
+ }
3015
+ return ok(void 0);
3016
+ },
3017
+ pluginContext,
3018
+ ...inputBackend !== void 0 ? { input: inputBackend } : {},
3019
+ ...audioBackend !== void 0 ? { audio: audioBackend } : {},
3020
+ get physics() {
3021
+ return readPhysicsWorld();
3022
+ },
3023
+ start() {
3024
+ const r = loop.start();
3025
+ if (r.ok) {
3026
+ execution.setEngineHealth("running");
3027
+ subscribeRendererErrors();
3028
+ }
3029
+ return r;
3030
+ },
3031
+ stop() {
3032
+ const r = loop.stop();
3033
+ if (r.ok) execution.setEngineHealth("stopped");
3034
+ unsubscribeRendererErrors();
3035
+ return r;
3036
+ },
3037
+ async dispose() {
3038
+ const state = loop.getState();
3039
+ if (state === "running" || state === "paused") loop.stop();
3040
+ else if (state !== "stopped") loop.setStopped();
3041
+ await pluginContext.fiber.dispose();
3042
+ unsubscribeRendererErrors();
3043
+ execution.setEngineHealth("stopped");
3044
+ return ok(void 0);
3045
+ },
3046
+ pause() {
3047
+ const r = loop.pause();
3048
+ if (r.ok) execution.setEngineHealth("idle");
3049
+ return r;
3050
+ },
3051
+ resume() {
3052
+ const r = loop.resume();
3053
+ if (r.ok) execution.setEngineHealth("running");
3054
+ return r;
3055
+ },
3056
+ stepFrame(deltaSeconds) {
3057
+ return loop.stepFrame(deltaSeconds);
3058
+ },
3059
+ onError(cb) {
3060
+ return fanout.add(cb);
3061
+ },
3062
+ setDrawSource(drawSource) {
3063
+ loop.setDrawSource(drawSource);
3064
+ },
3065
+ /**
3066
+ * Most recent device-loss error retained for host self-inspection.
3067
+ */
3068
+ get lastError() {
3069
+ return lastError;
3070
+ },
3071
+ ...rhiCapture !== void 0 ? { rhiCapture } : {},
3072
+ ...debugDraw !== void 0 ? { debugDraw } : {},
3073
+ ...remoteHandle !== void 0 ? { remote: remoteHandle } : {}
3074
+ };
3075
+ const readyResult = await renderer.ready;
3076
+ if (!readyResult.ok) {
3077
+ return err(readyResult.error);
3078
+ }
3079
+ return ok(stub);
3080
+ }
3081
+ function ensureFallbackCamera(world, aspect = 1) {
3082
+ const cameras = world.query({ with: [Camera] }).unwrap();
3083
+ if (!cameras[Symbol.iterator]().next().done) return void 0;
3084
+ return world.spawn(
3085
+ { component: Transform, data: { pos: [0, 0.6, 5] } },
3086
+ { component: Camera, data: perspective({ fov: Math.PI / 3, aspect, far: 1e3 }) }
3087
+ );
3088
+ }
3089
+
3090
+ // src/game-context.ts
3091
+ function gameHostPlugin(host) {
3092
+ return {
3093
+ name: "game-host",
3094
+ provide: "gameHost",
3095
+ apply(ctx) {
3096
+ ctx.provide("gameHost", host);
3097
+ }
3098
+ };
3099
+ }
3100
+
3101
+ // src/load-game-errors.ts
3102
+ var LoadGameErrorClass = class extends Error {
3103
+ code;
3104
+ expected;
3105
+ hint;
3106
+ detail;
3107
+ constructor(args) {
3108
+ super(`[LoadGameError ${args.code}] expected: ${args.expected}; hint: ${args.hint}`);
3109
+ this.name = "LoadGameError";
3110
+ this.code = args.code;
3111
+ this.expected = args.expected;
3112
+ this.hint = args.hint;
3113
+ this.detail = args.detail;
3114
+ }
3115
+ };
3116
+ var LoadGameError = LoadGameErrorClass;
3117
+ var loadGameErrorPolicy = {
3118
+ "module-not-found": {
3119
+ expected: "resolver should return a module with a native Cordis default export for the given slug",
3120
+ hint: "verify the game slug matches an existing template directory; check the resolver import path for typos"
3121
+ },
3122
+ "invalid-format": {
3123
+ expected: "resolved module must default-export a Cordis function, class, or object plugin",
3124
+ hint: "default-export one plugin and let the Host mount it into App.pluginContext"
3125
+ },
3126
+ "import-failed": {
3127
+ expected: "resolver should complete without throwing; import path, network, and build errors are forwarded here",
3128
+ hint: "inspect detail.cause for the original error (network failure, build error, dynamic import timeout, etc.)"
3129
+ }
3130
+ };
3131
+ var LOAD_GAME_EXPECTED = Object.fromEntries(
3132
+ Object.entries(loadGameErrorPolicy).map(([code, policy]) => [code, policy.expected])
3133
+ );
3134
+ var LOAD_GAME_ERROR_HINTS = Object.fromEntries(
3135
+ Object.entries(loadGameErrorPolicy).map(([code, policy]) => [code, policy.hint])
3136
+ );
3137
+ function isLoadGameError(err13) {
3138
+ return err13 instanceof LoadGameErrorClass;
3139
+ }
3140
+ function isPlugin(value) {
3141
+ return typeof value === "function" || typeof value === "object" && value !== null && "apply" in value && typeof value.apply === "function";
3142
+ }
3143
+ async function loadGame(slug, resolver) {
3144
+ let module;
3145
+ try {
3146
+ module = await resolver(slug);
3147
+ } catch (thrown) {
3148
+ if (thrown instanceof Error && thrown.message.includes(slug)) {
3149
+ return err(
3150
+ new LoadGameError({
3151
+ code: "module-not-found",
3152
+ expected: LOAD_GAME_EXPECTED["module-not-found"],
3153
+ hint: LOAD_GAME_ERROR_HINTS["module-not-found"],
3154
+ detail: { slug }
3155
+ })
3156
+ );
3157
+ }
3158
+ return err(
3159
+ new LoadGameError({
3160
+ code: "import-failed",
3161
+ expected: LOAD_GAME_EXPECTED["import-failed"],
3162
+ hint: LOAD_GAME_ERROR_HINTS["import-failed"],
3163
+ detail: { cause: thrown }
3164
+ })
3165
+ );
3166
+ }
3167
+ if (!isPlugin(module.default)) {
3168
+ const exportKeys = Object.keys(module);
3169
+ return err(
3170
+ new LoadGameError({
3171
+ code: "invalid-format",
3172
+ expected: LOAD_GAME_EXPECTED["invalid-format"],
3173
+ hint: LOAD_GAME_ERROR_HINTS["invalid-format"],
3174
+ detail: { exportKeys }
3175
+ })
3176
+ );
3177
+ }
3178
+ return ok(module.default);
3179
+ }
3180
+ function hasWebGpu() {
3181
+ const nav = globalThis.navigator;
3182
+ return nav?.gpu !== void 0;
3183
+ }
3184
+ async function loadBackend() {
3185
+ const backend = hasWebGpu() ? await import('@forgeax/engine-rhi-webgpu') : await import('@forgeax/engine-rhi-wgpu');
3186
+ if (!hasWebGpu()) await backend.ensureReady?.();
3187
+ return backend;
3188
+ }
3189
+ async function createBrowserRhiDebugRuntime() {
3190
+ const backend = await loadBackend();
3191
+ const attached = attachRecorder(backend);
3192
+ if (!attached.ok) throw new Error(attached.error.hint);
3193
+ const attachment = attached.value;
3194
+ return {
3195
+ rhi: attachment.backend.rhi,
3196
+ attachment,
3197
+ createShaderModule: backend.createShaderModule,
3198
+ rhiInstrumentation: createRhiInstrumentation(attachment),
3199
+ createReplayDevice: async () => {
3200
+ const adapter = await backend.rhi.requestAdapter();
3201
+ if (!adapter.ok) return adapter;
3202
+ return adapter.value.requestDevice();
3203
+ },
3204
+ attachRenderer(_renderer) {
3205
+ return () => {
3206
+ };
3207
+ }
3208
+ };
3209
+ }
3210
+ function createToolPreviewRecipe(options) {
3211
+ if (options.backend !== void 0 && options.backend !== "webgpu") {
3212
+ throw new TypeError("tool preview requires the real WebGPU backend");
3213
+ }
3214
+ const viewport = options.viewport ?? { width: 640, height: 360 };
3215
+ if (!Number.isSafeInteger(viewport.width) || !Number.isSafeInteger(viewport.height) || viewport.width <= 0 || viewport.height <= 0) {
3216
+ throw new TypeError("tool preview viewport must contain positive safe integers");
3217
+ }
3218
+ const deltaSeconds = options.deltaSeconds ?? 1 / 60;
3219
+ const frames = options.frames ?? 1;
3220
+ if (!Number.isFinite(deltaSeconds) || deltaSeconds < 0) {
3221
+ throw new TypeError("tool preview deltaSeconds must be finite and non-negative");
3222
+ }
3223
+ if (!Number.isSafeInteger(frames) || frames <= 0) {
3224
+ throw new TypeError("tool preview frames must be a positive safe integer");
3225
+ }
3226
+ const actions = (options.actions ?? []).map((action) => ({ ...action }));
3227
+ for (const action of actions) {
3228
+ if (!Number.isSafeInteger(action.frame) || action.frame < 0 || action.frame >= frames || action.name.length === 0 || action.value !== void 0 && !isSerializableValue(action.value)) {
3229
+ throw new TypeError("tool preview actions require a valid frame, name, and JSON value");
3230
+ }
3231
+ }
3232
+ return {
3233
+ backend: "webgpu",
3234
+ presentation: options.presentation ?? "hidden",
3235
+ viewport: { width: viewport.width, height: viewport.height },
3236
+ actions,
3237
+ deltaSeconds,
3238
+ frames
3239
+ };
3240
+ }
3241
+ var REQUIRED_EVENTS = [
3242
+ "canvas-created",
3243
+ "rhi-debug-armed",
3244
+ "renderer-created",
3245
+ "world-updated",
3246
+ "draw-submitted",
3247
+ "validation-complete"
3248
+ ];
3249
+ function validateToolPreviewTrace(trace) {
3250
+ if (trace.backend !== "webgpu") {
3251
+ return {
3252
+ ok: false,
3253
+ error: {
3254
+ code: "tool-preview-invalid-trace",
3255
+ expected: 'trace.backend === "webgpu"',
3256
+ hint: "run the same recipe with a real WebGPU adapter; RHI-null is not a preview backend",
3257
+ detail: { missing: "webgpu-backend", index: -1 }
3258
+ }
3259
+ };
3260
+ }
3261
+ let last = -1;
3262
+ for (const event of REQUIRED_EVENTS) {
3263
+ const index = trace.events.indexOf(event);
3264
+ if (index <= last) {
3265
+ return {
3266
+ ok: false,
3267
+ error: {
3268
+ code: "tool-preview-invalid-trace",
3269
+ expected: `event '${event}' occurs after the previous recipe phase`,
3270
+ hint: "arm RHI-debug before Renderer construction and retain update/draw/validation evidence",
3271
+ detail: { missing: event, index }
3272
+ }
3273
+ };
3274
+ }
3275
+ last = index;
3276
+ }
3277
+ return { ok: true, value: trace };
3278
+ }
3279
+
3280
+ // src/tool-preview/bootstrap.ts
3281
+ function layoutCanvas(canvas, recipe) {
3282
+ canvas.width = recipe.viewport.width;
3283
+ canvas.height = recipe.viewport.height;
3284
+ canvas.style.position = "fixed";
3285
+ canvas.style.inset = "0";
3286
+ canvas.style.width = `${recipe.viewport.width}px`;
3287
+ canvas.style.height = `${recipe.viewport.height}px`;
3288
+ canvas.style.opacity = "1";
3289
+ canvas.style.visibility = "visible";
3290
+ canvas.style.pointerEvents = "auto";
3291
+ }
3292
+ function appendCanvas(canvas, root) {
3293
+ if (canvas.isConnected) return;
3294
+ const parent = typeof Document !== "undefined" && root instanceof Document ? root.body : root ?? (typeof document === "undefined" ? void 0 : document.body);
3295
+ parent?.appendChild(canvas);
3296
+ }
3297
+ function removeOwnedCanvas(canvas, owned) {
3298
+ if (owned && canvas.isConnected) canvas.remove();
3299
+ }
3300
+ function bytesToDataUri(bytes, mediaType) {
3301
+ let binary = "";
3302
+ for (let offset = 0; offset < bytes.length; offset += 8192) {
3303
+ binary += String.fromCharCode(...bytes.subarray(offset, offset + 8192));
3304
+ }
3305
+ return `data:${mediaType};base64,${btoa(binary)}`;
3306
+ }
3307
+ async function sha256(bytes) {
3308
+ const digest = await crypto.subtle.digest("SHA-256", new Uint8Array(bytes).buffer);
3309
+ return `sha256:${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("")}`;
3310
+ }
3311
+ function dataUriBytes(uri) {
3312
+ const encoded = uri.slice(uri.indexOf(",") + 1);
3313
+ const binary = atob(encoded);
3314
+ return Uint8Array.from(binary, (char) => char.charCodeAt(0));
3315
+ }
3316
+ function generateRunId() {
3317
+ return `preview-${crypto.randomUUID()}`;
3318
+ }
3319
+ function countDrawCalls(tape) {
3320
+ return buildTapeIndex(tape).works.filter((work) => isRenderDraw(work.kind)).length;
3321
+ }
3322
+ function lastRenderDrawIndex(tape) {
3323
+ const works = buildTapeIndex(tape).works;
3324
+ return works.reduce((last, work) => isRenderDraw(work.kind) ? work.workIndex : last, -1);
3325
+ }
3326
+ function isRenderDraw(kind) {
3327
+ return kind === "draw" || kind === "drawIndexed" || kind === "drawIndirect" || kind === "drawIndexedIndirect";
3328
+ }
3329
+ function resourceSubjectDigest(resource) {
3330
+ if (resource === void 0) return void 0;
3331
+ const asset = typeof resource.asset === "object" && resource.asset !== null ? resource.asset : void 0;
3332
+ const candidates = [
3333
+ resource.observation?.subjectDigest,
3334
+ resource.digest,
3335
+ asset?.digest,
3336
+ asset?.programFingerprint
3337
+ ];
3338
+ return candidates.find((value) => typeof value === "string" && value.length > 0);
3339
+ }
3340
+ async function replayToolPreviewCapture(capture) {
3341
+ const analyzeStartedAtMs = performance.now();
3342
+ const tapeBytes = dataUriBytes(capture.tape.jsonUri);
3343
+ const parsed = decodeTape(tapeBytes);
3344
+ if (!parsed.ok) {
3345
+ return err({
3346
+ code: "tool-preview-bootstrap-failed",
3347
+ expected: "the finalized tape to deserialize for fresh-device replay",
3348
+ hint: parsed.error.hint,
3349
+ detail: { phase: "tape-parse", cause: parsed.error }
3350
+ });
3351
+ }
3352
+ const subjectDigest = resourceSubjectDigest(capture.resource);
3353
+ if (capture.resource !== void 0 && subjectDigest === void 0) {
3354
+ return err({
3355
+ code: "tool-preview-bootstrap-failed",
3356
+ expected: "the resource owner to publish a subject identity before evidence publication",
3357
+ hint: "repair AssetRegistry owner facts or post-frame observation; recipe and trace are not subject identity",
3358
+ detail: { phase: "resource-identity" }
3359
+ });
3360
+ }
3361
+ let runtime;
3362
+ try {
3363
+ runtime = await createBrowserRhiDebugRuntime();
3364
+ } catch (cause) {
3365
+ return err({
3366
+ code: "tool-preview-capability-unavailable",
3367
+ expected: "a fresh browser WebGPU runtime for offline replay",
3368
+ hint: "Run replay in an isolated browser process; do not reuse the capture device.",
3369
+ detail: { phase: "replay-runtime", cause }
3370
+ });
3371
+ }
3372
+ const replayDevice = await runtime.createReplayDevice();
3373
+ if (!replayDevice.ok) {
3374
+ return err({
3375
+ code: "tool-preview-capability-unavailable",
3376
+ expected: "a fresh WebGPU device for offline replay",
3377
+ hint: "the capture device cannot be reused as a substitute for fresh-device replay",
3378
+ detail: { phase: "replay-device", cause: replayDevice.error }
3379
+ });
3380
+ }
3381
+ const replayResult = await openReplay(parsed.value, {
3382
+ device: replayDevice.value,
3383
+ createShaderModule: runtime.createShaderModule
3384
+ });
3385
+ if (!replayResult.ok) {
3386
+ return err({
3387
+ code: "tool-preview-bootstrap-failed",
3388
+ expected: "the self-contained tape to replay on a fresh WebGPU device",
3389
+ hint: replayResult.error.hint,
3390
+ detail: { phase: "replay-create", cause: replayResult.error }
3391
+ });
3392
+ }
3393
+ const replay = replayResult.value;
3394
+ const drawCalls = countDrawCalls(parsed.value);
3395
+ if (drawCalls === 0) {
3396
+ await replay.dispose();
3397
+ return err({
3398
+ code: "tool-preview-bootstrap-failed",
3399
+ expected: "the preview recipe to submit at least one render draw",
3400
+ hint: "seed a visible mesh and camera before capturing the preview frame",
3401
+ detail: { phase: "replay-draws", cause: { appErrors: capture.appErrors } }
3402
+ });
3403
+ }
3404
+ const committedDrawIndex = lastRenderDrawIndex(parsed.value);
3405
+ if (committedDrawIndex < 0) {
3406
+ await replay.dispose();
3407
+ return err({
3408
+ code: "tool-preview-bootstrap-failed",
3409
+ expected: "the preview tape to contain a render draw after compute work",
3410
+ hint: "seed a visible render output after simulation dispatches before capturing the preview frame",
3411
+ detail: { phase: "replay-draws", cause: { appErrors: capture.appErrors } }
3412
+ });
3413
+ }
3414
+ const committed = await replay.inspectWork(committedDrawIndex, ["pixels"]);
3415
+ if (!committed.ok || committed.value.attachment === void 0) {
3416
+ await replay.dispose();
3417
+ return err({
3418
+ code: "tool-preview-bootstrap-failed",
3419
+ expected: "the fresh replay session to expose a color attachment for the final render work",
3420
+ hint: committed.ok ? "the tape contains no readable color attachment for PNG evidence" : committed.error.hint,
3421
+ detail: { phase: "replay-inspect", cause: committed.ok ? void 0 : committed.error }
3422
+ });
3423
+ }
3424
+ const attachment = committed.value.attachment;
3425
+ const width = attachment.width ?? 0;
3426
+ const height = attachment.height ?? 0;
3427
+ const rgba = attachment.format === void 0 ? void 0 : decodeToRgba8(attachment.bytes, attachment.format, width, height);
3428
+ if (rgba === null || rgba === void 0 || width <= 0 || height <= 0) {
3429
+ await replay.dispose();
3430
+ return err({
3431
+ code: "tool-preview-bootstrap-failed",
3432
+ expected: "the replayed color attachment to have a supported display format",
3433
+ hint: "move PNG encoding to the producer-owned preview shell for unsupported formats",
3434
+ detail: { phase: "png-readback", cause: { format: attachment.format, width, height } }
3435
+ });
3436
+ }
3437
+ const outputCanvas = document.createElement("canvas");
3438
+ outputCanvas.width = width;
3439
+ outputCanvas.height = height;
3440
+ const outputContext = outputCanvas.getContext("2d");
3441
+ if (outputContext === null) {
3442
+ await replay.dispose();
3443
+ return err({
3444
+ code: "tool-preview-bootstrap-failed",
3445
+ expected: "the browser to provide a 2D canvas context for replay PNG encoding",
3446
+ hint: "enable the browser canvas 2D context before running the preview host",
3447
+ detail: { phase: "png-encode" }
3448
+ });
3449
+ }
3450
+ outputContext.putImageData(new ImageData(rgba, width, height), 0, 0);
3451
+ await replay.dispose();
3452
+ const pixels = outputContext.getImageData(0, 0, width, height).data;
3453
+ let nonBlackPixels = 0;
3454
+ if (pixels !== void 0) {
3455
+ for (let offset = 0; offset < pixels.length; offset += 4) {
3456
+ if ((pixels[offset] ?? 0) + (pixels[offset + 1] ?? 0) + (pixels[offset + 2] ?? 0) > 0) {
3457
+ nonBlackPixels += 1;
3458
+ }
3459
+ }
3460
+ }
3461
+ if (nonBlackPixels === 0) {
3462
+ return err({
3463
+ code: "tool-preview-bootstrap-failed",
3464
+ expected: "the replayed PNG to contain non-black pixels",
3465
+ hint: "inspect the RHI tape and renderer draw submission before accepting the preview",
3466
+ detail: { phase: "png-non-black" }
3467
+ });
3468
+ }
3469
+ const analyzeDurationMs = performance.now() - analyzeStartedAtMs;
3470
+ const finalizeStartedAtMs = performance.now();
3471
+ const pngUri = outputCanvas.toDataURL("image/png");
3472
+ const pngBytes = dataUriBytes(pngUri);
3473
+ if (!capture.capturePng.uri.startsWith("data:image/png") || capture.capturePng.width <= 0 || capture.capturePng.height <= 0) {
3474
+ return err({
3475
+ code: "tool-preview-bootstrap-failed",
3476
+ expected: "the capture browser to provide a PNG before fresh replay",
3477
+ hint: "capture the rendered canvas before closing the capture carrier; do not reuse the replay PNG",
3478
+ detail: { phase: "capture-png" }
3479
+ });
3480
+ }
3481
+ const capturePngBytes = dataUriBytes(capture.capturePng.uri);
3482
+ if (capturePngBytes.byteLength === 0) {
3483
+ return err({
3484
+ code: "tool-preview-bootstrap-failed",
3485
+ expected: "the capture PNG to contain encoded bytes",
3486
+ hint: "repair the capture carrier readback before publishing evidence",
3487
+ detail: { phase: "capture-png" }
3488
+ });
3489
+ }
3490
+ const profileBytes = dataUriBytes(capture.profile.uri);
3491
+ const tapeDigest = await sha256(tapeBytes);
3492
+ const capturePngDigest = await sha256(capturePngBytes);
3493
+ const pngDigest = await sha256(pngBytes);
3494
+ const profileDigest = await sha256(profileBytes);
3495
+ const presentationDigest = await sha256(new TextEncoder().encode(capture.trace.presentation));
3496
+ const manifest = createPreviewArtifactManifest({
3497
+ schemaVersion: "2.0.0",
3498
+ identity: {
3499
+ runId: capture.tape.runId,
3500
+ snapshotDigest: capture.snapshot.digest,
3501
+ subjectDigest: subjectDigest ?? capture.snapshot.digest,
3502
+ presentationDigest,
3503
+ frameId: 0,
3504
+ captureId: capture.captureId
3505
+ },
3506
+ artifacts: [
3507
+ {
3508
+ owner: "rhi-debug",
3509
+ kind: "rhi-tape",
3510
+ role: "rhi-tape",
3511
+ uri: capture.tape.jsonUri,
3512
+ digest: tapeDigest,
3513
+ byteLength: tapeBytes.byteLength,
3514
+ mediaType: "application/json",
3515
+ derivedFrom: []
3516
+ },
3517
+ {
3518
+ owner: "visual",
3519
+ kind: "png",
3520
+ role: "capture",
3521
+ uri: capture.capturePng.uri,
3522
+ digest: capturePngDigest,
3523
+ byteLength: capturePngBytes.byteLength,
3524
+ mediaType: "image/png",
3525
+ derivedFrom: [tapeDigest]
3526
+ },
3527
+ {
3528
+ owner: "rhi-debug-replay",
3529
+ kind: "png",
3530
+ role: "fresh-replay",
3531
+ uri: pngUri,
3532
+ digest: pngDigest,
3533
+ byteLength: pngBytes.byteLength,
3534
+ mediaType: "image/png",
3535
+ derivedFrom: [tapeDigest, capturePngDigest]
3536
+ },
3537
+ {
3538
+ owner: "profiler",
3539
+ kind: "profile-capture",
3540
+ role: "profile-capture",
3541
+ uri: capture.profile.uri,
3542
+ digest: profileDigest,
3543
+ byteLength: profileBytes.byteLength,
3544
+ mediaType: "application/json",
3545
+ derivedFrom: []
3546
+ }
3547
+ ]
3548
+ });
3549
+ const finalizeDurationMs = performance.now() - finalizeStartedAtMs;
3550
+ const durationMs = capture.executeDurationMs + capture.captureDurationMs + analyzeDurationMs + finalizeDurationMs;
3551
+ const memory = performance.memory?.usedJSHeapSize;
3552
+ const browserJsHeapBytes = typeof memory === "number" && Number.isFinite(memory) && memory >= 0 ? Math.max(Math.round(memory), capture.browserJsHeapBytes ?? 0) : capture.browserJsHeapBytes;
3553
+ return ok({
3554
+ trace: capture.trace,
3555
+ captureId: capture.captureId,
3556
+ drawCalls,
3557
+ committedDrawIndex,
3558
+ nonBlackPixels,
3559
+ actionTrace: capture.actionTrace,
3560
+ tape: capture.tape,
3561
+ profile: capture.profile,
3562
+ capturePng: capture.capturePng,
3563
+ png: { uri: pngUri, width: outputCanvas.width, height: outputCanvas.height },
3564
+ manifest,
3565
+ artifacts: manifest.artifacts.flatMap((artifact) => {
3566
+ if (artifact.kind !== "rhi-tape" && artifact.kind !== "png" && artifact.kind !== "profile-capture") {
3567
+ return [];
3568
+ }
3569
+ return [
3570
+ {
3571
+ kind: artifact.kind,
3572
+ digest: artifact.digest,
3573
+ uri: artifact.uri,
3574
+ sizeBytes: artifact.byteLength
3575
+ }
3576
+ ];
3577
+ }),
3578
+ operationTiming: {
3579
+ startedAtMs: 0,
3580
+ endedAtMs: durationMs,
3581
+ durationMs,
3582
+ phases: {
3583
+ lookup: { status: "not-applicable" },
3584
+ lease: { status: "not-applicable" },
3585
+ transport: { status: "not-applicable" },
3586
+ execute: {
3587
+ status: "observed",
3588
+ durationMs: capture.executeDurationMs,
3589
+ workUnits: capture.recipe.frames
3590
+ },
3591
+ capture: { status: "observed", durationMs: capture.captureDurationMs },
3592
+ finalize: { status: "observed", durationMs: finalizeDurationMs },
3593
+ analyze: { status: "observed", durationMs: analyzeDurationMs }
3594
+ },
3595
+ unattributedMs: 0,
3596
+ resources: {
3597
+ "node-rss": { status: "not-applicable" },
3598
+ "browser-js-heap": browserJsHeapBytes === void 0 ? { status: "unavailable", reason: "performance.memory is not exposed" } : {
3599
+ status: "observed",
3600
+ bytes: browserJsHeapBytes,
3601
+ source: "maximum performance.memory.usedJSHeapSize across capture and replay"
3602
+ },
3603
+ "artifact-bytes": {
3604
+ status: "observed",
3605
+ bytes: tapeBytes.byteLength + dataUriBytes(capture.capturePng.uri).byteLength + pngBytes.byteLength + profileBytes.byteLength,
3606
+ source: "browser-produced artifact payloads"
3607
+ },
3608
+ "gpu-memory": {
3609
+ status: "unavailable",
3610
+ reason: "WebGPU does not expose portable allocation totals"
3611
+ }
3612
+ }
3613
+ },
3614
+ ...capture.resource === void 0 ? {} : { resource: capture.resource }
3615
+ });
3616
+ }
3617
+ async function createToolPreviewHost(options) {
3618
+ const recipe = createToolPreviewRecipe(options.recipe);
3619
+ const runId = options.runId ?? generateRunId();
3620
+ const doc = options.root ?? (typeof document === "undefined" ? void 0 : document);
3621
+ if (options.canvas === void 0 && doc === void 0) {
3622
+ return err({
3623
+ code: "tool-preview-capability-unavailable",
3624
+ expected: "a DOM document or caller-owned HTMLCanvasElement",
3625
+ hint: "run the browser host or provide an attached canvas; headless does not mean RHI-null",
3626
+ detail: { phase: "canvas" }
3627
+ });
3628
+ }
3629
+ const canvas = options.canvas ?? (typeof Document !== "undefined" && doc instanceof Document ? doc.createElement("canvas") : void 0);
3630
+ const ownsCanvas = options.canvas === void 0;
3631
+ if (canvas === void 0) {
3632
+ return err({
3633
+ code: "tool-preview-capability-unavailable",
3634
+ expected: "root document can create an HTMLCanvasElement",
3635
+ hint: "provide a document root or an existing canvas",
3636
+ detail: { phase: "canvas" }
3637
+ });
3638
+ }
3639
+ layoutCanvas(canvas, recipe);
3640
+ appendCanvas(canvas, options.root);
3641
+ if (!canvas.isConnected) {
3642
+ return err({
3643
+ code: "tool-preview-bootstrap-failed",
3644
+ expected: "preview canvas is connected before App creation",
3645
+ hint: "attach the canvas to the document or pass a connected caller-owned canvas",
3646
+ detail: { phase: "canvas" }
3647
+ });
3648
+ }
3649
+ let runtime;
3650
+ try {
3651
+ runtime = await createBrowserRhiDebugRuntime();
3652
+ } catch (cause) {
3653
+ removeOwnedCanvas(canvas, ownsCanvas);
3654
+ return err({
3655
+ code: "tool-preview-capability-unavailable",
3656
+ expected: "a real WebGPU adapter and device",
3657
+ hint: "install or enable WebGPU; do not replace this route with RHI-null",
3658
+ detail: { phase: "rhi-debug", cause }
3659
+ });
3660
+ }
3661
+ const profiler = createProfiler();
3662
+ const appResult = await createApp(
3663
+ canvas,
3664
+ {
3665
+ ...options.app ?? {},
3666
+ rhi: runtime.rhi,
3667
+ rhiInstrumentation: runtime.rhiInstrumentation,
3668
+ profiler
3669
+ },
3670
+ options.bundler
3671
+ );
3672
+ if (!appResult.ok) {
3673
+ await runtime.attachment.dispose();
3674
+ removeOwnedCanvas(canvas, ownsCanvas);
3675
+ return err({
3676
+ code: "tool-preview-bootstrap-failed",
3677
+ expected: "createApp constructs a real WebGPU Renderer",
3678
+ hint: "inspect the structured App/RHI error; the preview must not fall back to RHI-null",
3679
+ detail: { phase: "renderer", cause: appResult.error }
3680
+ });
3681
+ }
3682
+ const app = appResult.value;
3683
+ const appErrors = [];
3684
+ const unsubscribeAppErrors = app.onError((error) => appErrors.push(error));
3685
+ const rendererReady = await app.renderer.ready;
3686
+ if (!rendererReady.ok) {
3687
+ unsubscribeAppErrors();
3688
+ await app.dispose();
3689
+ await runtime.attachment.dispose();
3690
+ removeOwnedCanvas(canvas, ownsCanvas);
3691
+ return err({
3692
+ code: "tool-preview-bootstrap-failed",
3693
+ expected: "the real WebGPU Renderer to finish pipeline initialization before preview frames",
3694
+ hint: rendererReady.error.hint,
3695
+ detail: { phase: "renderer-ready", cause: rendererReady.error }
3696
+ });
3697
+ }
3698
+ let resourceFacts;
3699
+ try {
3700
+ const prepared = await options.prepare?.(app);
3701
+ if (prepared !== void 0) resourceFacts = prepared;
3702
+ } catch (cause) {
3703
+ unsubscribeAppErrors();
3704
+ await app.dispose();
3705
+ await runtime.attachment.dispose();
3706
+ removeOwnedCanvas(canvas, ownsCanvas);
3707
+ const bootstrapFailure = options.resource === void 0 ? {
3708
+ expected: "the project bootstrap to prepare the preview World",
3709
+ hint: "Repair the project Entry, assets, or scene before retrying the same recipe.",
3710
+ phase: "project-bootstrap"
3711
+ } : {
3712
+ expected: "the resource preview bootstrap to prepare the preview World",
3713
+ hint: "Repair the resource owner, cooked payload, or preview environment before retrying the same recipe.",
3714
+ phase: "resource-bootstrap"
3715
+ };
3716
+ return err({
3717
+ code: "tool-preview-bootstrap-failed",
3718
+ ...bootstrapFailure,
3719
+ detail: { phase: bootstrapFailure.phase, cause }
3720
+ });
3721
+ }
3722
+ const attached = app.renderer.attachWorld(app.world);
3723
+ if (!attached.ok) {
3724
+ unsubscribeAppErrors();
3725
+ await app.dispose();
3726
+ await runtime.attachment.dispose();
3727
+ removeOwnedCanvas(canvas, ownsCanvas);
3728
+ return err({
3729
+ code: "tool-preview-bootstrap-failed",
3730
+ expected: "the preview World to attach to the real Renderer before stepping",
3731
+ hint: attached.error.hint,
3732
+ detail: { phase: "world-attach", cause: attached.error }
3733
+ });
3734
+ }
3735
+ const profileSession = profiler.startCapture({ frameLimit: recipe.frames, eventLimit: 1024 });
3736
+ if (!profileSession.ok) {
3737
+ await app.dispose();
3738
+ await runtime.attachment.dispose();
3739
+ removeOwnedCanvas(canvas, ownsCanvas);
3740
+ return err({
3741
+ code: "tool-preview-bootstrap-failed",
3742
+ expected: "the App profiler to start a bounded capture",
3743
+ hint: profileSession.error.hint,
3744
+ detail: { phase: "profiler", cause: profileSession.error }
3745
+ });
3746
+ }
3747
+ const detach = runtime.attachRenderer(app.renderer);
3748
+ const trace = {
3749
+ events: ["canvas-created", "rhi-debug-armed", "renderer-created"],
3750
+ backend: "webgpu",
3751
+ adapter: "webgpu-adapter",
3752
+ presentation: recipe.presentation
3753
+ };
3754
+ let disposed = false;
3755
+ const capturePreview = async () => {
3756
+ if (disposed) {
3757
+ return err({
3758
+ code: "tool-preview-bootstrap-failed",
3759
+ expected: "preview host remains alive until capture completes",
3760
+ hint: "create a new preview host after dispose",
3761
+ detail: { phase: "capture" }
3762
+ });
3763
+ }
3764
+ const started = app.start();
3765
+ if (!started.ok)
3766
+ return err({
3767
+ ...started.error,
3768
+ code: "tool-preview-bootstrap-failed",
3769
+ detail: { phase: "start", cause: started.error }
3770
+ });
3771
+ const paused = app.pause();
3772
+ if (!paused.ok)
3773
+ return err({
3774
+ ...paused.error,
3775
+ code: "tool-preview-bootstrap-failed",
3776
+ detail: { phase: "pause", cause: paused.error }
3777
+ });
3778
+ const executeStartedAtMs = performance.now();
3779
+ const actionTrace = [];
3780
+ const encodedCapture = runtime.attachment.captureFrame();
3781
+ for (let frame = 0; frame < recipe.frames; frame += 1) {
3782
+ for (const action of recipe.actions.filter((candidate) => candidate.frame === frame)) {
3783
+ let accepted = false;
3784
+ try {
3785
+ accepted = await options.executeAction?.(action, app) === true;
3786
+ } catch (cause) {
3787
+ return err({
3788
+ code: "tool-preview-bootstrap-failed",
3789
+ expected: `project action '${action.name}' to execute at frame ${frame}`,
3790
+ hint: "Repair the project-owned preview action handler and rerun the same recipe.",
3791
+ detail: { phase: "action", cause }
3792
+ });
3793
+ }
3794
+ if (!accepted) {
3795
+ return err({
3796
+ code: "tool-preview-bootstrap-failed",
3797
+ expected: `project action '${action.name}' to have a registered handler`,
3798
+ hint: "Register the action in the project plugin or remove it from the recipe.",
3799
+ detail: { phase: "action", cause: { frame, name: action.name } }
3800
+ });
3801
+ }
3802
+ actionTrace.push(action);
3803
+ }
3804
+ const stepped = app.stepFrame(recipe.deltaSeconds);
3805
+ if (!stepped.ok)
3806
+ return err({
3807
+ ...stepped.error,
3808
+ code: "tool-preview-bootstrap-failed",
3809
+ detail: { phase: "frame", cause: stepped.error }
3810
+ });
3811
+ await new Promise((resolve) => setTimeout(resolve, 0));
3812
+ }
3813
+ await runtime.attachment.frameBoundary();
3814
+ await new Promise((resolve) => setTimeout(resolve, 0));
3815
+ await runtime.attachment.frameBoundary();
3816
+ if (options.collectResourceFacts !== void 0) {
3817
+ try {
3818
+ const collected = await options.collectResourceFacts(app, resourceFacts);
3819
+ if (collected !== void 0) resourceFacts = collected;
3820
+ } catch (cause) {
3821
+ return err({
3822
+ code: "tool-preview-bootstrap-failed",
3823
+ expected: "the resource owner to publish post-frame preview facts",
3824
+ hint: "Repair the producer-owned preview observation and retry the same recipe.",
3825
+ detail: { phase: "resource-observation", cause }
3826
+ });
3827
+ }
3828
+ }
3829
+ const executeDurationMs = performance.now() - executeStartedAtMs;
3830
+ const finalTrace = {
3831
+ ...trace,
3832
+ events: [...trace.events, "world-updated", "draw-submitted", "validation-complete"]
3833
+ };
3834
+ const captureStartedAtMs = performance.now();
3835
+ const profile = profileSession.value.finish();
3836
+ if (!profile.ok) {
3837
+ return err({
3838
+ code: "tool-preview-bootstrap-failed",
3839
+ expected: "the bounded App profiler to finish with ProfileCapture",
3840
+ hint: profile.error.hint,
3841
+ detail: { phase: "profiler-finish", cause: profile.error }
3842
+ });
3843
+ }
3844
+ const tape = await encodedCapture;
3845
+ if (!tape.ok)
3846
+ return err({
3847
+ code: "tool-preview-bootstrap-failed",
3848
+ expected: "RHI-debug produces a self-contained tape",
3849
+ hint: tape.error.hint,
3850
+ detail: { phase: "rhi-finalize", cause: tape.error }
3851
+ });
3852
+ const captureDurationMs = performance.now() - captureStartedAtMs;
3853
+ const tapeBytes = tape.value.bytes;
3854
+ const profileBytes = new TextEncoder().encode(JSON.stringify(profile.value));
3855
+ const capturePng = {
3856
+ uri: canvas.toDataURL("image/png"),
3857
+ width: canvas.width,
3858
+ height: canvas.height
3859
+ };
3860
+ const memory = performance.memory?.usedJSHeapSize;
3861
+ return ok({
3862
+ recipe,
3863
+ snapshot: options.snapshot,
3864
+ trace: finalTrace,
3865
+ captureId: profile.value.captureId,
3866
+ actionTrace,
3867
+ tape: {
3868
+ runId,
3869
+ jsonUri: bytesToDataUri(tapeBytes, "application/octet-stream"),
3870
+ blobUri: bytesToDataUri(new Uint8Array(), "application/octet-stream"),
3871
+ byteLength: tapeBytes.byteLength
3872
+ },
3873
+ profile: {
3874
+ captureId: profile.value.captureId,
3875
+ uri: bytesToDataUri(profileBytes, "application/json")
3876
+ },
3877
+ capturePng,
3878
+ executeDurationMs,
3879
+ captureDurationMs,
3880
+ ...typeof memory === "number" && Number.isFinite(memory) && memory >= 0 ? { browserJsHeapBytes: Math.round(memory) } : {},
3881
+ appErrors,
3882
+ ...resourceFacts === void 0 ? {} : { resource: resourceFacts }
3883
+ });
3884
+ };
3885
+ return ok({
3886
+ app,
3887
+ recipe,
3888
+ trace,
3889
+ capture: capturePreview,
3890
+ async run() {
3891
+ const capture = await capturePreview();
3892
+ if (!capture.ok) return capture;
3893
+ return replayToolPreviewCapture(capture.value);
3894
+ },
3895
+ async dispose() {
3896
+ if (disposed) return ok(void 0);
3897
+ disposed = true;
3898
+ detach();
3899
+ unsubscribeAppErrors();
3900
+ let disposeError;
3901
+ try {
3902
+ await options.onDispose?.(app);
3903
+ } catch (cause) {
3904
+ disposeError = cause;
3905
+ }
3906
+ const result = await app.dispose();
3907
+ removeOwnedCanvas(canvas, ownsCanvas);
3908
+ if (disposeError !== void 0) {
3909
+ return err({
3910
+ code: "tool-preview-bootstrap-failed",
3911
+ expected: "producer-owned preview attachments to dispose before the App",
3912
+ hint: "Repair the preview owner lifecycle and retry the same operation.",
3913
+ detail: { phase: "producer-dispose", cause: disposeError }
3914
+ });
3915
+ }
3916
+ return result.ok ? ok(void 0) : err({
3917
+ code: "tool-preview-bootstrap-failed",
3918
+ expected: "preview app disposes cleanly",
3919
+ hint: result.error.hint,
3920
+ detail: { phase: "dispose", cause: result.error }
3921
+ });
3922
+ }
3923
+ });
3924
+ }
3925
+ function joinToolPreviewEvidence(input) {
3926
+ const validated = validatePreviewArtifactManifest(input.manifest, input.required);
3927
+ if (!validated.ok) return err(validated.error);
3928
+ const artifacts = input.manifest.artifacts.map(
3929
+ (artifact) => createArtifactRef({
3930
+ kind: artifact.kind === "report" || artifact.kind === "contact-sheet" ? "tool-result" : artifact.kind,
3931
+ digest: artifact.digest,
3932
+ uri: artifact.uri,
3933
+ mediaType: artifact.mediaType,
3934
+ sizeBytes: artifact.byteLength
3935
+ })
3936
+ );
3937
+ return ok({ trace: input.trace, artifacts, manifest: input.manifest });
3938
+ }
3939
+ function createToolPreviewEvidence(trace, artifacts) {
3940
+ return { trace, artifacts: [...artifacts] };
3941
+ }
3942
+ function joinResourcePreviewEvidence(input) {
3943
+ if (input.manifest.identity.snapshotDigest !== input.snapshot.digest) {
3944
+ return err(snapshotStaleError(input.snapshot.digest, input.manifest.identity.snapshotDigest));
3945
+ }
3946
+ const validated = validatePreviewArtifactManifest(input.manifest, input.requiredRoles);
3947
+ return validated.ok ? ok(validated.value) : err(validated.error);
3948
+ }
3949
+
3950
+ // src/tool-preview/framing.ts
3951
+ function fitToolPreviewCameraToAabb(aabb, options) {
3952
+ const [minX, minY, minZ, maxX, maxY, maxZ] = aabb;
3953
+ if (![minX, minY, minZ, maxX, maxY, maxZ].every(Number.isFinite) || minX > maxX || minY > maxY || minZ > maxZ || !Number.isFinite(options.aspect) || options.aspect <= 0 || !Number.isFinite(options.fov) || options.fov <= 0 || options.fov >= Math.PI) {
3954
+ throw new TypeError("tool preview camera framing requires a finite AABB, aspect, and FOV");
3955
+ }
3956
+ const center = [
3957
+ (minX + maxX) / 2,
3958
+ (minY + maxY) / 2,
3959
+ (minZ + maxZ) / 2
3960
+ ];
3961
+ const radius = Math.max(
3962
+ Math.hypot((maxX - minX) / 2, (maxY - minY) / 2, (maxZ - minZ) / 2),
3963
+ 1e-3
3964
+ );
3965
+ const verticalHalfFov = options.fov / 2;
3966
+ const horizontalHalfFov = Math.atan(Math.tan(verticalHalfFov) * options.aspect);
3967
+ const limitingHalfFov = Math.min(verticalHalfFov, horizontalHalfFov);
3968
+ const padding = options.padding ?? 1.15;
3969
+ if (!Number.isFinite(padding) || padding < 1) {
3970
+ throw new TypeError("tool preview camera framing padding must be finite and at least one");
3971
+ }
3972
+ const distance = radius / Math.sin(limitingHalfFov) * padding;
3973
+ const depthMargin = radius * (padding - 1);
3974
+ return {
3975
+ center,
3976
+ radius,
3977
+ distance,
3978
+ near: Math.max(1e-3, distance - radius - depthMargin),
3979
+ far: distance + radius + depthMargin
3980
+ };
3981
+ }
3982
+
3983
+ export { APP_ERROR_HINTS, APP_EXPECTED, APP_PHASE_CATALOG, AppError, EXECUTION_CAPABILITY_NAMES, EXECUTION_REPORT_SCHEMA_VERSION, EXECUTION_REQUESTED_TIERS, EXECUTION_TIERS, LOAD_GAME_ERROR_HINTS, LOAD_GAME_EXPECTED, LoadGameError, assembleAssetRuntime, createApp, createExecutionReport, createToolPreviewEvidence, createToolPreviewHost, createToolPreviewRecipe, ensureFallbackCamera, executionBootstrapHostPlugin, fitToolPreviewCameraToAabb, gameHostPlugin, inputPlugin, isAppError, isExecutionReport, isLoadGameError, joinResourcePreviewEvidence, joinToolPreviewEvidence, loadBootstrapEntry, loadGame, measureCanvasDrawingBuffer, missingExecutionCapabilities, prepareBootstrapEntry, probeExecutionCapabilities, replayToolPreviewCapture, selectExecutionTier, syncCanvasDrawingBuffer, unavailableExecutionCapabilities, validateExecutionBootstrapData, validateToolPreviewTrace };
3984
+ //# sourceMappingURL=index.mjs.map
3985
+ //# sourceMappingURL=index.mjs.map