@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
@@ -0,0 +1,115 @@
1
+ // frame-loop-time-elapsed.test.ts — Time resource `elapsed` accumulator
2
+ // (solo bevy-examples round 20260713-212920).
3
+ //
4
+ // Regression guard for the friction that motivated the field: the Time resource
5
+ // was `{ delta }` only, so absolute-time-keyed behavior (pulsing, sin(elapsed))
6
+ // forced each system to hand-accumulate delta (drift + re-derivation). The frame-loop
7
+ // now writes `{ delta, elapsed }` where elapsed = Σ(clamped delta). These tests pin:
8
+ // 1. elapsed accumulates: after N frames of delta, elapsed ≈ N·delta,
9
+ // 2. elapsed is monotonic non-decreasing,
10
+ // 3. elapsed uses the CLAMPED delta (a huge raw gap advances elapsed by the ceiling,
11
+ // not the raw gap — no time jump),
12
+ // 4. delta is still present + correct (existing consumers unaffected).
13
+
14
+ import { Time, type TimeResource, World } from '@forgeax/engine-ecs';
15
+ import type { Renderer } from '@forgeax/engine-render';
16
+ import { describe, expect, it } from 'vitest';
17
+ import { createFrameLoop } from '../internal/frame-loop';
18
+
19
+ function makeSpyRenderer(): Renderer {
20
+ return {
21
+ backend: 'webgpu' as const,
22
+ ready: Promise.resolve({ ok: true, value: undefined }),
23
+ attach: () => ({ ok: true, value: undefined }),
24
+ detachWorld: () => {},
25
+ draw(): { ok: true; value: undefined } {
26
+ return { ok: true, value: undefined };
27
+ },
28
+ onError(): () => void {
29
+ return () => {};
30
+ },
31
+ dispose(): void {},
32
+ } as unknown as Renderer;
33
+ }
34
+
35
+ // Scheduler whose `now` advances by a fixed step per tick (deterministic delta).
36
+ function makeSyncScheduler(stepMs: number) {
37
+ let pending: ((t: number) => void) | null = null;
38
+ let clock = 0;
39
+ const raf = (cb: (t: number) => void): number => {
40
+ pending = cb;
41
+ return 1;
42
+ };
43
+ const caf = (): void => {
44
+ pending = null;
45
+ };
46
+ const now = (): number => {
47
+ clock += stepMs;
48
+ return clock;
49
+ };
50
+ const pump = (frames: number): void => {
51
+ for (let i = 0; i < frames; i++) {
52
+ const cb = pending;
53
+ pending = null;
54
+ if (cb === null) break;
55
+ cb(clock);
56
+ }
57
+ };
58
+ return { raf, caf, now, pump };
59
+ }
60
+
61
+ function readTime(world: World): TimeResource {
62
+ return world.getResource(Time);
63
+ }
64
+
65
+ describe('Time.elapsed — accumulation', () => {
66
+ it('after N frames of fixed delta, elapsed ≈ N·delta', () => {
67
+ const world = new World();
68
+ // 16 ms/tick → delta = 0.016 s (well under the default clamp ceiling).
69
+ const { raf, caf, now, pump } = makeSyncScheduler(16);
70
+ const loop = createFrameLoop({ world, renderer: makeSpyRenderer(), now, raf, caf });
71
+ loop.start();
72
+ pump(10);
73
+ const time = readTime(world);
74
+ expect(time.delta).toBeCloseTo(0.016, 6);
75
+ // 10 ticks, but start() sets the baseline at t=16 (first now()), so the first
76
+ // tick's delta is measured from there; elapsed is the running sum of clamped delta.
77
+ // Assert it equals delta * (number of ticks that ran) within a tick's slack.
78
+ expect(time.elapsed).toBeGreaterThan(0.1); // ~10 * 0.016 = 0.16
79
+ expect(time.elapsed).toBeLessThanOrEqual(0.016 * 10 + 1e-9);
80
+ loop.stop();
81
+ });
82
+
83
+ it('elapsed is monotonic non-decreasing across frames', () => {
84
+ const world = new World();
85
+ const { raf, caf, now, pump } = makeSyncScheduler(16);
86
+ const loop = createFrameLoop({ world, renderer: makeSpyRenderer(), now, raf, caf });
87
+ loop.start();
88
+ let prev = -1;
89
+ for (let i = 0; i < 20; i++) {
90
+ pump(1);
91
+ const e = readTime(world).elapsed;
92
+ expect(e).toBeGreaterThanOrEqual(prev);
93
+ prev = e;
94
+ }
95
+ loop.stop();
96
+ });
97
+
98
+ it('elapsed uses the CLAMPED delta — a huge raw frame gap does not jump elapsed', () => {
99
+ const world = new World();
100
+ // 5000 ms/tick → raw delta = 5 s, far above the default clamp ceiling. elapsed
101
+ // must advance by the CEILING per tick, not 5 s (no time jump — same clamp the
102
+ // delta field already applies).
103
+ const { raf, caf, now, pump } = makeSyncScheduler(5000);
104
+ const loop = createFrameLoop({ world, renderer: makeSpyRenderer(), now, raf, caf });
105
+ loop.start();
106
+ pump(3);
107
+ const time = readTime(world);
108
+ // delta is clamped, so a single frame's delta is well under the raw 5 s.
109
+ expect(time.delta).toBeLessThan(1);
110
+ // 3 clamped ticks → elapsed is a few * ceiling, nowhere near the raw 15 s.
111
+ expect(time.elapsed).toBeLessThan(3);
112
+ expect(time.elapsed).toBeCloseTo(time.delta * 3, 6);
113
+ loop.stop();
114
+ });
115
+ });
@@ -0,0 +1,61 @@
1
+ import { Time, World } from '@forgeax/engine-ecs';
2
+ import type { Renderer } from '@forgeax/engine-render';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ import { createFrameLoop } from '../internal/frame-loop';
6
+
7
+ function renderer(): Renderer {
8
+ return {
9
+ backend: 'webgpu',
10
+ ready: Promise.resolve({ ok: true, value: undefined }),
11
+ attach: () => ({ ok: true, value: undefined }),
12
+ detachWorld: () => {},
13
+ draw: () => ({ ok: true, value: undefined }),
14
+ onError: () => () => {},
15
+ onLost: () => () => {},
16
+ dispose: () => {},
17
+ } as unknown as Renderer;
18
+ }
19
+
20
+ function scheduler() {
21
+ let callback: ((timestamp: number) => void) | undefined;
22
+ return {
23
+ raf: (next: (timestamp: number) => void): number => {
24
+ callback = next;
25
+ return 1;
26
+ },
27
+ caf: (): void => {
28
+ callback = undefined;
29
+ },
30
+ tick(timestamp: number): void {
31
+ const next = callback;
32
+ callback = undefined;
33
+ next?.(timestamp);
34
+ },
35
+ };
36
+ }
37
+
38
+ describe('frame-loop Time forwarding', () => {
39
+ it('forwards one measured delta to own and injected Worlds', () => {
40
+ const own = new World();
41
+ const injected = new World();
42
+ const clock = scheduler();
43
+ const loop = createFrameLoop({
44
+ world: own,
45
+ renderer: renderer(),
46
+ now: (() => {
47
+ const values = [1000, 1016];
48
+ return () => values.shift() ?? 1016;
49
+ })(),
50
+ raf: clock.raf,
51
+ caf: clock.caf,
52
+ drawSource: () => ({ worlds: [own, injected], cameraOwner: 0, resourceOwner: 0 }),
53
+ });
54
+
55
+ loop.start().unwrap();
56
+ clock.tick(1016);
57
+
58
+ expect(own.getResource(Time).delta).toBeCloseTo(0.016);
59
+ expect(injected.getResource(Time).delta).toBeCloseTo(0.016);
60
+ });
61
+ });
@@ -0,0 +1,450 @@
1
+ // feat-20260708-composited-multi-world-rendering M3 / m3-t3 — AC-03 frame-loop
2
+ // [world] wrapping.
3
+ //
4
+ // The app frame-loop is the single point that shields single-world AI users from
5
+ // the multi-world draw signature: internally it wraps the current World into
6
+ // `[world]` and passes `{ cameraOwner: 0, resourceOwner: 0 }` (plan-strategy §7 M3). Engine.create /
7
+ // createApp public API is unchanged — the user still hands over one World.
8
+ //
9
+ // This test drives createFrameLoop directly (injected now / raf / caf seams) with
10
+ // a spy renderer and asserts the exact call shape:
11
+ // renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 })
12
+ //
13
+ // Test-first (red before m3-i3): the frame-loop currently calls
14
+ // renderer.draw(world). After migration these assertions pass and the single
15
+ // world identity path (worldId 0) is preserved (AC-03 regression guarantee).
16
+
17
+ import { Update, World } from '@forgeax/engine-ecs';
18
+ import type { Renderer, RenderWorldLease } from '@forgeax/engine-render';
19
+ import { describe, expect, it, vi } from 'vitest';
20
+ import { createFrameLoop } from '../internal/frame-loop';
21
+
22
+ interface DrawCall {
23
+ readonly request: unknown;
24
+ }
25
+
26
+ function lease(onDispose: () => void = () => {}): RenderWorldLease {
27
+ return {
28
+ worldIdentity: {},
29
+ generation: 0,
30
+ readChanges: vi.fn(),
31
+ querySpans: vi.fn(),
32
+ inspectCursor: vi.fn(),
33
+ dispose: onDispose,
34
+ } as unknown as RenderWorldLease;
35
+ }
36
+
37
+ function makeSpyRenderer(): { renderer: Renderer; calls: DrawCall[] } {
38
+ const calls: DrawCall[] = [];
39
+ const renderer = {
40
+ backend: 'webgpu' as const,
41
+ attach(): { ok: true; value: RenderWorldLease } {
42
+ return { ok: true, value: lease() };
43
+ },
44
+ draw(request: unknown): {
45
+ ok: true;
46
+ value: { frameId: number; deviceGeneration: number; completed: true };
47
+ } {
48
+ calls.push({ request });
49
+ return { ok: true, value: { frameId: calls.length, deviceGeneration: 0, completed: true } };
50
+ },
51
+ onError(): () => void {
52
+ return () => {
53
+ // no-op unsubscribe
54
+ };
55
+ },
56
+ dispose(): void {
57
+ // no-op
58
+ },
59
+ } as unknown as Renderer;
60
+ return { renderer, calls };
61
+ }
62
+
63
+ /**
64
+ * Synchronous rAF driver: fire the scheduled tick exactly `frames` times.
65
+ * The injected `raf` stores the next callback; `pump` invokes it, which in
66
+ * turn schedules the following one, so we can step the loop deterministically.
67
+ */
68
+ function makeSyncScheduler() {
69
+ let pending: ((t: number) => void) | null = null;
70
+ let clock = 0;
71
+ const raf = (cb: (t: number) => void): number => {
72
+ pending = cb;
73
+ return 1;
74
+ };
75
+ const caf = (): void => {
76
+ pending = null;
77
+ };
78
+ const now = (): number => {
79
+ clock += 16;
80
+ return clock;
81
+ };
82
+ const pump = (frames: number): void => {
83
+ for (let i = 0; i < frames; i++) {
84
+ const cb = pending;
85
+ pending = null;
86
+ if (cb === null) break;
87
+ cb(clock);
88
+ }
89
+ };
90
+ return { raf, caf, now, pump };
91
+ }
92
+
93
+ describe('M3 / m3-t3 — frame-loop wraps the single World into [world] with owner 0', () => {
94
+ it('attaches once before the first update without moving drawSource ahead of update', () => {
95
+ const events: string[] = [];
96
+ const world = new World();
97
+ world
98
+ .addSystem(Update, {
99
+ name: 'observeUpdateOrder',
100
+ queries: [],
101
+ fn: () => events.push('update'),
102
+ })
103
+ .unwrap();
104
+ const renderer = {
105
+ backend: 'webgpu' as const,
106
+ attach(): { ok: true; value: RenderWorldLease } {
107
+ events.push('attach');
108
+ return { ok: true, value: lease() };
109
+ },
110
+ draw(): { ok: true; value: { frameId: number; deviceGeneration: number; completed: true } } {
111
+ events.push('draw');
112
+ return { ok: true, value: { frameId: 1, deviceGeneration: 0, completed: true } };
113
+ },
114
+ onError: () => () => {},
115
+ dispose: () => {},
116
+ } as unknown as Renderer;
117
+ const { raf, caf, now, pump } = makeSyncScheduler();
118
+ const loop = createFrameLoop({
119
+ world,
120
+ renderer,
121
+ now,
122
+ raf,
123
+ caf,
124
+ drawSource: () => {
125
+ events.push('drawSource');
126
+ return undefined;
127
+ },
128
+ });
129
+
130
+ loop.start().unwrap();
131
+ pump(2);
132
+ loop.stop().unwrap();
133
+
134
+ expect(events).toEqual([
135
+ 'attach',
136
+ 'update',
137
+ 'drawSource',
138
+ 'draw',
139
+ 'update',
140
+ 'drawSource',
141
+ 'draw',
142
+ ]);
143
+ });
144
+
145
+ it('calls renderer.draw with one lease-bound world per running frame', () => {
146
+ const world = new World();
147
+ const { renderer, calls } = makeSpyRenderer();
148
+ const { raf, caf, now, pump } = makeSyncScheduler();
149
+
150
+ const loop = createFrameLoop({ world, renderer, now, raf, caf });
151
+ const started = loop.start();
152
+ expect(started.ok).toBe(true);
153
+
154
+ // start() schedules the first tick via raf (it does not run synchronously);
155
+ // pump three ticks so we observe 3 frames total.
156
+ pump(3);
157
+
158
+ expect(calls.length).toBeGreaterThanOrEqual(3);
159
+ for (const call of calls) {
160
+ expect(call.request).toMatchObject({
161
+ leases: [{ worldIdentity: expect.any(Object) }],
162
+ camera: { lease: expect.any(Object) },
163
+ environment: { lease: expect.any(Object) },
164
+ });
165
+ }
166
+
167
+ loop.stop();
168
+ });
169
+
170
+ it('does not draw a World whose update did not reach terminal publication', () => {
171
+ const world = new World();
172
+ world
173
+ .addSystem(Update, {
174
+ name: 'fail-update',
175
+ queries: [],
176
+ fn: () => {
177
+ throw new Error('update failed');
178
+ },
179
+ })
180
+ .unwrap();
181
+ const { renderer, calls } = makeSpyRenderer();
182
+ const onError = vi.fn();
183
+ const { raf, caf, now, pump } = makeSyncScheduler();
184
+ const loop = createFrameLoop({ world, renderer, now, raf, caf, onError });
185
+
186
+ loop.start().unwrap();
187
+ pump(1);
188
+
189
+ expect(calls).toHaveLength(0);
190
+ expect(onError).toHaveBeenCalledTimes(1);
191
+ loop.stop().unwrap();
192
+ });
193
+
194
+ it('does not mutate the public frame-loop contract: start/stop return Result.ok', () => {
195
+ const world = new World();
196
+ const { renderer } = makeSpyRenderer();
197
+ const { raf, caf, now } = makeSyncScheduler();
198
+ const loop = createFrameLoop({ world, renderer, now, raf, caf });
199
+
200
+ const started = loop.start();
201
+ expect(started.ok).toBe(true);
202
+ const stopped = loop.stop();
203
+ expect(stopped.ok).toBe(true);
204
+ });
205
+
206
+ it('forwards a draw Result.err through onError (single world path unchanged)', () => {
207
+ const world = new World();
208
+ const calls: DrawCall[] = [];
209
+ const rhiErr = { code: 'rhi-not-available' } as const;
210
+ const renderer = {
211
+ backend: 'webgpu' as const,
212
+ attach: () => ({ ok: true as const, value: lease() }),
213
+ draw(request: unknown): { ok: false; error: typeof rhiErr } {
214
+ calls.push({ request });
215
+ return { ok: false, error: rhiErr };
216
+ },
217
+ onError(): () => void {
218
+ return () => {
219
+ // no-op
220
+ };
221
+ },
222
+ dispose(): void {
223
+ // no-op
224
+ },
225
+ } as unknown as Renderer;
226
+ const onError = vi.fn();
227
+ const { raf, caf, now, pump } = makeSyncScheduler();
228
+
229
+ const loop = createFrameLoop({ world, renderer, now, raf, caf, onError });
230
+ loop.start();
231
+ // start() schedules the first tick via raf; pump once to run it.
232
+ pump(1);
233
+
234
+ expect(calls[0]?.request).toMatchObject({
235
+ camera: { lease: expect.any(Object) },
236
+ environment: { lease: expect.any(Object) },
237
+ });
238
+ expect(onError).toHaveBeenCalledWith(rhiErr);
239
+
240
+ loop.stop();
241
+ });
242
+
243
+ it('keeps the rAF heartbeat and submits receipt-bound frames', () => {
244
+ const world = new World();
245
+ const calls: unknown[] = [];
246
+ const renderer = {
247
+ backend: 'webgpu' as const,
248
+ attach: () => ({ ok: true as const, value: lease() }),
249
+ draw(): { ok: true; value: { frameId: number; deviceGeneration: number; completed: true } } {
250
+ calls.push(undefined);
251
+ return { ok: true, value: { frameId: calls.length, deviceGeneration: 0, completed: true } };
252
+ },
253
+ onError(): () => void {
254
+ return () => {};
255
+ },
256
+ dispose(): void {},
257
+ } as unknown as Renderer;
258
+ const { raf, caf, now, pump } = makeSyncScheduler();
259
+ const loop = createFrameLoop({ world, renderer, now, raf, caf });
260
+ loop.start();
261
+ pump(1);
262
+ expect(calls).toHaveLength(1);
263
+ pump(3);
264
+ expect(calls).toHaveLength(4);
265
+ loop.stop();
266
+ });
267
+
268
+ it('steps one deterministic frame through the same update and draw authority while paused', () => {
269
+ const world = new World();
270
+ const update = vi.spyOn(world, 'update');
271
+ const { renderer, calls } = makeSpyRenderer();
272
+ const { raf, caf, now } = makeSyncScheduler();
273
+ const loop = createFrameLoop({ world, renderer, now, raf, caf });
274
+
275
+ expect(loop.start().ok).toBe(true);
276
+ expect(loop.pause().ok).toBe(true);
277
+ const stepped = loop.stepFrame(1 / 60);
278
+
279
+ expect(stepped.ok).toBe(true);
280
+ expect(update).toHaveBeenCalledTimes(1);
281
+ expect(update).toHaveBeenCalledWith(1 / 60);
282
+ expect(calls).toHaveLength(1);
283
+ expect(calls[0]?.request).toMatchObject({
284
+ leases: [{ worldIdentity: expect.any(Object) }],
285
+ camera: { lease: expect.any(Object) },
286
+ environment: { lease: expect.any(Object) },
287
+ });
288
+
289
+ expect(loop.resume().ok).toBe(true);
290
+ expect(loop.stop().ok).toBe(true);
291
+ });
292
+
293
+ it('rejects deterministic stepping outside paused state and with an invalid delta', () => {
294
+ const world = new World();
295
+ const { renderer } = makeSpyRenderer();
296
+ const { raf, caf, now } = makeSyncScheduler();
297
+ const loop = createFrameLoop({ world, renderer, now, raf, caf });
298
+
299
+ const idle = loop.stepFrame(1 / 60);
300
+ expect(idle.ok).toBe(false);
301
+ if (!idle.ok) {
302
+ expect(idle.error.code).toBe('app-frame-step-invalid');
303
+ if ('detail' in idle.error) {
304
+ expect(idle.error.detail).toEqual({ state: 'idle', deltaSeconds: 1 / 60, reason: 'state' });
305
+ }
306
+ }
307
+
308
+ expect(loop.start().ok).toBe(true);
309
+ expect(loop.pause().ok).toBe(true);
310
+ const invalidDelta = loop.stepFrame(Number.NaN);
311
+ expect(invalidDelta.ok).toBe(false);
312
+ if (!invalidDelta.ok) {
313
+ expect(invalidDelta.error).toMatchObject({
314
+ code: 'app-frame-step-invalid',
315
+ detail: { reason: 'delta' },
316
+ });
317
+ }
318
+ expect(loop.resume().ok).toBe(true);
319
+ expect(loop.stop().ok).toBe(true);
320
+ });
321
+
322
+ it('detaches draw-source Worlds as soon as routing stops referencing them', () => {
323
+ const world = new World();
324
+ const overlay = new World();
325
+ const attached: World[] = [];
326
+ const detached: World[] = [];
327
+ const renderer = {
328
+ backend: 'webgpu' as const,
329
+ attach(candidate: World) {
330
+ attached.push(candidate);
331
+ return { ok: true as const, value: lease(() => detached.push(candidate)) };
332
+ },
333
+ draw: () => ({
334
+ ok: true as const,
335
+ value: { frameId: 1, deviceGeneration: 0, completed: true as const },
336
+ }),
337
+ onError: () => () => {},
338
+ dispose: () => {},
339
+ } as unknown as Renderer;
340
+ const { raf, caf, now, pump } = makeSyncScheduler();
341
+ const loop = createFrameLoop({
342
+ world,
343
+ renderer,
344
+ now,
345
+ raf,
346
+ caf,
347
+ drawSource: () => ({ worlds: [world, overlay], cameraOwner: 0, resourceOwner: 0 }),
348
+ });
349
+
350
+ loop.start().unwrap();
351
+ pump(2);
352
+ expect(attached).toEqual([world, overlay]);
353
+ expect(detached).toEqual([]);
354
+
355
+ loop.setDrawSource(undefined);
356
+ expect(detached).toEqual([overlay]);
357
+ loop.stop().unwrap();
358
+ expect(detached).toEqual([overlay, world]);
359
+ });
360
+
361
+ it('detaches active draw-source Worlds when the loop stops', () => {
362
+ const world = new World();
363
+ const overlay = new World();
364
+ const detached: World[] = [];
365
+ const renderer = {
366
+ backend: 'webgpu' as const,
367
+ attach: (candidate: World) => ({
368
+ ok: true as const,
369
+ value: lease(() => detached.push(candidate)),
370
+ }),
371
+ draw: () => ({
372
+ ok: true as const,
373
+ value: { frameId: 1, deviceGeneration: 0, completed: true as const },
374
+ }),
375
+ onError: () => () => {},
376
+ dispose: () => {},
377
+ } as unknown as Renderer;
378
+ const { raf, caf, now, pump } = makeSyncScheduler();
379
+ const loop = createFrameLoop({
380
+ world,
381
+ renderer,
382
+ now,
383
+ raf,
384
+ caf,
385
+ drawSource: () => ({ worlds: [world, overlay], cameraOwner: 0, resourceOwner: 0 }),
386
+ });
387
+
388
+ loop.start().unwrap();
389
+ pump(1);
390
+ loop.stop().unwrap();
391
+
392
+ expect(detached).toEqual([overlay, world]);
393
+ });
394
+
395
+ it('terminates a paused loop and detaches its primary World exactly once', () => {
396
+ const world = new World();
397
+ const detached: World[] = [];
398
+ const renderer = {
399
+ backend: 'webgpu' as const,
400
+ attach: (candidate: World) => ({
401
+ ok: true as const,
402
+ value: lease(() => detached.push(candidate)),
403
+ }),
404
+ draw: () => ({
405
+ ok: true as const,
406
+ value: { frameId: 1, deviceGeneration: 0, completed: true as const },
407
+ }),
408
+ onError: () => () => {},
409
+ dispose: () => {},
410
+ } as unknown as Renderer;
411
+ const { raf, caf, now, pump } = makeSyncScheduler();
412
+ const loop = createFrameLoop({ world, renderer, now, raf, caf });
413
+
414
+ loop.start().unwrap();
415
+ pump(1);
416
+ loop.pause().unwrap();
417
+ loop.stop().unwrap();
418
+
419
+ expect(loop.getState()).toBe('stopped');
420
+ expect(detached).toEqual([world]);
421
+ });
422
+
423
+ it('terminal setStopped releases attachments once across repeated cleanup', () => {
424
+ const world = new World();
425
+ const detached: World[] = [];
426
+ const renderer = {
427
+ backend: 'webgpu' as const,
428
+ attach: (candidate: World) => ({
429
+ ok: true as const,
430
+ value: lease(() => detached.push(candidate)),
431
+ }),
432
+ draw: () => ({
433
+ ok: true as const,
434
+ value: { frameId: 1, deviceGeneration: 0, completed: true as const },
435
+ }),
436
+ onError: () => () => {},
437
+ dispose: () => {},
438
+ } as unknown as Renderer;
439
+ const { raf, caf, now, pump } = makeSyncScheduler();
440
+ const loop = createFrameLoop({ world, renderer, now, raf, caf });
441
+
442
+ loop.start().unwrap();
443
+ pump(1);
444
+ loop.setStopped();
445
+ loop.setStopped();
446
+
447
+ expect(loop.getState()).toBe('stopped');
448
+ expect(detached).toEqual([world]);
449
+ });
450
+ });
@@ -0,0 +1,50 @@
1
+ import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
2
+
3
+ describe('kernel worker runtime', () => {
4
+ let originalOnMessage: typeof globalThis.onmessage;
5
+ let receive: (event: MessageEvent<unknown>) => void;
6
+
7
+ beforeAll(async () => {
8
+ originalOnMessage = globalThis.onmessage;
9
+ await import('../execution/kernel-worker-runtime');
10
+ receive = globalThis.onmessage as unknown as (event: MessageEvent<unknown>) => void;
11
+ });
12
+
13
+ afterAll(() => {
14
+ globalThis.onmessage = originalOnMessage;
15
+ });
16
+
17
+ it('dispatches a preloaded kernel without a per-frame import promise', async () => {
18
+ const moduleUrl = 'data:text/javascript,export default { run() { return undefined; } }';
19
+ const preloadControl = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
20
+ const preloadStatus = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
21
+
22
+ receive({
23
+ data: {
24
+ kind: 'kernel-preload',
25
+ moduleUrl,
26
+ control: preloadControl,
27
+ status: preloadStatus,
28
+ jobIndex: 0,
29
+ },
30
+ } as MessageEvent<unknown>);
31
+ await vi.waitFor(() => expect(Atomics.load(preloadControl, 0)).toBe(1));
32
+ expect(Atomics.load(preloadStatus, 0)).toBe(1);
33
+
34
+ const control = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
35
+ const status = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
36
+ receive({
37
+ data: {
38
+ kind: 'kernel-job',
39
+ moduleUrl,
40
+ binding: { length: 0, read: {}, write: {} },
41
+ control,
42
+ status,
43
+ jobIndex: 0,
44
+ },
45
+ } as MessageEvent<unknown>);
46
+
47
+ expect(Atomics.load(control, 0)).toBe(1);
48
+ expect(Atomics.load(status, 0)).toBe(1);
49
+ });
50
+ });
@@ -0,0 +1,13 @@
1
+ import { Time, World } from '@forgeax/engine-ecs';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ describe('M4 time ownership inventory', () => {
5
+ it('advances the protected Time resource only through World.update', () => {
6
+ const world = new World();
7
+ expect(() =>
8
+ world.insertResource(Time, { delta: 1, elapsed: 1, maxDeltaSeconds: 1 }),
9
+ ).toThrow();
10
+ world.update(1 / 60).unwrap();
11
+ expect(world.getResource(Time).delta).toBeCloseTo(1 / 60);
12
+ });
13
+ });
@@ -0,0 +1,15 @@
1
+ import { animationPlugin } from '@forgeax/engine-animation';
2
+ import { createWorldContext, World } from '@forgeax/engine-ecs';
3
+ import { scenePlugin } from '@forgeax/engine-scene';
4
+ import { describe, expect, it } from 'vitest';
5
+
6
+ describe('scene plugin policy', () => {
7
+ it('can be composed on a host-owned world', async () => {
8
+ const world = new World();
9
+ await createWorldContext(world, [scenePlugin()]);
10
+ });
11
+ });
12
+
13
+ it('extracted animation plugin has a stable identity', () => {
14
+ expect(animationPlugin().name).toBe('animation');
15
+ });