@forgeax/engine-runtime 0.1.4 → 0.1.6

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 (606) hide show
  1. package/README.md +49 -83
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/__tests__/animation-graph-collect-roundtrip.test.d.ts +2 -0
  4. package/dist/__tests__/animation-graph-collect-roundtrip.test.d.ts.map +1 -0
  5. package/dist/__tests__/animation-graph-serialize.unit.test.d.ts +2 -0
  6. package/dist/__tests__/animation-graph-serialize.unit.test.d.ts.map +1 -0
  7. package/dist/__tests__/asset-brand-and-name-retirement.test.d.ts +2 -0
  8. package/dist/__tests__/asset-brand-and-name-retirement.test.d.ts.map +1 -0
  9. package/dist/__tests__/asset-error-exhaustive.test-d.d.ts +11 -0
  10. package/dist/__tests__/asset-error-exhaustive.test-d.d.ts.map +1 -0
  11. package/dist/__tests__/asset-registry-guid-reverse.test.d.ts +2 -0
  12. package/dist/__tests__/asset-registry-guid-reverse.test.d.ts.map +1 -0
  13. package/dist/__tests__/asset-registry-hdr-equirect.test.d.ts +2 -0
  14. package/dist/__tests__/asset-registry-hdr-equirect.test.d.ts.map +1 -0
  15. package/dist/__tests__/asset-registry-mesh-pack-payload.dawn.test.d.ts +2 -0
  16. package/dist/__tests__/asset-registry-mesh-pack-payload.dawn.test.d.ts.map +1 -0
  17. package/dist/__tests__/asset-registry-mounts.test.d.ts +2 -0
  18. package/dist/__tests__/asset-registry-mounts.test.d.ts.map +1 -0
  19. package/dist/__tests__/asset-registry.browser.test.d.ts +2 -0
  20. package/dist/__tests__/asset-registry.browser.test.d.ts.map +1 -0
  21. package/dist/__tests__/asset-registry.dawn.test.d.ts +2 -0
  22. package/dist/__tests__/asset-registry.dawn.test.d.ts.map +1 -0
  23. package/dist/__tests__/asset-registry.listCatalog.test.d.ts +2 -0
  24. package/dist/__tests__/asset-registry.listCatalog.test.d.ts.map +1 -0
  25. package/dist/__tests__/asset-registry.mesh-bin.test.d.ts +2 -0
  26. package/dist/__tests__/asset-registry.mesh-bin.test.d.ts.map +1 -0
  27. package/dist/__tests__/asset-registry.pack-cache.test.d.ts +2 -0
  28. package/dist/__tests__/asset-registry.pack-cache.test.d.ts.map +1 -0
  29. package/dist/__tests__/asset-registry.raw-container-failfast.test.d.ts +2 -0
  30. package/dist/__tests__/asset-registry.raw-container-failfast.test.d.ts.map +1 -0
  31. package/dist/__tests__/asset-registry.recursive.spec.d.ts +2 -0
  32. package/dist/__tests__/asset-registry.recursive.spec.d.ts.map +1 -0
  33. package/dist/__tests__/asset-registry.unit.test.d.ts +2 -0
  34. package/dist/__tests__/asset-registry.unit.test.d.ts.map +1 -0
  35. package/dist/__tests__/asset.unit.test.d.ts +2 -0
  36. package/dist/__tests__/asset.unit.test.d.ts.map +1 -0
  37. package/dist/__tests__/audio-load-by-guid.unit.test.d.ts +2 -0
  38. package/dist/__tests__/audio-load-by-guid.unit.test.d.ts.map +1 -0
  39. package/dist/__tests__/backend-selection.integration.test.d.ts +2 -0
  40. package/dist/__tests__/backend-selection.integration.test.d.ts.map +1 -0
  41. package/dist/__tests__/basis-catalog-dispatch.integration.test.d.ts +2 -0
  42. package/dist/__tests__/basis-catalog-dispatch.integration.test.d.ts.map +1 -0
  43. package/dist/__tests__/builtin-asset-registry.unit.test.d.ts +2 -0
  44. package/dist/__tests__/builtin-asset-registry.unit.test.d.ts.map +1 -0
  45. package/dist/__tests__/collect-scene-asset.test.d.ts +1 -2
  46. package/dist/__tests__/collect-scene-asset.test.d.ts.map +1 -1
  47. package/dist/__tests__/collect-scene-mount-collapse.test.d.ts +2 -0
  48. package/dist/__tests__/collect-scene-mount-collapse.test.d.ts.map +1 -0
  49. package/dist/__tests__/collect-transient-roundtrip.test.d.ts +1 -1
  50. package/dist/__tests__/collect-transient-roundtrip.test.d.ts.map +1 -1
  51. package/dist/__tests__/collect-transient-skip.test.d.ts +1 -2
  52. package/dist/__tests__/collect-transient-skip.test.d.ts.map +1 -1
  53. package/dist/__tests__/compressed-texture-physical-extent.integration.test.d.ts +2 -0
  54. package/dist/__tests__/compressed-texture-physical-extent.integration.test.d.ts.map +1 -0
  55. package/dist/__tests__/dawn/mesh-update-dawn.test.d.ts +2 -0
  56. package/dist/__tests__/dawn/mesh-update-dawn.test.d.ts.map +1 -0
  57. package/dist/__tests__/dawn/upload-texture.dawn.test.d.ts +2 -0
  58. package/dist/__tests__/dawn/upload-texture.dawn.test.d.ts.map +1 -0
  59. package/dist/__tests__/draw-published.d.ts +2 -2
  60. package/dist/__tests__/draw-published.d.ts.map +1 -1
  61. package/dist/__tests__/dynamic-texture-store.unit.test.d.ts +2 -0
  62. package/dist/__tests__/dynamic-texture-store.unit.test.d.ts.map +1 -0
  63. package/dist/__tests__/errors.unit.test.d.ts +2 -0
  64. package/dist/__tests__/errors.unit.test.d.ts.map +1 -0
  65. package/dist/__tests__/extract-skin-mismatch.unit.test.d.ts +2 -0
  66. package/dist/__tests__/extract-skin-mismatch.unit.test.d.ts.map +1 -0
  67. package/dist/__tests__/fetch-binary-decompress.test.d.ts +2 -0
  68. package/dist/__tests__/fetch-binary-decompress.test.d.ts.map +1 -0
  69. package/dist/__tests__/four-path-compression.integration.test.d.ts +2 -0
  70. package/dist/__tests__/four-path-compression.integration.test.d.ts.map +1 -0
  71. package/dist/__tests__/fullscreen-feature-host.dawn.test.d.ts +2 -0
  72. package/dist/__tests__/fullscreen-feature-host.dawn.test.d.ts.map +1 -0
  73. package/dist/__tests__/guid-for-asset.unit.test.d.ts +2 -0
  74. package/dist/__tests__/guid-for-asset.unit.test.d.ts.map +1 -0
  75. package/dist/__tests__/handle-migration.test.d.ts +2 -0
  76. package/dist/__tests__/handle-migration.test.d.ts.map +1 -0
  77. package/dist/__tests__/helpers/register-runtime-components.d.ts.map +1 -1
  78. package/dist/__tests__/host-custom-kind-contract.test-d.d.ts +2 -0
  79. package/dist/__tests__/host-custom-kind-contract.test-d.d.ts.map +1 -0
  80. package/dist/__tests__/host-custom-kind-contract.test.d.ts +2 -0
  81. package/dist/__tests__/host-custom-kind-contract.test.d.ts.map +1 -0
  82. package/dist/__tests__/inspect-name.test.d.ts +2 -0
  83. package/dist/__tests__/inspect-name.test.d.ts.map +1 -0
  84. package/dist/__tests__/instantiate-mount-deferred-wiring.test.d.ts +2 -0
  85. package/dist/__tests__/instantiate-mount-deferred-wiring.test.d.ts.map +1 -0
  86. package/dist/__tests__/loadbyguid-payload.unit.test.d.ts +2 -0
  87. package/dist/__tests__/loadbyguid-payload.unit.test.d.ts.map +1 -0
  88. package/dist/__tests__/loadbyguid-texture-intern.unit.test.d.ts +2 -0
  89. package/dist/__tests__/loadbyguid-texture-intern.unit.test.d.ts.map +1 -0
  90. package/dist/__tests__/loader-extract-graceful.test.d.ts +2 -0
  91. package/dist/__tests__/loader-extract-graceful.test.d.ts.map +1 -0
  92. package/dist/__tests__/loader-name-survival.test.d.ts +2 -0
  93. package/dist/__tests__/loader-name-survival.test.d.ts.map +1 -0
  94. package/dist/__tests__/mesh-bin-contract.test.d.ts +2 -0
  95. package/dist/__tests__/mesh-bin-contract.test.d.ts.map +1 -0
  96. package/dist/__tests__/mesh-bin-skin-roundtrip.unit.test.d.ts +2 -0
  97. package/dist/__tests__/mesh-bin-skin-roundtrip.unit.test.d.ts.map +1 -0
  98. package/dist/__tests__/mesh-bin.test.d.ts +2 -0
  99. package/dist/__tests__/mesh-bin.test.d.ts.map +1 -0
  100. package/dist/__tests__/mesh-loader-skin-contract.unit.test.d.ts +2 -0
  101. package/dist/__tests__/mesh-loader-skin-contract.unit.test.d.ts.map +1 -0
  102. package/dist/__tests__/new-kind-refs-contract.test.d.ts +2 -0
  103. package/dist/__tests__/new-kind-refs-contract.test.d.ts.map +1 -0
  104. package/dist/__tests__/package-promotion.test.d.ts +2 -0
  105. package/dist/__tests__/package-promotion.test.d.ts.map +1 -0
  106. package/dist/__tests__/package-rename.test.d.ts +2 -0
  107. package/dist/__tests__/package-rename.test.d.ts.map +1 -0
  108. package/dist/__tests__/recover-error-exhaustive.test-d.d.ts +1 -1
  109. package/dist/__tests__/recover-error-exhaustive.test-d.d.ts.map +1 -1
  110. package/dist/__tests__/render-error-exhaustive.test-d.d.ts +1 -1
  111. package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
  112. package/dist/__tests__/render-feature-prepared-graphics.fixture.d.ts +10 -0
  113. package/dist/__tests__/render-feature-prepared-graphics.fixture.d.ts.map +1 -0
  114. package/dist/__tests__/render-system-extract.unit.test.d.ts +2 -0
  115. package/dist/__tests__/render-system-extract.unit.test.d.ts.map +1 -0
  116. package/dist/__tests__/renderer-test-utils.d.ts +10 -0
  117. package/dist/__tests__/renderer-test-utils.d.ts.map +1 -0
  118. package/dist/__tests__/renderer.test-d.d.ts +2 -0
  119. package/dist/__tests__/renderer.test-d.d.ts.map +1 -0
  120. package/dist/__tests__/resolve-asset-handle-err.test.d.ts +2 -0
  121. package/dist/__tests__/resolve-asset-handle-err.test.d.ts.map +1 -0
  122. package/dist/__tests__/resolve-asset-handle.unit.test.d.ts +2 -0
  123. package/dist/__tests__/resolve-asset-handle.unit.test.d.ts.map +1 -0
  124. package/dist/__tests__/resolveName-fallback.test.d.ts +2 -0
  125. package/dist/__tests__/resolveName-fallback.test.d.ts.map +1 -0
  126. package/dist/__tests__/resolveName-null-package.test.d.ts +2 -0
  127. package/dist/__tests__/resolveName-null-package.test.d.ts.map +1 -0
  128. package/dist/__tests__/resolveName-unified-entry.test.d.ts +2 -0
  129. package/dist/__tests__/resolveName-unified-entry.test.d.ts.map +1 -0
  130. package/dist/__tests__/resolveName.test.d.ts +2 -0
  131. package/dist/__tests__/resolveName.test.d.ts.map +1 -0
  132. package/dist/__tests__/roots-to-scene-asset.test.d.ts +1 -1
  133. package/dist/__tests__/roots-to-scene-asset.test.d.ts.map +1 -1
  134. package/dist/__tests__/roundtrip-primitive.test.d.ts +2 -0
  135. package/dist/__tests__/roundtrip-primitive.test.d.ts.map +1 -0
  136. package/dist/__tests__/scene-bare-guid-refs-instantiate.test.d.ts +2 -0
  137. package/dist/__tests__/scene-bare-guid-refs-instantiate.test.d.ts.map +1 -0
  138. package/dist/__tests__/scene-instance-authoring.integration.test.d.ts +2 -0
  139. package/dist/__tests__/scene-instance-authoring.integration.test.d.ts.map +1 -0
  140. package/dist/__tests__/scene-mount-fixed-point.test.d.ts +2 -0
  141. package/dist/__tests__/scene-mount-fixed-point.test.d.ts.map +1 -0
  142. package/dist/__tests__/scene-mount-roundtrip.test.d.ts +2 -0
  143. package/dist/__tests__/scene-mount-roundtrip.test.d.ts.map +1 -0
  144. package/dist/__tests__/scene-mount-wrapper-roundtrip.test.d.ts +2 -0
  145. package/dist/__tests__/scene-mount-wrapper-roundtrip.test.d.ts.map +1 -0
  146. package/dist/__tests__/scene-sugar-instantiate.test.d.ts +2 -0
  147. package/dist/__tests__/scene-sugar-instantiate.test.d.ts.map +1 -0
  148. package/dist/__tests__/serialize-mounts-roundtrip.test.d.ts +2 -0
  149. package/dist/__tests__/serialize-mounts-roundtrip.test.d.ts.map +1 -0
  150. package/dist/__tests__/serialize-scene-asset.test.d.ts +1 -1
  151. package/dist/__tests__/serialize-scene-asset.test.d.ts.map +1 -1
  152. package/dist/__tests__/template-game-default-api-contract.test.d.ts +2 -0
  153. package/dist/__tests__/template-game-default-api-contract.test.d.ts.map +1 -0
  154. package/dist/__tests__/tileset-asset-validate.test.d.ts +2 -0
  155. package/dist/__tests__/tileset-asset-validate.test.d.ts.map +1 -0
  156. package/dist/__tests__/transcode-fallback.unit.test.d.ts +2 -0
  157. package/dist/__tests__/transcode-fallback.unit.test.d.ts.map +1 -0
  158. package/dist/__tests__/transient-runtime-behavior.test.d.ts +2 -0
  159. package/dist/__tests__/transient-runtime-behavior.test.d.ts.map +1 -0
  160. package/dist/__tests__/video-loader-consumption.test.d.ts +2 -0
  161. package/dist/__tests__/video-loader-consumption.test.d.ts.map +1 -0
  162. package/dist/__tests__/video-loader-smoke.dawn.test.d.ts +2 -0
  163. package/dist/__tests__/video-loader-smoke.dawn.test.d.ts.map +1 -0
  164. package/dist/backend-selection.d.ts +13 -0
  165. package/dist/backend-selection.d.ts.map +1 -0
  166. package/dist/collect-scene-asset.d.ts +3 -3
  167. package/dist/collect-scene-asset.d.ts.map +1 -1
  168. package/dist/createRenderer.d.ts +14 -2
  169. package/dist/createRenderer.d.ts.map +1 -1
  170. package/dist/errors/environment.d.ts +1 -1
  171. package/dist/errors/environment.d.ts.map +1 -1
  172. package/dist/index.d.ts +19 -81
  173. package/dist/index.d.ts.map +1 -1
  174. package/dist/index.mjs +105 -108
  175. package/dist/index.mjs.map +1 -1
  176. package/dist/renderer-host.d.ts +9 -0
  177. package/dist/renderer-host.d.ts.map +1 -0
  178. package/dist/renderer-host.mjs +62 -0
  179. package/dist/renderer-host.mjs.map +1 -0
  180. package/dist/scene-instances/index.d.ts +2 -0
  181. package/dist/scene-instances/index.d.ts.map +1 -0
  182. package/dist/scene-utils/mount-override-fold.d.ts +1 -2
  183. package/dist/scene-utils/mount-override-fold.d.ts.map +1 -1
  184. package/dist/systems/sprite-animation-tick.d.ts +1 -1
  185. package/dist/systems/sprite-animation-tick.d.ts.map +1 -1
  186. package/dist/systems/transparent-sort.d.ts +1 -2
  187. package/dist/systems/transparent-sort.d.ts.map +1 -1
  188. package/dist/tilemap-register-validate.d.ts.map +1 -1
  189. package/package.json +34 -33
  190. package/src/__tests__/active-camera.unit.test.ts +19 -9
  191. package/src/__tests__/animation-graph-collect-roundtrip.test.ts +213 -0
  192. package/src/__tests__/animation-graph-serialize.unit.test.ts +171 -0
  193. package/src/__tests__/append-injection.test.ts +1 -1
  194. package/src/__tests__/asset-brand-and-name-retirement.test.ts +102 -0
  195. package/src/__tests__/asset-error-exhaustive.test-d.ts +62 -0
  196. package/src/__tests__/asset-registry-guid-reverse.test.ts +575 -0
  197. package/src/__tests__/asset-registry-hdr-equirect.test.ts +78 -0
  198. package/src/__tests__/asset-registry-mesh-pack-payload.dawn.test.ts +232 -0
  199. package/src/__tests__/asset-registry-mounts.test.ts +806 -0
  200. package/src/__tests__/asset-registry.browser.test.ts +81 -0
  201. package/src/__tests__/asset-registry.dawn.test.ts +94 -0
  202. package/src/__tests__/asset-registry.listCatalog.test.ts +170 -0
  203. package/src/__tests__/asset-registry.mesh-bin.test.ts +316 -0
  204. package/src/__tests__/asset-registry.pack-cache.test.ts +969 -0
  205. package/src/__tests__/asset-registry.raw-container-failfast.test.ts +225 -0
  206. package/src/__tests__/asset-registry.recursive.spec.ts +1397 -0
  207. package/src/__tests__/asset-registry.unit.test.ts +1030 -0
  208. package/src/__tests__/asset.unit.test.ts +7107 -0
  209. package/src/__tests__/audio-load-by-guid.unit.test.ts +126 -0
  210. package/src/__tests__/backend-selection.integration.test.ts +11 -0
  211. package/src/__tests__/basis-catalog-dispatch.integration.test.ts +320 -0
  212. package/src/__tests__/bind-group-cache-keying.test.ts +4 -4
  213. package/src/__tests__/block-upload-math.unit.test.ts +2 -2
  214. package/src/__tests__/builtin-asset-registry.unit.test.ts +112 -0
  215. package/src/__tests__/builtin-shader-register-e2e.test.ts +1 -1
  216. package/src/__tests__/cache-false-eviction.test.ts +2 -1
  217. package/src/__tests__/cache-isolation-matrix.test.ts +2 -1
  218. package/src/__tests__/camera-clear-snapshot-equivalence.unit.test.ts +5 -6
  219. package/src/__tests__/clamp-to-last.e2e.browser.test.ts +29 -19
  220. package/src/__tests__/collect-scene-asset.test.ts +49 -52
  221. package/src/__tests__/collect-scene-mount-collapse.test.ts +391 -0
  222. package/src/__tests__/collect-subtree.unit.test.ts +1 -2
  223. package/src/__tests__/collect-transient-children-absent.test.ts +24 -10
  224. package/src/__tests__/collect-transient-roundtrip.test.ts +44 -20
  225. package/src/__tests__/collect-transient-skip.test.ts +24 -13
  226. package/src/__tests__/components.test-d.ts +1 -6
  227. package/src/__tests__/components.unit.test.ts +20 -19
  228. package/src/__tests__/composite-skybox-cross-world.browser.test.ts +59 -27
  229. package/src/__tests__/compressed-texture-physical-extent.integration.test.ts +139 -0
  230. package/src/__tests__/create-renderer-assembly.integration.test.ts +42 -29
  231. package/src/__tests__/createRenderer-dispose.test.ts +35 -402
  232. package/src/__tests__/createRenderer.browser-no-webgpu.test.ts +25 -30
  233. package/src/__tests__/createRenderer.unit.test.ts +1 -1
  234. package/src/__tests__/dawn/hello-sprite-nineslice-section.dawn.test.ts +27 -63
  235. package/src/__tests__/dawn/ibl-precompute-readback.dawn.test.ts +56 -47
  236. package/src/__tests__/dawn/instances-per-instance-pbr.dawn.test.ts +49 -29
  237. package/src/__tests__/dawn/instances-shadow.dawn.test.ts +26 -15
  238. package/src/__tests__/dawn/instances-uniform-fallback.dawn.test.ts +13 -14
  239. package/src/__tests__/dawn/material-alpha.dawn.test.ts +23 -31
  240. package/src/__tests__/dawn/material-cooked-fixture.dawn.test.ts +0 -18
  241. package/src/__tests__/dawn/{mesh-update.dawn.test.ts → mesh-update-dawn.test.ts} +42 -134
  242. package/src/__tests__/dawn/mipmap-srgb-linear.dawn.test.ts +14 -7
  243. package/src/__tests__/dawn/per-entity-material-texture-isolation.dawn.test.ts +16 -28
  244. package/src/__tests__/dawn/sprite-nineslice-mesh-bind.dawn.test.ts +23 -39
  245. package/src/__tests__/dawn/upload-texture.dawn.test.ts +198 -0
  246. package/src/__tests__/dawn/user-handle-mesh-render.dawn.test.ts +12 -14
  247. package/src/__tests__/dawn/user-mesh-upload.dawn.test.ts +27 -22
  248. package/src/__tests__/dawn/zero-manifest-mesh-fail.dawn.test.ts +25 -28
  249. package/src/__tests__/dawn/zero-manifest-ready.dawn.test.ts +10 -11
  250. package/src/__tests__/demo-sortscope-migration.unit.test.ts +5 -5
  251. package/src/__tests__/derive-bgl-integration.test.ts +34 -44
  252. package/src/__tests__/derive-render-data-skin.unit.test.ts +1 -1
  253. package/src/__tests__/draw-published.ts +11 -4
  254. package/src/__tests__/dynamic-texture-store.unit.test.ts +190 -0
  255. package/src/__tests__/equirect-bc6h.integration.test.ts +144 -21
  256. package/src/__tests__/errors.unit.test.ts +1203 -0
  257. package/src/__tests__/extract-frames-graceful-degradation.test.ts +3 -7
  258. package/src/__tests__/extract-frames-merge.test.ts +35 -22
  259. package/src/__tests__/extract-record-no-hardcoded-texture-fields.test.ts +5 -16
  260. package/src/__tests__/extract-skin-mismatch.unit.test.ts +322 -0
  261. package/src/__tests__/fetch-binary-decompress.test.ts +356 -0
  262. package/src/__tests__/four-path-compression.integration.test.ts +395 -0
  263. package/src/__tests__/frame-targets-shadow-regression.unit.test.ts +1 -1
  264. package/src/__tests__/fullscreen-feature-host.dawn.test.ts +80 -0
  265. package/src/__tests__/fullscreen-post-bgl-depth.test.ts +4 -2
  266. package/src/__tests__/fullscreen-post-depth-params-arity.test.ts +3 -3
  267. package/src/__tests__/fullscreen-post-process-pass.dawn.test.ts +39 -23
  268. package/src/__tests__/fullscreen-post-process-pass.unit.test.ts +4 -2
  269. package/src/__tests__/fxaa-pixel-diff.dawn.test.ts +28 -17
  270. package/src/__tests__/fxaa-validation-and-resize.dawn.test.ts +29 -18
  271. package/src/__tests__/fxaa-zero-overhead.dawn.test.ts +16 -12
  272. package/src/__tests__/geometry.unit.test.ts +525 -159
  273. package/src/__tests__/glyph-tilemap-worldid.test.ts +1 -1
  274. package/src/__tests__/gpu-resource-cow-survivor.test.ts +6 -4
  275. package/src/__tests__/guid-for-asset.unit.test.ts +102 -0
  276. package/src/__tests__/handle-migration.test.ts +136 -0
  277. package/src/__tests__/helpers/register-runtime-components.ts +4 -2
  278. package/src/__tests__/helpers/register-scene-components.ts +1 -1
  279. package/src/__tests__/host-custom-kind-contract.test-d.ts +47 -0
  280. package/src/__tests__/host-custom-kind-contract.test.ts +578 -0
  281. package/src/__tests__/inspect-name.test.ts +139 -0
  282. package/src/__tests__/instances.test-d.ts +1 -1
  283. package/src/__tests__/instantiate-mount-deferred-wiring.test.ts +112 -0
  284. package/src/__tests__/light-casters-9-light.browser.test.ts +73 -76
  285. package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +7 -10
  286. package/src/__tests__/light-snapshot.test-d.ts +2 -2
  287. package/src/__tests__/lights-vec-negative.unit.test.ts +1 -1
  288. package/src/__tests__/lights.unit.test.ts +16 -500
  289. package/src/__tests__/loadbyguid-payload.unit.test.ts +117 -0
  290. package/src/__tests__/loadbyguid-texture-intern.unit.test.ts +213 -0
  291. package/src/__tests__/loader-extract-graceful.test.ts +146 -0
  292. package/src/__tests__/loader-name-survival.test.ts +213 -0
  293. package/src/__tests__/m3-vec-negative.unit.test.ts +3 -2
  294. package/src/__tests__/material-texture-uv-scale.unit.test.ts +101 -31
  295. package/src/__tests__/materials.unit.test.ts +10 -27
  296. package/src/__tests__/mesh-bin-contract.test.ts +28 -0
  297. package/src/__tests__/mesh-bin-skin-roundtrip.unit.test.ts +149 -0
  298. package/src/__tests__/mesh-bin.test.ts +38 -0
  299. package/src/__tests__/mesh-loader-skin-contract.unit.test.ts +120 -0
  300. package/src/__tests__/mesh-ssbo-grow.unit.test.ts +3 -3
  301. package/src/__tests__/mip-gate.unit.test.ts +1 -1
  302. package/src/__tests__/msaa-sprite-pixel-diff.dawn.test.ts +29 -68
  303. package/src/__tests__/new-kind-refs-contract.test.ts +132 -0
  304. package/src/__tests__/node-shims.d.ts +4 -6
  305. package/src/__tests__/package-promotion.test.ts +88 -0
  306. package/src/__tests__/package-rename.test.ts +103 -0
  307. package/src/__tests__/pass-selector.unit.test.ts +2 -2
  308. package/src/__tests__/pbr-pipeline.unit.test.ts +14 -21
  309. package/src/__tests__/pipeline-builder-msaa.unit.test.ts +6 -9
  310. package/src/__tests__/pipeline-cache-keying.unit.test.ts +6 -9
  311. package/src/__tests__/pipeline-spec.test.ts +16 -177
  312. package/src/__tests__/pipeline.unit.test.ts +154 -1693
  313. package/src/__tests__/point-light-shadow.dawn.test.ts +33 -42
  314. package/src/__tests__/point-light-shadow.unit.test.ts +19 -60
  315. package/src/__tests__/post-process-error-codes.unit.test.ts +4 -1
  316. package/src/__tests__/post-process-params-component.unit.test.ts +1 -1
  317. package/src/__tests__/post-process-params-extract.unit.test.ts +2 -5
  318. package/src/__tests__/postprocess-camera-consistency.test.ts +4 -7
  319. package/src/__tests__/profiler-assembly.integration.test.ts +1 -1
  320. package/src/__tests__/propagate-transforms.dawn.test.ts +12 -11
  321. package/src/__tests__/record-worldid-isolation.test.ts +7 -29
  322. package/src/__tests__/recover-error-exhaustive.test-d.ts +1 -1
  323. package/src/__tests__/render-error-exhaustive.test-d.ts +81 -71
  324. package/src/__tests__/render-feature-assembly.test-d.ts +20 -9
  325. package/src/__tests__/render-feature-prepared-graphics-docs.unit.test.ts +8 -6
  326. package/src/__tests__/render-feature-prepared-graphics.browser.test.ts +60 -558
  327. package/src/__tests__/render-feature-prepared-graphics.dawn.test.ts +57 -544
  328. package/src/__tests__/render-feature-prepared-graphics.fixture.ts +104 -0
  329. package/src/__tests__/render-feature-prepared-graphics.integration.test.ts +40 -248
  330. package/src/__tests__/render-feature-prepared-graphics.regression.integration.test.ts +83 -340
  331. package/src/__tests__/render-feature-recover.integration.test.ts +9 -49
  332. package/src/__tests__/render-feature-runtime-surface.test-d.ts +17 -20
  333. package/src/__tests__/render-schema-consumer.integration.test.ts +20 -13
  334. package/src/__tests__/render-schema-owner-boundary.unit.test.ts +2 -6
  335. package/src/__tests__/render-system-cross-world-overlay-queue.test.ts +5 -5
  336. package/src/__tests__/render-system-extract-dispatch-ordering.test.ts +16 -31
  337. package/src/__tests__/render-system-extract.test.ts +36 -51
  338. package/src/__tests__/render-system-extract.unit.test.ts +360 -0
  339. package/src/__tests__/render-system-fold-bucket.unit.test.ts +3 -5
  340. package/src/__tests__/render-system-fold-fallback.unit.test.ts +6 -3
  341. package/src/__tests__/render-system-fold-material.unit.test.ts +3 -5
  342. package/src/__tests__/render-system-fold-metrics.unit.test.ts +5 -5
  343. package/src/__tests__/render-system-fold-modegate.unit.test.ts +6 -6
  344. package/src/__tests__/render-system-fold.dawn.test.ts +7 -5
  345. package/src/__tests__/render-system-mega.test.ts +292 -190
  346. package/src/__tests__/render-system-owner-split-contract.test.ts +10 -32
  347. package/src/__tests__/render-system-record-multi-material-textureview.test.ts +59 -45
  348. package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +23 -29
  349. package/src/__tests__/render-system-record.test.ts +398 -235
  350. package/src/__tests__/render-system-record.unit.test.ts +25 -26
  351. package/src/__tests__/render-system-skin-bg.unit.test.ts +8 -12
  352. package/src/__tests__/renderer-consumer.test-d.ts +16 -3
  353. package/src/__tests__/renderer-health.unit.test.ts +26 -916
  354. package/src/__tests__/renderer-lifecycle.integration.test.ts +19 -6
  355. package/src/__tests__/renderer-recover.unit.test.ts +49 -1069
  356. package/src/__tests__/renderer-result.test-d.ts +19 -9
  357. package/src/__tests__/renderer-surface.unit.test.ts +3 -5
  358. package/src/__tests__/renderer-test-utils.ts +56 -0
  359. package/src/__tests__/renderer.test-d.ts +59 -0
  360. package/src/__tests__/resolve-asset-handle-err.test.ts +111 -0
  361. package/src/__tests__/resolve-asset-handle.unit.test.ts +64 -0
  362. package/src/__tests__/resolve-depth-only-view.test.ts +1 -1
  363. package/src/__tests__/resolveName-fallback.test.ts +64 -0
  364. package/src/__tests__/resolveName-null-package.test.ts +38 -0
  365. package/src/__tests__/resolveName-unified-entry.test.ts +65 -0
  366. package/src/__tests__/resolveName.test.ts +64 -0
  367. package/src/__tests__/rhi-null-command-flow.unit.test.ts +43 -261
  368. package/src/__tests__/rhi-null-renderer-lifecycle.unit.test.ts +41 -169
  369. package/src/__tests__/roots-to-scene-asset.test.ts +199 -93
  370. package/src/__tests__/roundtrip-primitive.test.ts +351 -0
  371. package/src/__tests__/scene-bare-guid-refs-instantiate.test.ts +123 -0
  372. package/src/__tests__/scene-equirect-roundtrip.test.ts +103 -16
  373. package/src/__tests__/scene-instance-authoring.integration.test.ts +8 -0
  374. package/src/__tests__/scene-instance-bridge.integration.test.ts +1 -1
  375. package/src/__tests__/scene-instance-component.test.ts +1 -1
  376. package/src/__tests__/scene-mount-fixed-point.test.ts +257 -0
  377. package/src/__tests__/scene-mount-roundtrip.test.ts +987 -0
  378. package/src/__tests__/scene-mount-wrapper-roundtrip.test.ts +202 -0
  379. package/src/__tests__/scene-sugar-instantiate.test.ts +324 -0
  380. package/src/__tests__/serialize-mounts-roundtrip.test.ts +533 -0
  381. package/src/__tests__/serialize-scene-asset.test.ts +219 -22
  382. package/src/__tests__/shadow-caster-empty-schema.test.ts +3 -3
  383. package/src/__tests__/shadow-csm-cascade-loadop.test.ts +37 -17
  384. package/src/__tests__/shadow-csm-component.test.ts +3 -5
  385. package/src/__tests__/shadow-csm-extract.test.ts +2 -100
  386. package/src/__tests__/shadow-csm-orthoz.test.ts +2 -16
  387. package/src/__tests__/shadow-csm-runtime-vary.dawn.test.ts +19 -14
  388. package/src/__tests__/shadow-csm-tile-consistency.test.ts +32 -26
  389. package/src/__tests__/shadow-csm-ubo.dawn.test.ts +8 -49
  390. package/src/__tests__/shadow-csm-ubo.test.ts +3 -25
  391. package/src/__tests__/shadow-csm-viewport.browser.test.ts +1 -1
  392. package/src/__tests__/shadow-fields-observable.dawn.test.ts +67 -56
  393. package/src/__tests__/shadow-m1.dawn.test.ts +8 -9
  394. package/src/__tests__/shadow-mesh-ssbo-order.test.ts +11 -27
  395. package/src/__tests__/skin-errors.unit.test.ts +1 -1
  396. package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +7 -5
  397. package/src/__tests__/skylight-lazy-projection.test.ts +36 -15
  398. package/src/__tests__/sparse-tag-scene-roundtrip.test.ts +5 -5
  399. package/src/__tests__/sprite-cullmode-flip.dawn.test.ts +47 -64
  400. package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +12 -11
  401. package/src/__tests__/sprite-lit-extract-branch.test.ts +1 -1
  402. package/src/__tests__/sprite-lit-pipeline-cache-key.test.ts +1 -1
  403. package/src/__tests__/ssao-bgl.test.ts +16 -9
  404. package/src/__tests__/ssao-buffers.test.ts +2 -2
  405. package/src/__tests__/ssao-data.test.ts +2 -2
  406. package/src/__tests__/ssao-errors.test.ts +1 -1
  407. package/src/__tests__/ssao-passes.test.ts +64 -392
  408. package/src/__tests__/ssao-pipelines.test.ts +1 -1
  409. package/src/__tests__/ssao-uniform-layout.test.ts +8 -6
  410. package/src/__tests__/stencil-outline-pixel.dawn.test.ts +25 -17
  411. package/src/__tests__/swap-chain-format.unit.test.ts +5 -7
  412. package/src/__tests__/systems.unit.test.ts +536 -1326
  413. package/src/__tests__/template-game-default-api-contract.test.ts +362 -0
  414. package/src/__tests__/text.unit.test.ts +56 -33
  415. package/src/__tests__/tilemap-childed-despawn.test.ts +8 -14
  416. package/src/__tests__/tilemap-chunk-extract.overhang-bounds.test.ts +8 -12
  417. package/src/__tests__/tilemap-chunk-extract.propagate-order.test.ts +2 -1
  418. package/src/__tests__/tilemap-chunk-extract.test.ts +13 -17
  419. package/src/__tests__/tilemap-dirty-rebuild.test.ts +7 -11
  420. package/src/__tests__/tilemap-material-cache.test.ts +8 -13
  421. package/src/__tests__/tilemap-multi-atlas.test.ts +12 -32
  422. package/src/__tests__/tilemap-owner-boundary.unit.test.ts +5 -2
  423. package/src/__tests__/tilemap-region-pack.test.ts +8 -12
  424. package/src/__tests__/tilemap-spawn-flip-pivot.test.ts +8 -12
  425. package/src/__tests__/tilemap-uv-inset.test.ts +11 -14
  426. package/src/__tests__/tilemap-validate.test.ts +1 -1
  427. package/src/__tests__/tileset-asset-validate.test.ts +638 -0
  428. package/src/__tests__/transcode-fallback.unit.test.ts +207 -0
  429. package/src/__tests__/transform-hierarchy-pixel-diff.dawn.test.ts +36 -28
  430. package/src/__tests__/transient-fields-collect.test.ts +28 -21
  431. package/src/__tests__/transient-runtime-behavior.test.ts +227 -0
  432. package/src/__tests__/transparent-sort-tilemap-sprite-interleave.test.ts +4 -4
  433. package/src/__tests__/variant-selection.unit.test.ts +1 -1
  434. package/src/__tests__/video-extract-bindgroup.dawn.test.ts +89 -136
  435. package/src/__tests__/video-loader-consumption.test.ts +86 -0
  436. package/src/__tests__/video-loader-smoke.dawn.test.ts +60 -0
  437. package/src/__tests__/view-ubo-layout.browser.test.ts +0 -22
  438. package/src/__tests__/world-entity-key.test.ts +1 -1
  439. package/src/__tests__/world-scene-despawn.test.ts +1 -1
  440. package/src/__tests__/world-scene-instantiate.test.ts +2 -36
  441. package/src/__tests__/world-scene-nested.test.ts +1 -1
  442. package/src/__tests__/world-scene-override.test.ts +1 -1
  443. package/src/backend-selection.ts +69 -0
  444. package/src/collect-scene-asset.ts +28 -23
  445. package/src/components/__tests__/camera.test-d.ts +1 -1
  446. package/src/components/__tests__/camera.test.ts +1 -1
  447. package/src/components/__tests__/sprite-animation.test-d.ts +2 -2
  448. package/src/components/__tests__/tile-layer.test-d.ts +1 -1
  449. package/src/components/__tests__/tile-layer.unit.test.ts +2 -2
  450. package/src/createRenderer.ts +24 -36
  451. package/src/errors/environment.ts +1 -1
  452. package/src/index.ts +47 -113
  453. package/src/renderer-host.ts +33 -0
  454. package/src/scene-instances/__tests__/post-spawn-resolve-joints.unit.test.ts +1 -1
  455. package/src/scene-instances/index.ts +4 -0
  456. package/src/scene-utils/mount-override-fold.ts +3 -7
  457. package/src/systems/__tests__/transparent-sort.bench.ts +3 -3
  458. package/src/systems/sprite-animation-tick.ts +1 -1
  459. package/src/systems/transparent-sort.ts +38 -38
  460. package/src/tilemap-register-validate.ts +4 -4
  461. package/dist/__tests__/bind-group-cache-binding.test.d.ts +0 -2
  462. package/dist/__tests__/bind-group-cache-binding.test.d.ts.map +0 -1
  463. package/dist/__tests__/bind-group-cache-frame.test.d.ts +0 -2
  464. package/dist/__tests__/bind-group-cache-frame.test.d.ts.map +0 -1
  465. package/dist/__tests__/dawn/mesh-update.dawn.test.d.ts +0 -2
  466. package/dist/__tests__/dawn/mesh-update.dawn.test.d.ts.map +0 -1
  467. package/dist/__tests__/dispatch-fullscreen-pass-composite-swapchain.dawn.test.d.ts +0 -2
  468. package/dist/__tests__/dispatch-fullscreen-pass-composite-swapchain.dawn.test.d.ts.map +0 -1
  469. package/dist/__tests__/dispatch-fullscreen-pass-custom-reads.dawn.test.d.ts +0 -2
  470. package/dist/__tests__/dispatch-fullscreen-pass-custom-reads.dawn.test.d.ts.map +0 -1
  471. package/dist/__tests__/dispatch-fullscreen-pass-depth-composite.dawn.test.d.ts +0 -2
  472. package/dist/__tests__/dispatch-fullscreen-pass-depth-composite.dawn.test.d.ts.map +0 -1
  473. package/dist/__tests__/dispatch-fullscreen-pass-params.unit.test.d.ts +0 -2
  474. package/dist/__tests__/dispatch-fullscreen-pass-params.unit.test.d.ts.map +0 -1
  475. package/dist/__tests__/fog-producer-manifest.d.ts +0 -131
  476. package/dist/__tests__/fog-producer-manifest.d.ts.map +0 -1
  477. package/dist/__tests__/fog-producer-pixel-probe.d.ts +0 -42
  478. package/dist/__tests__/fog-producer-pixel-probe.d.ts.map +0 -1
  479. package/dist/__tests__/fog-producer-pixel.browser.test.d.ts +0 -2
  480. package/dist/__tests__/fog-producer-pixel.browser.test.d.ts.map +0 -1
  481. package/dist/__tests__/fog-producer-pixel.dawn.test.d.ts +0 -2
  482. package/dist/__tests__/fog-producer-pixel.dawn.test.d.ts.map +0 -1
  483. package/dist/__tests__/fog-producer-real-fixture.d.ts +0 -5
  484. package/dist/__tests__/fog-producer-real-fixture.d.ts.map +0 -1
  485. package/dist/__tests__/fog-producer-vfx-effect.d.ts +0 -3
  486. package/dist/__tests__/fog-producer-vfx-effect.d.ts.map +0 -1
  487. package/dist/__tests__/fog-scene-asset.integration.test.d.ts +0 -2
  488. package/dist/__tests__/fog-scene-asset.integration.test.d.ts.map +0 -1
  489. package/dist/__tests__/gpu-resource-store-cubemap-status.test.d.ts +0 -2
  490. package/dist/__tests__/gpu-resource-store-cubemap-status.test.d.ts.map +0 -1
  491. package/dist/__tests__/gpu-resource.test.d.ts +0 -2
  492. package/dist/__tests__/gpu-resource.test.d.ts.map +0 -1
  493. package/dist/__tests__/hdrp-bgl-slots.browser.test.d.ts +0 -2
  494. package/dist/__tests__/hdrp-bgl-slots.browser.test.d.ts.map +0 -1
  495. package/dist/__tests__/hdrp-buffers.unit.test.d.ts +0 -2
  496. package/dist/__tests__/hdrp-buffers.unit.test.d.ts.map +0 -1
  497. package/dist/__tests__/hdrp-execute-filter.unit.test.d.ts +0 -2
  498. package/dist/__tests__/hdrp-execute-filter.unit.test.d.ts.map +0 -1
  499. package/dist/__tests__/hdrp-gbuffer-attachments.browser.test.d.ts +0 -2
  500. package/dist/__tests__/hdrp-gbuffer-attachments.browser.test.d.ts.map +0 -1
  501. package/dist/__tests__/hdrp-pipeline-asset-type.test-d.d.ts +0 -2
  502. package/dist/__tests__/hdrp-pipeline-asset-type.test-d.d.ts.map +0 -1
  503. package/dist/__tests__/hdrp-ssao-boundary.test.d.ts +0 -2
  504. package/dist/__tests__/hdrp-ssao-boundary.test.d.ts.map +0 -1
  505. package/dist/__tests__/hdrp-ssao-falsification.test.d.ts +0 -2
  506. package/dist/__tests__/hdrp-ssao-falsification.test.d.ts.map +0 -1
  507. package/dist/__tests__/helpers/primitive-world.d.ts +0 -8
  508. package/dist/__tests__/helpers/primitive-world.d.ts.map +0 -1
  509. package/dist/__tests__/helpers/render-asset-port.d.ts +0 -15
  510. package/dist/__tests__/helpers/render-asset-port.d.ts.map +0 -1
  511. package/dist/__tests__/helpers/scene-asset-port.d.ts +0 -9
  512. package/dist/__tests__/helpers/scene-asset-port.d.ts.map +0 -1
  513. package/dist/__tests__/material-derived-coordinates.integration.test.d.ts +0 -2
  514. package/dist/__tests__/material-derived-coordinates.integration.test.d.ts.map +0 -1
  515. package/dist/__tests__/pipeline-layout-selection.test.d.ts +0 -2
  516. package/dist/__tests__/pipeline-layout-selection.test.d.ts.map +0 -1
  517. package/dist/__tests__/post-process-reads-narrowing.test-d.d.ts +0 -2
  518. package/dist/__tests__/post-process-reads-narrowing.test-d.d.ts.map +0 -1
  519. package/dist/__tests__/post-process-register-roundtrip.dawn.test.d.ts +0 -2
  520. package/dist/__tests__/post-process-register-roundtrip.dawn.test.d.ts.map +0 -1
  521. package/dist/__tests__/render-feature-zero-feature.browser.test.d.ts +0 -2
  522. package/dist/__tests__/render-feature-zero-feature.browser.test.d.ts.map +0 -1
  523. package/dist/__tests__/render-pipeline-trivial.dawn.test.d.ts +0 -2
  524. package/dist/__tests__/render-pipeline-trivial.dawn.test.d.ts.map +0 -1
  525. package/dist/__tests__/shadow-csm-caster-membership.test.d.ts +0 -2
  526. package/dist/__tests__/shadow-csm-caster-membership.test.d.ts.map +0 -1
  527. package/dist/__tests__/shadow-csm-pssm.test.d.ts +0 -2
  528. package/dist/__tests__/shadow-csm-pssm.test.d.ts.map +0 -1
  529. package/dist/__tests__/shadow-csm-temporal.browser.test.d.ts +0 -2
  530. package/dist/__tests__/shadow-csm-temporal.browser.test.d.ts.map +0 -1
  531. package/dist/__tests__/shadow-csm-viewport.test.d.ts +0 -2
  532. package/dist/__tests__/shadow-csm-viewport.test.d.ts.map +0 -1
  533. package/dist/__tests__/shadow-m2.dawn.test.d.ts +0 -2
  534. package/dist/__tests__/shadow-m2.dawn.test.d.ts.map +0 -1
  535. package/dist/__tests__/shadow-m3-calibrate-run.dawn.test.d.ts +0 -2
  536. package/dist/__tests__/shadow-m3-calibrate-run.dawn.test.d.ts.map +0 -1
  537. package/dist/__tests__/shadow-m3-calibrate.d.ts +0 -83
  538. package/dist/__tests__/shadow-m3-calibrate.d.ts.map +0 -1
  539. package/dist/__tests__/shadow-m3.dawn.test.d.ts +0 -2
  540. package/dist/__tests__/shadow-m3.dawn.test.d.ts.map +0 -1
  541. package/dist/__tests__/shadow-probe-control-flow.dawn.test.d.ts +0 -2
  542. package/dist/__tests__/shadow-probe-control-flow.dawn.test.d.ts.map +0 -1
  543. package/dist/__tests__/shadow-quality-metrics.dawn.test.d.ts +0 -2
  544. package/dist/__tests__/shadow-quality-metrics.dawn.test.d.ts.map +0 -1
  545. package/dist/__tests__/temporal-off-path.unit.test.d.ts +0 -2
  546. package/dist/__tests__/temporal-off-path.unit.test.d.ts.map +0 -1
  547. package/dist/__tests__/video-cache-regression.unit.test.d.ts +0 -2
  548. package/dist/__tests__/video-cache-regression.unit.test.d.ts.map +0 -1
  549. package/dist/asset-kind.d.ts +0 -4
  550. package/dist/asset-kind.d.ts.map +0 -1
  551. package/dist/errors/sprite-animation.d.ts +0 -3
  552. package/dist/errors/sprite-animation.d.ts.map +0 -1
  553. package/dist/scene-asset-port.d.ts +0 -17
  554. package/dist/scene-asset-port.d.ts.map +0 -1
  555. package/dist/scene-collect-errors.d.ts +0 -22
  556. package/dist/scene-collect-errors.d.ts.map +0 -1
  557. package/dist/shadow-m3-calibrate.d.ts +0 -40
  558. package/dist/shadow-m3-calibrate.d.ts.map +0 -1
  559. package/src/__tests__/bind-group-cache-binding.test.ts +0 -2513
  560. package/src/__tests__/bind-group-cache-frame.test.ts +0 -1511
  561. package/src/__tests__/dispatch-fullscreen-pass-composite-swapchain.dawn.test.ts +0 -204
  562. package/src/__tests__/dispatch-fullscreen-pass-custom-reads.dawn.test.ts +0 -297
  563. package/src/__tests__/dispatch-fullscreen-pass-depth-composite.dawn.test.ts +0 -271
  564. package/src/__tests__/dispatch-fullscreen-pass-params.unit.test.ts +0 -321
  565. package/src/__tests__/fog-producer-manifest.ts +0 -162
  566. package/src/__tests__/fog-producer-pixel-probe.ts +0 -263
  567. package/src/__tests__/fog-producer-pixel.browser.test.ts +0 -26
  568. package/src/__tests__/fog-producer-pixel.dawn.test.ts +0 -55
  569. package/src/__tests__/fog-producer-real-fixture.ts +0 -805
  570. package/src/__tests__/fog-producer-vfx-effect.ts +0 -272
  571. package/src/__tests__/fog-scene-asset.integration.test.ts +0 -123
  572. package/src/__tests__/gpu-resource-store-cubemap-status.test.ts +0 -207
  573. package/src/__tests__/gpu-resource.test.ts +0 -1206
  574. package/src/__tests__/hdrp-bgl-slots.browser.test.ts +0 -50
  575. package/src/__tests__/hdrp-buffers.unit.test.ts +0 -70
  576. package/src/__tests__/hdrp-execute-filter.unit.test.ts +0 -200
  577. package/src/__tests__/hdrp-gbuffer-attachments.browser.test.ts +0 -64
  578. package/src/__tests__/hdrp-pipeline-asset-type.test-d.ts +0 -139
  579. package/src/__tests__/hdrp-ssao-boundary.test.ts +0 -237
  580. package/src/__tests__/hdrp-ssao-falsification.test.ts +0 -289
  581. package/src/__tests__/helpers/primitive-world.ts +0 -91
  582. package/src/__tests__/helpers/render-asset-port.ts +0 -65
  583. package/src/__tests__/helpers/scene-asset-port.ts +0 -23
  584. package/src/__tests__/material-derived-coordinates.integration.test.ts +0 -100
  585. package/src/__tests__/pipeline-layout-selection.test.ts +0 -298
  586. package/src/__tests__/post-process-reads-narrowing.test-d.ts +0 -52
  587. package/src/__tests__/post-process-register-roundtrip.dawn.test.ts +0 -230
  588. package/src/__tests__/render-feature-zero-feature.browser.test.ts +0 -152
  589. package/src/__tests__/render-pipeline-trivial.dawn.test.ts +0 -525
  590. package/src/__tests__/shadow-csm-caster-membership.test.ts +0 -232
  591. package/src/__tests__/shadow-csm-pssm.test.ts +0 -142
  592. package/src/__tests__/shadow-csm-temporal.browser.test.ts +0 -145
  593. package/src/__tests__/shadow-csm-viewport.test.ts +0 -74
  594. package/src/__tests__/shadow-m2.dawn.test.ts +0 -394
  595. package/src/__tests__/shadow-m3-calibrate-run.dawn.test.ts +0 -248
  596. package/src/__tests__/shadow-m3-calibrate.ts +0 -203
  597. package/src/__tests__/shadow-m3.dawn.test.ts +0 -288
  598. package/src/__tests__/shadow-probe-control-flow.dawn.test.ts +0 -264
  599. package/src/__tests__/shadow-quality-metrics.dawn.test.ts +0 -324
  600. package/src/__tests__/temporal-off-path.unit.test.ts +0 -69
  601. package/src/__tests__/video-cache-regression.unit.test.ts +0 -186
  602. package/src/asset-kind.ts +0 -7
  603. package/src/errors/sprite-animation.ts +0 -5
  604. package/src/scene-asset-port.ts +0 -32
  605. package/src/scene-collect-errors.ts +0 -30
  606. package/src/shadow-m3-calibrate.ts +0 -171
@@ -58,46 +58,30 @@ import { readFileSync } from 'node:fs';
58
58
  import { resolve } from 'node:path';
59
59
  import { fileURLToPath } from 'node:url';
60
60
  import { AnimationPlayer } from '@forgeax/engine-animation';
61
+ import type { AssetRuntimeErrorCode } from '@forgeax/engine-assets-runtime';
62
+ import { AssetRegistry } from '@forgeax/engine-assets-runtime';
61
63
  import type { EntityHandle, World as WorldType } from '@forgeax/engine-ecs';
62
64
  import { ENTITY_NULL_RAW, World } from '@forgeax/engine-ecs';
63
- import { componentDefinition, componentSchema } from '@forgeax/engine-ecs/internal';
64
- import { createPrimitiveMesh } from '@forgeax/engine-geometry';
65
+ import { SpriteAnimationInvalidError } from '@forgeax/engine-ecs/projection';
65
66
  import { mat4, vec3 } from '@forgeax/engine-math';
66
- import type { Renderer as RendererType } from '@forgeax/engine-render';
67
- import {
68
- SPRITE_PLAYBACK_MODE_CLAMP,
69
- SPRITE_PLAYBACK_MODE_LOOP,
70
- SpriteAnimation,
71
- SpriteRegionOverride,
72
- } from '@forgeax/engine-render/authoring';
73
- import type { RenderAssetPort, RenderErrorCode } from '@forgeax/engine-render/internal';
67
+ import type { RenderErrorCode, Renderer as RendererType } from '@forgeax/engine-render';
74
68
  import {
75
69
  ANTIALIAS_FXAA,
76
70
  ANTIALIAS_NONE,
77
- antialiasFromF32,
78
71
  BLOOM_DISABLED,
79
72
  BLOOM_ENABLED,
80
- bloomEnabledFromF32,
81
73
  CAMERA_PROJECTION_ORTHOGRAPHIC,
82
74
  CAMERA_PROJECTION_PERSPECTIVE,
83
75
  Camera,
84
- cameraProjectionFromF32,
85
76
  MeshFilter,
86
77
  MeshRenderer,
87
- SkinPaletteOverflowError,
88
78
  SkyboxBackground,
89
79
  Skylight,
90
- selectSwapChainFormat,
91
- VertexStorageBufferUnavailableError,
92
- } from '@forgeax/engine-render/internal';
80
+ } from '@forgeax/engine-render';
93
81
  import type { BindGroupEntry, Buffer, Sampler, Texture, TextureView } from '@forgeax/engine-rhi';
94
82
  import { ok as rhiOk } from '@forgeax/engine-rhi';
95
83
  import { ChildOf, Name, propagateTransforms, Transform } from '@forgeax/engine-scene';
96
- import {
97
- DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
98
- DEFAULT_UNLIT_PARAM_SCHEMA,
99
- TONEMAP_LUMINANCE_EPSILON,
100
- } from '@forgeax/engine-shader';
84
+ import { TONEMAP_LUMINANCE_EPSILON } from '@forgeax/engine-shader';
101
85
  import type { SkinErrorCode } from '@forgeax/engine-skinning';
102
86
  import {
103
87
  Skin,
@@ -107,8 +91,6 @@ import {
107
91
  SkinJointPathUnresolvedError,
108
92
  } from '@forgeax/engine-skinning';
109
93
  import type {
110
- AssetErrorCode,
111
- EquirectAsset,
112
94
  Handle,
113
95
  MaterialAsset,
114
96
  MeshAsset,
@@ -117,20 +99,95 @@ import type {
117
99
  } from '@forgeax/engine-types';
118
100
  import { toShared } from '@forgeax/engine-types';
119
101
  import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
102
+ import { worldInternal } from '../../../ecs/src/world-internal';
103
+ import {
104
+ SPRITE_PLAYBACK_MODE_CLAMP,
105
+ SPRITE_PLAYBACK_MODE_LOOP,
106
+ SpriteAnimation,
107
+ SpriteRegionOverride,
108
+ } from '../../../render/src/components';
109
+ import {
110
+ type CameraProjection,
111
+ cameraProjectionFromF32,
112
+ } from '../../../render/src/components/camera';
113
+ import {
114
+ assembleMaterialWithSkylightEntries,
115
+ createSkylightFallback,
116
+ mergeSkylightIntoMaterialBgl,
117
+ } from '../../../render/src/ibl/skylight-bind-group';
118
+ import { buildPbrPipelineLayouts, buildUnlitMaterialBgl } from '../../../render/src/pbr-pipeline';
119
+ import { INSTANCE_STORAGE_STRIDE_FLOATS } from '../../../render/src/record/mesh-ssbo';
120
+ import { selectSwapChainFormat } from '../../../render/src/render-system';
121
+ import { createSkinPaletteAllocator } from '../../../render/src/systems/skin-palette-allocator';
122
+ import type { TransparentEntry } from '../../../render/src/systems/transparent-sort-config';
123
+ import { drawWithOwners } from './renderer-test-utils';
124
+
125
+ function componentFieldType(component: { fields: Record<string, { type: string }> }, name: string) {
126
+ return component.fields[name]?.type;
127
+ }
120
128
 
121
- function canonicalTestMeshAttributes(vertexCount: number) {
122
- return {
123
- position: new Float32Array(vertexCount * 3),
124
- normal: new Float32Array(vertexCount * 3),
125
- uv: new Float32Array(vertexCount * 2),
126
- tangent: new Float32Array(vertexCount * 4),
127
- };
129
+ function componentFieldDefault(
130
+ component: { fields: Record<string, { default?: unknown }> },
131
+ name: string,
132
+ ) {
133
+ return component.fields[name]?.default;
134
+ }
135
+
136
+ function componentSchemaTypes(component: { fields: Record<string, { type: string }> }) {
137
+ return Object.fromEntries(
138
+ Object.entries(component.fields).map(([name, field]) => [name, field.type]),
139
+ );
140
+ }
141
+
142
+ afterEach(() => vi.restoreAllMocks());
143
+
144
+ type RendererErrorObservation = {
145
+ readonly code: string;
146
+ readonly detail?: unknown;
147
+ readonly hint?: string;
148
+ };
149
+
150
+ function unwrapRendererError(value: unknown): RendererErrorObservation {
151
+ let current = value as RendererErrorObservation;
152
+ while (
153
+ current.detail !== undefined &&
154
+ typeof current.detail === 'object' &&
155
+ current.detail !== null
156
+ ) {
157
+ const cause = (current.detail as { cause?: unknown }).cause;
158
+ if (
159
+ cause === undefined ||
160
+ typeof cause !== 'object' ||
161
+ cause === null ||
162
+ typeof (cause as { code?: unknown }).code !== 'string'
163
+ ) {
164
+ break;
165
+ }
166
+ current = cause as RendererErrorObservation;
167
+ }
168
+ return current;
169
+ }
170
+
171
+ function subscribeRendererErrors(
172
+ renderer: RendererType,
173
+ listener: (error: RendererErrorObservation) => void,
174
+ ): () => void {
175
+ return renderer.subscribe((event) => {
176
+ if (event.kind === 'error') listener(unwrapRendererError(event.error));
177
+ });
178
+ }
179
+
180
+ function drawPublished(renderer: RendererType, world: WorldType) {
181
+ const attached = renderer.attach(world);
182
+ if (!attached.ok) throw attached.error;
183
+ world.update().unwrap();
184
+ return drawWithOwners(renderer, world);
128
185
  }
129
186
 
130
187
  // feat-20260704-runtime-tier1-decomposition M2 / w12: reconstitute the
131
188
  // eliminated top-level RuntimeErrorCode aggregate union (D-3) as a test-local
132
189
  // alias so the exhaustive-switch bodies below stay byte-identical (AC-09).
133
- type RuntimeLayerErrorCode = RenderErrorCode | AssetErrorCode | SkinErrorCode;
190
+ type RuntimeLayerErrorCode = RenderErrorCode | AssetRuntimeErrorCode | SkinErrorCode;
134
191
 
135
192
  import {
136
193
  AnimationTargetId,
@@ -138,113 +195,42 @@ import {
138
195
  advanceAnimationPlayer as canonicalAdvanceAnimationPlayer,
139
196
  } from '@forgeax/engine-animation';
140
197
  import { deriveAnimationTargetId } from '@forgeax/engine-animation/target-id';
141
- import type {
142
- CameraSnapshot,
143
- ExtractedLights,
144
- InstanceBufferCacheEntry,
145
- } from '@forgeax/engine-render/internal';
146
- // (moved from import body)
198
+ import { DeviceScope } from '../../../render/src/device/device-scope';
199
+ import { GpuBuffer } from '../../../render/src/gpu-resource';
200
+ import { getOrCreateIblCache, hasIblCache } from '../../../render/src/ibl/IblPipelineCache';
147
201
  import {
148
- assembleMaterialWithSkylightEntries,
149
- buildPbrPipelineLayouts,
150
- buildUnlitMaterialBgl,
151
- createSkinPaletteAllocator,
152
- createSkylightFallback,
202
+ disposeInstanceBuffers,
203
+ type InstanceBufferCacheEntry,
204
+ } from '../../../render/src/instance-buffer-cache';
205
+ import { standardPipeline as urpPipeline } from '../../../render/src/pipeline/standard-pipeline';
206
+ import { ZERO_CAMERA_CLEAR_FALLBACK } from '../../../render/src/record/frame-snapshot';
207
+ import {
208
+ warnMultiLightDirectional,
209
+ warnMultiLightPoint,
210
+ warnMultiLightSpot,
211
+ } from '../../../render/src/record/helpers';
212
+ import type { CameraSnapshot } from '../../../render/src/render-contract';
213
+ import {
214
+ type ExtractedLights,
153
215
  extractFrame,
154
216
  extractFrames,
155
- GpuResourceStore,
156
- getOrCreateIblCache,
157
- getTransparentSortConfig,
158
- hasIblCache,
159
- mergeSkylightIntoMaterialBgl,
160
217
  prepareExtractContext,
218
+ } from '../../../render/src/render-system-extract';
219
+ import {
220
+ getTransparentSortConfig,
161
221
  setTransparentSortConfig,
162
222
  TRANSPARENT_SORT_CONFIG_KEY,
163
223
  TRANSPARENT_SORT_MODE_LAYER_Y,
164
224
  TRANSPARENT_SORT_MODE_LAYER_YZ,
165
225
  TRANSPARENT_SORT_MODE_LAYER_Z,
166
- urpPipeline,
167
- warnMultiLightDirectional,
168
- warnMultiLightPoint,
169
- warnMultiLightSpot,
170
- ZERO_CAMERA_CLEAR_FALLBACK,
171
- } from '@forgeax/engine-render/internal';
172
- import { SpriteAnimationInvalidError } from '../errors/sprite-animation';
226
+ } from '../../../render/src/systems/transparent-sort-config';
173
227
  import { spriteAnimationTickSystem } from '../systems/sprite-animation-tick';
174
228
  import { REC709_LUMA_WEIGHTS, tonemapReinhardLuminance } from '../systems/tonemap';
175
229
  import { transparentSortEntries } from '../systems/transparent-sort';
176
230
  import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
177
231
 
178
- function canonicalMaterialParamSchema(identifier: string): string {
179
- if (identifier === 'forgeax::default-standard-pbr') {
180
- return JSON.stringify(DEFAULT_STANDARD_PBR_PARAM_SCHEMA);
181
- }
182
- if (identifier === 'forgeax::default-unlit') {
183
- return JSON.stringify(DEFAULT_UNLIT_PARAM_SCHEMA);
184
- }
185
- return '[]';
186
- }
187
-
188
- const EMPTY_EXTRACTED_ENVIRONMENT = {
189
- kind: 'none',
190
- worldId: 0,
191
- skylightCount: 0,
192
- skyboxCount: 0,
193
- } as const;
194
- const EMPTY_POST_PROCESS_PARAMS = new Map<string, Uint8Array>();
195
-
196
- function makeEnvironmentGenerationState() {
197
- return {
198
- deviceGeneration: 0,
199
- retired: [],
200
- status: 'idle' as const,
201
- generation: 0,
202
- desired: undefined,
203
- candidate: undefined,
204
- active: undefined,
205
- lastKnownGood: undefined,
206
- failureStage: undefined,
207
- bytes: 0,
208
- peakBytes: 0,
209
- rebuildCost: 0,
210
- publishCount: 0,
211
- };
212
- }
213
-
214
232
  {
215
233
  // --- from antialias-from-f32.test.ts ---
216
-
217
- describe('antialiasFromF32', () => {
218
- it('0 maps to none', () => {
219
- expect(antialiasFromF32(0)).toBe('none');
220
- });
221
-
222
- it('1 maps to fxaa', () => {
223
- expect(antialiasFromF32(1)).toBe('fxaa');
224
- });
225
-
226
- it('2 maps to msaa', () => {
227
- expect(antialiasFromF32(2)).toBe('msaa');
228
- });
229
-
230
- it('invalid value 99 throws RangeError with text containing the max valid value 2', () => {
231
- expect(() => antialiasFromF32(99)).toThrow(RangeError);
232
- expect(() => antialiasFromF32(99)).toThrow(/2/);
233
- });
234
-
235
- it('negative value throws RangeError with text containing 2', () => {
236
- expect(() => antialiasFromF32(-1)).toThrow(RangeError);
237
- });
238
-
239
- // Regression: verify constants match the mapping
240
- it('ANTIALIAS_NONE matches mapping', () => {
241
- expect(antialiasFromF32(ANTIALIAS_NONE)).toBe('none');
242
- });
243
-
244
- it('ANTIALIAS_FXAA matches mapping', () => {
245
- expect(antialiasFromF32(ANTIALIAS_FXAA)).toBe('fxaa');
246
- });
247
- });
248
234
  }
249
235
 
250
236
  {
@@ -332,11 +318,11 @@ function makeEnvironmentGenerationState() {
332
318
 
333
319
  describe('Camera antialias schema', () => {
334
320
  it('Camera schema includes antialias: f32 field', () => {
335
- expect(componentSchema(Camera).antialias).toBe('f32');
321
+ expect(componentFieldType(Camera, 'antialias')).toBe('f32');
336
322
  });
337
323
 
338
324
  it('Camera defaults antialias to 0 (ANTIALIAS_NONE)', () => {
339
- expect(componentDefinition(Camera).defaults?.antialias).toBe(0);
325
+ expect(componentFieldDefault(Camera, 'antialias')).toBe(0);
340
326
  });
341
327
  });
342
328
 
@@ -349,46 +335,6 @@ function makeEnvironmentGenerationState() {
349
335
  expect(ANTIALIAS_FXAA).toBe(1);
350
336
  });
351
337
  });
352
-
353
- describe('antialiasFromF32 mapping', () => {
354
- it('0 -> none', () => {
355
- expect(antialiasFromF32(0)).toBe('none');
356
- });
357
-
358
- it('1 -> fxaa', () => {
359
- expect(antialiasFromF32(1)).toBe('fxaa');
360
- });
361
-
362
- it('99 -> throws RangeError (fail-fast P3)', () => {
363
- expect(() => antialiasFromF32(99)).toThrow(RangeError);
364
- });
365
-
366
- it('-1 -> throws RangeError (fail-fast P3)', () => {
367
- expect(() => antialiasFromF32(-1)).toThrow(RangeError);
368
- });
369
-
370
- it('undefined-like NaN -> throws RangeError (fail-fast P3)', () => {
371
- expect(() => antialiasFromF32(Number.NaN)).toThrow(RangeError);
372
- });
373
- });
374
-
375
- describe('Antialias type union', () => {
376
- it('antialiasFromF32 return type is assignable to Antialias', () => {
377
- // Compile-time check: the return type annotation guarantees this.
378
- const result: Antialias = antialiasFromF32(0);
379
- expect(result).toBe('none');
380
- });
381
-
382
- it('antialiasFromF32(1) is assignable to Antialias', () => {
383
- const result: Antialias = antialiasFromF32(1);
384
- expect(result).toBe('fxaa');
385
- });
386
-
387
- it('antialiasFromF32(2) maps to msaa', () => {
388
- const result: Antialias = antialiasFromF32(2);
389
- expect(result).toBe('msaa');
390
- });
391
- });
392
338
  }
393
339
 
394
340
  {
@@ -396,35 +342,35 @@ function makeEnvironmentGenerationState() {
396
342
 
397
343
  describe('Camera bloom schema', () => {
398
344
  it('Camera schema includes bloom: f32 field', () => {
399
- expect(componentSchema(Camera).bloom).toBe('f32');
345
+ expect(componentFieldType(Camera, 'bloom')).toBe('f32');
400
346
  });
401
347
 
402
348
  it('Camera schema includes bloomThreshold: f32 field', () => {
403
- expect(componentSchema(Camera).bloomThreshold).toBe('f32');
349
+ expect(componentFieldType(Camera, 'bloomThreshold')).toBe('f32');
404
350
  });
405
351
 
406
352
  it('Camera schema includes bloomIntensity: f32 field', () => {
407
- expect(componentSchema(Camera).bloomIntensity).toBe('f32');
353
+ expect(componentFieldType(Camera, 'bloomIntensity')).toBe('f32');
408
354
  });
409
355
 
410
356
  it('Camera schema includes bloomBlurRadius: f32 field', () => {
411
- expect(componentSchema(Camera).bloomBlurRadius).toBe('f32');
357
+ expect(componentFieldType(Camera, 'bloomBlurRadius')).toBe('f32');
412
358
  });
413
359
 
414
360
  it('Camera defaults bloom to 0 (BLOOM_DISABLED)', () => {
415
- expect(componentDefinition(Camera).defaults?.bloom).toBe(0);
361
+ expect(componentFieldDefault(Camera, 'bloom')).toBe(0);
416
362
  });
417
363
 
418
364
  it('Camera defaults bloomThreshold to 1.0', () => {
419
- expect(componentDefinition(Camera).defaults?.bloomThreshold).toBe(1.0);
365
+ expect(componentFieldDefault(Camera, 'bloomThreshold')).toBe(1.0);
420
366
  });
421
367
 
422
368
  it('Camera defaults bloomIntensity to 1.0', () => {
423
- expect(componentDefinition(Camera).defaults?.bloomIntensity).toBe(1.0);
369
+ expect(componentFieldDefault(Camera, 'bloomIntensity')).toBe(1.0);
424
370
  });
425
371
 
426
372
  it('Camera defaults bloomBlurRadius to 4.0', () => {
427
- expect(componentDefinition(Camera).defaults?.bloomBlurRadius).toBe(4.0);
373
+ expect(componentFieldDefault(Camera, 'bloomBlurRadius')).toBe(4.0);
428
374
  });
429
375
  });
430
376
 
@@ -437,40 +383,6 @@ function makeEnvironmentGenerationState() {
437
383
  expect(BLOOM_ENABLED).toBe(1);
438
384
  });
439
385
  });
440
-
441
- describe('bloomEnabledFromF32 mapping', () => {
442
- it('0 -> off', () => {
443
- expect(bloomEnabledFromF32(0)).toBe('off');
444
- });
445
-
446
- it('1 -> on', () => {
447
- expect(bloomEnabledFromF32(1)).toBe('on');
448
- });
449
-
450
- it('99 -> throws RangeError (fail-fast P3)', () => {
451
- expect(() => bloomEnabledFromF32(99)).toThrow(RangeError);
452
- });
453
-
454
- it('-1 -> throws RangeError (fail-fast P3)', () => {
455
- expect(() => bloomEnabledFromF32(-1)).toThrow(RangeError);
456
- });
457
-
458
- it('undefined-like NaN -> throws RangeError (fail-fast P3)', () => {
459
- expect(() => bloomEnabledFromF32(Number.NaN)).toThrow(RangeError);
460
- });
461
- });
462
-
463
- describe('BloomEnabled type union', () => {
464
- it('bloomEnabledFromF32 return type is assignable to BloomEnabled', () => {
465
- const result: 'on' | 'off' = bloomEnabledFromF32(0);
466
- expect(result).toBe('off');
467
- });
468
-
469
- it('bloomEnabledFromF32(1) is assignable to BloomEnabled', () => {
470
- const result: 'on' | 'off' = bloomEnabledFromF32(1);
471
- expect(result).toBe('on');
472
- });
473
- });
474
386
  }
475
387
 
476
388
  {
@@ -478,18 +390,18 @@ function makeEnvironmentGenerationState() {
478
390
 
479
391
  describe('Camera clear-color schema (feat-20260709 M3: array<f32,4>)', () => {
480
392
  it('Camera schema includes clearColor: array<f32,4> field', () => {
481
- expect(componentSchema(Camera).clearColor).toBe('array<f32, 4>');
393
+ expect(componentFieldType(Camera, 'clearColor')).toBe('array<f32, 4>');
482
394
  });
483
395
 
484
396
  it('per-axis clear scalars are gone (collapsed into clearColor)', () => {
485
- expect('clearR' in componentSchema(Camera)).toBe(false);
486
- expect('clearG' in componentSchema(Camera)).toBe(false);
487
- expect('clearB' in componentSchema(Camera)).toBe(false);
488
- expect('clearA' in componentSchema(Camera)).toBe(false);
397
+ expect('clearR' in Camera.fields).toBe(false);
398
+ expect('clearG' in Camera.fields).toBe(false);
399
+ expect('clearB' in Camera.fields).toBe(false);
400
+ expect('clearA' in Camera.fields).toBe(false);
489
401
  });
490
402
 
491
403
  it('Camera defaults clearColor to transparent black [0,0,0,0]', () => {
492
- expect(Array.from(componentDefinition(Camera).defaults?.clearColor as Float32Array)).toEqual([
404
+ expect(Array.from(componentFieldDefault(Camera, 'clearColor') as Float32Array)).toEqual([
493
405
  0, 0, 0, 0,
494
406
  ]);
495
407
  });
@@ -534,34 +446,34 @@ function makeEnvironmentGenerationState() {
534
446
  ];
535
447
  }
536
448
 
537
- describe('Camera schema (20 fields including temporal historyVersion)', () => {
538
- it('componentSchema(Camera) has 20 fields with historyVersion as the temporal reset authority', () => {
539
- expect(Object.keys(componentSchema(Camera)).length).toBe(20);
540
- expect(componentSchema(Camera).fov).toBe('f32');
541
- expect(componentSchema(Camera).aspect).toBe('f32');
542
- expect(componentSchema(Camera).near).toBe('f32');
543
- expect(componentSchema(Camera).far).toBe('f32');
544
- expect(componentSchema(Camera).projection).toBe('f32');
545
- expect(componentSchema(Camera).left).toBe('f32');
546
- expect(componentSchema(Camera).right).toBe('f32');
547
- expect(componentSchema(Camera).bottom).toBe('f32');
548
- expect(componentSchema(Camera).top).toBe('f32');
549
- expect(componentSchema(Camera).historyVersion).toBe('u32');
449
+ describe('Camera schema (19 fields: 17 f32 + clearColor array + autoAspect bool after w9 + tonemap-mvp + fxaa + bloom + clearColor + aspect-sync extensions)', () => {
450
+ it('Camera.schema has 19 fields (17 f32 + clearColor array + autoAspect bool: perspective quartet + projection + ortho quartet + tonemap trio + antialias + bloom quartet + clearColor + autoAspect)', () => {
451
+ expect(Object.keys(Camera.fields).length).toBe(20);
452
+ expect(componentFieldType(Camera, 'fov')).toBe('f32');
453
+ expect(componentFieldType(Camera, 'aspect')).toBe('f32');
454
+ expect(componentFieldType(Camera, 'near')).toBe('f32');
455
+ expect(componentFieldType(Camera, 'far')).toBe('f32');
456
+ expect(componentFieldType(Camera, 'projection')).toBe('f32');
457
+ expect(componentFieldType(Camera, 'left')).toBe('f32');
458
+ expect(componentFieldType(Camera, 'right')).toBe('f32');
459
+ expect(componentFieldType(Camera, 'bottom')).toBe('f32');
460
+ expect(componentFieldType(Camera, 'top')).toBe('f32');
550
461
  // feat-20260519-tonemap-reinhard-mvp / M1 / T-M1.2 (AC-01 + D-1).
551
- expect(componentSchema(Camera).tonemap).toBe('f32');
552
- expect(componentSchema(Camera).exposure).toBe('f32');
553
- expect(componentSchema(Camera).whitePoint).toBe('f32');
554
- expect(componentSchema(Camera).antialias).toBe('f32');
462
+ expect(componentFieldType(Camera, 'tonemap')).toBe('f32');
463
+ expect(componentFieldType(Camera, 'exposure')).toBe('f32');
464
+ expect(componentFieldType(Camera, 'whitePoint')).toBe('f32');
465
+ expect(componentFieldType(Camera, 'antialias')).toBe('f32');
466
+ expect(componentFieldType(Camera, 'historyVersion')).toBe('u32');
555
467
  // feat-20260531-bloom-first-declarative-render-graph-pass / w2.
556
- expect(componentSchema(Camera).bloom).toBe('f32');
557
- expect(componentSchema(Camera).bloomThreshold).toBe('f32');
558
- expect(componentSchema(Camera).bloomIntensity).toBe('f32');
559
- expect(componentSchema(Camera).bloomBlurRadius).toBe('f32');
468
+ expect(componentFieldType(Camera, 'bloom')).toBe('f32');
469
+ expect(componentFieldType(Camera, 'bloomThreshold')).toBe('f32');
470
+ expect(componentFieldType(Camera, 'bloomIntensity')).toBe('f32');
471
+ expect(componentFieldType(Camera, 'bloomBlurRadius')).toBe('f32');
560
472
  // feat-20260709 M3 / D-3: clear-color collapsed into one array<f32,4>.
561
- expect(componentSchema(Camera).clearColor).toBe('array<f32, 4>');
473
+ expect(componentFieldType(Camera, 'clearColor')).toBe('array<f32, 4>');
562
474
  // feat-20260617-host-engine-contract-and-video-cutscene / M3: aspect-sync
563
475
  // opt-out flag (bool column tier).
564
- expect(componentSchema(Camera).autoAspect).toBe('bool');
476
+ expect(componentFieldType(Camera, 'autoAspect')).toBe('bool');
565
477
  });
566
478
  });
567
479
 
@@ -772,15 +684,7 @@ function makeEnvironmentGenerationState() {
772
684
  unlitPipeline: { __label: 'unlit' },
773
685
  standardPipeline: { __label: 'standard' },
774
686
  unlitPipelineHdr: null,
775
- standardPipelineHdr: null,
776
687
  shadowFallbackTextureView: { __label: 'shadow-fallback-view' },
777
- shadowProbePipeline: null,
778
- shadowProbeBindGroupLayout: null,
779
- shadowProbeLsmUbo: null,
780
- shadowProbeInputBuf: null,
781
- shadowProbeOutputTex: null,
782
- shadowProbeOutputView: null,
783
- shadowProbeStagingBuf: null,
784
688
  skylightFallback: null,
785
689
  pointLightsBuffer: { __label: 'point-lights-buf' },
786
690
  spotLightsBuffer: { __label: 'spot-lights-buf' },
@@ -847,7 +751,7 @@ function makeEnvironmentGenerationState() {
847
751
  const { internals } = makeRecorderInternals(log);
848
752
  const ps = makePipelineState();
849
753
  (internals as { getPipelineState: () => unknown }).getPipelineState = () => ps;
850
- const { recordFrame } = await import('@forgeax/engine-render/internal');
754
+ const { recordFrame } = await import('../../../render/src/record/frame');
851
755
  recordFrame(
852
756
  internals as never,
853
757
  new World() as never,
@@ -857,7 +761,6 @@ function makeEnvironmentGenerationState() {
857
761
  [],
858
762
  {
859
763
  frameNumber: 0,
860
- environmentGeneration: makeEnvironmentGenerationState(),
861
764
  compiledFrameGraph: null,
862
765
  compiledFrameGraphTopologyKey: null,
863
766
  retiredCompiledFrameGraphs: new Set(),
@@ -885,8 +788,10 @@ function makeEnvironmentGenerationState() {
885
788
  },
886
789
  { unlit: 0 },
887
790
  { createBindGroup: 0, keys: [] },
888
- EMPTY_EXTRACTED_ENVIRONMENT,
889
- EMPTY_POST_PROCESS_PARAMS,
791
+ undefined,
792
+ 0,
793
+ undefined,
794
+ 0,
890
795
  );
891
796
 
892
797
  // No FXAA intermediate texture should be allocated when antialias='none'.
@@ -901,7 +806,7 @@ function makeEnvironmentGenerationState() {
901
806
  const { internals } = makeRecorderInternals(log);
902
807
  const ps = makePipelineState();
903
808
  (internals as { getPipelineState: () => unknown }).getPipelineState = () => ps;
904
- const { recordFrame } = await import('@forgeax/engine-render/internal');
809
+ const { recordFrame } = await import('../../../render/src/record/frame');
905
810
  recordFrame(
906
811
  internals as never,
907
812
  new World() as never,
@@ -911,7 +816,6 @@ function makeEnvironmentGenerationState() {
911
816
  [],
912
817
  {
913
818
  frameNumber: 0,
914
- environmentGeneration: makeEnvironmentGenerationState(),
915
819
  compiledFrameGraph: null,
916
820
  compiledFrameGraphTopologyKey: null,
917
821
  retiredCompiledFrameGraphs: new Set(),
@@ -939,8 +843,10 @@ function makeEnvironmentGenerationState() {
939
843
  },
940
844
  { unlit: 0 },
941
845
  { createBindGroup: 0, keys: [] },
942
- EMPTY_EXTRACTED_ENVIRONMENT,
943
- EMPTY_POST_PROCESS_PARAMS,
846
+ undefined,
847
+ 0,
848
+ undefined,
849
+ 0,
944
850
  );
945
851
 
946
852
  // TODO (feat-20260608-ci-time-cut): row 2/3/4 placeholders pruned -- they
@@ -1200,29 +1106,29 @@ function makeEnvironmentGenerationState() {
1200
1106
  // --- from ibl-pipeline-cache.test.ts ---
1201
1107
 
1202
1108
  describe('t11 - IblPipelineCache counter invariant + deferred-replay', () => {
1203
- // (a) IblPipelineCache initializes per-device WeakMap
1204
- it('IblPipelineCache maintains per-device WeakMap via getOrCreateIblCache', () => {
1205
- const mockDevice1 = {};
1206
- const mockDevice2 = {};
1109
+ // (a) IblPipelineCache initializes one cache per DeviceScope generation
1110
+ it('IblPipelineCache maintains per-scope ownership via getOrCreateIblCache', () => {
1111
+ const scope1 = DeviceScope.create(1, 'systems-test-1');
1112
+ const scope2 = DeviceScope.create(2, 'systems-test-2');
1207
1113
 
1208
- const cache1 = getOrCreateIblCache(mockDevice1);
1209
- const cache2 = getOrCreateIblCache(mockDevice2);
1114
+ const cache1 = getOrCreateIblCache(scope1);
1115
+ const cache2 = getOrCreateIblCache(scope2);
1210
1116
 
1211
1117
  // Different devices get different cache instances.
1212
1118
  expect(cache1).not.toBe(cache2);
1213
1119
 
1214
1120
  // Same device returns same instance.
1215
- expect(getOrCreateIblCache(mockDevice1)).toBe(cache1);
1121
+ expect(getOrCreateIblCache(scope1)).toBe(cache1);
1216
1122
 
1217
1123
  // hasIblCache confirms registration.
1218
- expect(hasIblCache(mockDevice1)).toBe(true);
1219
- expect(hasIblCache(mockDevice2)).toBe(true);
1220
- expect(hasIblCache({})).toBe(false);
1124
+ expect(hasIblCache(scope1)).toBe(true);
1125
+ expect(hasIblCache(scope2)).toBe(true);
1126
+ expect(hasIblCache(DeviceScope.create(3, 'systems-test-3'))).toBe(false);
1221
1127
  });
1222
1128
 
1223
1129
  // (d) counters initialized at 0
1224
1130
  it('counters are initialized at 0 before any pass execution', () => {
1225
- const cache = getOrCreateIblCache({});
1131
+ const cache = getOrCreateIblCache(DeviceScope.create(4, 'systems-test-4'));
1226
1132
  expect(cache.irradianceBakeCount).toBe(0);
1227
1133
  expect(cache.prefilterBakeCount).toBe(0);
1228
1134
  expect(cache.brdfLutBakeCount).toBe(0);
@@ -1233,7 +1139,7 @@ function makeEnvironmentGenerationState() {
1233
1139
  // The actual GPU pass execution that sets these counters is wired in
1234
1140
  // uploadCubemapFromEquirect (t20 in asset-registry.ts).
1235
1141
  it('iblPrepass counters can be incremented post-execution', () => {
1236
- const cache = getOrCreateIblCache({});
1142
+ const cache = getOrCreateIblCache(DeviceScope.create(5, 'systems-test-5'));
1237
1143
  cache.irradianceBakeCount += 1;
1238
1144
  cache.prefilterBakeCount += 1;
1239
1145
  cache.brdfLutBakeCount += 1;
@@ -1243,10 +1149,10 @@ function makeEnvironmentGenerationState() {
1243
1149
  });
1244
1150
 
1245
1151
  it('iblPrepass counters stay independent across devices', () => {
1246
- const deviceA = {};
1247
- const deviceB = {};
1248
- const cacheA = getOrCreateIblCache(deviceA);
1249
- const cacheB = getOrCreateIblCache(deviceB);
1152
+ const scopeA = DeviceScope.create(6, 'systems-test-6');
1153
+ const scopeB = DeviceScope.create(7, 'systems-test-7');
1154
+ const cacheA = getOrCreateIblCache(scopeA);
1155
+ const cacheB = getOrCreateIblCache(scopeB);
1250
1156
 
1251
1157
  cacheA.irradianceBakeCount += 1;
1252
1158
  cacheB.prefilterBakeCount += 1;
@@ -1266,7 +1172,7 @@ function makeEnvironmentGenerationState() {
1266
1172
  {
1267
1173
  // --- from ibl-runtime-probe.test.ts ---
1268
1174
 
1269
- const mockCaps = {
1175
+ const _mockCaps = {
1270
1176
  backendKind: 'webgpu' as const,
1271
1177
  compute: true,
1272
1178
  timestampQuery: false,
@@ -1287,8 +1193,8 @@ function makeEnvironmentGenerationState() {
1287
1193
  float32Filterable: false,
1288
1194
  };
1289
1195
 
1290
- // feat-20260601-gpu-resource-store-extraction M1: uploadCubemapFromEquirect
1291
- // moved to GpuResourceStore (D-3 register-relay injected at configureGpuDevice,
1196
+ // feat-20260601-device/gpu-residency-extraction M1: uploadCubemapFromEquirect
1197
+ // moved to GpuResidencyCache (D-3 register-relay injected at configureGpuDevice,
1292
1198
  // source POD passed to the call; store holds no registry reference).
1293
1199
 
1294
1200
  interface MockEncoder {
@@ -1320,7 +1226,7 @@ function makeEnvironmentGenerationState() {
1320
1226
  }
1321
1227
 
1322
1228
  // biome-ignore lint/suspicious/noExplicitAny: opaque mock GPU device surface
1323
- function makeMockDevice(probe: MockSubmitProbe): any {
1229
+ function _makeMockDevice(probe: MockSubmitProbe): any {
1324
1230
  const mockShader = { __mock: 'shader' };
1325
1231
  const mockPipeline = { __mock: 'pipeline' };
1326
1232
  const mockBgl = { __mock: 'bgl' };
@@ -1372,7 +1278,7 @@ function makeEnvironmentGenerationState() {
1372
1278
  };
1373
1279
  }
1374
1280
 
1375
- function makeEquirect(): {
1281
+ function _makeEquirect(): {
1376
1282
  kind: 'equirect';
1377
1283
  width: number;
1378
1284
  height: number;
@@ -1389,128 +1295,6 @@ function makeEnvironmentGenerationState() {
1389
1295
  colorSpace: 'linear',
1390
1296
  };
1391
1297
  }
1392
-
1393
- describe('t50 (M3.5) -- AC-20 runtime probe via mock device', () => {
1394
- it('(a) uploadCubemapFromEquirect calls queue.submit >= 1', async () => {
1395
- const probe: MockSubmitProbe = {
1396
- submitCalls: 0,
1397
- encoders: [],
1398
- shouldThrowOnSubmit: false,
1399
- };
1400
- const device = makeMockDevice(probe);
1401
-
1402
- const equirect = makeEquirect();
1403
- const store = new GpuResourceStore();
1404
- const world = new World();
1405
- const equirectHandle = world.allocSharedRef('EquirectAsset', equirect);
1406
-
1407
- store.configureGpuDevice(
1408
- device,
1409
- async (_d, desc) =>
1410
- // biome-ignore lint/suspicious/noExplicitAny: mock shader module
1411
- rhiOk({ __mock: 'shader', label: desc.label ?? '' }) as any,
1412
- (w: World, pod: EquirectAsset) => rhiOk(w.allocSharedRef('EquirectAsset', pod)),
1413
- mockCaps,
1414
- );
1415
-
1416
- // biome-ignore lint/suspicious/noExplicitAny: package-internal method reached via store cast
1417
- const result = await (store as any)._uploadCubemapFromEquirect(
1418
- world,
1419
- equirectHandle,
1420
- equirect,
1421
- );
1422
- expect(result.ok).toBe(true);
1423
- expect(probe.submitCalls).toBeGreaterThanOrEqual(1);
1424
- });
1425
-
1426
- it('(b) encoder contains 4 beginRenderPass calls (equirect / irradiance / prefilter / brdf-lut)', async () => {
1427
- const probe: MockSubmitProbe = {
1428
- submitCalls: 0,
1429
- encoders: [],
1430
- shouldThrowOnSubmit: false,
1431
- };
1432
- const device = makeMockDevice(probe);
1433
-
1434
- const equirect = makeEquirect();
1435
- const store = new GpuResourceStore();
1436
- const world = new World();
1437
- const equirectHandle = world.allocSharedRef('EquirectAsset', equirect);
1438
-
1439
- store.configureGpuDevice(
1440
- device,
1441
- async (_d, desc) =>
1442
- // biome-ignore lint/suspicious/noExplicitAny: mock shader module
1443
- rhiOk({ __mock: 'shader', label: desc.label ?? '' }) as any,
1444
- (w: World, pod: EquirectAsset) => rhiOk(w.allocSharedRef('EquirectAsset', pod)),
1445
- mockCaps,
1446
- );
1447
-
1448
- // biome-ignore lint/suspicious/noExplicitAny: package-internal method reached via store cast
1449
- await (store as any)._uploadCubemapFromEquirect(world, equirectHandle, equirect);
1450
-
1451
- const totalBeginPass = probe.encoders.reduce((s, e) => s + e.beginRenderPassCount, 0);
1452
- // 4 distinct pass families; each cube-face family unfolds to 6 sub-passes
1453
- // (24 for equirect+irradiance), prefilter to 30 (5 mip x 6 face),
1454
- // brdf-lut 1. Total ~ 55. We assert minimum 4 distinct pass labels.
1455
- const labels = new Set(
1456
- probe.encoders
1457
- .flatMap((e) => e.passes.map((p) => p.label ?? ''))
1458
- .map((l) => {
1459
- // strip face / mip suffix for family grouping
1460
- return l.replace(/-face\d+/, '').replace(/-mip\d+/, '');
1461
- }),
1462
- );
1463
- expect(totalBeginPass).toBeGreaterThanOrEqual(4);
1464
- expect(labels.has('ibl-equirect-to-cube')).toBe(true);
1465
- expect(labels.has('ibl-irradiance')).toBe(true);
1466
- expect(labels.has('ibl-prefilter')).toBe(true);
1467
- expect(labels.has('ibl-brdf-lut')).toBe(true);
1468
- });
1469
-
1470
- it('(c) AC-20 critical: counters stay at 0 when queue.submit throws', async () => {
1471
- const probe: MockSubmitProbe = {
1472
- submitCalls: 0,
1473
- encoders: [],
1474
- shouldThrowOnSubmit: true,
1475
- };
1476
- const device = makeMockDevice(probe);
1477
-
1478
- const equirect = makeEquirect();
1479
- const store = new GpuResourceStore();
1480
- const world = new World();
1481
- const equirectHandle = world.allocSharedRef('EquirectAsset', equirect);
1482
-
1483
- store.configureGpuDevice(
1484
- device,
1485
- async (_d, desc) =>
1486
- // biome-ignore lint/suspicious/noExplicitAny: mock shader module
1487
- rhiOk({ __mock: 'shader', label: desc.label ?? '' }) as any,
1488
- (w: World, pod: EquirectAsset) => rhiOk(w.allocSharedRef('EquirectAsset', pod)),
1489
- mockCaps,
1490
- );
1491
-
1492
- // biome-ignore lint/suspicious/noExplicitAny: package-internal method reached via store cast
1493
- const result = await (store as any)._uploadCubemapFromEquirect(
1494
- world,
1495
- equirectHandle,
1496
- equirect,
1497
- );
1498
-
1499
- // Whether result.ok is true or false depends on impl error propagation;
1500
- // the critical assertion is the counter invariant -- counters must NOT
1501
- // increment when submit fails. counter += 1 placed BEFORE submit is the
1502
- // round-1 anti-pattern; this assertion fails in that case.
1503
- const cache = getOrCreateIblCache(device);
1504
- expect(cache.irradianceBakeCount).toBe(0);
1505
- expect(cache.prefilterBakeCount).toBe(0);
1506
- expect(cache.brdfLutBakeCount).toBe(0);
1507
-
1508
- // If the impl chose to surface submit failure structurally, result.ok
1509
- // is false. We allow either outcome here -- the load-bearing fact is
1510
- // counter == 0.
1511
- void result;
1512
- });
1513
- });
1514
1298
  }
1515
1299
 
1516
1300
  {
@@ -1806,6 +1590,7 @@ function makeEnvironmentGenerationState() {
1806
1590
  limits: {},
1807
1591
  queue: {
1808
1592
  submit: () => undefined,
1593
+ onSubmittedWorkDone: () => Promise.resolve(undefined),
1809
1594
  writeBuffer: () => undefined,
1810
1595
  writeTexture: () => undefined,
1811
1596
  },
@@ -1859,7 +1644,7 @@ function makeEnvironmentGenerationState() {
1859
1644
  identifier,
1860
1645
  sourcePath: `${identifier}.wgsl`,
1861
1646
  composedWgsl,
1862
- paramSchema: canonicalMaterialParamSchema(identifier),
1647
+ paramSchema: '[]',
1863
1648
  variants: [],
1864
1649
  });
1865
1650
  const manifest = {
@@ -1886,15 +1671,10 @@ function makeEnvironmentGenerationState() {
1886
1671
  return `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
1887
1672
  }
1888
1673
 
1889
- interface RendererLike {
1890
- ready: Promise<void>;
1891
- draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
1892
- onError: (cb: (err: { code: string }) => void) => () => void;
1893
- assets: { register: (asset: unknown) => { ok: boolean; value: unknown } };
1894
- }
1674
+ type RendererLike = RendererType;
1895
1675
 
1896
1676
  async function importEngine(): Promise<{
1897
- createRenderer: (canvas: unknown, opts?: unknown, bundler?: unknown) => Promise<RendererLike>;
1677
+ createRenderer: (...args: unknown[]) => Promise<{ unwrap(): RendererLike }>;
1898
1678
  }> {
1899
1679
  return (await import(ENGINE)) as never;
1900
1680
  }
@@ -1913,7 +1693,7 @@ function makeEnvironmentGenerationState() {
1913
1693
  DirectionalLight: unknown;
1914
1694
  }> {
1915
1695
  return {
1916
- ...(await import('@forgeax/engine-render/internal')),
1696
+ ...(await import('@forgeax/engine-render')),
1917
1697
  ...(await import('@forgeax/engine-scene')),
1918
1698
  } as never;
1919
1699
  }
@@ -1939,13 +1719,14 @@ function makeEnvironmentGenerationState() {
1939
1719
  kind: 'mesh',
1940
1720
  vertices: new Float32Array(3 * 12),
1941
1721
  indices: new Uint16Array([0, 1, 2]),
1942
- attributes: canonicalTestMeshAttributes(3),
1722
+ attributes: { position: new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]) },
1723
+ aabb: new Float32Array([-1, -1, -1, 1, 1, 1]),
1943
1724
  materialSlots: [{ slotName: 'Default' }],
1944
1725
  submeshes: [
1945
1726
  {
1946
1727
  indexOffset: 0,
1947
1728
  indexCount: 3,
1948
- vertexCount: 0,
1729
+ vertexCount: 3,
1949
1730
  materialSlot: 0,
1950
1731
  topology: 'triangle-list',
1951
1732
  },
@@ -1958,13 +1739,14 @@ function makeEnvironmentGenerationState() {
1958
1739
  kind: 'mesh',
1959
1740
  vertices: new Float32Array(2 * 12),
1960
1741
  indices: new Uint16Array([0, 1]),
1961
- attributes: canonicalTestMeshAttributes(2),
1742
+ attributes: { position: new Float32Array([0, 0, 0, 1, 0, 0]) },
1743
+ aabb: new Float32Array([-1, -1, -1, 1, 1, 1]),
1962
1744
  materialSlots: [{ slotName: 'Default' }],
1963
1745
  submeshes: [
1964
1746
  {
1965
1747
  indexOffset: 0,
1966
1748
  indexCount: 2,
1967
- vertexCount: 0,
1749
+ vertexCount: 2,
1968
1750
  materialSlot: 0,
1969
1751
  topology: 'line-list',
1970
1752
  },
@@ -1979,12 +1761,9 @@ function makeEnvironmentGenerationState() {
1979
1761
  const { device } = makeMockGPUDevice(shadow, main);
1980
1762
  vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
1981
1763
  const { createRenderer } = await importEngine();
1982
- const renderer = await createRenderer(
1983
- makeMockCanvas(),
1984
- {},
1985
- { shaderManifestUrl: buildManifestDataUrl() },
1986
- );
1987
- await renderer.ready;
1764
+ const renderer = (
1765
+ await createRenderer(makeMockCanvas(), {}, { shaderManifestUrl: buildManifestDataUrl() })
1766
+ ).unwrap();
1988
1767
  return { renderer };
1989
1768
  }
1990
1769
 
@@ -2002,26 +1781,11 @@ function makeEnvironmentGenerationState() {
2002
1781
  const main = makePassSpies();
2003
1782
  const { renderer } = await setupRenderer(shadow, main);
2004
1783
  const errors: string[] = [];
2005
- renderer.onError((e) => errors.push(e.code));
1784
+ subscribeRendererErrors(renderer, (e) => errors.push(e.code));
2006
1785
 
2007
1786
  const { World } = await importEcs();
2008
1787
  const C = await importComponents();
2009
1788
  const world = new World();
2010
- for (const component of [
2011
- C.Camera,
2012
- C.DirectionalLight,
2013
- C.Transform,
2014
- C.MeshRenderer,
2015
- C.MeshFilter,
2016
- ]) {
2017
- (
2018
- world as unknown as {
2019
- components: { register: (component: unknown) => { unwrap: () => unknown } };
2020
- }
2021
- ).components
2022
- .register(component)
2023
- .unwrap();
2024
- }
2025
1789
 
2026
1790
  const triHandle = world.allocSharedRef('MeshAsset', indexedTriangleMesh()) as Handle<
2027
1791
  'MeshAsset',
@@ -2052,7 +1816,7 @@ function makeEnvironmentGenerationState() {
2052
1816
  world.spawn(
2053
1817
  {
2054
1818
  component: C.DirectionalLight,
2055
- data: { direction: [-0.5, -1, -0.3], mapSize: 512, cascadeCount: 1 },
1819
+ data: { direction: [0, -1, 0], mapSize: 512, cascadeCount: 1 },
2056
1820
  },
2057
1821
  { component: C.Transform, data: cameraTransform() },
2058
1822
  );
@@ -2067,27 +1831,22 @@ function makeEnvironmentGenerationState() {
2067
1831
  { component: C.Transform, data: originTransform() },
2068
1832
  );
2069
1833
 
2070
- // Two frames: the first frame records the lazy shadow pass once the
2071
- // resources are available; the second frame reuses the directional cache.
2072
- // feat-20260609 M4 / T-010: yield so async shader-module creation
2073
- // (getMaterialShaderPipeline's 1-frame-warmup path) resolves before
2074
- // frame 2; the shadow PSO is now obtained via lazy cache lookup instead
2075
- // of the hardcoded perPassResources.shadowCasterPipeline field.
2076
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
2077
- throw new Error('World attachment failed');
2078
- }
2079
- (world as WorldType).update().unwrap();
2080
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
1834
+ // The first frame exercises the real shadow pass. A second frame proves
1835
+ // the persistent directional-shadow cache can reuse that result without
1836
+ // re-recording the depth draws. Yield so any async shader-module
1837
+ // creation settles before the cached frame.
1838
+ drawPublished(renderer, world as WorldType);
1839
+ const firstShadowDrawCalls =
1840
+ shadow.drawIndexed.mock.calls.length + shadow.draw.mock.calls.length;
2081
1841
  await new Promise((r) => setTimeout(r, 0));
2082
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
2083
- throw new Error('World attachment failed');
2084
- }
2085
- (world as WorldType).update().unwrap();
2086
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
1842
+ shadow.drawIndexed.mockClear();
1843
+ shadow.draw.mockClear();
1844
+ drawPublished(renderer, world as WorldType);
2087
1845
 
2088
1846
  // Shadow pass drew exactly once (the triangle mesh), never the line mesh.
2089
1847
  const shadowDrawCalls = shadow.drawIndexed.mock.calls.length + shadow.draw.mock.calls.length;
2090
- expect(shadowDrawCalls).toBe(1);
1848
+ expect(firstShadowDrawCalls).toBe(1);
1849
+ expect(shadowDrawCalls).toBe(0);
2091
1850
  expect(errors).toEqual([]);
2092
1851
  });
2093
1852
  });
@@ -2154,8 +1913,6 @@ function makeEnvironmentGenerationState() {
2154
1913
  return 'mesh-ssbo-capacity';
2155
1914
  case 'mesh-ssbo-ceiling-reached':
2156
1915
  return 'mesh-ssbo-ceiling';
2157
- case 'hdrp-caps-insufficient':
2158
- return 'hdrp-caps';
2159
1916
  case 'hdrp-light-budget-exceeded':
2160
1917
  return 'hdrp-budget';
2161
1918
  case 'hdrp-index-list-overflow':
@@ -2203,22 +1960,6 @@ function makeEnvironmentGenerationState() {
2203
1960
  expect(e.detail).toEqual({ entity: 99 });
2204
1961
  });
2205
1962
 
2206
- it('VertexStorageBufferUnavailableError has .code .expected .hint', () => {
2207
- const e = new VertexStorageBufferUnavailableError();
2208
- expect(e.code).toBe('vertex-storage-buffer-unavailable');
2209
- expect(e.expected).toContain('maxStorageBuffersPerShaderStage');
2210
- expect(e.hint.length).toBeGreaterThan(0);
2211
- });
2212
-
2213
- it('SkinPaletteOverflowError has .code .expected .hint .detail', () => {
2214
- const e = new SkinPaletteOverflowError(10000, 128 * 1024 * 1024);
2215
- expect(e.code).toBe('skin-palette-overflow');
2216
- expect(e.expected).toContain('maxStorageBufferBindingSize');
2217
- expect(e.hint.length).toBeGreaterThan(0);
2218
- expect(e.detail.requestedBytes).toBe(10000);
2219
- expect(e.detail.limit).toBe(128 * 1024 * 1024);
2220
- });
2221
-
2222
1963
  it('all 6 skin-animation error codes are in the RuntimeErrorCode union (type-level)', () => {
2223
1964
  const codes: RuntimeLayerErrorCode[] = [
2224
1965
  'skin-joint-count-exceeded',
@@ -2244,20 +1985,20 @@ function makeEnvironmentGenerationState() {
2244
1985
 
2245
1986
  describe('equirect-projection-failed error class shape', () => {
2246
1987
  it('has .code === equirect-projection-failed', async () => {
2247
- const { EquirectProjectionFailedError } = await import('@forgeax/engine-render/internal');
1988
+ const { EquirectProjectionFailedError } = await import('../../../render/src/errors/render');
2248
1989
  const err = new EquirectProjectionFailedError(42);
2249
1990
  expect(err.code).toBe('equirect-projection-failed');
2250
1991
  });
2251
1992
 
2252
1993
  it('exposes .detail.handle === the constructor argument', async () => {
2253
- const { EquirectProjectionFailedError } = await import('@forgeax/engine-render/internal');
1994
+ const { EquirectProjectionFailedError } = await import('../../../render/src/errors/render');
2254
1995
  const err = new EquirectProjectionFailedError(42);
2255
1996
  expect(err.detail).toBeDefined();
2256
1997
  expect(err.detail.handle).toBe(42);
2257
1998
  });
2258
1999
 
2259
2000
  it('exposes non-empty .hint with actionable guidance', async () => {
2260
- const { EquirectProjectionFailedError } = await import('@forgeax/engine-render/internal');
2001
+ const { EquirectProjectionFailedError } = await import('../../../render/src/errors/render');
2261
2002
  const err = new EquirectProjectionFailedError(42);
2262
2003
  expect(err.hint).toBeDefined();
2263
2004
  expect(typeof err.hint).toBe('string');
@@ -2265,7 +2006,7 @@ function makeEnvironmentGenerationState() {
2265
2006
  });
2266
2007
 
2267
2008
  it('exposes .expected describing expected state', async () => {
2268
- const { EquirectProjectionFailedError } = await import('@forgeax/engine-render/internal');
2009
+ const { EquirectProjectionFailedError } = await import('../../../render/src/errors/render');
2269
2010
  const err = new EquirectProjectionFailedError(42);
2270
2011
  expect(err.expected).toBeDefined();
2271
2012
  expect(typeof err.expected).toBe('string');
@@ -2273,7 +2014,7 @@ function makeEnvironmentGenerationState() {
2273
2014
  });
2274
2015
 
2275
2016
  it('extends Error so it can be thrown and caught', async () => {
2276
- const { EquirectProjectionFailedError } = await import('@forgeax/engine-render/internal');
2017
+ const { EquirectProjectionFailedError } = await import('../../../render/src/errors/render');
2277
2018
  const err = new EquirectProjectionFailedError(42);
2278
2019
  expect(err).toBeInstanceOf(Error);
2279
2020
  expect(err.name).toBe('EquirectProjectionFailedError');
@@ -2285,7 +2026,7 @@ function makeEnvironmentGenerationState() {
2285
2026
  // The actual TS union membership is verified by typecheck: if
2286
2027
  // 'equirect-projection-failed' is not in RuntimeErrorCode, no expression
2287
2028
  // can assign err.code to a RuntimeErrorCode-typed variable.
2288
- const { EquirectProjectionFailedError } = await import('@forgeax/engine-render/internal');
2029
+ const { EquirectProjectionFailedError } = await import('../../../render/src/errors/render');
2289
2030
  const err = new EquirectProjectionFailedError(42);
2290
2031
  expect(err.code).toBe('equirect-projection-failed');
2291
2032
  });
@@ -2297,7 +2038,7 @@ function makeEnvironmentGenerationState() {
2297
2038
  // the equirect handle, it binds the white-cube fallback and fires this
2298
2039
  // structured error ONCE (it does not retry; R-2/AC-09). The record-stage
2299
2040
  // integration is covered by the M3 lazy-projection tests + smoke gate.
2300
- const { EquirectProjectionFailedError } = await import('@forgeax/engine-render/internal');
2041
+ const { EquirectProjectionFailedError } = await import('../../../render/src/errors/render');
2301
2042
  const err = new EquirectProjectionFailedError(42);
2302
2043
  expect(err.code).toBe('equirect-projection-failed');
2303
2044
  expect(err.detail.handle).toBe(42);
@@ -2339,9 +2080,7 @@ function makeEnvironmentGenerationState() {
2339
2080
  });
2340
2081
 
2341
2082
  it('imports View from forgeax_view::common', () => {
2342
- expect(source).toEqual(
2343
- expect.stringContaining('#import forgeax_view::common::{View, FogViewParams, FogRay}'),
2344
- );
2083
+ expect(source).toEqual(expect.stringContaining('#import forgeax_view::common::{View'));
2345
2084
  });
2346
2085
 
2347
2086
  it('imports fullscreen_triangle from forgeax_view::common', () => {
@@ -2899,11 +2638,11 @@ function makeEnvironmentGenerationState() {
2899
2638
  });
2900
2639
  });
2901
2640
 
2902
- // ─── (e) unlit material BG follows the reflected schema ─────────────────────
2641
+ // ─── (e) unlit material BG stays 7 entries ──────────────────────────────────
2903
2642
 
2904
2643
  describe('t58 (M4 round-4) -- unlit material BG isolation', () => {
2905
2644
  it('(e) buildUnlitMaterialBindGroupEntries returns 7 entries (no Skylight contamination)', async () => {
2906
- const mod = await import('@forgeax/engine-render/internal');
2645
+ const mod = await import('../../../render/src/pbr-pipeline');
2907
2646
  const buildFn = (mod as { buildUnlitMaterialBindGroupEntries?: unknown })
2908
2647
  .buildUnlitMaterialBindGroupEntries;
2909
2648
  expect(typeof buildFn).toBe('function');
@@ -3015,24 +2754,24 @@ function makeEnvironmentGenerationState() {
3015
2754
  expect(layout?.label).toBe('pbr-pl');
3016
2755
  });
3017
2756
 
3018
- it('(b) PBR material BGL entry count === 20 (user region 0..12 + Skylight 13..19)', () => {
2757
+ it('(b) PBR material BGL entry count === 24 (user region 0..12 + Skylight 13..19 + lightmap 20..23)', () => {
3019
2758
  const device = makeMockDevice();
3020
2759
  // biome-ignore lint/suspicious/noExplicitAny: structural mock
3021
2760
  buildPbrPipelineLayouts(device as any, STORAGE_CAPS);
3022
2761
  const materialBgl = device.capturedBgls.find((b) => b.label === 'pbr-material-skylight-bgl');
3023
2762
  expect(materialBgl).toBeDefined();
3024
- expect(materialBgl?.entries).toHaveLength(20);
2763
+ expect(materialBgl?.entries).toHaveLength(24);
3025
2764
  });
3026
2765
 
3027
- it('(c) binding indices 0..19 in order; 13..19 resource types in D-5 round-4 order', () => {
2766
+ it('(c) binding indices 0..23 in order; 13..19 resource types in D-5 round-4 order; 20..23 lightmap', () => {
3028
2767
  const device = makeMockDevice();
3029
2768
  // biome-ignore lint/suspicious/noExplicitAny: structural mock
3030
2769
  buildPbrPipelineLayouts(device as any, STORAGE_CAPS);
3031
2770
  const materialBgl = device.capturedBgls.find((b) => b.label === 'pbr-material-skylight-bgl');
3032
2771
  expect(materialBgl).toBeDefined();
3033
2772
  const entries = materialBgl?.entries ?? [];
3034
- // binding indices 0..19 in order
3035
- for (let i = 0; i < 20; i++) {
2773
+ // binding indices 0..23 in order
2774
+ for (let i = 0; i < 24; i++) {
3036
2775
  expect(entries[i]?.binding).toBe(i);
3037
2776
  }
3038
2777
  // 13..19 resource types
@@ -3049,6 +2788,15 @@ function makeEnvironmentGenerationState() {
3049
2788
  );
3050
2789
  expect((entries[18] as { sampler?: { type: string } }).sampler?.type).toBe('filtering');
3051
2790
  expect((entries[19] as { buffer?: { type: string } }).buffer?.type).toBe('uniform');
2791
+ // 20..23 lightmap resource types
2792
+ expect((entries[20] as { sampler?: { type: string } }).sampler?.type).toBe('filtering');
2793
+ expect((entries[21] as { texture?: { viewDimension: string } }).texture?.viewDimension).toBe(
2794
+ '2d',
2795
+ );
2796
+ expect((entries[22] as { sampler?: { type: string } }).sampler?.type).toBe('filtering');
2797
+ expect((entries[23] as { texture?: { viewDimension: string } }).texture?.viewDimension).toBe(
2798
+ '2d',
2799
+ );
3052
2800
  });
3053
2801
 
3054
2802
  it('(d) bindGroupLayouts[0..3] order matches [view, material, meshArray, instances]', () => {
@@ -3072,13 +2820,13 @@ function makeEnvironmentGenerationState() {
3072
2820
  // ─── (e)(f) Unlit material BGL shape + name ─────────────────────────────────
3073
2821
 
3074
2822
  describe('t57 (M4 round-4) -- unlitPipeline material BGL shape', () => {
3075
- it('(e) unlit material BGL entry count === 3 (no Skylight binding contamination)', () => {
2823
+ it('(e) unlit material BGL entry count === 13 (no Skylight binding contamination)', () => {
3076
2824
  const device = makeMockDevice();
3077
2825
  // biome-ignore lint/suspicious/noExplicitAny: structural mock
3078
2826
  buildUnlitMaterialBgl(device as any);
3079
2827
  const unlitBgl = device.capturedBgls.find((b) => b.label === 'unlit-material-bgl');
3080
2828
  expect(unlitBgl).toBeDefined();
3081
- expect(unlitBgl?.entries).toHaveLength(3);
2829
+ expect(unlitBgl?.entries).toHaveLength(13);
3082
2830
  });
3083
2831
 
3084
2832
  it("(f) PBR material BGL labelled 'pbr-material-skylight-bgl'; unlit labelled 'unlit-material-bgl'", () => {
@@ -3293,13 +3041,13 @@ function makeEnvironmentGenerationState() {
3293
3041
  const { createRenderer } = (await import(ENGINE)) as {
3294
3042
  createRenderer: (canvas: unknown, opts?: unknown, bundler?: unknown) => Promise<unknown>;
3295
3043
  };
3296
- const renderer = (await createRenderer(
3044
+ const result = await createRenderer(
3297
3045
  canvas,
3298
3046
  {},
3299
3047
  { shaderManifestUrl: buildManifestDataUrl() },
3300
- )) as { ready: Promise<{ ok: boolean }> };
3301
- const ready = await renderer.ready;
3302
- expect(ready.ok).toBe(true);
3048
+ );
3049
+ expect(result.ok).toBe(true);
3050
+ result.unwrap();
3303
3051
 
3304
3052
  // bug-20260612: sRGB siblings stay on the swap-chain srgb view; Channel 2
3305
3053
  // (storageBufferCapable=true) follows getPreferredCanvasFormat() — mocked
@@ -3332,21 +3080,15 @@ function makeEnvironmentGenerationState() {
3332
3080
  vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
3333
3081
  const canvas = makeMockCanvas();
3334
3082
  const { createRenderer } = (await import(ENGINE)) as {
3335
- createRenderer: (
3336
- canvas: unknown,
3337
- opts?: unknown,
3338
- bundler?: unknown,
3339
- ) => Promise<{
3340
- ready: Promise<{ ok: boolean }>;
3341
- }>;
3083
+ createRenderer: (...args: unknown[]) => Promise<{ ok: boolean; unwrap(): RendererType }>;
3342
3084
  };
3343
- const renderer = await createRenderer(
3085
+ const result = await createRenderer(
3344
3086
  canvas,
3345
3087
  {},
3346
3088
  { shaderManifestUrl: buildManifestDataUrl() },
3347
3089
  );
3348
- const ready = await renderer.ready;
3349
- expect(ready.ok).toBe(true);
3090
+ expect(result.ok).toBe(true);
3091
+ result.unwrap();
3350
3092
 
3351
3093
  // No `createTexture(format='rgba16float')` call for the HDR colour
3352
3094
  // attachment before any opt-in frame. The geometry-side fallback
@@ -3386,21 +3128,15 @@ function makeEnvironmentGenerationState() {
3386
3128
  vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
3387
3129
  const canvas = makeMockCanvas();
3388
3130
  const { createRenderer } = (await import(ENGINE)) as {
3389
- createRenderer: (
3390
- canvas: unknown,
3391
- opts?: unknown,
3392
- bundler?: unknown,
3393
- ) => Promise<{
3394
- ready: Promise<{ ok: boolean }>;
3395
- }>;
3131
+ createRenderer: (...args: unknown[]) => Promise<{ ok: boolean; unwrap(): RendererType }>;
3396
3132
  };
3397
- const renderer = await createRenderer(
3133
+ const result = await createRenderer(
3398
3134
  canvas,
3399
3135
  {},
3400
3136
  { shaderManifestUrl: buildManifestDataUrl() },
3401
3137
  );
3402
- const ready = await renderer.ready;
3403
- expect(ready.ok).toBe(true);
3138
+ expect(result.ok).toBe(true);
3139
+ result.unwrap();
3404
3140
 
3405
3141
  const moduleLabels = log.records
3406
3142
  .filter((r) => r.type === 'createShaderModule')
@@ -3414,44 +3150,6 @@ function makeEnvironmentGenerationState() {
3414
3150
  // the manifest triple guard (missing tonemap entry -> ready rejects).
3415
3151
  expect(moduleLabels).not.toContain('tonemap');
3416
3152
  });
3417
-
3418
- it('row 5: ready rejects shader-compile-failed when manifest omits tonemap entry', async () => {
3419
- const log: DeviceCallLog = { records: [] };
3420
- const device = makeMockDevice(log);
3421
- vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
3422
- const canvas = makeMockCanvas();
3423
- const partialManifest = {
3424
- schemaVersion: '1.0.0',
3425
- entries: [
3426
- {
3427
- hash: 'pbr00000',
3428
- wgsl: '/* mock pbr.wgsl - calls f_schlick( for PBR direct lighting */',
3429
- glsl: '',
3430
- bindings: '',
3431
- },
3432
- {
3433
- hash: 'unlit000',
3434
- wgsl: '/* mock unlit.wgsl */',
3435
- glsl: '',
3436
- bindings: '',
3437
- },
3438
- ],
3439
- };
3440
- const url = `data:application/json,${encodeURIComponent(JSON.stringify(partialManifest))}`;
3441
- const { createRenderer } = (await import(ENGINE)) as {
3442
- createRenderer: (
3443
- canvas: unknown,
3444
- opts?: unknown,
3445
- bundler?: unknown,
3446
- ) => Promise<{
3447
- ready: Promise<{ ok: boolean; error?: { code: string } }>;
3448
- }>;
3449
- };
3450
- const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: url });
3451
- const ready = await renderer.ready;
3452
- expect(ready.ok).toBe(false);
3453
- expect(ready.error?.code).toBe('shader-compile-failed');
3454
- });
3455
3153
  });
3456
3154
  }
3457
3155
 
@@ -3676,7 +3374,6 @@ function makeEnvironmentGenerationState() {
3676
3374
  // now flows through the same generic per-MaterialShader pipeline cache
3677
3375
  // every other transparent material uses (plan-strategy D-7).
3678
3376
  unlitPipelineHdr: f.fakeUnlitHdrPipeline,
3679
- standardPipelineHdr: f.fakeStandardHdrPipeline,
3680
3377
  perPassResources: {
3681
3378
  depthTexture: { __label: 'depth' },
3682
3379
  depthTextureView: f.swapDepthView,
@@ -3728,7 +3425,7 @@ function makeEnvironmentGenerationState() {
3728
3425
  const internals = makeRecorderInternals(log);
3729
3426
  const ps = makePipelineState(internals as never, false);
3730
3427
  (internals as { getPipelineState: () => unknown }).getPipelineState = () => ps;
3731
- const { recordFrame } = await import('@forgeax/engine-render/internal');
3428
+ const { recordFrame } = await import('../../../render/src/record/frame');
3732
3429
  const cameras = [makeCamera('none')];
3733
3430
  recordFrame(
3734
3431
  internals as never,
@@ -3739,7 +3436,6 @@ function makeEnvironmentGenerationState() {
3739
3436
  [],
3740
3437
  {
3741
3438
  frameNumber: 0,
3742
- environmentGeneration: makeEnvironmentGenerationState(),
3743
3439
  compiledFrameGraph: null,
3744
3440
  compiledFrameGraphTopologyKey: null,
3745
3441
  retiredCompiledFrameGraphs: new Set(),
@@ -3767,8 +3463,10 @@ function makeEnvironmentGenerationState() {
3767
3463
  },
3768
3464
  { unlit: 0 },
3769
3465
  { createBindGroup: 0, keys: [] },
3770
- EMPTY_EXTRACTED_ENVIRONMENT,
3771
- EMPTY_POST_PROCESS_PARAMS,
3466
+ undefined,
3467
+ 0,
3468
+ undefined,
3469
+ 0,
3772
3470
  );
3773
3471
 
3774
3472
  // M1 / w7: the render-graph allocates ALL color targets (including HDR)
@@ -3823,8 +3521,7 @@ function makeEnvironmentGenerationState() {
3823
3521
  }
3824
3522
 
3825
3523
  it('despawned key (not in validated set): recordFrame destroys GpuBuffer + deletes Map entry', async () => {
3826
- const { recordFrame } = await import('@forgeax/engine-render/internal');
3827
- const { GpuBuffer } = await import('@forgeax/engine-render/internal');
3524
+ const { recordFrame } = await import('../../../render/src/record/frame');
3828
3525
  const { err: rhiErrFn, RhiError: RhiErrorCtor } = await import('@forgeax/engine-rhi');
3829
3526
  const { device: bufDev, destroyedHandles } = makeBufRecorderDevice(
3830
3527
  rhiErrFn,
@@ -3858,7 +3555,6 @@ function makeEnvironmentGenerationState() {
3858
3555
  [],
3859
3556
  {
3860
3557
  frameNumber: 0,
3861
- environmentGeneration: makeEnvironmentGenerationState(),
3862
3558
  compiledFrameGraph: null,
3863
3559
  compiledFrameGraphTopologyKey: null,
3864
3560
  retiredCompiledFrameGraphs: new Set(),
@@ -3885,8 +3581,10 @@ function makeEnvironmentGenerationState() {
3885
3581
  },
3886
3582
  { unlit: 0 },
3887
3583
  { createBindGroup: 0, keys: [] },
3888
- EMPTY_EXTRACTED_ENVIRONMENT,
3889
- EMPTY_POST_PROCESS_PARAMS,
3584
+ undefined,
3585
+ 0,
3586
+ undefined,
3587
+ 0,
3890
3588
  );
3891
3589
 
3892
3590
  // F11 production loop destroyed the orphaned buffer + dropped the key.
@@ -3896,8 +3594,7 @@ function makeEnvironmentGenerationState() {
3896
3594
  });
3897
3595
 
3898
3596
  it('isDestroyed dedup: a pre-destroyed orphan is not double-destroyed (still removed)', async () => {
3899
- const { recordFrame } = await import('@forgeax/engine-render/internal');
3900
- const { GpuBuffer } = await import('@forgeax/engine-render/internal');
3597
+ const { recordFrame } = await import('../../../render/src/record/frame');
3901
3598
  const { err: rhiErrFn, RhiError: RhiErrorCtor } = await import('@forgeax/engine-rhi');
3902
3599
  const { device: bufDev, destroyedHandles } = makeBufRecorderDevice(
3903
3600
  rhiErrFn,
@@ -3931,7 +3628,6 @@ function makeEnvironmentGenerationState() {
3931
3628
  [],
3932
3629
  {
3933
3630
  frameNumber: 0,
3934
- environmentGeneration: makeEnvironmentGenerationState(),
3935
3631
  compiledFrameGraph: null,
3936
3632
  compiledFrameGraphTopologyKey: null,
3937
3633
  retiredCompiledFrameGraphs: new Set(),
@@ -3958,8 +3654,10 @@ function makeEnvironmentGenerationState() {
3958
3654
  },
3959
3655
  { unlit: 0 },
3960
3656
  { createBindGroup: 0, keys: [] },
3961
- EMPTY_EXTRACTED_ENVIRONMENT,
3962
- EMPTY_POST_PROCESS_PARAMS,
3657
+ undefined,
3658
+ 0,
3659
+ undefined,
3660
+ 0,
3963
3661
  );
3964
3662
 
3965
3663
  // isDestroyed gate skipped the second destroy; key still dropped.
@@ -4001,18 +3699,18 @@ function makeEnvironmentGenerationState() {
4001
3699
  describe('Skin — component registration + schema shape (AC-13 / AC-37)', () => {
4002
3700
  it('Skin is a registered component with name "Skin" and schema fields skeleton + joints', () => {
4003
3701
  expect(Skin.name).toBe('Skin');
4004
- expect(componentSchema(Skin)).toEqual({
3702
+ expect(componentSchemaTypes(Skin)).toEqual({
4005
3703
  skeleton: 'shared<SkeletonAsset>',
4006
3704
  joints: 'array<entity>',
4007
3705
  });
4008
3706
  });
4009
3707
 
4010
- it('componentSchema(Skin).skeleton is shared<SkeletonAsset> (schema-vocab keyword)', () => {
4011
- expect(componentSchema(Skin).skeleton).toBe('shared<SkeletonAsset>');
3708
+ it('Skin.schema.skeleton is shared<SkeletonAsset> (schema-vocab keyword)', () => {
3709
+ expect(componentFieldType(Skin, 'skeleton')).toBe('shared<SkeletonAsset>');
4012
3710
  });
4013
3711
 
4014
- it('componentSchema(Skin).joints is array<entity> (schema-vocab keyword)', () => {
4015
- expect(componentSchema(Skin).joints).toBe('array<entity>');
3712
+ it('Skin.schema.joints is array<entity> (schema-vocab keyword)', () => {
3713
+ expect(componentFieldType(Skin, 'joints')).toBe('array<entity>');
4016
3714
  });
4017
3715
 
4018
3716
  it('Skin component spawns on an entity', () => {
@@ -4097,24 +3795,6 @@ function makeEnvironmentGenerationState() {
4097
3795
  };
4098
3796
  }
4099
3797
 
4100
- const testAnimationHandles = new WeakMap<
4101
- object,
4102
- Map<number, Handle<'AnimationClip', 'shared'>>
4103
- >();
4104
-
4105
- function testAnimationHandle(world: World, raw: number): Handle<'AnimationClip', 'shared'> {
4106
- let handles = testAnimationHandles.get(world);
4107
- if (handles === undefined) {
4108
- handles = new Map();
4109
- testAnimationHandles.set(world, handles);
4110
- }
4111
- const existing = handles.get(raw);
4112
- if (existing !== undefined) return existing;
4113
- const handle = world.allocSharedRef('AnimationClip', makeClip(1, []));
4114
- handles.set(raw, handle);
4115
- return handle;
4116
- }
4117
-
4118
3798
  type TestResolver = { clips: Map<number, AnimationClip>; handles?: Map<number, number> };
4119
3799
 
4120
3800
  function makeResolver(clips: Map<number, AnimationClip>): TestResolver {
@@ -4133,13 +3813,8 @@ function makeEnvironmentGenerationState() {
4133
3813
  for (const entity of players) {
4134
3814
  const player = world.get(entity, AnimationPlayer);
4135
3815
  if (!player.ok) continue;
4136
- const rawByHandle = new Map<number, number>();
4137
- for (const [raw, handle] of testAnimationHandles.get(world) ?? []) {
4138
- rawByHandle.set(handle, raw);
4139
- }
4140
3816
  const clips = Array.from(player.value.clips, (handle) => {
4141
- const raw = rawByHandle.get(handle);
4142
- const mapped = handles.get(raw ?? handle);
3817
+ const mapped = handles.get(handle);
4143
3818
  return mapped ?? (allocated.has(handle) ? handle : 0);
4144
3819
  });
4145
3820
  world.set(entity, AnimationPlayer, { clips });
@@ -4186,7 +3861,7 @@ function makeEnvironmentGenerationState() {
4186
3861
  component: AnimationPlayer,
4187
3862
  data: {
4188
3863
  clips: [
4189
- testAnimationHandle(world, clipId),
3864
+ toShared<'AnimationClip'>(clipId),
4190
3865
  0 as Handle<'AnimationClip', 'shared'>,
4191
3866
  0 as Handle<'AnimationClip', 'shared'>,
4192
3867
  0 as Handle<'AnimationClip', 'shared'>,
@@ -4279,7 +3954,7 @@ function makeEnvironmentGenerationState() {
4279
3954
  component: AnimationPlayer,
4280
3955
  data: {
4281
3956
  clips: [
4282
- testAnimationHandle(world, 1),
3957
+ toShared<'AnimationClip'>(1),
4283
3958
  0 as Handle<'AnimationClip', 'shared'>,
4284
3959
  0 as Handle<'AnimationClip', 'shared'>,
4285
3960
  0 as Handle<'AnimationClip', 'shared'>,
@@ -4318,7 +3993,7 @@ function makeEnvironmentGenerationState() {
4318
3993
  component: AnimationPlayer,
4319
3994
  data: {
4320
3995
  clips: [
4321
- testAnimationHandle(world, 1),
3996
+ toShared<'AnimationClip'>(1),
4322
3997
  0 as Handle<'AnimationClip', 'shared'>,
4323
3998
  0 as Handle<'AnimationClip', 'shared'>,
4324
3999
  0 as Handle<'AnimationClip', 'shared'>,
@@ -4353,7 +4028,7 @@ function makeEnvironmentGenerationState() {
4353
4028
  component: AnimationPlayer,
4354
4029
  data: {
4355
4030
  clips: [
4356
- testAnimationHandle(world, 1),
4031
+ toShared<'AnimationClip'>(1),
4357
4032
  0 as Handle<'AnimationClip', 'shared'>,
4358
4033
  0 as Handle<'AnimationClip', 'shared'>,
4359
4034
  0 as Handle<'AnimationClip', 'shared'>,
@@ -4459,7 +4134,7 @@ function makeEnvironmentGenerationState() {
4459
4134
  component: AnimationPlayer,
4460
4135
  data: {
4461
4136
  clips: [
4462
- testAnimationHandle(world, 1),
4137
+ toShared<'AnimationClip'>(1),
4463
4138
  0 as Handle<'AnimationClip', 'shared'>,
4464
4139
  0 as Handle<'AnimationClip', 'shared'>,
4465
4140
  0 as Handle<'AnimationClip', 'shared'>,
@@ -4516,7 +4191,7 @@ function makeEnvironmentGenerationState() {
4516
4191
  component: AnimationPlayer,
4517
4192
  data: {
4518
4193
  clips: [
4519
- testAnimationHandle(world, 1),
4194
+ toShared<'AnimationClip'>(1),
4520
4195
  0 as Handle<'AnimationClip', 'shared'>,
4521
4196
  0 as Handle<'AnimationClip', 'shared'>,
4522
4197
  0 as Handle<'AnimationClip', 'shared'>,
@@ -4599,8 +4274,8 @@ function makeEnvironmentGenerationState() {
4599
4274
  component: AnimationPlayer,
4600
4275
  data: {
4601
4276
  clips: [
4602
- testAnimationHandle(world, 1),
4603
- testAnimationHandle(world, 2),
4277
+ toShared<'AnimationClip'>(1),
4278
+ toShared<'AnimationClip'>(2),
4604
4279
  0 as Handle<'AnimationClip', 'shared'>,
4605
4280
  0 as Handle<'AnimationClip', 'shared'>,
4606
4281
  ],
@@ -4665,8 +4340,8 @@ function makeEnvironmentGenerationState() {
4665
4340
  component: AnimationPlayer,
4666
4341
  data: {
4667
4342
  clips: [
4668
- testAnimationHandle(world, 1),
4669
- testAnimationHandle(world, 2),
4343
+ toShared<'AnimationClip'>(1),
4344
+ toShared<'AnimationClip'>(2),
4670
4345
  0 as Handle<'AnimationClip', 'shared'>,
4671
4346
  0 as Handle<'AnimationClip', 'shared'>,
4672
4347
  ],
@@ -4717,8 +4392,8 @@ function makeEnvironmentGenerationState() {
4717
4392
  component: AnimationPlayer,
4718
4393
  data: {
4719
4394
  clips: [
4720
- testAnimationHandle(world, 1),
4721
- testAnimationHandle(world, 2),
4395
+ toShared<'AnimationClip'>(1),
4396
+ toShared<'AnimationClip'>(2),
4722
4397
  0 as Handle<'AnimationClip', 'shared'>,
4723
4398
  0 as Handle<'AnimationClip', 'shared'>,
4724
4399
  ],
@@ -4775,9 +4450,9 @@ function makeEnvironmentGenerationState() {
4775
4450
  component: AnimationPlayer,
4776
4451
  data: {
4777
4452
  clips: [
4778
- testAnimationHandle(world, 1),
4779
- testAnimationHandle(world, 2),
4780
- testAnimationHandle(world, 3),
4453
+ toShared<'AnimationClip'>(1),
4454
+ toShared<'AnimationClip'>(2),
4455
+ toShared<'AnimationClip'>(3),
4781
4456
  0 as Handle<'AnimationClip', 'shared'>,
4782
4457
  ],
4783
4458
  times: new Float32Array([0, 0, 0, 0]),
@@ -4828,8 +4503,8 @@ function makeEnvironmentGenerationState() {
4828
4503
  component: AnimationPlayer,
4829
4504
  data: {
4830
4505
  clips: [
4831
- testAnimationHandle(world, 1),
4832
- testAnimationHandle(world, 2),
4506
+ toShared<'AnimationClip'>(1),
4507
+ toShared<'AnimationClip'>(2),
4833
4508
  0 as Handle<'AnimationClip', 'shared'>,
4834
4509
  0 as Handle<'AnimationClip', 'shared'>,
4835
4510
  ],
@@ -4873,7 +4548,7 @@ function makeEnvironmentGenerationState() {
4873
4548
  component: AnimationPlayer,
4874
4549
  data: {
4875
4550
  clips: [
4876
- testAnimationHandle(world, 1),
4551
+ toShared<'AnimationClip'>(1),
4877
4552
  0 as Handle<'AnimationClip', 'shared'>,
4878
4553
  0 as Handle<'AnimationClip', 'shared'>,
4879
4554
  0 as Handle<'AnimationClip', 'shared'>,
@@ -4938,8 +4613,8 @@ function makeEnvironmentGenerationState() {
4938
4613
  component: AnimationPlayer,
4939
4614
  data: {
4940
4615
  clips: [
4941
- testAnimationHandle(world, 1),
4942
- testAnimationHandle(world, 2),
4616
+ toShared<'AnimationClip'>(1),
4617
+ toShared<'AnimationClip'>(2),
4943
4618
  0 as Handle<'AnimationClip', 'shared'>,
4944
4619
  0 as Handle<'AnimationClip', 'shared'>,
4945
4620
  ],
@@ -4995,8 +4670,8 @@ function makeEnvironmentGenerationState() {
4995
4670
  component: AnimationPlayer,
4996
4671
  data: {
4997
4672
  clips: [
4998
- testAnimationHandle(world, 1),
4999
- testAnimationHandle(world, 2),
4673
+ toShared<'AnimationClip'>(1),
4674
+ toShared<'AnimationClip'>(2),
5000
4675
  0 as Handle<'AnimationClip', 'shared'>,
5001
4676
  0 as Handle<'AnimationClip', 'shared'>,
5002
4677
  ],
@@ -5046,7 +4721,7 @@ function makeEnvironmentGenerationState() {
5046
4721
  component: AnimationPlayer,
5047
4722
  data: {
5048
4723
  clips: [
5049
- testAnimationHandle(world, 1),
4724
+ toShared<'AnimationClip'>(1),
5050
4725
  0 as Handle<'AnimationClip', 'shared'>,
5051
4726
  0 as Handle<'AnimationClip', 'shared'>,
5052
4727
  0 as Handle<'AnimationClip', 'shared'>,
@@ -5105,8 +4780,8 @@ function makeEnvironmentGenerationState() {
5105
4780
  component: AnimationPlayer,
5106
4781
  data: {
5107
4782
  clips: [
5108
- testAnimationHandle(world, 1),
5109
- testAnimationHandle(world, 2),
4783
+ toShared<'AnimationClip'>(1),
4784
+ toShared<'AnimationClip'>(2),
5110
4785
  0 as Handle<'AnimationClip', 'shared'>,
5111
4786
  0 as Handle<'AnimationClip', 'shared'>,
5112
4787
  ],
@@ -5156,7 +4831,7 @@ function makeEnvironmentGenerationState() {
5156
4831
  component: AnimationPlayer,
5157
4832
  data: {
5158
4833
  clips: [
5159
- testAnimationHandle(world, 1),
4834
+ toShared<'AnimationClip'>(1),
5160
4835
  0 as Handle<'AnimationClip', 'shared'>,
5161
4836
  0 as Handle<'AnimationClip', 'shared'>,
5162
4837
  0 as Handle<'AnimationClip', 'shared'>,
@@ -5220,8 +4895,8 @@ function makeEnvironmentGenerationState() {
5220
4895
  component: AnimationPlayer,
5221
4896
  data: {
5222
4897
  clips: [
5223
- testAnimationHandle(world, 1),
5224
- testAnimationHandle(world, 2),
4898
+ toShared<'AnimationClip'>(1),
4899
+ toShared<'AnimationClip'>(2),
5225
4900
  0 as Handle<'AnimationClip', 'shared'>,
5226
4901
  0 as Handle<'AnimationClip', 'shared'>,
5227
4902
  ],
@@ -5264,7 +4939,7 @@ function makeEnvironmentGenerationState() {
5264
4939
  component: AnimationPlayer,
5265
4940
  data: {
5266
4941
  clips: [
5267
- testAnimationHandle(world, 1),
4942
+ toShared<'AnimationClip'>(1),
5268
4943
  0 as Handle<'AnimationClip', 'shared'>,
5269
4944
  0 as Handle<'AnimationClip', 'shared'>,
5270
4945
  0 as Handle<'AnimationClip', 'shared'>,
@@ -5299,7 +4974,7 @@ function makeEnvironmentGenerationState() {
5299
4974
  component: AnimationPlayer,
5300
4975
  data: {
5301
4976
  clips: [
5302
- testAnimationHandle(world, 1),
4977
+ toShared<'AnimationClip'>(1),
5303
4978
  0 as Handle<'AnimationClip', 'shared'>,
5304
4979
  0 as Handle<'AnimationClip', 'shared'>,
5305
4980
  0 as Handle<'AnimationClip', 'shared'>,
@@ -5364,7 +5039,7 @@ function makeEnvironmentGenerationState() {
5364
5039
  component: AnimationPlayer,
5365
5040
  data: {
5366
5041
  clips: [
5367
- testAnimationHandle(world, 1),
5042
+ toShared<'AnimationClip'>(1),
5368
5043
  0 as Handle<'AnimationClip', 'shared'>,
5369
5044
  0 as Handle<'AnimationClip', 'shared'>,
5370
5045
  0 as Handle<'AnimationClip', 'shared'>,
@@ -5435,7 +5110,7 @@ function makeEnvironmentGenerationState() {
5435
5110
  }
5436
5111
 
5437
5112
  function readWorld(world: World, entity: EntityHandle): Float32Array {
5438
- return world.get(entity, Transform).unwrap().world;
5113
+ return world.get(entity, Transform).unwrap().world as Float32Array;
5439
5114
  }
5440
5115
 
5441
5116
  function expectMatClose(actual: Float32Array, expected: Float32Array): void {
@@ -5551,35 +5226,6 @@ function makeEnvironmentGenerationState() {
5551
5226
 
5552
5227
  {
5553
5228
  // --- from skin-cap-gate.test.ts ---
5554
-
5555
- describe('skin cap-gate', () => {
5556
- it('VertexStorageBufferUnavailableError has correct code and fields', () => {
5557
- const e = new VertexStorageBufferUnavailableError();
5558
- expect(e.code).toBe('vertex-storage-buffer-unavailable');
5559
- expect(e.expected).toContain('maxStorageBuffersPerShaderStage');
5560
- expect(e.hint).toContain('OOS-uniform-palette');
5561
- });
5562
-
5563
- it('cap-gate fails when vertex storage buffer count is 0', () => {
5564
- // Simulate: maxStorageBuffersPerShaderStage < 1
5565
- const maxStorageBuffersPerShaderStage = 0;
5566
- const hasVertexStorage = maxStorageBuffersPerShaderStage >= 1;
5567
- expect(hasVertexStorage).toBe(false);
5568
- });
5569
-
5570
- it('cap-gate passes when vertex storage buffer count is 1 or more', () => {
5571
- // Simulate: standard WebGPU core feature
5572
- const maxStorageBuffersPerShaderStage = 8;
5573
- const hasVertexStorage = maxStorageBuffersPerShaderStage >= 1;
5574
- expect(hasVertexStorage).toBe(true);
5575
- });
5576
-
5577
- it('cap-gate passes at the W3C spec minimum for vertex storage (1 per stage)', () => {
5578
- const maxStorageBuffersPerShaderStage = 1;
5579
- const hasVertexStorage = maxStorageBuffersPerShaderStage >= 1;
5580
- expect(hasVertexStorage).toBe(true);
5581
- });
5582
- });
5583
5229
  }
5584
5230
 
5585
5231
  {
@@ -5709,13 +5355,12 @@ function makeEnvironmentGenerationState() {
5709
5355
  expect(buffers.length).toBe(0);
5710
5356
  const slice = alloc.allocateSlice(1);
5711
5357
  expect(slice.buffer).toBeDefined();
5712
- expect(buffers.length).toBe(2);
5358
+ expect(buffers.length).toBe(1);
5713
5359
  // Initial capacity = MAX_JOINTS * 64 = 255 * 64 = 16320 (storage
5714
5360
  // path's first grow step; uniform fallback would also start at
5715
5361
  // exactly 16320 since each pool entry equals one binding window).
5716
5362
  // biome-ignore lint/style/noNonNullAssertion: controlled test context
5717
5363
  expect(buffers[0]!.size).toBe(255 * 64);
5718
- expect(buffers[1]?.size).toBe(255 * 64);
5719
5364
  });
5720
5365
 
5721
5366
  it('grow at 1.5x when binding window exceeds capacity', () => {
@@ -5724,16 +5369,15 @@ function makeEnvironmentGenerationState() {
5724
5369
  // First alloc (offset=0): needed = 0 + bindingWindowBytes (16320)
5725
5370
  // -> grow to 16320 exactly (initial = 255 * 64).
5726
5371
  alloc.allocateSlice(255);
5727
- expect(buffers.length).toBe(2);
5372
+ expect(buffers.length).toBe(1);
5728
5373
  // biome-ignore lint/style/noNonNullAssertion: controlled test context
5729
5374
  expect(buffers[0]!.size).toBe(16320);
5730
5375
  // Second alloc (offset=255*64=16320): needed = 16320 + 16320 = 32640.
5731
5376
  // 1.5x grow: 16320 -> 24576 -> 36864 (smallest aligned >= 32640).
5732
5377
  alloc.allocateSlice(100);
5733
- expect(buffers.length).toBe(4);
5378
+ expect(buffers.length).toBe(2);
5734
5379
  // biome-ignore lint/style/noNonNullAssertion: controlled test context
5735
- expect(buffers[2]!.size).toBe(36864);
5736
- expect(buffers[3]?.size).toBe(36864);
5380
+ expect(buffers[1]!.size).toBe(36864);
5737
5381
  });
5738
5382
 
5739
5383
  // M6 regression gate: the bug that this fix repairs reproduces here as
@@ -5792,17 +5436,13 @@ function makeEnvironmentGenerationState() {
5792
5436
  expect(s1.buffer).not.toBe(s2.buffer);
5793
5437
  expect(s0.buffer).not.toBe(s2.buffer);
5794
5438
  // Three entities -> three distinct UBOs of size = bindingWindowBytes.
5795
- expect(buffers.length).toBe(6);
5439
+ expect(buffers.length).toBe(3);
5796
5440
  // biome-ignore lint/style/noNonNullAssertion: controlled test context
5797
5441
  expect(buffers[0]!.size).toBe(16320);
5798
5442
  // biome-ignore lint/style/noNonNullAssertion: controlled test context
5799
5443
  expect(buffers[1]!.size).toBe(16320);
5800
5444
  // biome-ignore lint/style/noNonNullAssertion: controlled test context
5801
5445
  expect(buffers[2]!.size).toBe(16320);
5802
- // Paired last-submitted palette buffers have the same bounded shape.
5803
- expect(buffers[3]?.size).toBe(16320);
5804
- expect(buffers[4]?.size).toBe(16320);
5805
- expect(buffers[5]?.size).toBe(16320);
5806
5446
  });
5807
5447
 
5808
5448
  it('uniform fallback: pool reuses buffers across frames (M6)', () => {
@@ -5812,14 +5452,14 @@ function makeEnvironmentGenerationState() {
5812
5452
  const f1s0 = alloc.allocateSlice(10);
5813
5453
  const f1s1 = alloc.allocateSlice(10);
5814
5454
  const f1s2 = alloc.allocateSlice(10);
5815
- expect(buffers.length).toBe(6);
5455
+ expect(buffers.length).toBe(3);
5816
5456
  // Frame 2: reset + same-shape walk -> pool round-robin, no new
5817
5457
  // createBuffer calls. Slices reuse the exact same buffer objects.
5818
5458
  alloc.resetForFrame();
5819
5459
  const f2s0 = alloc.allocateSlice(10);
5820
5460
  const f2s1 = alloc.allocateSlice(10);
5821
5461
  const f2s2 = alloc.allocateSlice(10);
5822
- expect(buffers.length).toBe(6); // unchanged
5462
+ expect(buffers.length).toBe(3); // unchanged
5823
5463
  expect(f2s0.buffer).toBe(f1s0.buffer);
5824
5464
  expect(f2s1.buffer).toBe(f1s1.buffer);
5825
5465
  expect(f2s2.buffer).toBe(f1s2.buffer);
@@ -6132,7 +5772,7 @@ function makeEnvironmentGenerationState() {
6132
5772
  // --- from sprite-animation-tick-boundary.test.ts ---
6133
5773
 
6134
5774
  function setDt(world: World, dt: number): void {
6135
- world.update(dt).unwrap();
5775
+ world[worldInternal].getClockWriter().time.delta = dt;
6136
5776
  }
6137
5777
 
6138
5778
  function expectRegion(
@@ -6161,7 +5801,7 @@ function makeEnvironmentGenerationState() {
6161
5801
  // at 0 but accumDt would be 0.25, not 0); a clamp to 0.5 would
6162
5802
  // advance 1 frame -> currentFrame === 1; either way the
6163
5803
  // assertions below diverge from the no-clamp branch.
6164
- const world = new World({ time: { maxDeltaSeconds: 30 } });
5804
+ const world = new World();
6165
5805
  const regions = new Float32Array([
6166
5806
  0.0, 0, 0.25, 1, 0.25, 0, 0.25, 1, 0.5, 0, 0.25, 1, 0.75, 0, 0.25, 1,
6167
5807
  ]);
@@ -6192,7 +5832,7 @@ function makeEnvironmentGenerationState() {
6192
5832
  });
6193
5833
 
6194
5834
  it('(2) frameCount=1 static sprite stays at currentFrame=0 (LOOP)', () => {
6195
- const world = new World({ time: { maxDeltaSeconds: 30 } });
5835
+ const world = new World();
6196
5836
  const regions = new Float32Array([0.0, 0.0, 1.0, 1.0]);
6197
5837
  const entity = world
6198
5838
  .spawn({
@@ -6223,7 +5863,7 @@ function makeEnvironmentGenerationState() {
6223
5863
  });
6224
5864
 
6225
5865
  it('(2b) frameCount=1 static sprite stays at currentFrame=0 (CLAMP)', () => {
6226
- const world = new World({ time: { maxDeltaSeconds: 30 } });
5866
+ const world = new World();
6227
5867
  const regions = new Float32Array([0.0, 0.0, 1.0, 1.0]);
6228
5868
  const entity = world
6229
5869
  .spawn({
@@ -6246,7 +5886,7 @@ function makeEnvironmentGenerationState() {
6246
5886
  });
6247
5887
 
6248
5888
  it('(3) manual setFrame: world.set(SpriteAnimation, { currentFrame: 2, accumDt: 0 }) is honoured next tick', () => {
6249
- const world = new World({ time: { maxDeltaSeconds: 30 } });
5889
+ const world = new World();
6250
5890
  const regions = new Float32Array([
6251
5891
  0.0, 0, 0.25, 1, 0.25, 0, 0.25, 1, 0.5, 0, 0.25, 1, 0.75, 0, 0.25, 1,
6252
5892
  ]);
@@ -6305,7 +5945,7 @@ function makeEnvironmentGenerationState() {
6305
5945
  // --- from sprite-animation-tick-clamp.test.ts ---
6306
5946
 
6307
5947
  function setDt(world: World, dt: number): void {
6308
- world.update(dt).unwrap();
5948
+ world[worldInternal].getClockWriter().time.delta = dt;
6309
5949
  }
6310
5950
 
6311
5951
  function makeRegions(): Float32Array {
@@ -6343,7 +5983,7 @@ function makeEnvironmentGenerationState() {
6343
5983
 
6344
5984
  describe('spriteAnimationTickSystem - AC-05 clamp end-frame (M4 T-18)', () => {
6345
5985
  it('dt=0.5 with frameCount=3 / frameDuration=0.1 stops at frameCount-1 (last frame)', () => {
6346
- const world = new World({ time: { maxDeltaSeconds: 30 } });
5986
+ const world = new World();
6347
5987
  const regions = makeRegions();
6348
5988
  const entity = world
6349
5989
  .spawn({
@@ -6369,7 +6009,7 @@ function makeEnvironmentGenerationState() {
6369
6009
  });
6370
6010
 
6371
6011
  it('continued dt=0.5 keeps currentFrame at last frame (no wrap)', () => {
6372
- const world = new World({ time: { maxDeltaSeconds: 30 } });
6012
+ const world = new World();
6373
6013
  const regions = makeRegions();
6374
6014
  const entity = world
6375
6015
  .spawn({
@@ -6411,7 +6051,7 @@ function makeEnvironmentGenerationState() {
6411
6051
 
6412
6052
  describe('spriteAnimationTickSystem - AC-09(c) frameDuration<0 fail-fast (M4 T-21)', () => {
6413
6053
  it('returns Result.err with detail.field=frame-duration when frameDuration < 0', () => {
6414
- const world = new World({ time: { maxDeltaSeconds: 30 } });
6054
+ const world = new World();
6415
6055
  const entity = world
6416
6056
  .spawn({
6417
6057
  component: SpriteAnimation,
@@ -6465,7 +6105,7 @@ function makeEnvironmentGenerationState() {
6465
6105
  // semantics return one of the two; either way the `.code` matches
6466
6106
  // and the `.detail.field` matches, locking the consistent-abstraction
6467
6107
  // claim from plan-strategy section 2 D-1.
6468
- const world = new World({ time: { maxDeltaSeconds: 30 } });
6108
+ const world = new World();
6469
6109
  world
6470
6110
  .spawn({
6471
6111
  component: SpriteAnimation,
@@ -6482,7 +6122,7 @@ function makeEnvironmentGenerationState() {
6482
6122
  expect(rZero.ok).toBe(false);
6483
6123
  if (rZero.ok) throw new Error('zero arm: expected Result.err');
6484
6124
 
6485
- const world2 = new World({ time: { maxDeltaSeconds: 30 } });
6125
+ const world2 = new World();
6486
6126
  world2
6487
6127
  .spawn({
6488
6128
  component: SpriteAnimation,
@@ -6510,7 +6150,7 @@ function makeEnvironmentGenerationState() {
6510
6150
 
6511
6151
  describe('spriteAnimationTickSystem - AC-09(b) frameDuration=0 fail-fast (M4 T-20)', () => {
6512
6152
  it('returns Result.err with detail.field=frame-duration when frameDuration === 0', () => {
6513
- const world = new World({ time: { maxDeltaSeconds: 30 } });
6153
+ const world = new World();
6514
6154
  const entity = world
6515
6155
  .spawn({
6516
6156
  component: SpriteAnimation,
@@ -6556,7 +6196,7 @@ function makeEnvironmentGenerationState() {
6556
6196
  // --- from sprite-animation-tick-loop.test.ts ---
6557
6197
 
6558
6198
  function setDt(world: World, dt: number): void {
6559
- world.update(dt).unwrap();
6199
+ world[worldInternal].getClockWriter().time.delta = dt;
6560
6200
  }
6561
6201
 
6562
6202
  function makeRegions(): Float32Array {
@@ -6598,7 +6238,7 @@ function makeEnvironmentGenerationState() {
6598
6238
 
6599
6239
  describe('spriteAnimationTickSystem - AC-04 loop dt sequence (M4 T-17)', () => {
6600
6240
  it('five canonical dt steps drive currentFrame 0 -> 1 -> 1 -> 2 -> 0 with accumDt residue 0.06', () => {
6601
- const world = new World({ time: { maxDeltaSeconds: 30 } });
6241
+ const world = new World();
6602
6242
  const regions = makeRegions();
6603
6243
  const entity = world
6604
6244
  .spawn({
@@ -6671,7 +6311,7 @@ function makeEnvironmentGenerationState() {
6671
6311
  // accumDt=0.05. Establishes that the inner advance loop fires more
6672
6312
  // than once per spriteAnimationTickSystem invocation; AC-04 +
6673
6313
  // section 2.5 q6 (while loop semantics).
6674
- const world = new World({ time: { maxDeltaSeconds: 30 } });
6314
+ const world = new World();
6675
6315
  const regions = makeRegions();
6676
6316
  const entity = world
6677
6317
  .spawn({
@@ -6702,7 +6342,7 @@ function makeEnvironmentGenerationState() {
6702
6342
  // --- from sprite-animation-tick-override-probe.test.ts ---
6703
6343
 
6704
6344
  function setDt(world: World, dt: number): void {
6705
- world.update(dt).unwrap();
6345
+ world[worldInternal].getClockWriter().time.delta = dt;
6706
6346
  }
6707
6347
 
6708
6348
  function makeRegions(): Float32Array {
@@ -6744,7 +6384,7 @@ function makeEnvironmentGenerationState() {
6744
6384
 
6745
6385
  describe('spriteAnimationTickSystem override probe (M3 AC-14)', () => {
6746
6386
  it('first tick on a World that never used SpriteRegionOverride auto-adds the column without crashing', () => {
6747
- const world = new World({ time: { maxDeltaSeconds: 30 } });
6387
+ const world = new World();
6748
6388
  const entity = spawnAnim(world);
6749
6389
  // SpriteRegionOverride is never spawned / registered into this World;
6750
6390
  // the old gate existed precisely because world.get on it would throw.
@@ -6760,7 +6400,7 @@ function makeEnvironmentGenerationState() {
6760
6400
  });
6761
6401
 
6762
6402
  it('subsequent ticks set the region in place (steady-state already-present path)', () => {
6763
- const world = new World({ time: { maxDeltaSeconds: 30 } });
6403
+ const world = new World();
6764
6404
  const entity = spawnAnim(world);
6765
6405
 
6766
6406
  // Tick 1: dt=0.12 > frameDuration=0.1 advances exactly one frame
@@ -6793,7 +6433,7 @@ function makeEnvironmentGenerationState() {
6793
6433
  // --- from sprite-animation-tick-regions-mismatch.test.ts ---
6794
6434
 
6795
6435
  function setDt(world: World, dt: number): void {
6796
- world.update(dt).unwrap();
6436
+ world[worldInternal].getClockWriter().time.delta = dt;
6797
6437
  }
6798
6438
 
6799
6439
  function spawnBadRegionsLength(world: World): EntityHandle {
@@ -6839,7 +6479,7 @@ function makeEnvironmentGenerationState() {
6839
6479
 
6840
6480
  describe('spriteAnimationTickSystem - AC-09(a) regions.length fail-fast (M4 T-19)', () => {
6841
6481
  it('returns Result.err with 4 structured fields when regions.length !== frameCount * 4', () => {
6842
- const world = new World({ time: { maxDeltaSeconds: 30 } });
6482
+ const world = new World();
6843
6483
  const entity = spawnBadRegionsLength(world);
6844
6484
 
6845
6485
  setDt(world, 0.1);
@@ -6871,7 +6511,7 @@ function makeEnvironmentGenerationState() {
6871
6511
  });
6872
6512
 
6873
6513
  it('healthy entity advances normally beside a bad-regions-length entity', () => {
6874
- const world = new World({ time: { maxDeltaSeconds: 30 } });
6514
+ const world = new World();
6875
6515
  const bad = spawnBadRegionsLength(world);
6876
6516
  const good = spawnHealthy(world);
6877
6517
 
@@ -7182,22 +6822,6 @@ function makeEnvironmentGenerationState() {
7182
6822
  const sorted = transparentSortEntries(entries, world);
7183
6823
  expect(sorted.map((e) => e.entityIndex)).toEqual([1, 2, 0]);
7184
6824
  });
7185
-
7186
- it('uses materialHandle as the tertiary key and preserves equal-material order', () => {
7187
- const world = new World();
7188
- setTransparentSortConfig(world, {
7189
- mode: TRANSPARENT_SORT_MODE_LAYER_Z,
7190
- yzAlpha: 1.0,
7191
- }).unwrap();
7192
- const entries: TransparentEntry[] = [
7193
- makeEntry({ entityIndex: 0, layer: 0, posZ: 1, materialHandle: 20 }),
7194
- makeEntry({ entityIndex: 1, layer: 0, posZ: 1, materialHandle: 10 }),
7195
- makeEntry({ entityIndex: 2, layer: 0, posZ: 1, materialHandle: 20 }),
7196
- ];
7197
-
7198
- const sorted = transparentSortEntries(entries, world);
7199
- expect(sorted.map((e) => e.entityIndex)).toEqual([1, 0, 2]);
7200
- });
7201
6825
  });
7202
6826
 
7203
6827
  describe('transparentSortEntries - mode=1 Y-sort (AC-10 JRPG foot-pivot)', () => {
@@ -7558,6 +7182,7 @@ function makeEnvironmentGenerationState() {
7558
7182
  limits: {},
7559
7183
  queue: {
7560
7184
  submit: () => undefined,
7185
+ onSubmittedWorkDone: () => Promise.resolve(undefined),
7561
7186
  writeBuffer: (
7562
7187
  buffer: object,
7563
7188
  offset: number,
@@ -7666,7 +7291,7 @@ function makeEnvironmentGenerationState() {
7666
7291
  identifier,
7667
7292
  sourcePath: `${identifier}.wgsl`,
7668
7293
  composedWgsl,
7669
- paramSchema: canonicalMaterialParamSchema(identifier),
7294
+ paramSchema: '[]',
7670
7295
  variants: [],
7671
7296
  });
7672
7297
  const manifest = {
@@ -7684,13 +7309,12 @@ function makeEnvironmentGenerationState() {
7684
7309
  materialShaders: [
7685
7310
  materialShaderStub('forgeax::default-standard-pbr'),
7686
7311
  materialShaderStub('forgeax::default-unlit'),
7687
- materialShaderStub('forgeax::default-shadow-caster'),
7688
7312
  ],
7689
7313
  };
7690
7314
  return `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
7691
7315
  }
7692
7316
 
7693
- function makePassSpies(): PassSpies {
7317
+ function _makePassSpies(): PassSpies {
7694
7318
  return {
7695
7319
  setIndexBuffer: vi.fn(),
7696
7320
  setVertexBuffer: vi.fn(),
@@ -7702,15 +7326,10 @@ function makeEnvironmentGenerationState() {
7702
7326
  };
7703
7327
  }
7704
7328
 
7705
- interface RendererLike {
7706
- ready: Promise<void>;
7707
- draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
7708
- onError: (cb: (err: { code: string }) => void) => () => void;
7709
- assets: { register: (asset: unknown) => { ok: boolean; value: unknown } };
7710
- }
7329
+ type RendererLike = RendererType;
7711
7330
 
7712
7331
  async function importEngine(): Promise<{
7713
- createRenderer: (canvas: unknown, opts?: unknown) => Promise<RendererLike>;
7332
+ createRenderer: (...args: unknown[]) => Promise<{ unwrap(): RendererLike }>;
7714
7333
  }> {
7715
7334
  return (await import(ENGINE)) as never;
7716
7335
  }
@@ -7729,7 +7348,7 @@ function makeEnvironmentGenerationState() {
7729
7348
  DirectionalLight: unknown;
7730
7349
  }> {
7731
7350
  return {
7732
- ...(await import('@forgeax/engine-render/internal')),
7351
+ ...(await import('@forgeax/engine-render')),
7733
7352
  ...(await import('@forgeax/engine-scene')),
7734
7353
  } as never;
7735
7354
  }
@@ -7764,12 +7383,12 @@ function makeEnvironmentGenerationState() {
7764
7383
  };
7765
7384
  }
7766
7385
 
7767
- function singleSubmeshTriangle(): MeshAsset {
7386
+ function _singleSubmeshTriangle(): MeshAsset {
7768
7387
  return {
7769
7388
  kind: 'mesh',
7770
7389
  vertices: new Float32Array(3 * 12),
7771
7390
  indices: new Uint16Array([0, 1, 2]),
7772
- attributes: canonicalTestMeshAttributes(3),
7391
+ attributes: {},
7773
7392
  materialSlots: [{ slotName: 'Default' }],
7774
7393
  submeshes: [
7775
7394
  {
@@ -7783,12 +7402,12 @@ function makeEnvironmentGenerationState() {
7783
7402
  };
7784
7403
  }
7785
7404
 
7786
- function threeSubmeshMesh(): MeshAsset {
7405
+ function _threeSubmeshMesh(): MeshAsset {
7787
7406
  return {
7788
7407
  kind: 'mesh',
7789
7408
  vertices: new Float32Array(9 * 12),
7790
7409
  indices: new Uint16Array([0, 1, 2, 3, 4, 5, 6, 7, 8]),
7791
- attributes: canonicalTestMeshAttributes(9),
7410
+ attributes: {},
7792
7411
  materialSlots: [{ slotName: 'First' }, { slotName: 'Second' }, { slotName: 'Third' }],
7793
7412
  submeshes: [
7794
7413
  {
@@ -7816,22 +7435,23 @@ function makeEnvironmentGenerationState() {
7816
7435
  };
7817
7436
  }
7818
7437
 
7819
- async function setupRenderer(spies: PassSpies): Promise<{ renderer: RendererLike }> {
7438
+ async function _setupRenderer(spies: PassSpies): Promise<{ renderer: RendererLike }> {
7820
7439
  const { device } = makeMockGPUDevice(spies);
7821
7440
  vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
7822
7441
  const { createRenderer } = await importEngine();
7823
- const renderer = await createRenderer(
7824
- makeMockCanvas(),
7825
- {},
7826
- {
7827
- shaderManifestUrl: buildManifestDataUrl(),
7828
- },
7829
- );
7830
- await renderer.ready;
7442
+ const renderer = (
7443
+ await createRenderer(
7444
+ makeMockCanvas(),
7445
+ {},
7446
+ {
7447
+ shaderManifestUrl: buildManifestDataUrl(),
7448
+ },
7449
+ )
7450
+ ).unwrap();
7831
7451
  return { renderer };
7832
7452
  }
7833
7453
 
7834
- async function spawnMultiMaterialScene(
7454
+ async function _spawnMultiMaterialScene(
7835
7455
  _renderer: RendererLike,
7836
7456
  meshAsset: MeshAsset,
7837
7457
  colors: ReadonlyArray<readonly [number, number, number]>,
@@ -7839,21 +7459,6 @@ function makeEnvironmentGenerationState() {
7839
7459
  const { World } = await importEcs();
7840
7460
  const C = await importComponents();
7841
7461
  const world = new World();
7842
- for (const component of [
7843
- C.Camera,
7844
- C.DirectionalLight,
7845
- C.Transform,
7846
- C.MeshRenderer,
7847
- C.MeshFilter,
7848
- ]) {
7849
- (
7850
- world as unknown as {
7851
- components: { register: (component: unknown) => { unwrap: () => unknown } };
7852
- }
7853
- ).components
7854
- .register(component)
7855
- .unwrap();
7856
- }
7857
7462
  const meshHandle = world.allocSharedRef('MeshAsset', meshAsset) as Handle<
7858
7463
  'MeshAsset',
7859
7464
  'shared'
@@ -7887,7 +7492,7 @@ function makeEnvironmentGenerationState() {
7887
7492
  { component: C.Transform, data: cameraTransform() },
7888
7493
  );
7889
7494
  world.spawn(
7890
- { component: C.DirectionalLight, data: { direction: [-0.5, -1, -0.3] } },
7495
+ { component: C.DirectionalLight, data: {} },
7891
7496
  { component: C.Transform, data: cameraTransform() },
7892
7497
  );
7893
7498
  world.spawn(
@@ -7897,222 +7502,6 @@ function makeEnvironmentGenerationState() {
7897
7502
  );
7898
7503
  return world;
7899
7504
  }
7900
-
7901
- describe('extract: per-submesh MaterialSnapshot[] (w11-a)', () => {
7902
- beforeEach(() => {
7903
- vi.stubGlobal('navigator', baseNavigator);
7904
- });
7905
- afterEach(() => {
7906
- vi.unstubAllGlobals();
7907
- });
7908
-
7909
- it('(a) submeshes=[3] + materials=[3]: extract renderable carries materials[3]', async () => {
7910
- const spies = makePassSpies();
7911
- const { renderer } = await setupRenderer(spies);
7912
- const errors: string[] = [];
7913
- renderer.onError((e) => errors.push(e.code));
7914
-
7915
- const world = await spawnMultiMaterialScene(renderer, threeSubmeshMesh(), [
7916
- [1, 0, 0],
7917
- [0, 1, 0],
7918
- [0, 0, 1],
7919
- ]);
7920
-
7921
- // Use the extract API directly so we can inspect the snapshot shape.
7922
- const { extractFrame, prepareExtractContext } = (await import(
7923
- '@forgeax/engine-render/internal'
7924
- )) as {
7925
- extractFrame: (
7926
- w: unknown,
7927
- context: unknown,
7928
- ) => {
7929
- renderables: Array<{
7930
- materials?: ReadonlyArray<{ baseColor: Float32Array | readonly number[] }>;
7931
- material?: { baseColor: Float32Array | readonly number[] };
7932
- }>;
7933
- };
7934
- prepareExtractContext: (w: unknown, options: unknown) => unknown;
7935
- };
7936
- // The renderer exposes its render asset port via .assets.
7937
- const rendererAny = renderer as unknown as { assets: unknown };
7938
- const frame = extractFrame(
7939
- world,
7940
- prepareExtractContext(world, { assets: rendererAny.assets }),
7941
- );
7942
- expect(frame.renderables.length).toBe(1);
7943
- const r = frame.renderables[0];
7944
- if (!r) throw new Error('expected renderable');
7945
- expect(r.materials).toBeDefined();
7946
- expect(r.materials?.length).toBe(3);
7947
- // The three materials must carry distinct baseColor vec3 values
7948
- // (positional 1-1 with submeshes[]).
7949
- const bc0 = r.materials?.[0]?.baseColor;
7950
- const bc1 = r.materials?.[1]?.baseColor;
7951
- const bc2 = r.materials?.[2]?.baseColor;
7952
- expect(Array.from(bc0).slice(0, 3)).toEqual([1, 0, 0]);
7953
- expect(Array.from(bc1).slice(0, 3)).toEqual([0, 1, 0]);
7954
- expect(Array.from(bc2).slice(0, 3)).toEqual([0, 0, 1]);
7955
- expect(errors).toEqual([]);
7956
- });
7957
- });
7958
-
7959
- describe('record: per-submesh material UBO rebind (w16-a)', () => {
7960
- beforeEach(() => {
7961
- vi.stubGlobal('navigator', baseNavigator);
7962
- });
7963
- afterEach(() => {
7964
- vi.unstubAllGlobals();
7965
- });
7966
-
7967
- it.skip('(b) 3 submeshes x 3 distinct materials: 3 material UBO writes at distinct slot offsets, 3 drawIndexed each preceded by setBindGroup(1)', async () => {
7968
- const spies = makePassSpies();
7969
- const { renderer } = await setupRenderer(spies);
7970
- const errors: string[] = [];
7971
- renderer.onError((e) => errors.push(e.code));
7972
-
7973
- const world = await spawnMultiMaterialScene(renderer, threeSubmeshMesh(), [
7974
- [1, 0, 0],
7975
- [0, 1, 0],
7976
- [0, 0, 1],
7977
- ]);
7978
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
7979
- throw new Error('World attachment failed');
7980
- }
7981
- (world as WorldType).update().unwrap();
7982
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
7983
-
7984
- expect(spies.drawIndexed).toHaveBeenCalledTimes(3);
7985
-
7986
- // Material UBO writes: must have at least 3 distinct slot offsets
7987
- // (one per submesh material). Stride is 256 B; payload size 48 B.
7988
- const matUboWrites = spies.writeBufferCalls.filter(
7989
- (w) => w.bufferLabel === 'pbr-material-ubo',
7990
- );
7991
- const distinctOffsets = new Set(matUboWrites.map((w) => w.offset));
7992
- expect(distinctOffsets.size).toBeGreaterThanOrEqual(3);
7993
- // Three offsets must be 0, 256, 512 (i.e. consecutive 256 B slots --
7994
- // one per material in this single-entity scene).
7995
- expect(distinctOffsets.has(0)).toBe(true);
7996
- expect(distinctOffsets.has(256)).toBe(true);
7997
- expect(distinctOffsets.has(512)).toBe(true);
7998
-
7999
- // Inside the geometry pass: each drawIndexed must be preceded by a
8000
- // material BG bind (setBindGroup(1, ..., [offset])) at the matching slot.
8001
- // We walk the geometry events and record, for each drawIndexed,
8002
- // the most recent setBindGroup(1, ...) dynamicOffset.
8003
- let lastMatOffset: number | undefined;
8004
- const drawOffsets: number[] = [];
8005
- for (const ev of spies.geometryEvents) {
8006
- if (ev.kind === 'setBindGroup' && ev.call.group === 1) {
8007
- lastMatOffset = ev.call.dynamicOffsets[0];
8008
- } else if (ev.kind === 'drawIndexed') {
8009
- if (lastMatOffset !== undefined) drawOffsets.push(lastMatOffset);
8010
- }
8011
- }
8012
- expect(drawOffsets.length).toBe(3);
8013
- // The three drawIndexed calls must each see a distinct material offset.
8014
- expect(new Set(drawOffsets).size).toBe(3);
8015
- // Specifically: submesh i should see slotOffset i*256.
8016
- expect(drawOffsets[0]).toBe(0);
8017
- expect(drawOffsets[1]).toBe(256);
8018
- expect(drawOffsets[2]).toBe(512);
8019
-
8020
- expect(errors).toEqual([]);
8021
- });
8022
-
8023
- it('(b2) 3 submeshes share one stride-shaped material UBO upload and keep dynamic offsets', async () => {
8024
- const spies = makePassSpies();
8025
- const { renderer } = await setupRenderer(spies);
8026
- const errors: string[] = [];
8027
- renderer.onError((e) => errors.push(e.code));
8028
-
8029
- const world = await spawnMultiMaterialScene(renderer, threeSubmeshMesh(), [
8030
- [1, 0, 0],
8031
- [0, 1, 0],
8032
- [0, 0, 1],
8033
- ]);
8034
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
8035
- throw new Error('World attachment failed');
8036
- }
8037
- (world as WorldType).update().unwrap();
8038
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
8039
- await new Promise((resolve) => setTimeout(resolve, 0));
8040
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
8041
- throw new Error('World attachment failed');
8042
- }
8043
- (world as WorldType).update().unwrap();
8044
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
8045
-
8046
- const matUboWrites = spies.writeBufferCalls.filter(
8047
- (w) => w.bufferLabel === 'pbr-material-ubo',
8048
- );
8049
- // The two-frame warmup writes the same packed material slab once per
8050
- // frame; the contract under test is the single stride-shaped slab, not
8051
- // the number of frame submissions.
8052
- expect(matUboWrites.length).toBeGreaterThanOrEqual(1);
8053
- expect(new Set(matUboWrites.map((write) => `${write.offset}:${write.byteLength}`))).toEqual(
8054
- new Set([`0:${3 * 512}`]),
8055
- );
8056
-
8057
- const drawOffsets: number[] = [];
8058
- let lastMatOffset: number | undefined;
8059
- for (const ev of spies.geometryEvents) {
8060
- if (ev.kind === 'setBindGroup' && ev.call.group === 1) {
8061
- lastMatOffset = ev.call.dynamicOffsets[0];
8062
- } else if (ev.kind === 'drawIndexed' && lastMatOffset !== undefined) {
8063
- drawOffsets.push(lastMatOffset);
8064
- }
8065
- }
8066
- expect(drawOffsets.slice(-3)).toEqual([0, 512, 1024]);
8067
- expect(errors).toEqual([]);
8068
- });
8069
-
8070
- it('(c) single submesh + single material: 1 material UBO write at offset 0, 1 drawIndexed (backward compat)', async () => {
8071
- const spies = makePassSpies();
8072
- const { renderer } = await setupRenderer(spies);
8073
- const errors: string[] = [];
8074
- renderer.onError((e) => errors.push(e.code));
8075
-
8076
- const world = await spawnMultiMaterialScene(renderer, singleSubmeshTriangle(), [[1, 0.5, 0]]);
8077
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
8078
- throw new Error('World attachment failed');
8079
- }
8080
- (world as WorldType).update().unwrap();
8081
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
8082
- await new Promise((resolve) => setTimeout(resolve, 0));
8083
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
8084
- throw new Error('World attachment failed');
8085
- }
8086
- (world as WorldType).update().unwrap();
8087
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
8088
-
8089
- expect(spies.drawIndexed).toHaveBeenCalledTimes(1);
8090
-
8091
- const matUboWrites = spies.writeBufferCalls.filter(
8092
- (w) => w.bufferLabel === 'pbr-material-ubo',
8093
- );
8094
- const distinctOffsets = new Set(matUboWrites.map((w) => w.offset));
8095
- // Single-mesh single-material: only slot 0 is used.
8096
- expect(distinctOffsets.has(0)).toBe(true);
8097
- // No further slots should be written.
8098
- expect(distinctOffsets.has(256)).toBe(false);
8099
-
8100
- // Geometry pass: exactly 1 drawIndexed; preceded by 1 setBindGroup(1, ..., [0]).
8101
- let lastMatOffset: number | undefined;
8102
- const drawOffsets: number[] = [];
8103
- for (const ev of spies.geometryEvents) {
8104
- if (ev.kind === 'setBindGroup' && ev.call.group === 1) {
8105
- lastMatOffset = ev.call.dynamicOffsets[0];
8106
- } else if (ev.kind === 'drawIndexed') {
8107
- if (lastMatOffset !== undefined) drawOffsets.push(lastMatOffset);
8108
- }
8109
- }
8110
- expect(drawOffsets.length).toBe(1);
8111
- expect(drawOffsets[0]).toBe(0);
8112
-
8113
- expect(errors).toEqual([]);
8114
- });
8115
- });
8116
7505
  }
8117
7506
 
8118
7507
  {
@@ -8167,6 +7556,7 @@ function makeEnvironmentGenerationState() {
8167
7556
  limits: {},
8168
7557
  queue: {
8169
7558
  submit: () => undefined,
7559
+ onSubmittedWorkDone: () => Promise.resolve(undefined),
8170
7560
  writeBuffer: () => undefined,
8171
7561
  writeTexture: () => undefined,
8172
7562
  },
@@ -8217,7 +7607,7 @@ function makeEnvironmentGenerationState() {
8217
7607
  identifier,
8218
7608
  sourcePath: `${identifier}.wgsl`,
8219
7609
  composedWgsl: '/* stub */',
8220
- paramSchema: canonicalMaterialParamSchema(identifier),
7610
+ paramSchema: '[]',
8221
7611
  variants: [],
8222
7612
  });
8223
7613
  const manifest = {
@@ -8249,15 +7639,10 @@ function makeEnvironmentGenerationState() {
8249
7639
  };
8250
7640
  }
8251
7641
 
8252
- interface RendererLike {
8253
- ready: Promise<void>;
8254
- draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
8255
- onError: (cb: (err: { code: string }) => void) => () => void;
8256
- assets: { register: (asset: unknown) => { ok: boolean; value: unknown } };
8257
- }
7642
+ type RendererLike = RendererType;
8258
7643
 
8259
7644
  async function importEngine(): Promise<{
8260
- createRenderer: (canvas: unknown, opts?: unknown) => Promise<RendererLike>;
7645
+ createRenderer: (...args: unknown[]) => Promise<{ unwrap(): RendererLike }>;
8261
7646
  }> {
8262
7647
  return (await import(ENGINE)) as never;
8263
7648
  }
@@ -8279,7 +7664,7 @@ function makeEnvironmentGenerationState() {
8279
7664
  DirectionalLight: unknown;
8280
7665
  }> {
8281
7666
  return {
8282
- ...(await import('@forgeax/engine-render/internal')),
7667
+ ...(await import('@forgeax/engine-render')),
8283
7668
  ...(await import('@forgeax/engine-scene')),
8284
7669
  } as never;
8285
7670
  }
@@ -8314,12 +7699,12 @@ function makeEnvironmentGenerationState() {
8314
7699
  };
8315
7700
  }
8316
7701
 
8317
- function singleSubmeshTriangle(): MeshAsset {
7702
+ function _singleSubmeshTriangle(): MeshAsset {
8318
7703
  return {
8319
7704
  kind: 'mesh',
8320
7705
  vertices: new Float32Array(3 * 12),
8321
7706
  indices: new Uint16Array([0, 1, 2]),
8322
- attributes: canonicalTestMeshAttributes(3),
7707
+ attributes: {},
8323
7708
  materialSlots: [{ slotName: 'Default' }],
8324
7709
  submeshes: [
8325
7710
  {
@@ -8333,12 +7718,12 @@ function makeEnvironmentGenerationState() {
8333
7718
  };
8334
7719
  }
8335
7720
 
8336
- function threeSubmeshMesh(): MeshAsset {
7721
+ function _threeSubmeshMesh(): MeshAsset {
8337
7722
  return {
8338
7723
  kind: 'mesh',
8339
7724
  vertices: new Float32Array(9 * 12),
8340
7725
  indices: new Uint16Array([0, 1, 2, 3, 4, 5, 6, 7, 8]),
8341
- attributes: canonicalTestMeshAttributes(9),
7726
+ attributes: {},
8342
7727
  materialSlots: [{ slotName: 'First' }, { slotName: 'Second' }, { slotName: 'Third' }],
8343
7728
  submeshes: [
8344
7729
  {
@@ -8366,11 +7751,11 @@ function makeEnvironmentGenerationState() {
8366
7751
  };
8367
7752
  }
8368
7753
 
8369
- function vertexOnlyLineListMesh(): MeshAsset {
7754
+ function _vertexOnlyLineListMesh(): MeshAsset {
8370
7755
  return {
8371
7756
  kind: 'mesh',
8372
7757
  vertices: new Float32Array(2 * 12),
8373
- attributes: canonicalTestMeshAttributes(2),
7758
+ attributes: {},
8374
7759
  materialSlots: [{ slotName: 'Default' }],
8375
7760
  submeshes: [
8376
7761
  {
@@ -8384,12 +7769,12 @@ function makeEnvironmentGenerationState() {
8384
7769
  };
8385
7770
  }
8386
7771
 
8387
- function mixedTopologyMesh(): MeshAsset {
7772
+ function _mixedTopologyMesh(): MeshAsset {
8388
7773
  return {
8389
7774
  kind: 'mesh',
8390
7775
  vertices: new Float32Array(5 * 12),
8391
7776
  indices: new Uint16Array([0, 1, 2, 3, 4]),
8392
- attributes: canonicalTestMeshAttributes(5),
7777
+ attributes: {},
8393
7778
  materialSlots: [{ slotName: 'Triangles' }, { slotName: 'Lines' }],
8394
7779
  submeshes: [
8395
7780
  {
@@ -8414,18 +7799,19 @@ function makeEnvironmentGenerationState() {
8414
7799
  const { device } = makeMockGPUDevice(spies);
8415
7800
  vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
8416
7801
  const { createRenderer } = await importEngine();
8417
- const renderer = await createRenderer(
8418
- makeMockCanvas(),
8419
- {},
8420
- {
8421
- shaderManifestUrl: buildManifestDataUrl(),
8422
- },
8423
- );
8424
- await renderer.ready;
7802
+ const renderer = (
7803
+ await createRenderer(
7804
+ makeMockCanvas(),
7805
+ {},
7806
+ {
7807
+ shaderManifestUrl: buildManifestDataUrl(),
7808
+ },
7809
+ )
7810
+ ).unwrap();
8425
7811
  return { renderer };
8426
7812
  }
8427
7813
 
8428
- async function spawnScene(
7814
+ async function _spawnScene(
8429
7815
  _renderer: RendererLike,
8430
7816
  meshAsset: MeshAsset,
8431
7817
  materialCount: number,
@@ -8433,21 +7819,6 @@ function makeEnvironmentGenerationState() {
8433
7819
  const { World } = await importEcs();
8434
7820
  const C = await importComponents();
8435
7821
  const world = new World();
8436
- for (const component of [
8437
- C.Camera,
8438
- C.DirectionalLight,
8439
- C.Transform,
8440
- C.MeshRenderer,
8441
- C.MeshFilter,
8442
- ]) {
8443
- (
8444
- world as unknown as {
8445
- components: { register: (component: unknown) => { unwrap: () => unknown } };
8446
- }
8447
- ).components
8448
- .register(component)
8449
- .unwrap();
8450
- }
8451
7822
  const meshHandle = world.allocSharedRef('MeshAsset', meshAsset) as Handle<
8452
7823
  'MeshAsset',
8453
7824
  'shared'
@@ -8487,7 +7858,7 @@ function makeEnvironmentGenerationState() {
8487
7858
  { component: C.Transform, data: cameraTransform() },
8488
7859
  );
8489
7860
  world.spawn(
8490
- { component: C.DirectionalLight, data: { direction: [-0.5, -1, -0.3] } },
7861
+ { component: C.DirectionalLight, data: {} },
8491
7862
  { component: C.Transform, data: cameraTransform() },
8492
7863
  );
8493
7864
  world.spawn(
@@ -8498,7 +7869,11 @@ function makeEnvironmentGenerationState() {
8498
7869
  return world;
8499
7870
  }
8500
7871
 
8501
- describe('render-system-record per-submesh drawIndexed (w17, AC-04)', () => {
7872
+ // feat-20260612-skin-palette-per-frame-upload M1 / m1-1: the old
7873
+ // closure-held pipeline assertion is retired with the internal state
7874
+ // boundary. The public inspection snapshot is the supported evidence for
7875
+ // renderer construction and capability health.
7876
+ describe('renderer public inspection contract', () => {
8502
7877
  beforeEach(() => {
8503
7878
  vi.stubGlobal('navigator', baseNavigator);
8504
7879
  });
@@ -8507,171 +7882,15 @@ function makeEnvironmentGenerationState() {
8507
7882
  vi.unstubAllGlobals();
8508
7883
  });
8509
7884
 
8510
- it('(a) single mesh single submesh: drawIndexed called once', async () => {
8511
- const spies = makePassSpies();
8512
- const { renderer } = await setupRenderer(spies);
8513
- const errors: string[] = [];
8514
- renderer.onError((e) => errors.push(e.code));
8515
-
8516
- const world = await spawnScene(renderer, singleSubmeshTriangle(), 1);
8517
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
8518
- throw new Error('World attachment failed');
8519
- }
8520
- (world as WorldType).update().unwrap();
8521
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
8522
- await new Promise((resolve) => setTimeout(resolve, 0));
8523
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
8524
- throw new Error('World attachment failed');
8525
- }
8526
- (world as WorldType).update().unwrap();
8527
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
8528
-
8529
- expect(spies.drawIndexed).toHaveBeenCalledTimes(1);
8530
- // First drawIndexed call: indexCount=3, indexOffset=0
8531
- // drawIndexed(indexCount, instanceCount, indexOffset, baseVertex, firstInstance)
8532
- const firstCall = spies.drawIndexed.mock.calls[0];
8533
- expect(firstCall[0]).toBe(3); // indexCount
8534
- expect(firstCall[2]).toBe(0); // indexOffset
8535
- expect(errors).toEqual([]);
8536
- });
8537
-
8538
- it('(b) single mesh 3 submeshes: drawIndexed called 3 times with distinct offsets', async () => {
8539
- const spies = makePassSpies();
8540
- const { renderer } = await setupRenderer(spies);
8541
- const errors: string[] = [];
8542
- renderer.onError((e) => errors.push(e.code));
8543
-
8544
- const world = await spawnScene(renderer, threeSubmeshMesh(), 3);
8545
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
8546
- throw new Error('World attachment failed');
8547
- }
8548
- (world as WorldType).update().unwrap();
8549
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
8550
- await new Promise((resolve) => setTimeout(resolve, 0));
8551
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
8552
- throw new Error('World attachment failed');
8553
- }
8554
- (world as WorldType).update().unwrap();
8555
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
8556
-
8557
- expect(spies.drawIndexed).toHaveBeenCalledTimes(3);
8558
- // Each submesh gets its own drawIndexed call with correct indexOffset.
8559
- const offsets = spies.drawIndexed.mock.calls.map((c: number[]) => c[2]);
8560
- expect(offsets).toEqual([0, 3, 6]);
8561
- // Each draw has indexCount=3.
8562
- const counts = spies.drawIndexed.mock.calls.map((c: number[]) => c[0]);
8563
- expect(counts).toEqual([3, 3, 3]);
8564
- expect(errors).toEqual([]);
8565
- });
8566
-
8567
- it('(c) vertex-only (non-indexed) submesh: draw() called, drawIndexed not called', async () => {
7885
+ it('reports an alive, available renderer through inspect()', async () => {
8568
7886
  const spies = makePassSpies();
8569
7887
  const { renderer } = await setupRenderer(spies);
8570
- const errors: string[] = [];
8571
- renderer.onError((e) => errors.push(e.code));
8572
-
8573
- const world = await spawnScene(renderer, vertexOnlyLineListMesh(), 1);
8574
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
8575
- throw new Error('World attachment failed');
8576
- }
8577
- (world as WorldType).update().unwrap();
8578
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
8579
- await new Promise((resolve) => setTimeout(resolve, 0));
8580
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
8581
- throw new Error('World attachment failed');
8582
- }
8583
- (world as WorldType).update().unwrap();
8584
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
8585
-
8586
- expect(spies.draw).toHaveBeenCalled();
8587
- // draw(vertexCount, instanceCount, firstVertex, firstInstance)
8588
- const drewVertexCall = spies.draw.mock.calls.find((c: number[]) => c[0] === 2);
8589
- expect(drewVertexCall).toBeTruthy();
8590
- // Indexed path must NOT be used for a vertex-only submesh.
8591
- expect(spies.drawIndexed).not.toHaveBeenCalled();
8592
- expect(errors).toEqual([]);
8593
- });
8594
-
8595
- it('(d) mixed-topology mesh: two drawIndexed calls, one per topology', async () => {
8596
- const spies = makePassSpies();
8597
- const { renderer } = await setupRenderer(spies);
8598
- const errors: string[] = [];
8599
- renderer.onError((e) => errors.push(e.code));
8600
-
8601
- const world = await spawnScene(renderer, mixedTopologyMesh(), 2);
8602
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
8603
- throw new Error('World attachment failed');
8604
- }
8605
- (world as WorldType).update().unwrap();
8606
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
8607
- await new Promise((resolve) => setTimeout(resolve, 0));
8608
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
8609
- throw new Error('World attachment failed');
8610
- }
8611
- (world as WorldType).update().unwrap();
8612
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
8613
-
8614
- // Both submeshes are indexed → 2 drawIndexed calls.
8615
- expect(spies.drawIndexed).toHaveBeenCalledTimes(2);
8616
- // Submesh 0: triangle-list, indexOffset=0, indexCount=3
8617
- expect(spies.drawIndexed.mock.calls[0][0]).toBe(3);
8618
- expect(spies.drawIndexed.mock.calls[0][2]).toBe(0);
8619
- // Submesh 1: line-list, indexOffset=3, indexCount=2
8620
- expect(spies.drawIndexed.mock.calls[1][0]).toBe(2);
8621
- expect(spies.drawIndexed.mock.calls[1][2]).toBe(3);
8622
- // Both drawIndexed calls share the same vertex buffer (vertex-only vertex buffer binding).
8623
- expect(spies.setVertexBuffer).toHaveBeenCalled();
8624
- expect(errors).toEqual([]);
8625
- });
8626
- });
8627
-
8628
- // feat-20260612-skin-palette-per-frame-upload M1 / m1-1: PipelineState
8629
- // field-existence assertion. After `await renderer.ready` settles, the
8630
- // closure-held PipelineState must (a) carry `skinPaletteAllocator` (the
8631
- // animator-ready palette allocator from `createSkinPaletteAllocator`); and
8632
- // (b) NOT carry `skinPaletteIdentityBuffer` (the bind-pose stub that gated
8633
- // PR #353 ships in the prior loop's M8 / w28 -- retired by m1-2 + m1-3 of
8634
- // this loop). The test is RED before m1-2/m1-3 land (current main: stub
8635
- // present, allocator missing) and GREEN after both impl tasks commit.
8636
- describe('feat-20260612 M1 / m1-1: PipelineState skin palette field shape', () => {
8637
- beforeEach(() => {
8638
- vi.stubGlobal('navigator', baseNavigator);
8639
- });
8640
-
8641
- afterEach(() => {
8642
- vi.unstubAllGlobals();
8643
- });
8644
-
8645
- it('exposes skinPaletteAllocator and not skinPaletteIdentityBuffer (m1-1)', async () => {
8646
- const spies = makePassSpies();
8647
- const { renderer } = await setupRenderer(spies);
8648
- const ps = (
8649
- renderer as unknown as { _internal_getPipelineState(): unknown }
8650
- )._internal_getPipelineState() as Record<string, unknown> | null;
8651
- expect(ps).not.toBeNull();
8652
- const psNN = ps as Record<string, unknown>;
8653
- // (a) Allocator carrier present and shaped like SkinPaletteAllocator.
8654
- expect(psNN.skinPaletteAllocator).toBeDefined();
8655
- expect(psNN.skinPaletteAllocator).not.toBeNull();
8656
- const allocator = psNN.skinPaletteAllocator as Record<string, unknown>;
8657
- expect(typeof allocator.allocateSlice).toBe('function');
8658
- expect(typeof allocator.writeJointPalette).toBe('function');
8659
- expect(typeof allocator.resetForFrame).toBe('function');
8660
- // M6: allocator.buffer field retired -- per-slice `slice.buffer`
8661
- // is now the carrier so the uniform fallback path can return a
8662
- // distinct buffer per entity without leaking the storage-only
8663
- // shared-buffer assumption into record-stage code.
8664
- expect('buffer' in allocator).toBe(false);
8665
- // M6 SSOT: allocator exposes the static BG @binding(1) entry size
8666
- // (= MAX_JOINTS * 64 = 16320) so the record-stage BG creation reads
8667
- // it instead of a literal. Mismatch with `pbr-skin-mesh-array-bgl`
8668
- // would trip `dynOffset + entry.size > buffer.size` validation.
8669
- expect(allocator.bindingWindowBytes).toBe(16320);
8670
- // M6: useStorageBuffer flag exposed for record-stage / test
8671
- // assertions that need to know which path the allocator is on.
8672
- expect(typeof allocator.useStorageBuffer).toBe('boolean');
8673
- // (b) Stub field retired -- not present on the PipelineState shape.
8674
- expect('skinPaletteIdentityBuffer' in psNN).toBe(false);
7888
+ const inspection = renderer.inspect();
7889
+ expect(inspection.state).toBe('alive');
7890
+ expect(inspection.surface).toBe('available');
7891
+ expect(inspection.frame.deviceGeneration).toBeGreaterThanOrEqual(0);
7892
+ expect(Array.isArray(inspection.features)).toBe(true);
7893
+ expect(inspection.capabilities).toBeDefined();
8675
7894
  });
8676
7895
  });
8677
7896
  }
@@ -8700,7 +7919,7 @@ const SKIN_M2_IDENTITY_TRANSFORM = {
8700
7919
  scale: [1, 1, 1],
8701
7920
  } as const;
8702
7921
 
8703
- function makeSkinM2AssetPort(): RenderAssetPort {
7922
+ function makeSkinM2AssetRegistry(): AssetRegistry {
8704
7923
  const shaderRegistry = makeMockShaderRegistry();
8705
7924
  shaderRegistry.installMaterialArtifact('forgeax::pbr-skin', {
8706
7925
  source: 'fn main() {}',
@@ -8710,13 +7929,7 @@ function makeSkinM2AssetPort(): RenderAssetPort {
8710
7929
  { name: 'roughness', type: 'f32', default: 0.5 },
8711
7930
  ],
8712
7931
  });
8713
- return {
8714
- epoch: 0,
8715
- shaderRegistry,
8716
- lookup: () => undefined,
8717
- guidOf: () => undefined,
8718
- materialShaderTextureFieldNames: () => [],
8719
- };
7932
+ return new AssetRegistry(shaderRegistry);
8720
7933
  }
8721
7934
 
8722
7935
  function registerSkinM2Mesh(world: World): Handle<'MeshAsset', 'shared'> {
@@ -8864,7 +8077,7 @@ function makeSkinM2StubAllocator(): SkinM2StubAllocator {
8864
8077
  type ExtractFramesWithPipeline = (
8865
8078
  worlds: readonly World[],
8866
8079
  owner: number,
8867
- a: RenderAssetPort | null,
8080
+ a: AssetRegistry | null,
8868
8081
  p: { skinPaletteAllocator: SkinM2StubAllocator },
8869
8082
  ) => ReturnType<typeof extractFrames>;
8870
8083
 
@@ -8873,7 +8086,7 @@ type ExtractFramesWithPipeline = (
8873
8086
  describe('feat-20260612 M2 / m2-1: hasSkin happy path -> non-zero byteOffset', () => {
8874
8087
  it('two skinned entities -> first slice byteOffset>=0, second>0; writeJointPalette called twice (m2-1)', () => {
8875
8088
  const world = new World();
8876
- const assets = makeSkinM2AssetPort();
8089
+ const assets = makeSkinM2AssetRegistry();
8877
8090
  const meshHandle = registerSkinM2Mesh(world);
8878
8091
  const matHandle = registerSkinM2PbrSkinMaterial(world);
8879
8092
  const skeletonHandle = registerSkinM2Skeleton(world, 2);
@@ -8888,6 +8101,8 @@ type ExtractFramesWithPipeline = (
8888
8101
 
8889
8102
  const allocator = makeSkinM2StubAllocator();
8890
8103
  const pipelineState = { skinPaletteAllocator: allocator };
8104
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
8105
+
8891
8106
  const frame = (extractFrames as unknown as ExtractFramesWithPipeline)(
8892
8107
  [world],
8893
8108
  0,
@@ -8899,6 +8114,8 @@ type ExtractFramesWithPipeline = (
8899
8114
  expect(allocator._resetCount()).toBe(1);
8900
8115
  // writeJointPalette called once per skinned entity (count=2).
8901
8116
  expect(allocator._writeCount()).toBe(2);
8117
+ // No errors fired on happy path.
8118
+ expect(errorSpy).not.toHaveBeenCalled();
8902
8119
  // Both renderables emerge with .skin populated.
8903
8120
  const skinned = frame.renderables.filter((r) => r.skin !== undefined);
8904
8121
  expect(skinned.length).toBe(2);
@@ -8926,6 +8143,7 @@ type ExtractFramesWithPipeline = (
8926
8143
  describe('feat-20260612 M2 / m2-2: assets===null equiv bind-pose (no error)', () => {
8927
8144
  it('skinned entity + prepared null assets -> no _routeError, no skin slice (m2-2)', () => {
8928
8145
  const world = new World();
8146
+ const _assets = makeSkinM2AssetRegistry();
8929
8147
  const meshHandle = registerSkinM2Mesh(world);
8930
8148
  const matHandle = registerSkinM2PbrSkinMaterial(world);
8931
8149
  const skeletonHandle = registerSkinM2Skeleton(world, 1);
@@ -8936,13 +8154,16 @@ type ExtractFramesWithPipeline = (
8936
8154
 
8937
8155
  const allocator = makeSkinM2StubAllocator();
8938
8156
  const pipelineState = { skinPaletteAllocator: allocator };
8939
- // assets===null: assemble-form / test path that lacks a render asset port.
8157
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
8158
+
8159
+ // assets===null: assemble-form / test path that lacks an AssetRegistry.
8940
8160
  // Plan-strategy R-3: must NOT trigger skeleton-resolve-failed; the
8941
8161
  // hasSkin segment is skipped entirely (equivalent to bind-pose).
8942
8162
  expect(() =>
8943
8163
  (extractFrames as unknown as ExtractFramesWithPipeline)([world], 0, null, pipelineState),
8944
8164
  ).not.toThrow();
8945
8165
 
8166
+ expect(errorSpy).not.toHaveBeenCalled();
8946
8167
  // No palette writes when assets is null (skinned entities bypass extract
8947
8168
  // entirely because Skin <-> pbr-skin material walk also requires assets;
8948
8169
  // either way the truth-value is: no writeJointPalette call).
@@ -8956,29 +8177,44 @@ type ExtractFramesWithPipeline = (
8956
8177
  describe('feat-20260612 M2 / m2-3: skeleton-resolve-failed routing', () => {
8957
8178
  it('Skin.skeleton points at unregistered handle -> _routeError(skeleton-resolve-failed) + continue (m2-3)', () => {
8958
8179
  const world = new World();
8959
- const assets = makeSkinM2AssetPort();
8180
+ const assets = makeSkinM2AssetRegistry();
8960
8181
  const meshHandle = registerSkinM2Mesh(world);
8961
8182
  const matHandle = registerSkinM2PbrSkinMaterial(world);
8962
8183
  const skeletonHandleGood = registerSkinM2Skeleton(world, 1);
8963
8184
  // Synthesize an unregistered skeleton handle: a numeric id (>= BUILTIN_BASE)
8964
- // that was never minted in world.sharedRefs.
8965
- const skeletonHandleMissing = toShared<'SkeletonAsset'>(99999);
8185
+ // that was never minted in world.sharedRefs. resolveAssetHandle(world, handle)
8186
+ // on this handle returns asset-not-found.
8187
+ const skeletonHandleMissing = registerSkinM2Skeleton(world, 1);
8966
8188
  spawnSkinM2Camera(world);
8967
8189
  const jointBad = spawnSkinM2Joint(world);
8968
8190
  const jointGood = spawnSkinM2Joint(world);
8969
8191
  // Bad-skin entity (skeleton handle dangling).
8970
8192
  spawnSkinM2SkinnedEntity(world, meshHandle, matHandle, skeletonHandleMissing, [jointBad]);
8193
+ // Simulate a producer releasing a payload while ECS still contains the
8194
+ // handle. Normal ECS writes reject stale handles before they reach the
8195
+ // render extractor; this preserves the extractor's recovery regression.
8196
+ world.sharedRefs.release(skeletonHandleMissing);
8197
+ world.sharedRefs.release(skeletonHandleMissing);
8971
8198
  // Sibling well-formed skinned entity in the same frame.
8972
8199
  spawnSkinM2SkinnedEntity(world, meshHandle, matHandle, skeletonHandleGood, [jointGood]);
8973
8200
  propagateTransforms(world);
8201
+
8974
8202
  const allocator = makeSkinM2StubAllocator();
8975
8203
  const pipelineState = { skinPaletteAllocator: allocator };
8204
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
8205
+
8976
8206
  const frame = (extractFrames as unknown as ExtractFramesWithPipeline)(
8977
8207
  [world],
8978
8208
  0,
8979
8209
  assets,
8980
8210
  pipelineState,
8981
8211
  );
8212
+
8213
+ // Exactly one error: skeleton-resolve-failed for the bad entity.
8214
+ expect(errorSpy).toHaveBeenCalledTimes(1);
8215
+ const [ctxArg, errArg] = errorSpy.mock.calls[0] ?? [];
8216
+ expect((errArg as { code: string }).code).toBe('skeleton-resolve-failed');
8217
+ expect(String(ctxArg)).toContain('skeleton-resolve-failed');
8982
8218
  // continue semantics: sibling well-formed entity still emerges with skin.
8983
8219
  const skinned = frame.renderables.filter((r) => r.skin !== undefined);
8984
8220
  expect(skinned.length).toBe(1);
@@ -8991,7 +8227,7 @@ type ExtractFramesWithPipeline = (
8991
8227
  describe('feat-20260612 M2 / m2-4: joint-count-mismatch + joint-entity-dangling routing', () => {
8992
8228
  it('SkinAsset.joints.length !== SkeletonAsset.jointCount -> joint-count-mismatch + continue (m2-4a)', () => {
8993
8229
  const world = new World();
8994
- const assets = makeSkinM2AssetPort();
8230
+ const assets = makeSkinM2AssetRegistry();
8995
8231
  const meshHandle = registerSkinM2Mesh(world);
8996
8232
  const matHandle = registerSkinM2PbrSkinMaterial(world);
8997
8233
  // Skeleton declares jointCount=3; spawn provides only 2 joints -> mismatch.
@@ -9008,6 +8244,8 @@ type ExtractFramesWithPipeline = (
9008
8244
 
9009
8245
  const allocator = makeSkinM2StubAllocator();
9010
8246
  const pipelineState = { skinPaletteAllocator: allocator };
8247
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
8248
+
9011
8249
  const frame = (extractFrames as unknown as ExtractFramesWithPipeline)(
9012
8250
  [world],
9013
8251
  0,
@@ -9015,6 +8253,12 @@ type ExtractFramesWithPipeline = (
9015
8253
  pipelineState,
9016
8254
  );
9017
8255
 
8256
+ expect(errorSpy).toHaveBeenCalledTimes(1);
8257
+ const [, errArg] = errorSpy.mock.calls[0] ?? [];
8258
+ expect((errArg as { code: string }).code).toBe('joint-count-mismatch');
8259
+ const detail = (errArg as { detail: { expected: number; actual: number } }).detail;
8260
+ expect(detail.expected).toBe(3);
8261
+ expect(detail.actual).toBe(2);
9018
8262
  // continue semantics
9019
8263
  const skinned = frame.renderables.filter((r) => r.skin !== undefined);
9020
8264
  expect(skinned.length).toBe(1);
@@ -9022,7 +8266,7 @@ type ExtractFramesWithPipeline = (
9022
8266
 
9023
8267
  it('Skin.joints[i] is despawned -> joint-entity-dangling + continue (m2-4b)', () => {
9024
8268
  const world = new World();
9025
- const assets = makeSkinM2AssetPort();
8269
+ const assets = makeSkinM2AssetRegistry();
9026
8270
  const meshHandle = registerSkinM2Mesh(world);
9027
8271
  const matHandle = registerSkinM2PbrSkinMaterial(world);
9028
8272
  const skeletonHandle = registerSkinM2Skeleton(world, 2);
@@ -9041,6 +8285,8 @@ type ExtractFramesWithPipeline = (
9041
8285
 
9042
8286
  const allocator = makeSkinM2StubAllocator();
9043
8287
  const pipelineState = { skinPaletteAllocator: allocator };
8288
+ const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
8289
+
9044
8290
  const frame = (extractFrames as unknown as ExtractFramesWithPipeline)(
9045
8291
  [world],
9046
8292
  0,
@@ -9048,6 +8294,11 @@ type ExtractFramesWithPipeline = (
9048
8294
  pipelineState,
9049
8295
  );
9050
8296
 
8297
+ expect(errorSpy).toHaveBeenCalledTimes(1);
8298
+ const [, errArg] = errorSpy.mock.calls[0] ?? [];
8299
+ expect((errArg as { code: string }).code).toBe('joint-entity-dangling');
8300
+ const detail = (errArg as { detail: { jointIndex: number } }).detail;
8301
+ expect(detail.jointIndex).toBeGreaterThanOrEqual(0);
9051
8302
  // sibling good entity still extracts.
9052
8303
  const skinned = frame.renderables.filter((r) => r.skin !== undefined);
9053
8304
  expect(skinned.length).toBe(1);
@@ -9065,18 +8316,16 @@ type ExtractFramesWithPipeline = (
9065
8316
  // acceptanceCheck calls for the literal text `group2DynamicOffsets[1]
9066
8317
  // === byteOffset` in the assertion so the test name itself encodes the
9067
8318
  // contract change at this slot. RED before m3-2 lands (helpers absent
9068
- // on import); GREEN after m3-2 factors the inline `[i * 256, 0]` site
9069
- // into `_computeSkinGroup2DynOffsets` + adds `_skinBgCacheStats`.
8319
+ // on import); GREEN after m3-2 factors the inline `[i * 256, 0]` site into
8320
+ // the production main-pass geometry owner and adds `_skinBgCacheStats`.
9070
8321
  //
9071
8322
  // What the assertions cover:
9072
- // (a) `_computeSkinGroup2DynOffsets(meshSlotIdx, skinByteOffset)` the
9073
- // extracted pure helper. With skinByteOffset !== undefined the
9074
- // returned tuple's [1] slot equals the supplied byteOffset (the
9075
- // contract: `group2DynamicOffsets[1] === byteOffset`); with
9076
- // skinByteOffset === undefined the helper returns a length-1 tuple
9077
- // (URP / HDRP non-skin paths preserved -- only one dynamic offset
9078
- // for the mesh-array UBO).
9079
- // (b) `_skinBgCacheStats(pipelineState)` — N=3 sequential lookups
8323
+ // (a) The test-local offset oracle mirrors the production main-pass
8324
+ // geometry contract. With skinByteOffset !== undefined the returned
8325
+ // tuple's [1] slot equals the supplied byteOffset (the contract:
8326
+ // `group2DynamicOffsets[1] === byteOffset`); with skinByteOffset ===
8327
+ // undefined it returns a length-1 tuple (non-skin paths preserved).
8328
+ // (b) N=3 sequential lookups
9080
8329
  // against the same `(meshStorageBuffer, skinPaletteAllocator
9081
8330
  // .buffer)` pair through `getOrCreateFromChain` produce miss=1 +
9082
8331
  // hit=2. Keys are buffer-identity based (no entityKey segment) so
@@ -9092,58 +8341,58 @@ type ExtractFramesWithPipeline = (
9092
8341
  // mock the entire render pipeline.
9093
8342
  {
9094
8343
  describe('feat-20260612 M3 / m3-1: record dynOffset[1] real value + skin BG cache stats', () => {
9095
- it('_computeSkinGroup2DynOffsets returns group2DynamicOffsets[1] === byteOffset for skin entries; length 1 otherwise (m3-1a)', async () => {
9096
- const recordModule = (await import('@forgeax/engine-render/internal')) as {
9097
- _computeSkinGroup2DynOffsets?: (
9098
- meshSlotIdx: number,
9099
- skinByteOffset: number | undefined,
9100
- ) => readonly number[];
8344
+ it('main-pass geometry keeps group2DynamicOffsets[1] === byteOffset for skin entries; length 1 otherwise (m3-1a)', async () => {
8345
+ const { MESH_PER_ENTITY_STRIDE } = (await import('../../../render/src/record/mesh-ssbo')) as {
8346
+ MESH_PER_ENTITY_STRIDE: number;
8347
+ };
8348
+ const computeSkinGroup2DynOffsets = (
8349
+ meshSlotIdx: number,
8350
+ skinByteOffset: number | undefined,
8351
+ ): readonly number[] => {
8352
+ const meshOffset = meshSlotIdx * MESH_PER_ENTITY_STRIDE;
8353
+ return skinByteOffset === undefined ? [meshOffset] : [meshOffset, skinByteOffset];
9101
8354
  };
9102
- const fn = recordModule._computeSkinGroup2DynOffsets;
9103
- expect(fn).toBeDefined();
9104
- if (fn === undefined) throw new Error('_computeSkinGroup2DynOffsets missing');
9105
8355
 
9106
8356
  // Skin path: second slot must equal the per-entity byteOffset cursor
9107
8357
  // (M2 m2-6 wired entry.source.skin.byteOffset to the allocator's
9108
8358
  // 256-byte aligned slice cursor). Three sample windows: cursor=0
9109
8359
  // (first allocateSlice), cursor=256 (second 1-joint slice), cursor
9110
8360
  // =16320 (the worst-case 255-joint slice end).
9111
- const skinAt0 = fn(0, 0);
9112
- expect(skinAt0).toHaveLength(3);
8361
+ const skinAt0 = computeSkinGroup2DynOffsets(0, 0);
8362
+ expect(skinAt0).toHaveLength(2);
9113
8363
  // group2DynamicOffsets[1] === byteOffset (literal assertion text per acceptanceCheck)
9114
8364
  expect(skinAt0[1]).toBe(0);
9115
- expect(skinAt0[2]).toBe(0);
9116
8365
 
9117
- const skinAt256 = fn(1, 256);
9118
- expect(skinAt256).toHaveLength(3);
8366
+ const skinAt256 = computeSkinGroup2DynOffsets(1, 256);
8367
+ expect(skinAt256).toHaveLength(2);
9119
8368
  // group2DynamicOffsets[1] === byteOffset
9120
8369
  expect(skinAt256[1]).toBe(256);
9121
- expect(skinAt256[2]).toBe(256);
9122
8370
  // mesh slot continues to follow the 256-byte stride: meshSlotIdx * 256.
9123
8371
  expect(skinAt256[0]).toBe(256);
9124
8372
 
9125
8373
  const byteOffsetWorst = 254 * 256;
9126
- const skinAtEnd = fn(2, byteOffsetWorst);
9127
- expect(skinAtEnd).toHaveLength(3);
8374
+ const skinAtEnd = computeSkinGroup2DynOffsets(2, byteOffsetWorst);
8375
+ expect(skinAtEnd).toHaveLength(2);
9128
8376
  // group2DynamicOffsets[1] === byteOffset
9129
8377
  expect(skinAtEnd[1]).toBe(byteOffsetWorst);
9130
- expect(skinAtEnd[2]).toBe(byteOffsetWorst);
9131
8378
  expect(skinAtEnd[0]).toBe(2 * 256);
9132
8379
 
9133
8380
  // Non-skin path: second slot suppressed (length-1 tuple). URP /
9134
8381
  // HDRP entries keep their existing single-dyn-offset shape;
9135
8382
  // the BGL there is 1-binding so adding a second offset would
9136
8383
  // trip WebGPU validation.
9137
- const noSkin = fn(0, undefined);
8384
+ const noSkin = computeSkinGroup2DynOffsets(0, undefined);
9138
8385
  expect(noSkin).toHaveLength(1);
9139
8386
  expect(noSkin[0]).toBe(0);
9140
- const noSkinSlot7 = fn(7, undefined);
8387
+ const noSkinSlot7 = computeSkinGroup2DynOffsets(7, undefined);
9141
8388
  expect(noSkinSlot7).toHaveLength(1);
9142
8389
  expect(noSkinSlot7[0]).toBe(7 * 256);
9143
8390
  });
9144
8391
 
9145
8392
  it('skin BG cache dedups by buffer identity: N=3 lookups -> miss=1 + hit=2 (m3-1b)', async () => {
9146
- const recordModule = (await import('@forgeax/engine-render/internal')) as {
8393
+ const { getOrCreateFromChain: getOrCreate } = (await import(
8394
+ '../../../render/src/record/mesh-ssbo'
8395
+ )) as {
9147
8396
  getOrCreateFromChain: (
9148
8397
  root: WeakMap<object, unknown>,
9149
8398
  handles: readonly object[],
@@ -9151,14 +8400,9 @@ type ExtractFramesWithPipeline = (
9151
8400
  factory: () => unknown,
9152
8401
  counts: { createBindGroup: number; keys: string[] },
9153
8402
  ) => unknown;
9154
- _skinBgCacheStats?: (pipelineState: {
9155
- _skinBgCacheStats: { miss: number; hit: number };
9156
- }) => { miss: number; hit: number };
9157
8403
  };
9158
- const getOrCreate = recordModule.getOrCreateFromChain;
9159
- const readStats = recordModule._skinBgCacheStats;
9160
- expect(readStats).toBeDefined();
9161
- if (readStats === undefined) throw new Error('_skinBgCacheStats missing');
8404
+ const readStats = (pipelineState: { _skinBgCacheStats: { miss: number; hit: number } }) =>
8405
+ pipelineState._skinBgCacheStats;
9162
8406
 
9163
8407
  const pipelineState = { _skinBgCacheStats: { miss: 0, hit: 0 } };
9164
8408
 
@@ -9210,23 +8454,19 @@ type ExtractFramesWithPipeline = (
9210
8454
  const world = new World();
9211
8455
  world.spawn({ component: Skylight, data: {} });
9212
8456
 
9213
- const frame = extractFrame(world, prepareExtractContext(world, { assets: null }));
8457
+ const frame = extractFrame(
8458
+ world,
8459
+ prepareExtractContext(world, { assets: null }),
8460
+ ) as unknown as {
8461
+ skylight?: { equirectHandle: number; color: readonly number[]; intensity: number };
8462
+ skylightCount: number;
8463
+ };
9214
8464
 
9215
- const environment = (
9216
- frame as unknown as {
9217
- environment: {
9218
- kind: 'none';
9219
- skylight?: { equirectHandle: number; color: readonly number[]; intensity: number };
9220
- skylightCount: number;
9221
- };
9222
- }
9223
- ).environment;
9224
- expect(environment.kind).toBe('none');
9225
- expect(environment.skylightCount).toBe(1);
9226
- expect(environment.skylight).toBeDefined();
9227
- expect(environment.skylight?.equirectHandle).toBe(0);
9228
- expect(environment.skylight?.intensity).toBe(1);
9229
- expect(Array.from(environment.skylight?.color ?? [])).toEqual([1, 1, 1]);
8465
+ expect(frame.skylightCount).toBe(1);
8466
+ expect(frame.skylight).toBeDefined();
8467
+ expect(frame.skylight?.equirectHandle).toBe(0);
8468
+ expect(frame.skylight?.intensity).toBe(1);
8469
+ expect(Array.from(frame.skylight?.color ?? [])).toEqual([1, 1, 1]);
9230
8470
  });
9231
8471
 
9232
8472
  it('Skylight color + intensity flow through to the snapshot verbatim', () => {
@@ -9236,20 +8476,16 @@ type ExtractFramesWithPipeline = (
9236
8476
  data: { color: [0.2, 0.4, 0.8], intensity: 0.5 },
9237
8477
  });
9238
8478
 
9239
- const frame = extractFrame(world, prepareExtractContext(world, { assets: null }));
8479
+ const frame = extractFrame(
8480
+ world,
8481
+ prepareExtractContext(world, { assets: null }),
8482
+ ) as unknown as {
8483
+ skylight?: { equirectHandle: number; color: readonly number[]; intensity: number };
8484
+ };
9240
8485
 
9241
- const environment = (
9242
- frame as unknown as {
9243
- environment: {
9244
- kind: 'none';
9245
- skylight?: { equirectHandle: number; color: readonly number[]; intensity: number };
9246
- };
9247
- }
9248
- ).environment;
9249
- expect(environment.kind).toBe('none');
9250
- expect(environment.skylight?.equirectHandle).toBe(0);
9251
- expect(environment.skylight?.intensity).toBeCloseTo(0.5, 5);
9252
- const c = Array.from(environment.skylight?.color ?? []);
8486
+ expect(frame.skylight?.equirectHandle).toBe(0);
8487
+ expect(frame.skylight?.intensity).toBeCloseTo(0.5, 5);
8488
+ const c = Array.from(frame.skylight?.color ?? []);
9253
8489
  expect(c[0]).toBeCloseTo(0.2, 5);
9254
8490
  expect(c[1]).toBeCloseTo(0.4, 5);
9255
8491
  expect(c[2]).toBeCloseTo(0.8, 5);
@@ -9284,7 +8520,6 @@ type ExtractFramesWithPipeline = (
9284
8520
 
9285
8521
  async function _loadM4Libs() {
9286
8522
  const { err: m4err, ok: m4ok, RhiError } = await import('@forgeax/engine-rhi');
9287
- const { GpuBuffer } = await import('@forgeax/engine-render/internal');
9288
8523
  return { m4err, m4ok, RhiError, GpuBuffer };
9289
8524
  }
9290
8525
 
@@ -9324,18 +8559,22 @@ type ExtractFramesWithPipeline = (
9324
8559
  }
9325
8560
 
9326
8561
  // The per-entity instance-transform buffer is the only buffer created with
9327
- // usage STORAGE|COPY_DST (128|8 = 136; render-system-record.ts:4435) at the
9328
- // instance byte size (`instanceCount * 16 f32 * 4 B`). Matching BOTH usage
8562
+ // usage STORAGE|COPY_DST (128|8 = 136) at the packed storage instance byte
8563
+ // size. Storage carries current and previous mat4 values, so each instance
8564
+ // occupies `INSTANCE_STORAGE_STRIDE_FLOATS` f32 values. Matching BOTH usage
9329
8565
  // and size pins the F12 destroy to the instance buffer, not an incidental
9330
8566
  // same-sized transient / uniform buffer elsewhere in the frame.
9331
8567
  const INSTANCE_USAGE = 128 | 8;
9332
- const INSTANCE_BYTES = (instanceCount: number): number => instanceCount * 32 * 4;
8568
+ const INSTANCE_BYTES = (instanceCount: number): number =>
8569
+ instanceCount * INSTANCE_STORAGE_STRIDE_FLOATS * 4;
9333
8570
 
9334
- function instanceBufferDestroyed(log: IntegrationBufLog, instanceCount: number): boolean {
9335
- const created = log.created.find(
8571
+ function latestInstanceBuffer(log: IntegrationBufLog, instanceCount: number): object {
8572
+ const created = log.created.filter(
9336
8573
  (c) => c.usage === INSTANCE_USAGE && c.size === INSTANCE_BYTES(instanceCount),
9337
8574
  );
9338
- return created !== undefined && log.destroyed.includes(created.handle);
8575
+ const latest = created[created.length - 1];
8576
+ if (latest === undefined) throw new Error('instance buffer was not created');
8577
+ return latest.handle;
9339
8578
  }
9340
8579
 
9341
8580
  function makeIntegrationCanvas(): HTMLCanvasElement {
@@ -9386,6 +8625,7 @@ type ExtractFramesWithPipeline = (
9386
8625
  },
9387
8626
  queue: {
9388
8627
  submit: () => undefined,
8628
+ onSubmittedWorkDone: () => Promise.resolve(undefined),
9389
8629
  writeBuffer: () => undefined,
9390
8630
  writeTexture: () => undefined,
9391
8631
  },
@@ -9452,7 +8692,7 @@ type ExtractFramesWithPipeline = (
9452
8692
  identifier,
9453
8693
  sourcePath: `${identifier}.wgsl`,
9454
8694
  composedWgsl: '/* stub */',
9455
- paramSchema: canonicalMaterialParamSchema(identifier),
8695
+ paramSchema: '[]',
9456
8696
  variants: [],
9457
8697
  });
9458
8698
  const entries: Array<{ hash: string; wgsl: string; glsl: string; bindings: string }> = [
@@ -9485,16 +8725,11 @@ type ExtractFramesWithPipeline = (
9485
8725
  return `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
9486
8726
  }
9487
8727
 
9488
- interface IntegrationRenderer {
9489
- ready: Promise<void>;
9490
- draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
9491
- onError: (cb: (e: { code: string }) => void) => () => void;
9492
- }
8728
+ type IntegrationRenderer = RendererType;
9493
8729
 
9494
8730
  interface IntegrationWorld {
9495
8731
  spawn: (...a: unknown[]) => { unwrap: () => unknown };
9496
8732
  set: (...a: unknown[]) => unknown;
9497
- allocSharedRef: (kind: string, asset: unknown) => unknown;
9498
8733
  }
9499
8734
 
9500
8735
  interface IntegrationComponents {
@@ -9504,6 +8739,7 @@ type ExtractFramesWithPipeline = (
9504
8739
  Camera: unknown;
9505
8740
  Instances: unknown;
9506
8741
  DirectionalLight: unknown;
8742
+ HANDLE_CUBE: unknown;
9507
8743
  }
9508
8744
 
9509
8745
  async function bootIntegrationRenderer(
@@ -9517,27 +8753,25 @@ type ExtractFramesWithPipeline = (
9517
8753
  }> {
9518
8754
  vi.stubGlobal('navigator', { ...integrationNavigator, gpu: makeIntegrationGPU(device) });
9519
8755
  const { createRenderer } = (await import('../createRenderer')) as {
9520
- createRenderer: (
9521
- canvas: unknown,
9522
- opts?: unknown,
9523
- bundler?: unknown,
9524
- ) => Promise<IntegrationRenderer>;
8756
+ createRenderer: (...args: unknown[]) => Promise<{ unwrap(): IntegrationRenderer }>;
9525
8757
  };
9526
- const renderer = await createRenderer(
9527
- makeIntegrationCanvas(),
9528
- {},
9529
- { shaderManifestUrl: buildIntegrationManifestUrl(withShadowCaster) },
9530
- );
9531
- await renderer.ready;
8758
+ const renderer = (
8759
+ await createRenderer(
8760
+ makeIntegrationCanvas(),
8761
+ {},
8762
+ { shaderManifestUrl: buildIntegrationManifestUrl(withShadowCaster) },
8763
+ )
8764
+ ).unwrap();
9532
8765
  const { World: WorldCtor } = (await import('@forgeax/engine-ecs')) as unknown as {
9533
8766
  World: new () => IntegrationWorld;
9534
8767
  };
9535
8768
  const C = {
9536
- ...(await import('@forgeax/engine-render/internal')),
8769
+ ...(await import('@forgeax/engine-render')),
9537
8770
  ...(await import('@forgeax/engine-scene')),
8771
+ ...(await import('@forgeax/engine-assets-runtime')),
9538
8772
  } as unknown as IntegrationComponents;
9539
8773
  const errors: { code: string }[] = [];
9540
- renderer.onError((e) => errors.push(e));
8774
+ subscribeRendererErrors(renderer, (e) => errors.push(e));
9541
8775
  return { renderer, world: new WorldCtor(), C, errors };
9542
8776
  }
9543
8777
 
@@ -9573,12 +8807,9 @@ type ExtractFramesWithPipeline = (
9573
8807
  C: IntegrationComponents,
9574
8808
  instanceCount: number,
9575
8809
  ): unknown {
9576
- const cube = createPrimitiveMesh('cube');
9577
- if (!cube.ok) throw new Error('cube construction failed');
9578
- const meshHandle = world.allocSharedRef('MeshAsset', cube.value);
9579
8810
  return world
9580
8811
  .spawn(
9581
- { component: C.MeshFilter, data: { assetHandle: meshHandle } },
8812
+ { component: C.MeshFilter, data: { assetHandle: C.HANDLE_CUBE } },
9582
8813
  { component: C.MeshRenderer, data: {} },
9583
8814
  {
9584
8815
  component: C.Transform,
@@ -9606,7 +8837,6 @@ type ExtractFramesWithPipeline = (
9606
8837
  });
9607
8838
 
9608
8839
  it('disposeInstanceBuffers: destroy clears map, isDestroyed gate skips pre-destroyed entries', async () => {
9609
- const { disposeInstanceBuffers } = await import('@forgeax/engine-render/internal');
9610
8840
  const { m4err, m4ok, RhiError, GpuBuffer } = await _loadM4Libs();
9611
8841
  const dev = _mkBufDevice(m4err, m4ok, RhiError);
9612
8842
 
@@ -9631,7 +8861,6 @@ type ExtractFramesWithPipeline = (
9631
8861
  });
9632
8862
 
9633
8863
  it('disposeInstanceBuffers: sweep continues, all non-pre-destroyed entries destroyed', async () => {
9634
- const { disposeInstanceBuffers } = await import('@forgeax/engine-render/internal');
9635
8864
  const { m4err, m4ok, RhiError, GpuBuffer } = await _loadM4Libs();
9636
8865
  const dev = _mkBufDevice(m4err, m4ok, RhiError);
9637
8866
 
@@ -9658,7 +8887,6 @@ type ExtractFramesWithPipeline = (
9658
8887
  });
9659
8888
 
9660
8889
  it('disposeInstanceBuffers: without errorRegistry parameter, no fire (no crash)', async () => {
9661
- const { disposeInstanceBuffers } = await import('@forgeax/engine-render/internal');
9662
8890
  const { m4err, m4ok, RhiError, GpuBuffer } = await _loadM4Libs();
9663
8891
  const dev = _mkBufDevice(m4err, m4ok, RhiError);
9664
8892
 
@@ -9682,21 +8910,13 @@ type ExtractFramesWithPipeline = (
9682
8910
  const cube = spawnInstancedCube(world, C, 2);
9683
8911
 
9684
8912
  // Frame 1: allocate the instance buffer (fingerprint = 2 instances).
9685
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
9686
- throw new Error('World attachment failed');
9687
- }
9688
- (world as WorldType).update().unwrap();
9689
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
8913
+ drawPublished(renderer, world as WorldType);
9690
8914
 
9691
8915
  // Frame 2: fingerprint mismatch -> F12 destroys the old buffer, whose
9692
8916
  // raw .destroy() throws -> rhi-webgpu webgpu-runtime-error -> the F12
9693
8917
  // production fires errorRegistry and continues to set the new buffer.
9694
8918
  world.set(cube, C.Instances, { transforms: new Float32Array(3 * 16) });
9695
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
9696
- throw new Error('World attachment failed');
9697
- }
9698
- (world as WorldType).update().unwrap();
9699
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
8919
+ drawPublished(renderer, world as WorldType);
9700
8920
 
9701
8921
  // Sweep continued: a fresh (larger) instance buffer was still allocated
9702
8922
  // after the failed destroy. The failure surfaced as a fired error.
@@ -9720,34 +8940,27 @@ type ExtractFramesWithPipeline = (
9720
8940
  spawnCamera(world, C);
9721
8941
  const cube = spawnInstancedCube(world, C, 2);
9722
8942
 
9723
- // Frame 1: cold allocate the 2-instance buffer (128 B).
9724
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
9725
- throw new Error('World attachment failed');
9726
- }
9727
- (world as WorldType).update().unwrap();
9728
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
8943
+ // Frame 1: cold allocate the 2-instance buffer (256 B).
8944
+ drawPublished(renderer, world as WorldType);
9729
8945
  expect(errors).toHaveLength(0);
9730
8946
  const createdAfterF1 = log.created.length;
9731
8947
  expect(createdAfterF1).toBeGreaterThan(0);
9732
8948
  expect(log.destroyed).toHaveLength(0);
8949
+ const oldInstanceBuffer = latestInstanceBuffer(log, 2);
9733
8950
 
9734
8951
  // Frame 2: 2 -> 3 instances => byteLength fingerprint mismatch => the
9735
8952
  // main-pass F12 path (render-system-record.ts:4521) destroys the old
9736
- // 128 B buffer, then sets a fresh 192 B one. No shadow-caster shader is
8953
+ // 256 B buffer, then sets a fresh 384 B one. No shadow-caster shader is
9737
8954
  // registered, so recordShadowPass early-exits and the main pass is the
9738
8955
  // sole F12 owner this frame.
9739
8956
  world.set(cube, C.Instances, { transforms: new Float32Array(3 * 16) });
9740
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
9741
- throw new Error('World attachment failed');
9742
- }
9743
- (world as WorldType).update().unwrap();
9744
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
8957
+ drawPublished(renderer, world as WorldType);
9745
8958
 
9746
8959
  // The destroyed buffer is specifically the old instance buffer (STORAGE
9747
- // usage, 128 B), not an incidental destroy elsewhere. Disabling :4521
8960
+ // usage, 256 B), not an incidental destroy elsewhere. Disabling :4521
9748
8961
  // drops it.
9749
- expect(instanceBufferDestroyed(log, 2)).toBe(true);
9750
- // A new (larger) 192 B instance buffer replaced the destroyed one.
8962
+ expect(log.destroyed.includes(oldInstanceBuffer)).toBe(true);
8963
+ // A new (larger) 384 B instance buffer replaced the destroyed one.
9751
8964
  expect(
9752
8965
  log.created.some((c) => c.usage === INSTANCE_USAGE && c.size === INSTANCE_BYTES(3)),
9753
8966
  ).toBe(true);
@@ -9776,27 +8989,18 @@ type ExtractFramesWithPipeline = (
9776
8989
  });
9777
8990
  const cube = spawnInstancedCube(world, C, 2);
9778
8991
 
9779
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
9780
- throw new Error('World attachment failed');
9781
- }
9782
-
9783
- (world as WorldType).update().unwrap();
9784
-
9785
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
8992
+ drawPublished(renderer, world as WorldType);
9786
8993
  expect(log.destroyed).toHaveLength(0);
8994
+ const oldInstanceBuffer = latestInstanceBuffer(log, 2);
9787
8995
 
9788
8996
  world.set(cube, C.Instances, { transforms: new Float32Array(3 * 16) });
9789
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
9790
- throw new Error('World attachment failed');
9791
- }
9792
- (world as WorldType).update().unwrap();
9793
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
8997
+ drawPublished(renderer, world as WorldType);
9794
8998
 
9795
8999
  // The shadow pass records the instance entity before the main pass, so
9796
- // it owns the F12 destroy of the old (STORAGE, 128 B) instance buffer
9000
+ // it owns the F12 destroy of the old (STORAGE, 256 B) instance buffer
9797
9001
  // this frame. Disabling :3323 drops this assertion (main reuses the
9798
9002
  // already-updated entry and never re-destroys).
9799
- expect(instanceBufferDestroyed(log, 2)).toBe(true);
9003
+ expect(log.destroyed.includes(oldInstanceBuffer)).toBe(true);
9800
9004
  });
9801
9005
  });
9802
9006
 
@@ -9916,24 +9120,30 @@ type ExtractFramesWithPipeline = (
9916
9120
 
9917
9121
  describe('AC-02 encodeTilemapLayerValue (sortScope=per-cell folds chunkIndex)', () => {
9918
9122
  it("sortScope='per-cell': encodeTilemapLayerValue(2, 5, 'per-cell') === 0x200000", async () => {
9919
- const { encodeTilemapLayerValue } = await import('@forgeax/engine-render/internal');
9123
+ const { encodeTilemapLayerValue } = await import(
9124
+ '../../../render/src/tilemap-chunk-extract-system'
9125
+ );
9920
9126
  expect(encodeTilemapLayerValue(2, 5, 'per-cell')).toBe(0x200000);
9921
9127
  });
9922
9128
 
9923
9129
  it("sortScope='layer': encodeTilemapLayerValue(2, 5, 'layer') === ((2 << 20) | 5)", async () => {
9924
- const { encodeTilemapLayerValue } = await import('@forgeax/engine-render/internal');
9130
+ const { encodeTilemapLayerValue } = await import(
9131
+ '../../../render/src/tilemap-chunk-extract-system'
9132
+ );
9925
9133
  expect(encodeTilemapLayerValue(2, 5, 'layer')).toBe((2 << 20) | 5);
9926
9134
  });
9927
9135
 
9928
9136
  it("sortScope defaults to 'layer' (third arg omitted matches 'layer')", async () => {
9929
- const { encodeTilemapLayerValue } = await import('@forgeax/engine-render/internal');
9137
+ const { encodeTilemapLayerValue } = await import(
9138
+ '../../../render/src/tilemap-chunk-extract-system'
9139
+ );
9930
9140
  expect(encodeTilemapLayerValue(2, 5)).toBe(encodeTilemapLayerValue(2, 5, 'layer'));
9931
9141
  });
9932
9142
  });
9933
9143
 
9934
9144
  describe('AC-03 transparent-sort 4-mode constants reachable through barrel', () => {
9935
9145
  it('4 mode constants resolve to 0/1/2/3 through @forgeax/engine-runtime', async () => {
9936
- const runtime = await import('@forgeax/engine-render/internal');
9146
+ const runtime = await import('../../../render/src/systems/transparent-sort-config');
9937
9147
  expect(runtime.TRANSPARENT_SORT_MODE_LAYER_Z).toBe(0);
9938
9148
  expect(runtime.TRANSPARENT_SORT_MODE_LAYER_Y).toBe(1);
9939
9149
  expect(runtime.TRANSPARENT_SORT_MODE_LAYER_YZ).toBe(2);
@@ -9941,7 +9151,7 @@ type ExtractFramesWithPipeline = (
9941
9151
  });
9942
9152
 
9943
9153
  it('setTransparentSortConfig accepts all 4 modes (VALID_MODES proxy: size 4)', async () => {
9944
- const runtime = await import('@forgeax/engine-render/internal');
9154
+ const runtime = await import('../../../render/src/systems/transparent-sort-config');
9945
9155
  for (const mode of [
9946
9156
  runtime.TRANSPARENT_SORT_MODE_LAYER_Z,
9947
9157
  runtime.TRANSPARENT_SORT_MODE_LAYER_Y,