@forgeax/engine-app 0.0.0-dev.8d955ade1c79

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