@forgeax/engine-runtime 0.1.3 → 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
@@ -10,7 +10,7 @@
10
10
  // - packages/runtime/src/__tests__/createRenderer.test.ts
11
11
  // - packages/runtime/src/__tests__/dispatch-sort.test.ts
12
12
  // - packages/runtime/src/__tests__/engine-metrics.test.ts
13
- // - packages/runtime/src/__tests__/gpu-resource-store-caps-guard.test.ts
13
+ // - packages/runtime/src/__tests__/device/gpu-residency-caps-guard.test.ts
14
14
  // - packages/runtime/src/__tests__/pass-selector.test.ts
15
15
  // - packages/runtime/src/__tests__/pipeline-builder.test.ts
16
16
  // - packages/runtime/src/__tests__/pipeline-cache-key-topology.test.ts
@@ -27,7 +27,7 @@
27
27
  // - packages/runtime/src/__tests__/renderer-ready.test.ts
28
28
  // - packages/runtime/src/__tests__/renderstate-pipeline-cache.test.ts
29
29
  // - packages/runtime/src/__tests__/storage-buffer-caps.test.ts
30
- // - packages/runtime/src/__tests__/gpu-resource-store.test.ts
30
+ // - packages/runtime/src/__tests__/device/gpu-residency.test.ts
31
31
  // - packages/runtime/src/__tests__/render-data.test.ts
32
32
  // - packages/runtime/src/__tests__/hdrp-bgl-slots.test.ts
33
33
  // - packages/runtime/src/__tests__/hdrp-caps-gate.test.ts
@@ -46,58 +46,22 @@
46
46
  import { existsSync, readFileSync } from 'node:fs';
47
47
  import { dirname, resolve } from 'node:path';
48
48
  import { fileURLToPath } from 'node:url';
49
+ import { HANDLE_CUBE, HANDLE_TRIANGLE, resolveAssetHandle } from '@forgeax/engine-assets-runtime';
49
50
  import type { World as WorldType } from '@forgeax/engine-ecs';
50
51
  import { defineComponent, World } from '@forgeax/engine-ecs';
51
52
  import {
53
+ buildMeshAttributeMapForUvSets,
52
54
  createBoxGeometry,
53
55
  createConeGeometry,
54
56
  createCylinderGeometry,
55
57
  createPlaneGeometry,
56
- createPrimitiveMesh,
57
58
  createSphereGeometry,
58
59
  createTorusGeometry,
59
60
  PROCEDURAL_FLOATS_PER_VERTEX,
60
61
  } from '@forgeax/engine-geometry';
61
- import {
62
- createInputSnapshot,
63
- INPUT_SNAPSHOT_RESOURCE_KEY,
64
- type InputSnapshot,
65
- } from '@forgeax/engine-input';
66
62
  import { type Mat4, mat4, type Vec3, vec3 } from '@forgeax/engine-math';
67
63
  import type { Renderer as RendererType } from '@forgeax/engine-render';
68
- import {
69
- assertStorageBufferCap,
70
- bin,
71
- buildPbrPipelineLayouts,
72
- buildPbrViewBglEntries,
73
- buildPipelineForMaterialShader,
74
- Camera,
75
- type ClusterBinError,
76
- cacheKeyOf,
77
- calculateSphereClusterBounds,
78
- clusterSpaceObjectAabb,
79
- createEngineMetrics,
80
- createHdrpBindGroupLayoutDescriptor,
81
- deriveCullingRadius,
82
- deriveRenderDataCubemap,
83
- deriveRenderDataMesh,
84
- deriveRenderDataTexture,
85
- extractFrame,
86
- GpuResourceStore,
87
- HdrpInstallError,
88
- MeshFilter,
89
- MeshRenderer,
90
- matchPass,
91
- ndcPositionToCluster,
92
- type PbrCaps,
93
- type PipelineBuilderContext,
94
- type PipelineBuilderShaderModuleFactory,
95
- type PipelineSpec,
96
- prepareExtractContext,
97
- sortDispatchByQueue,
98
- validateClusterGrid,
99
- viewZToZSlice,
100
- } from '@forgeax/engine-render/internal';
64
+ import { Camera, MeshFilter, MeshRenderer } from '@forgeax/engine-render';
101
65
  import {
102
66
  err,
103
67
  ok,
@@ -117,8 +81,6 @@ import {
117
81
  } from '@forgeax/engine-rhi-null';
118
82
  import { ChildOf, Transform } from '@forgeax/engine-scene';
119
83
  import {
120
- DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
121
- DEFAULT_UNLIT_PARAM_SCHEMA,
122
84
  findVariantByKey,
123
85
  type MaterialShaderEntry,
124
86
  type MaterialShaderManifestEntry,
@@ -133,26 +95,49 @@ import {
133
95
  type PassSelector,
134
96
  type PrimitiveTopology,
135
97
  type RenderPipelineAsset,
136
- type StencilFaceState,
137
98
  type TextureAsset,
138
99
  toShared,
139
100
  } from '@forgeax/engine-types';
140
101
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
141
102
  import {
142
- canonicalDefaultMaterialVariants,
143
- makeMockShaderRegistry,
144
- } from './helpers/mock-shader-registry';
145
- import { internPrimitiveMesh } from './helpers/primitive-world';
146
-
147
- function canonicalMaterialParamSchema(identifier: string): string {
148
- if (identifier === 'forgeax::default-standard-pbr') {
149
- return JSON.stringify(DEFAULT_STANDARD_PBR_PARAM_SCHEMA);
150
- }
151
- if (identifier === 'forgeax::default-unlit') {
152
- return JSON.stringify(DEFAULT_UNLIT_PARAM_SCHEMA);
153
- }
154
- return '[]';
155
- }
103
+ bin,
104
+ type ClusterBinError,
105
+ calculateSphereClusterBounds,
106
+ clusterSpaceObjectAabb,
107
+ deriveCullingRadius,
108
+ ndcPositionToCluster,
109
+ viewZToZSlice,
110
+ } from '../../../render/src/cluster-binner';
111
+ import { GpuResidencyCache } from '../../../render/src/device/gpu-residency';
112
+ import { createEngineMetrics } from '../../../render/src/engine-metrics';
113
+ import { assertStorageBufferCap } from '../../../render/src/light-buffer-layout';
114
+ import {
115
+ buildPbrPipelineLayouts,
116
+ buildPbrViewBglEntries,
117
+ type PbrCaps,
118
+ } from '../../../render/src/pbr-pipeline';
119
+ import {
120
+ HdrpInstallError,
121
+ validateClusterGrid,
122
+ } from '../../../render/src/pipeline/standard-pipeline';
123
+ import {
124
+ buildPipelineForMaterialShader,
125
+ type PipelineBuilderContext,
126
+ type PipelineBuilderShaderModuleFactory,
127
+ } from '../../../render/src/pipeline-builder';
128
+ import {
129
+ cacheKeyOf,
130
+ createHdrpBindGroupLayoutDescriptor,
131
+ type PipelineSpec,
132
+ } from '../../../render/src/pipeline-spec';
133
+ import {
134
+ deriveRenderDataCubemap,
135
+ deriveRenderDataMesh,
136
+ deriveRenderDataTexture,
137
+ } from '../../../render/src/render-data';
138
+ import { extractFrame, prepareExtractContext } from '../../../render/src/render-system-extract';
139
+ import { matchPass } from '../../../render/src/systems/pass-selector';
140
+ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
156
141
 
157
142
  function makeExplicitNullRhi(spies: {
158
143
  setIndexBuffer: ReturnType<typeof vi.fn>;
@@ -236,8 +221,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
236
221
 
237
222
  {
238
223
  // --- from create-renderer-fallback.test.ts ---
239
- const ENGINE = new URL('../../../render/src/renderer/renderer-factory.ts', import.meta.url).href;
240
- const ERRORS = new URL('../../../render/src/renderer/environment-error.ts', import.meta.url).href;
224
+ const _ENGINE = new URL('../../../render/src/assembly/factory.ts', import.meta.url).href;
225
+ const _ERRORS = new URL('../../../render/src/assembly/backend-contract.ts', import.meta.url).href;
241
226
 
242
227
  // ─── RhiError shape ──────────────────────────────────────────────────────────
243
228
 
@@ -250,7 +235,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
250
235
 
251
236
  // ─── Canvas mock ─────────────────────────────────────────────────────────────
252
237
 
253
- function makeMockCanvas(opts: { webgpu?: 'context' | 'null' } = {}): HTMLCanvasElement {
238
+ function _makeMockCanvas(opts: { webgpu?: 'context' | 'null' } = {}): HTMLCanvasElement {
254
239
  const canvas = {
255
240
  width: 800,
256
241
  height: 600,
@@ -315,7 +300,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
315
300
  return { device };
316
301
  }
317
302
 
318
- function makeMockChannel3Module(adapterCase: 'ok' | 'null'): {
303
+ function _makeMockChannel3Module(adapterCase: 'ok' | 'null'): {
319
304
  rhi: unknown;
320
305
  ensureReady: () => Promise<void>;
321
306
  } {
@@ -376,94 +361,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
376
361
  });
377
362
 
378
363
  // ─── Tests ───────────────────────────────────────────────────────────────────
379
-
380
- describe('createRenderer — Channel 3 integration (navigator.gpu completely absent)', () => {
381
- // ── AC-01: navigator.gpu absent, Channel 3 ok → createRenderer resolves ──
382
-
383
- it('AC-01: createRenderer resolves with Renderer when navigator.gpu is absent and Channel 3 works', async () => {
384
- // navigator.gpu completely absent (no gpu property)
385
- vi.stubGlobal('navigator', { ...baseNavigator });
386
- // Mock Channel 3 (rhi-wgpu) with a working backend (ok adapter)
387
- vi.resetModules();
388
- vi.doMock('@forgeax/engine-rhi-wgpu', () => makeMockChannel3Module('ok'));
389
-
390
- const canvas = makeMockCanvas({ webgpu: 'context' });
391
- const { createRenderer } = (await import(ENGINE)) as {
392
- createRenderer: (
393
- canvas: unknown,
394
- opts?: { shaderManifestUrl?: string | undefined },
395
- bundler?: unknown,
396
- ) => Promise<{ backend: string; draw: unknown }>;
397
- };
398
-
399
- const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
400
-
401
- // AC-01 assertion: renderer resolves successfully
402
- expect(renderer.backend).toBe('webgpu');
403
- // AC-01 assertion: draw is a function
404
- expect(typeof renderer.draw).toBe('function');
405
- // AC-01 assertion: device is populated
406
- const rendererAny = renderer as unknown as Record<string, unknown>;
407
- expect(rendererAny.device).toBeDefined();
408
- });
409
-
410
- // ── AC-03: Both channels fail → structured compound error ─────────────────
411
-
412
- it('AC-03: throws EngineEnvironmentError with structured webgpuError and wgpuError when both channels fail', async () => {
413
- // Channel 2: navigator.gpu completely absent (falls to Channel 3 directly
414
- // via loadBackendPack). Mock Channel 3 to fail.
415
- vi.stubGlobal('navigator', { ...baseNavigator });
416
- // Mock Channel 3 with a failing adapter
417
- vi.resetModules();
418
- vi.doMock('@forgeax/engine-rhi-wgpu', () => makeMockChannel3Module('null'));
419
-
420
- const canvas = makeMockCanvas({ webgpu: 'context' });
421
- const { createRenderer } = (await import(ENGINE)) as {
422
- createRenderer: (canvas: unknown) => Promise<unknown>;
423
- };
424
- const { EngineEnvironmentError } = (await import(ERRORS)) as {
425
- EngineEnvironmentError: new (...args: unknown[]) => Error;
426
- };
427
-
428
- try {
429
- await createRenderer(canvas);
430
- expect.fail('should have thrown EngineEnvironmentError');
431
- } catch (err: unknown) {
432
- // AC-03 assertion: error is an EngineEnvironmentError
433
- expect(err).toBeInstanceOf(EngineEnvironmentError);
434
-
435
- const e = err as {
436
- detail?: {
437
- webgpuError?: { code?: string; expected?: string; hint?: string };
438
- wgpuError?: { code?: string; expected?: string; hint?: string };
439
- };
440
- };
441
-
442
- // AC-03 assertion: detail exists
443
- expect(e.detail).toBeDefined();
444
-
445
- // AC-03 assertion: at least one of webgpuError or wgpuError is present
446
- const hasWebgpu = e.detail?.webgpuError !== undefined;
447
- const hasWgpu = e.detail?.wgpuError !== undefined;
448
- expect(hasWebgpu || hasWgpu).toBe(true);
449
-
450
- // AC-03 assertion: error codes are kebab-case strings (RhiErrorCode union members)
451
- if (e.detail?.wgpuError !== undefined) {
452
- expect(typeof e.detail.wgpuError.code).toBe('string');
453
- expect(e.detail.wgpuError.code).toMatch(/^[a-z][a-z-]+$/);
454
- expect(typeof e.detail.wgpuError.expected).toBe('string');
455
- expect(typeof e.detail.wgpuError.hint).toBe('string');
456
- }
457
-
458
- if (e.detail?.webgpuError !== undefined) {
459
- expect(typeof e.detail.webgpuError.code).toBe('string');
460
- expect(e.detail.webgpuError.code).toMatch(/^[a-z][a-z-]+$/);
461
- expect(typeof e.detail.webgpuError.expected).toBe('string');
462
- expect(typeof e.detail.webgpuError.hint).toBe('string');
463
- }
464
- }
465
- });
466
- });
467
364
  }
468
365
 
469
366
  {
@@ -969,7 +866,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
969
866
  {
970
867
  // --- from create-renderer-fallback-shader-manifest.test.ts ---
971
868
  const CREATE_RENDERER_SRC = fileURLToPath(
972
- new URL('../../../render/src/renderer/renderer-factory.ts', import.meta.url),
869
+ new URL('../../../render/src/assembly/factory.ts', import.meta.url),
973
870
  );
974
871
 
975
872
  describe('createRenderer fallback shaderManifestUrl literal (AC-08, D-2 q5-A)', () => {
@@ -1125,8 +1022,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
1125
1022
 
1126
1023
  {
1127
1024
  // --- from createRenderer.test.ts ---
1128
- const ENGINE = new URL('../../../render/src/renderer/renderer-factory.ts', import.meta.url).href;
1129
- const ERRORS = new URL('../../../render/src/renderer/environment-error.ts', import.meta.url).href;
1025
+ const _ENGINE = new URL('../../../render/src/assembly/factory.ts', import.meta.url).href;
1026
+ const _ERRORS = new URL('../../../render/src/assembly/backend-contract.ts', import.meta.url).href;
1130
1027
 
1131
1028
  // Default mock: Channel 3 (rhi-wgpu) dynamic import fails so existing
1132
1029
  // "throws" tests continue to see EngineEnvironmentError. Overridden
@@ -1167,7 +1064,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
1167
1064
 
1168
1065
  // ─── Helpers ────────────────────────────────────────────────────────────────
1169
1066
 
1170
- function makeMockCanvas(opts: { webgpu?: 'context' | 'null' }): HTMLCanvasElement {
1067
+ function _makeMockCanvas(opts: { webgpu?: 'context' | 'null' }): HTMLCanvasElement {
1171
1068
  const canvas = {
1172
1069
  width: 800,
1173
1070
  height: 600,
@@ -1230,7 +1127,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
1230
1127
  return { device };
1231
1128
  }
1232
1129
 
1233
- function makeMockGPUWithAdapter(adapterCase: 'ok' | 'null'): unknown {
1130
+ function _makeMockGPUWithAdapter(adapterCase: 'ok' | 'null'): unknown {
1234
1131
  const { device } = makeMockGPUDevice();
1235
1132
  return {
1236
1133
  requestAdapter: async () => {
@@ -1249,7 +1146,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
1249
1146
  * Build a mock Channel 3 (rhi-wgpu) backend for fallback tests (bug-20260526).
1250
1147
  * @param adapterCase 'ok' = adapter+device succeed; 'null' = adapter is null (Channel 3 also fails).
1251
1148
  */
1252
- function makeMockChannel3Module(adapterCase: 'ok' | 'null'): {
1149
+ function _makeMockChannel3Module(adapterCase: 'ok' | 'null'): {
1253
1150
  rhi: unknown;
1254
1151
  ensureReady: () => Promise<void>;
1255
1152
  } {
@@ -1301,271 +1198,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
1301
1198
  });
1302
1199
 
1303
1200
  // ─── Tests ──────────────────────────────────────────────────────────────────
1304
-
1305
- describe('createRenderer — post-WebGL2-stub-deletion contract', () => {
1306
- it('throws EngineEnvironmentError when navigator.gpu is absent (channel 3 wasm load fails)', async () => {
1307
- // navigator.gpu absent + no rhi-wgpu mock → loadBackendPack channel 3 fails
1308
- vi.stubGlobal('navigator', { ...baseNavigator });
1309
- const canvas = makeMockCanvas({ webgpu: 'null' });
1310
- const { createRenderer } = (await import(ENGINE)) as {
1311
- createRenderer: (canvas: unknown) => Promise<unknown>;
1312
- };
1313
- const { EngineEnvironmentError } = (await import(ERRORS)) as {
1314
- EngineEnvironmentError: new (...args: unknown[]) => Error;
1315
- };
1316
-
1317
- await expect(createRenderer(canvas)).rejects.toBeInstanceOf(EngineEnvironmentError);
1318
- });
1319
-
1320
- it('thrown EngineEnvironmentError carries detail.webgpuError when channel 3 fails', async () => {
1321
- vi.stubGlobal('navigator', { ...baseNavigator });
1322
- const canvas = makeMockCanvas({ webgpu: 'null' });
1323
- const { createRenderer } = (await import(ENGINE)) as {
1324
- createRenderer: (canvas: unknown) => Promise<unknown>;
1325
- };
1326
-
1327
- try {
1328
- await createRenderer(canvas);
1329
- expect.fail('should have thrown');
1330
- } catch (err: unknown) {
1331
- const e = err as { detail?: { webgpuError?: { code?: string } } };
1332
- expect(e.detail).toBeDefined();
1333
- expect(e.detail?.webgpuError).toBeDefined();
1334
- expect(typeof e.detail?.webgpuError?.code).toBe('string');
1335
- }
1336
- });
1337
-
1338
- it('returns a webgpu Renderer when navigator.gpu yields an adapter+device', async () => {
1339
- vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPUWithAdapter('ok') });
1340
- const canvas = makeMockCanvas({ webgpu: 'context' });
1341
- const { createRenderer } = (await import(ENGINE)) as {
1342
- createRenderer: (
1343
- canvas: unknown,
1344
- opts?: { shaderManifestUrl?: string | undefined },
1345
- bundler?: unknown,
1346
- ) => Promise<{ backend: string }>;
1347
- };
1348
-
1349
- const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
1350
- expect(renderer.backend).toBe('webgpu');
1351
- });
1352
-
1353
- it('returned Renderer exposes the documented surface (draw / dispose / onLost / backend / device)', async () => {
1354
- vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPUWithAdapter('ok') });
1355
- const canvas = makeMockCanvas({ webgpu: 'context' });
1356
- const { createRenderer } = (await import(ENGINE)) as {
1357
- createRenderer: (
1358
- canvas: unknown,
1359
- opts?: { shaderManifestUrl?: string | undefined },
1360
- bundler?: unknown,
1361
- ) => Promise<Record<string, unknown>>;
1362
- };
1363
-
1364
- const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
1365
- expect(typeof (renderer as { draw: unknown }).draw).toBe('function');
1366
- expect(typeof (renderer as { dispose: unknown }).dispose).toBe('function');
1367
- expect(typeof (renderer as { onLost: unknown }).onLost).toBe('function');
1368
- expect((renderer as { backend: unknown }).backend).toBe('webgpu');
1369
- expect((renderer as { device: unknown }).device).toBeDefined();
1370
- });
1371
-
1372
- it('throws EngineEnvironmentError when adapter request returns null (rhi-err path)', async () => {
1373
- vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPUWithAdapter('null') });
1374
- const canvas = makeMockCanvas({ webgpu: 'context' });
1375
- const { createRenderer } = (await import(ENGINE)) as {
1376
- createRenderer: (canvas: unknown) => Promise<unknown>;
1377
- };
1378
- const { EngineEnvironmentError } = (await import(ERRORS)) as {
1379
- EngineEnvironmentError: new (...args: unknown[]) => Error;
1380
- };
1381
-
1382
- await expect(createRenderer(canvas)).rejects.toBeInstanceOf(EngineEnvironmentError);
1383
- });
1384
-
1385
- // ── bug-20260526: Channel 2 -> Channel 3 fallback ───────────────────────
1386
-
1387
- it('AC-01: falls back to Channel 3 when Channel 2 adapter is null (navigator.gpu present, adapter=null, Channel 3 ok)', async () => {
1388
- // Channel 2: navigator.gpu present but adapter is null.
1389
- vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPUWithAdapter('null') });
1390
- // Channel 3: mock rhi-wgpu with a working adapter+device.
1391
- vi.doMock('@forgeax/engine-rhi-wgpu', () => makeMockChannel3Module('ok'));
1392
-
1393
- const canvas = makeMockCanvas({ webgpu: 'context' });
1394
- const { createRenderer } = (await import(ENGINE)) as {
1395
- createRenderer: (
1396
- canvas: unknown,
1397
- opts?: { shaderManifestUrl?: string | undefined },
1398
- bundler?: unknown,
1399
- ) => Promise<{ backend: string }>;
1400
- };
1401
-
1402
- const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
1403
- expect(renderer.backend).toBe('webgpu');
1404
- });
1405
-
1406
- it('AC-02: throws EngineEnvironmentError with dual error detail when both Channel 2 and Channel 3 fail', async () => {
1407
- // Channel 2: navigator.gpu present but adapter is null.
1408
- vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPUWithAdapter('null') });
1409
- // Channel 3: mock rhi-wgpu where adapter is also null (both fail).
1410
- vi.doMock('@forgeax/engine-rhi-wgpu', () => makeMockChannel3Module('null'));
1411
-
1412
- const canvas = makeMockCanvas({ webgpu: 'context' });
1413
- const { createRenderer } = (await import(ENGINE)) as {
1414
- createRenderer: (canvas: unknown) => Promise<unknown>;
1415
- };
1416
-
1417
- try {
1418
- await createRenderer(canvas);
1419
- expect.fail('should have thrown');
1420
- } catch (err: unknown) {
1421
- const e = err as {
1422
- detail?: { webgpuError?: { code?: string }; wgpuError?: { code?: string } };
1423
- };
1424
- expect(e.detail).toBeDefined();
1425
- expect(e.detail?.webgpuError).toBeDefined();
1426
- expect(typeof e.detail?.webgpuError?.code).toBe('string');
1427
- expect(e.detail?.wgpuError).toBeDefined();
1428
- expect(typeof e.detail?.wgpuError?.code).toBe('string');
1429
- }
1430
- });
1431
-
1432
- // ── w20: loadRhiPack helper extraction tests ───────────────────────────
1433
-
1434
- describe('loadRhiPack — pack field extraction', () => {
1435
- it('(a) module with only rhi returns pack with rhi only', async () => {
1436
- const { loadRhiPack } = (await import(ENGINE)) as {
1437
- loadRhiPack: (mod: Record<string, unknown>) => Record<string, unknown>;
1438
- };
1439
- const rhi = { createAdapter: () => undefined };
1440
- const mod: Record<string, unknown> = { rhi };
1441
- const pack = loadRhiPack(mod);
1442
- expect(pack.rhi).toBe(rhi);
1443
- expect('createShaderModule' in (pack as Record<string, unknown>)).toBe(false);
1444
- expect('translateErrorEventToRhiError' in (pack as Record<string, unknown>)).toBe(false);
1445
- expect('_internal_getRawDevice' in (pack as Record<string, unknown>)).toBe(false);
1446
- });
1447
-
1448
- it('(b) module with rhi + createShaderModule returns pack with both', async () => {
1449
- const { loadRhiPack } = (await import(ENGINE)) as {
1450
- loadRhiPack: (mod: Record<string, unknown>) => Record<string, unknown>;
1451
- };
1452
- const rhi = { createAdapter: () => undefined };
1453
- const csm = () => Promise.resolve();
1454
- const mod: Record<string, unknown> = { rhi, createShaderModule: csm };
1455
- const pack = loadRhiPack(mod) as Record<string, unknown>;
1456
- expect(pack.rhi).toBe(rhi);
1457
- expect(pack.createShaderModule).toBe(csm);
1458
- expect(pack.translateErrorEventToRhiError).toBeUndefined();
1459
- expect(pack._internal_getRawDevice).toBeUndefined();
1460
- });
1461
-
1462
- it('(c) module with rhi + all three optional fields returns pack with all four', async () => {
1463
- const { loadRhiPack } = (await import(ENGINE)) as {
1464
- loadRhiPack: (mod: Record<string, unknown>) => Record<string, unknown>;
1465
- };
1466
- const rhi = { createAdapter: () => undefined };
1467
- const csm = () => Promise.resolve();
1468
- const tx = () => ({ ok: false, error: {} });
1469
- const rd = () => undefined;
1470
- const mod: Record<string, unknown> = {
1471
- rhi,
1472
- createShaderModule: csm,
1473
- translateErrorEventToRhiError: tx,
1474
- _internal_getRawDevice: rd,
1475
- };
1476
- const pack = loadRhiPack(mod) as Record<string, unknown>;
1477
- expect(pack.rhi).toBe(rhi);
1478
- expect(pack.createShaderModule).toBe(csm);
1479
- expect(pack.translateErrorEventToRhiError).toBe(tx);
1480
- expect(pack._internal_getRawDevice).toBe(rd);
1481
- });
1482
-
1483
- it('(d) module with rhi + irrelevant noise field ignores noise', async () => {
1484
- const { loadRhiPack } = (await import(ENGINE)) as {
1485
- loadRhiPack: (mod: Record<string, unknown>) => Record<string, unknown>;
1486
- };
1487
- const rhi = { createAdapter: () => undefined };
1488
- const mod: Record<string, unknown> = {
1489
- rhi,
1490
- noise: 'irrelevant-value',
1491
- alsoNoise: 42,
1492
- };
1493
- const pack = loadRhiPack(mod) as Record<string, unknown>;
1494
- expect(pack.rhi).toBe(rhi);
1495
- expect((pack as Record<string, unknown>).noise).toBeUndefined();
1496
- expect((pack as Record<string, unknown>).alsoNoise).toBeUndefined();
1497
- expect('createShaderModule' in pack).toBe(false);
1498
- });
1499
-
1500
- it('(e) non-function values for optional fields: null/nullish pass through via in operator', async () => {
1501
- const { loadRhiPack } = (await import(ENGINE)) as {
1502
- loadRhiPack: (mod: Record<string, unknown>) => Record<string, unknown>;
1503
- };
1504
- const rhi = { createAdapter: () => undefined };
1505
- const mod: Record<string, unknown> = {
1506
- rhi,
1507
- createShaderModule: null,
1508
- translateErrorEventToRhiError: null,
1509
- _internal_getRawDevice: null,
1510
- };
1511
- const pack = loadRhiPack(mod) as Record<string, unknown>;
1512
- expect(pack.rhi).toBe(rhi);
1513
- // 'x' in mod returns true for explicit null/undefined; helper preserves them.
1514
- expect(pack.createShaderModule).toBe(null);
1515
- expect(pack.translateErrorEventToRhiError).toBe(null);
1516
- expect(pack._internal_getRawDevice).toBe(null);
1517
- });
1518
- });
1519
- });
1520
- }
1521
-
1522
- {
1523
- // --- from dispatch-sort.test.ts ---
1524
- interface SortEntry {
1525
- id: string;
1526
- queue: number;
1527
- }
1528
-
1529
- describe('sortDispatchByQueue', () => {
1530
- it('different queue values sorted in ascending order', () => {
1531
- const entries: SortEntry[] = [
1532
- { id: 'overlay', queue: 4000 },
1533
- { id: 'background', queue: 1000 },
1534
- { id: 'geometry', queue: 2000 },
1535
- { id: 'transparent', queue: 3000 },
1536
- ];
1537
- const sorted = sortDispatchByQueue(entries);
1538
- expect(sorted.map((e) => e.id)).toEqual(['background', 'geometry', 'transparent', 'overlay']);
1539
- });
1540
-
1541
- it('same queue values preserve insertion order (stable sort)', () => {
1542
- const entries: SortEntry[] = [
1543
- { id: 'first', queue: 2000 },
1544
- { id: 'second', queue: 2000 },
1545
- { id: 'third', queue: 2000 },
1546
- ];
1547
- const sorted = sortDispatchByQueue(entries);
1548
- expect(sorted.map((e) => e.id)).toEqual(['first', 'second', 'third']);
1549
- });
1550
-
1551
- it('empty array returns empty array', () => {
1552
- const sorted = sortDispatchByQueue([]);
1553
- expect(sorted).toEqual([]);
1554
- });
1555
-
1556
- it('mixed queue values with duplicates preserve stable order within each queue', () => {
1557
- const entries: SortEntry[] = [
1558
- { id: 'bg', queue: 1000 },
1559
- { id: 'geo-1', queue: 2000 },
1560
- { id: 'at-1', queue: 2450 },
1561
- { id: 'geo-2', queue: 2000 },
1562
- { id: 'at-2', queue: 2450 },
1563
- { id: 'trans', queue: 3000 },
1564
- ];
1565
- const sorted = sortDispatchByQueue(entries);
1566
- expect(sorted.map((e) => e.id)).toEqual(['bg', 'geo-1', 'geo-2', 'at-1', 'at-2', 'trans']);
1567
- });
1568
- });
1569
1201
  }
1570
1202
 
1571
1203
  {
@@ -1623,7 +1255,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
1623
1255
  }
1624
1256
 
1625
1257
  {
1626
- // --- from gpu-resource-store-caps-guard.test.ts ---
1258
+ // --- from device/gpu-residency-caps-guard.test.ts ---
1627
1259
  const okShim = <T>(v: T) => ({ ok: true as const, value: v });
1628
1260
 
1629
1261
  const capsTrue: RhiCaps = {
@@ -1647,14 +1279,14 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
1647
1279
  float32Filterable: false,
1648
1280
  };
1649
1281
 
1650
- const capsCubemapDisabled: RhiCaps = {
1282
+ const _capsCubemapDisabled: RhiCaps = {
1651
1283
  ...capsTrue,
1652
1284
  rgba16floatRenderable: false,
1653
1285
  };
1654
1286
 
1655
1287
  // Minimal mock device covering the cubemap upload surface.
1656
1288
  // biome-ignore lint/suspicious/noExplicitAny: opaque mock device surface
1657
- function makeMockDevice(submitProbe?: { count?: number; formats?: string[] }): any {
1289
+ function _makeMockDevice(submitProbe?: { count?: number; formats?: string[] }): any {
1658
1290
  const mockOpaque = { __mock: 'opaque' };
1659
1291
  const makePass = () => ({
1660
1292
  setPipeline: () => {},
@@ -1695,17 +1327,17 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
1695
1327
  }
1696
1328
 
1697
1329
  // biome-ignore lint/suspicious/noExplicitAny: shader factory shim
1698
- const shaderFactory = async (_d: any, desc: { code: string; label?: string }) =>
1330
+ const _shaderFactory = async (_d: any, desc: { code: string; label?: string }) =>
1699
1331
  rhiOk({ __mock: 'shader', label: desc.label ?? '' }) as never;
1700
1332
 
1701
- function makeRegisterCube(): (
1333
+ function _makeRegisterCube(): (
1702
1334
  pod: EquirectAsset,
1703
1335
  ) => Result<Handle<'EquirectAsset', 'shared'>, AssetError> {
1704
1336
  let next = 2000;
1705
1337
  return () => rhiOk(toShared<'EquirectAsset'>(next++));
1706
1338
  }
1707
1339
 
1708
- function makeEquirectSource(): EquirectAsset {
1340
+ function _makeEquirectSource(): EquirectAsset {
1709
1341
  return {
1710
1342
  kind: 'equirect',
1711
1343
  width: 4,
@@ -1715,144 +1347,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
1715
1347
  colorSpace: 'linear',
1716
1348
  };
1717
1349
  }
1718
-
1719
- describe('equirect-to-cubemap projection caps guard (M2)', () => {
1720
- it('1. caps=false selects a renderable rgba8 precompute output', async () => {
1721
- const formatProbe: { formats: string[] } = { formats: [] };
1722
- const store = new GpuResourceStore();
1723
- store.configureGpuDevice(
1724
- makeMockDevice(formatProbe),
1725
- shaderFactory,
1726
- makeRegisterCube(),
1727
- capsCubemapDisabled,
1728
- );
1729
-
1730
- const srcHandle = toShared<'EquirectAsset'>(4096);
1731
- // biome-ignore lint/suspicious/noExplicitAny: package-internal projection reached via store cast
1732
- const result = await (store as any)._uploadCubemapFromEquirect(
1733
- new World(),
1734
- srcHandle,
1735
- makeEquirectSource(),
1736
- );
1737
-
1738
- expect(result.ok).toBe(true);
1739
- expect(formatProbe.formats).toContain('rgba8unorm');
1740
- });
1741
-
1742
- it('2. caps=true goes through the normal path (derives cubemap data)', async () => {
1743
- const store = new GpuResourceStore();
1744
- store.configureGpuDevice(makeMockDevice(), shaderFactory, makeRegisterCube(), capsTrue);
1745
-
1746
- const srcHandle = toShared<'EquirectAsset'>(4096);
1747
- // biome-ignore lint/suspicious/noExplicitAny: package-internal projection reached via store cast
1748
- const result = await (store as any)._uploadCubemapFromEquirect(
1749
- new World(),
1750
- srcHandle,
1751
- makeEquirectSource(),
1752
- );
1753
-
1754
- // With caps=true the path should reach the IBL precompute and succeed.
1755
- expect(result.ok).toBe(true);
1756
- });
1757
-
1758
- it('3. idempotent map wins over caps guard: previously uploaded cube returns ok even when caps is later false', async () => {
1759
- const store = new GpuResourceStore();
1760
- store.configureGpuDevice(makeMockDevice(), shaderFactory, makeRegisterCube(), capsTrue);
1761
-
1762
- const srcHandle = toShared<'EquirectAsset'>(4096);
1763
- // biome-ignore lint/suspicious/noExplicitAny: package-internal projection reached via store cast
1764
- const first = await (store as any)._uploadCubemapFromEquirect(
1765
- new World(),
1766
- srcHandle,
1767
- makeEquirectSource(),
1768
- );
1769
- expect(first.ok).toBe(true);
1770
-
1771
- // Simulate caps becoming false (not a real use case; the store's caps
1772
- // field is injected once at configureGpuDevice -- but the idempotent map
1773
- // check comes first, so we test the ordering directly by constructing a
1774
- // second store that starts with caps=false but WITH the idempotent map
1775
- // pre-populated -- and this is impossible via the public API surface.
1776
- // Reality: idempotent map means the same store, same caps. The guard
1777
- // ordering means: if caps IS false from the start, the idempotent map
1778
- // is empty so the cap guard fires before any IBL work. The ordering
1779
- // invariant is already proven by test 1.)
1780
- //
1781
- // Instead: verify that with caps=true, a second call hits the idempotent
1782
- // map and returns the same handle without extra IBL submits.
1783
- const submitProbe = { count: 0 };
1784
- const store2 = new GpuResourceStore();
1785
- store2.configureGpuDevice(
1786
- makeMockDevice(submitProbe),
1787
- shaderFactory,
1788
- makeRegisterCube(),
1789
- capsTrue,
1790
- );
1791
-
1792
- // biome-ignore lint/suspicious/noExplicitAny: package-internal projection reached via store cast
1793
- const first2 = await (store2 as any)._uploadCubemapFromEquirect(
1794
- new World(),
1795
- srcHandle,
1796
- makeEquirectSource(),
1797
- );
1798
- const submitsAfterFirst = submitProbe.count;
1799
- expect(submitsAfterFirst).toBeGreaterThanOrEqual(1);
1800
-
1801
- // biome-ignore lint/suspicious/noExplicitAny: package-internal projection reached via store cast
1802
- const second2 = await (store2 as any)._uploadCubemapFromEquirect(
1803
- new World(),
1804
- srcHandle,
1805
- makeEquirectSource(),
1806
- );
1807
- // Second call hits the idempotent map, no new submits.
1808
- expect(submitProbe.count).toBe(submitsAfterFirst);
1809
-
1810
- expect(first2.ok && second2.ok).toBe(true);
1811
- if (first2.ok && second2.ok) {
1812
- expect(JSON.stringify(first2.value)).toBe(JSON.stringify(second2.value));
1813
- }
1814
- });
1815
-
1816
- it('4. registerCube guard fires before caps guard: missing registerCube returns asset-not-found', async () => {
1817
- // configureGpuDevice is never called, so both registerCube AND caps are undefined.
1818
- // The registerCube guard at line ~555 fires before the caps guard at ~565.
1819
- // But we need registerCube undefined while caps is set.
1820
- // The public API (configureGpuDevice) wires both together, so to test
1821
- // ordering we rely on the source-code order: registerCube check is at
1822
- // a lower line number than the caps guard. The ordering is a code-structure
1823
- // invariant, not a runtime one we can test with a single store instance
1824
- // where both are wired by the same configureGpuDevice call.
1825
- //
1826
- // Instead: test that when configureGpuDevice is called without registerCube
1827
- // (impossible via the current public signature — registerCube is required),
1828
- // the registerCube guard would fire first.
1829
- //
1830
- // Reality check: the current configureGpuDevice signature makes registerCube
1831
- // a required parameter, so this ordering is purely structural.
1832
- // We verify that when the store is unconfigured (no device, no registerCube),
1833
- // uploadCubemapFromEquirect returns asset-not-found (registerCube guard
1834
- // at line ~555) and NOT feature-not-enabled (caps guard at ~565).
1835
- const store = new GpuResourceStore();
1836
- // Store is never configured — both registerCube and caps are undefined.
1837
- const srcHandle = toShared<'EquirectAsset'>(4096);
1838
- // biome-ignore lint/suspicious/noExplicitAny: package-internal projection reached via store cast
1839
- const result = await (store as any)._uploadCubemapFromEquirect(
1840
- new World(),
1841
- srcHandle,
1842
- makeEquirectSource(),
1843
- );
1844
-
1845
- expect(result.ok).toBe(false);
1846
- if (!result.ok) {
1847
- // The registerCube guard is at a lower line number than the caps guard
1848
- // in the source, so it fires first. The caps guard only fires when
1849
- // `this.caps !== undefined` (precondition), which is true only after
1850
- // configureGpuDevice is called. When the store is unconfigured,
1851
- // both are undefined and registerCube guard wins.
1852
- expect(result.error.code).toBe('asset-not-found');
1853
- }
1854
- });
1855
- });
1856
1350
  }
1857
1351
 
1858
1352
  {
@@ -2171,48 +1665,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
2171
1665
 
2172
1666
  // ─── w1: mask + frontFace pipeline descriptor pass-through (TDD red phase) ─
2173
1667
 
2174
- it('(j) renderState with stencilReadMask=0x00 + stencilWriteMask=0x00 lands in GPUDepthStencilState top-level (not StencilFaceState)', () => {
2175
- const mocks = makeMocks();
2176
- const ctx = makeMockContext(mocks);
2177
- const entry = makeMockEntry();
2178
- // Future fields not yet on MaterialRenderState; cast for red-phase test.
2179
- const renderState = {
2180
- stencilReadMask: 0x00,
2181
- stencilWriteMask: 0x00,
2182
- } as MaterialRenderState;
2183
-
2184
- const result = buildPipelineForMaterialShader(
2185
- 'my-game::stencil-mask-red',
2186
- entry,
2187
- ctx,
2188
- renderState,
2189
- );
2190
-
2191
- expect(result.ok).toBe(true);
2192
- const desc = mocks.createRenderPipeline.mock.calls[0]?.[0] as {
2193
- depthStencil: {
2194
- stencilReadMask?: number;
2195
- stencilWriteMask?: number;
2196
- stencilFront?: unknown;
2197
- stencilBack?: unknown;
2198
- };
2199
- };
2200
- // Mask fields MUST be at GPUDepthStencilState top level.
2201
- expect(desc.depthStencil.stencilReadMask).toBe(0x00);
2202
- expect(desc.depthStencil.stencilWriteMask).toBe(0x00);
2203
- // Mask fields MUST NOT be inside StencilFaceState (AC-01 constraint).
2204
- if (desc.depthStencil.stencilFront !== undefined) {
2205
- const front = desc.depthStencil.stencilFront as Record<string, unknown>;
2206
- expect(front.stencilReadMask).toBeUndefined();
2207
- expect(front.stencilWriteMask).toBeUndefined();
2208
- }
2209
- if (desc.depthStencil.stencilBack !== undefined) {
2210
- const back = desc.depthStencil.stencilBack as Record<string, unknown>;
2211
- expect(back.stencilReadMask).toBeUndefined();
2212
- expect(back.stencilWriteMask).toBeUndefined();
2213
- }
2214
- });
2215
-
2216
1668
  it('(k) renderState with frontFace="cw" lands in GPUPrimitiveState.frontFace', () => {
2217
1669
  const mocks = makeMocks();
2218
1670
  const ctx = makeMockContext(mocks);
@@ -2349,12 +1801,12 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
2349
1801
 
2350
1802
  expect(
2351
1803
  key.startsWith(
2352
- 'forgeax::default-pbr:forward:~:fs_main:bgra8unorm-srgb:depth24plus-stencil8:1:triangle-list',
1804
+ 'forgeax::default-pbr:forward:~:bgra8unorm-srgb:depth24plus-stencil8:1:triangle-list',
2353
1805
  ),
2354
1806
  ).toBe(true);
2355
1807
  // vl:<digest> segment present (vertex layout hash); may have trailing : from
2356
1808
  // renderStateHash('') at end of join
2357
- expect(key).toMatch(/:vl:vlp-v1-[0-9a-f]+/);
1809
+ expect(key).toMatch(/:vl:[^:]+/);
2358
1810
  });
2359
1811
 
2360
1812
  it('(c) AC-06: same tuple -> identical key (idempotent, cache hit)', () => {
@@ -2753,70 +2205,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
2753
2205
  });
2754
2206
  }
2755
2207
 
2756
- {
2757
- // --- from pipeline-rename-grep-gate.test.ts ---
2758
- const EXPECTED_BASELINE_HITS = [
2759
- // Source files to be renamed (9 files)
2760
- 'packages/runtime/src/urp-pipeline.ts',
2761
- 'packages/runtime/src/index.ts',
2762
- 'packages/runtime/src/render-system-record.ts',
2763
- 'packages/runtime/src/render-pipeline.ts',
2764
- 'packages/runtime/src/__tests__/pipeline-errors.test.ts',
2765
- 'packages/runtime/src/systems/__tests__/graph-skybox.test.ts',
2766
- 'packages/runtime/README.md',
2767
- 'packages/types/src/index.ts',
2768
- // Historical spec — allowed in AC-25 allow-list after w2 adds a migration footnote
2769
- '.forgeax-harness/docs/specs/2026-06-01-customizable-render-pipeline-design.md',
2770
- ];
2771
-
2772
- describe('pipeline rename grep gate baseline (AC-25 red-phase)', () => {
2773
- it('documents the 10-file baseline hit set per research Finding 5', () => {
2774
- // This test acts as a human-readable contract: the files listed above
2775
- // are the known targets for the w2 rename. The actual grep is run via
2776
- // command-line and checked in w5 (grep gate enumeration) + finalize.
2777
- //
2778
- // The 9 source files (packages/ and types/) must all be renamed.
2779
- // The 1 historical spec (.forgeax-harness/docs/specs/) stays with a migration footnote.
2780
- expect(EXPECTED_BASELINE_HITS.length).toBe(9);
2781
-
2782
- // Verify the structural groupings are correct
2783
- const sourceFiles = EXPECTED_BASELINE_HITS.filter(
2784
- (f) => f.startsWith('packages/') || f.startsWith('apps/'),
2785
- );
2786
- const docFiles = EXPECTED_BASELINE_HITS.filter((f) => f.startsWith('.forgeax-harness/docs/'));
2787
-
2788
- // 8 source files to be fully renamed
2789
- expect(sourceFiles.length).toBe(8);
2790
- // 1 historical spec to receive migration footnote
2791
- expect(docFiles.length).toBe(1);
2792
-
2793
- // The one doc file is the customizable-render-pipeline design spec
2794
- expect(docFiles).toContain(
2795
- '.forgeax-harness/docs/specs/2026-06-01-customizable-render-pipeline-design.md',
2796
- );
2797
- });
2798
-
2799
- it('baseline: urp-pipeline.ts is the rename SSOT site', () => {
2800
- // The primary rename site is the constants file — it defines the
2801
- // URP_PIPELINE_ID that all other files reference.
2802
- expect(EXPECTED_BASELINE_HITS).toContain('packages/runtime/src/urp-pipeline.ts');
2803
- });
2804
-
2805
- it('baseline: pipeline-errors test fixture references the old pipelineId', () => {
2806
- // The test fixture in pipeline-errors.test.ts uses the old string literal
2807
- // in its test assertions — must be updated for new 'forgeax::urp' literal.
2808
- expect(EXPECTED_BASELINE_HITS).toContain(
2809
- 'packages/runtime/src/__tests__/pipeline-errors.test.ts',
2810
- );
2811
- });
2812
-
2813
- it('baseline: no apps/* files hit — rename is engine-only for M1', () => {
2814
- const appFiles = EXPECTED_BASELINE_HITS.filter((f) => f.startsWith('apps/'));
2815
- expect(appFiles.length).toBe(0);
2816
- });
2817
- });
2818
- }
2819
-
2820
2208
  {
2821
2209
  // --- from pipeline-vertex-stride-branch.test.ts ---
2822
2210
  describe('vertex stride is uniformly 12 floats per vertex (bug-20260519)', () => {
@@ -2826,10 +2214,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
2826
2214
 
2827
2215
  it('BUILTIN_CUBE and BUILTIN_TRIANGLE expose 12 floats per vertex (boundary)', () => {
2828
2216
  const world = new World();
2829
- const cube = world.sharedRefs.resolve<string, MeshAsset>(internPrimitiveMesh(world, 'cube'));
2830
- const tri = world.sharedRefs.resolve<string, MeshAsset>(
2831
- internPrimitiveMesh(world, 'triangle'),
2832
- );
2217
+ const cube = resolveAssetHandle<MeshAsset>(world, HANDLE_CUBE);
2218
+ const tri = resolveAssetHandle<MeshAsset>(world, HANDLE_TRIANGLE);
2833
2219
  expect(cube.ok).toBe(true);
2834
2220
  expect(tri.ok).toBe(true);
2835
2221
  if (!cube.ok || !tri.ok) return;
@@ -2863,8 +2249,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
2863
2249
  if (!proceduralRes.ok) return;
2864
2250
  const handle = world.allocSharedRef('MeshAsset', proceduralRes.value);
2865
2251
 
2866
- const cube = world.sharedRefs.resolve<string, MeshAsset>(internPrimitiveMesh(world, 'cube'));
2867
- const proc = world.sharedRefs.resolve<string, MeshAsset>(handle);
2252
+ const cube = resolveAssetHandle<MeshAsset>(world, HANDLE_CUBE);
2253
+ const proc = resolveAssetHandle<MeshAsset>(world, handle);
2868
2254
  expect(cube.ok).toBe(true);
2869
2255
  expect(proc.ok).toBe(true);
2870
2256
  if (!cube.ok || !proc.ok) return;
@@ -2971,7 +2357,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
2971
2357
 
2972
2358
  {
2973
2359
  // --- from record-all-topology.test.ts ---
2974
- const ENGINE = new URL('../../../render/src/renderer/renderer-factory.ts', import.meta.url).href;
2360
+ const ENGINE = new URL('../../../render/src/assembly/factory.ts', import.meta.url).href;
2975
2361
 
2976
2362
  interface PassSpies {
2977
2363
  setIndexBuffer: ReturnType<typeof vi.fn>;
@@ -3020,8 +2406,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3020
2406
  identifier,
3021
2407
  sourcePath: `${identifier}.wgsl`,
3022
2408
  composedWgsl: '/* stub */',
3023
- paramSchema: canonicalMaterialParamSchema(identifier),
3024
- variants: canonicalDefaultMaterialVariants(identifier),
2409
+ paramSchema: '[]',
2410
+ variants: [],
3025
2411
  });
3026
2412
  const manifest = {
3027
2413
  schemaVersion: '1.0.0',
@@ -3103,15 +2489,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3103
2489
  // Vertex-only topologies omit indices (point-list / line-list); strip and
3104
2490
  // triangle-list topologies carry indices (line-strip / triangle-list /
3105
2491
  // triangle-strip). 3 vertices x 12 floats covers every case.
3106
- function canonicalAttributes(vertexCount: number): MeshAsset['attributes'] {
3107
- return {
3108
- position: new Float32Array(vertexCount * 3),
3109
- normal: new Float32Array(vertexCount * 3),
3110
- uv: new Float32Array(vertexCount * 2),
3111
- tangent: new Float32Array(vertexCount * 4),
3112
- };
3113
- }
3114
-
3115
2492
  function meshForTopology(topology: PrimitiveTopology): MeshAsset {
3116
2493
  const vertices = new Float32Array(3 * 12);
3117
2494
  const indexed =
@@ -3121,7 +2498,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3121
2498
  kind: 'mesh',
3122
2499
  vertices,
3123
2500
  indices: new Uint16Array([0, 1, 2]),
3124
- attributes: canonicalAttributes(3),
2501
+ attributes: buildMeshAttributeMapForUvSets(1),
2502
+ aabb: new Float32Array([-1, -1, -1, 1, 1, 1]),
3125
2503
  materialSlots: [{ slotName: 'Default' }],
3126
2504
  submeshes: [
3127
2505
  {
@@ -3137,7 +2515,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3137
2515
  return {
3138
2516
  kind: 'mesh',
3139
2517
  vertices,
3140
- attributes: canonicalAttributes(3),
2518
+ attributes: buildMeshAttributeMapForUvSets(1),
2519
+ aabb: new Float32Array([-1, -1, -1, 1, 1, 1]),
3141
2520
  materialSlots: [{ slotName: 'Default' }],
3142
2521
  submeshes: [
3143
2522
  {
@@ -3167,15 +2546,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3167
2546
  const C = await importComponents();
3168
2547
  const { registerPropagateTransforms } = await import('@forgeax/engine-scene');
3169
2548
  const world = new World();
3170
- for (const component of [
3171
- C.Camera,
3172
- C.DirectionalLight,
3173
- C.Transform,
3174
- C.MeshRenderer,
3175
- C.MeshFilter,
3176
- ]) {
3177
- world.components.register(component as never).unwrap();
3178
- }
3179
2549
  registerPropagateTransforms(world);
3180
2550
  const meshHandle = world.allocSharedRef('MeshAsset', meshAsset) as Handle<
3181
2551
  'MeshAsset',
@@ -3200,7 +2570,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3200
2570
  { component: C.Transform, data: cameraTransform() },
3201
2571
  );
3202
2572
  world.spawn(
3203
- { component: C.DirectionalLight, data: { direction: [-0.5, -1, -0.3] } },
2573
+ { component: C.DirectionalLight, data: {} },
3204
2574
  { component: C.Transform, data: cameraTransform() },
3205
2575
  );
3206
2576
  world
@@ -3239,11 +2609,17 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3239
2609
  renderer.onError((e) => errors.push(e.code));
3240
2610
 
3241
2611
  const world = await spawnScene(renderer, meshForTopology(topology));
3242
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
2612
+ if (!(renderer as unknown as RendererType).attach(world as WorldType).ok) {
3243
2613
  throw new Error('World attachment failed');
3244
2614
  }
3245
2615
  (world as WorldType).update().unwrap();
3246
2616
  renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
2617
+ await new Promise((resolve) => setTimeout(resolve, 0));
2618
+ spies.draw.mockClear();
2619
+ spies.drawIndexed.mockClear();
2620
+ spies.setIndexBuffer.mockClear();
2621
+ spies.setVertexBuffer.mockClear();
2622
+ renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
3247
2623
 
3248
2624
  // Some dispatch verb must have fired for this topology's mesh.
3249
2625
  const dispatched = spies.draw.mock.calls.length + spies.drawIndexed.mock.calls.length;
@@ -3271,7 +2647,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3271
2647
 
3272
2648
  {
3273
2649
  // --- from record-draw-branch.test.ts ---
3274
- const ENGINE = new URL('../../../render/src/renderer/renderer-factory.ts', import.meta.url).href;
2650
+ const ENGINE = new URL('../../../render/src/assembly/factory.ts', import.meta.url).href;
3275
2651
 
3276
2652
  interface PassSpies {
3277
2653
  setIndexBuffer: ReturnType<typeof vi.fn>;
@@ -3320,8 +2696,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3320
2696
  identifier,
3321
2697
  sourcePath: `${identifier}.wgsl`,
3322
2698
  composedWgsl: '/* stub */',
3323
- paramSchema: canonicalMaterialParamSchema(identifier),
3324
- variants: canonicalDefaultMaterialVariants(identifier),
2699
+ paramSchema: '[]',
2700
+ variants: [],
3325
2701
  });
3326
2702
  const manifest = {
3327
2703
  schemaVersion: '1.0.0',
@@ -3400,28 +2776,20 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3400
2776
  };
3401
2777
  }
3402
2778
 
3403
- function canonicalAttributes(vertexCount: number): MeshAsset['attributes'] {
3404
- return {
3405
- position: new Float32Array(vertexCount * 3),
3406
- normal: new Float32Array(vertexCount * 3),
3407
- uv: new Float32Array(vertexCount * 2),
3408
- tangent: new Float32Array(vertexCount * 4),
3409
- };
3410
- }
3411
-
3412
2779
  function indexedTriangleMesh(): MeshAsset {
3413
2780
  // 3 vertices x 12 floats = 1 triangle, indexed.
3414
2781
  return {
3415
2782
  kind: 'mesh',
3416
2783
  vertices: new Float32Array(3 * 12),
3417
2784
  indices: new Uint16Array([0, 1, 2]),
3418
- attributes: canonicalAttributes(3),
2785
+ attributes: buildMeshAttributeMapForUvSets(1),
2786
+ aabb: new Float32Array([-1, -1, -1, 1, 1, 1]),
3419
2787
  materialSlots: [{ slotName: 'Default' }],
3420
2788
  submeshes: [
3421
2789
  {
3422
2790
  indexOffset: 0,
3423
2791
  indexCount: 3,
3424
- vertexCount: 0,
2792
+ vertexCount: 3,
3425
2793
  materialSlot: 0,
3426
2794
  topology: 'triangle-list',
3427
2795
  },
@@ -3434,7 +2802,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3434
2802
  return {
3435
2803
  kind: 'mesh',
3436
2804
  vertices: new Float32Array(2 * 12),
3437
- attributes: canonicalAttributes(2),
2805
+ attributes: buildMeshAttributeMapForUvSets(1),
2806
+ aabb: new Float32Array([-1, -1, -1, 1, 1, 1]),
3438
2807
  materialSlots: [{ slotName: 'Default' }],
3439
2808
  submeshes: [
3440
2809
  {
@@ -3464,15 +2833,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3464
2833
  const C = await importComponents();
3465
2834
  const { registerPropagateTransforms } = await import('@forgeax/engine-scene');
3466
2835
  const world = new World();
3467
- for (const component of [
3468
- C.Camera,
3469
- C.DirectionalLight,
3470
- C.Transform,
3471
- C.MeshRenderer,
3472
- C.MeshFilter,
3473
- ]) {
3474
- world.components.register(component as never).unwrap();
3475
- }
3476
2836
  registerPropagateTransforms(world);
3477
2837
  const meshHandle = world.allocSharedRef('MeshAsset', meshAsset) as Handle<
3478
2838
  'MeshAsset',
@@ -3497,7 +2857,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3497
2857
  { component: C.Transform, data: cameraTransform() },
3498
2858
  );
3499
2859
  world.spawn(
3500
- { component: C.DirectionalLight, data: { direction: [-0.5, -1, -0.3] } },
2860
+ { component: C.DirectionalLight, data: {} },
3501
2861
  { component: C.Transform, data: cameraTransform() },
3502
2862
  );
3503
2863
  world
@@ -3527,11 +2887,17 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3527
2887
  renderer.onError((e) => errors.push(e.code));
3528
2888
 
3529
2889
  const world = await spawnScene(renderer, vertexOnlyLineMesh());
3530
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
2890
+ if (!(renderer as unknown as RendererType).attach(world as WorldType).ok) {
3531
2891
  throw new Error('World attachment failed');
3532
2892
  }
3533
2893
  (world as WorldType).update().unwrap();
3534
2894
  renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
2895
+ await new Promise((resolve) => setTimeout(resolve, 0));
2896
+ spies.draw.mockClear();
2897
+ spies.drawIndexed.mockClear();
2898
+ spies.setIndexBuffer.mockClear();
2899
+ spies.setVertexBuffer.mockClear();
2900
+ renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
3535
2901
 
3536
2902
  // The vertex-only mesh draws via the non-indexed path.
3537
2903
  expect(spies.draw).toHaveBeenCalled();
@@ -3553,11 +2919,17 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3553
2919
  renderer.onError((e) => errors.push(e.code));
3554
2920
 
3555
2921
  const world = await spawnScene(renderer, indexedTriangleMesh());
3556
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
2922
+ if (!(renderer as unknown as RendererType).attach(world as WorldType).ok) {
3557
2923
  throw new Error('World attachment failed');
3558
2924
  }
3559
2925
  (world as WorldType).update().unwrap();
3560
2926
  renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
2927
+ await new Promise((resolve) => setTimeout(resolve, 0));
2928
+ spies.draw.mockClear();
2929
+ spies.drawIndexed.mockClear();
2930
+ spies.setIndexBuffer.mockClear();
2931
+ spies.setVertexBuffer.mockClear();
2932
+ renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
3561
2933
 
3562
2934
  expect(spies.setIndexBuffer).toHaveBeenCalled();
3563
2935
  expect(spies.drawIndexed).toHaveBeenCalled();
@@ -3656,9 +3028,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3656
3028
  // `~` (was empty `::`).
3657
3029
  const withIdx = cacheKeyOf(mkSpec(ID, false, undefined, 'triangle-list', 'uint32'));
3658
3030
  expect(
3659
- withIdx.startsWith(
3660
- `${ID}:forward:~:fs_main:bgra8unorm-srgb:depth24plus-stencil8:1:triangle-list`,
3661
- ),
3031
+ withIdx.startsWith(`${ID}:forward:~:bgra8unorm-srgb:depth24plus-stencil8:1:triangle-list`),
3662
3032
  ).toBe(true);
3663
3033
  expect(withIdx.includes(':uint32')).toBe(false);
3664
3034
  });
@@ -3706,7 +3076,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3706
3076
  component: Transform,
3707
3077
  data: { pos: [1, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
3708
3078
  },
3709
- { component: MeshFilter, data: { assetHandle: internPrimitiveMesh(world, 'cube') } },
3079
+ { component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
3710
3080
  { component: MeshRenderer, data: { materials: [MATERIAL_SENTINEL] } },
3711
3081
  )
3712
3082
  .unwrap();
@@ -3734,7 +3104,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3734
3104
  component: Transform,
3735
3105
  data: { pos: [1, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
3736
3106
  },
3737
- { component: MeshFilter, data: { assetHandle: internPrimitiveMesh(world, 'cube') } },
3107
+ { component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
3738
3108
  { component: MeshRenderer, data: { materials: [MATERIAL_SENTINEL] } },
3739
3109
  )
3740
3110
  .unwrap();
@@ -3771,7 +3141,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3771
3141
  component: Transform,
3772
3142
  data: { pos: [1, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
3773
3143
  },
3774
- { component: MeshFilter, data: { assetHandle: internPrimitiveMesh(world, 'cube') } },
3144
+ { component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
3775
3145
  { component: MeshRenderer, data: { materials: [MATERIAL_SENTINEL] } },
3776
3146
  { component: ChildOf, data: { parent } },
3777
3147
  )
@@ -3808,7 +3178,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3808
3178
  component: Transform,
3809
3179
  data: { pos: [n, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
3810
3180
  },
3811
- { component: MeshFilter, data: { assetHandle: internPrimitiveMesh(world, 'cube') } },
3181
+ { component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
3812
3182
  { component: MeshRenderer, data: { materials: [MATERIAL_SENTINEL] } },
3813
3183
  { component: ChildOf, data: { parent } },
3814
3184
  )
@@ -3840,7 +3210,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3840
3210
  component: Transform,
3841
3211
  data: { pos: [0, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
3842
3212
  },
3843
- { component: MeshFilter, data: { assetHandle: internPrimitiveMesh(world, 'cube') } },
3213
+ { component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
3844
3214
  { component: MeshRenderer, data: { materials: [MATERIAL_SENTINEL] } },
3845
3215
  )
3846
3216
  .unwrap();
@@ -3871,7 +3241,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3871
3241
  component: Transform,
3872
3242
  data: { pos: [0, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
3873
3243
  },
3874
- { component: MeshFilter, data: { assetHandle: internPrimitiveMesh(world, 'cube') } },
3244
+ { component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
3875
3245
  { component: MeshRenderer, data: { materials: [MATERIAL_SENTINEL] } },
3876
3246
  )
3877
3247
  .unwrap();
@@ -3882,7 +3252,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3882
3252
  component: Transform,
3883
3253
  data: { pos: [2, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] },
3884
3254
  },
3885
- { component: MeshFilter, data: { assetHandle: internPrimitiveMesh(world, 'cube') } },
3255
+ { component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
3886
3256
  { component: MeshRenderer, data: { materials: [MATERIAL_SENTINEL] } },
3887
3257
  )
3888
3258
  .unwrap();
@@ -3896,9 +3266,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3896
3266
 
3897
3267
  {
3898
3268
  // --- from renderer-draw-world.test.ts ---
3899
- const ENGINE = new URL('../../../render/src/renderer/renderer-factory.ts', import.meta.url).href;
3900
- const RENDERER = new URL('../../../render/src/renderer/renderer-factory.ts', import.meta.url)
3901
- .href;
3269
+ const _ENGINE = new URL('../../../render/src/assembly/factory.ts', import.meta.url).href;
3270
+ const RENDERER = new URL('../../../render/src/assembly/factory.ts', import.meta.url).href;
3902
3271
 
3903
3272
  // ─── Mock helpers ───────────────────────────────────────────────────────────
3904
3273
 
@@ -3923,7 +3292,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3923
3292
  webgpu?: 'context' | 'null';
3924
3293
  }
3925
3294
 
3926
- function makeMockCanvas(opts: CanvasOptions): HTMLCanvasElement {
3295
+ function _makeMockCanvas(opts: CanvasOptions): HTMLCanvasElement {
3927
3296
  const canvas = {
3928
3297
  width: 800,
3929
3298
  height: 600,
@@ -3950,7 +3319,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3950
3319
  return canvas;
3951
3320
  }
3952
3321
 
3953
- function makeMockGPUDevice(): { device: unknown } {
3322
+ function _makeMockGPUDevice(): { device: unknown } {
3954
3323
  const lost = new Promise<unknown>(() => undefined);
3955
3324
  const device = {
3956
3325
  __mockTag: 'gpu-device',
@@ -3996,7 +3365,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
3996
3365
  return { device };
3997
3366
  }
3998
3367
 
3999
- function makeMockGPU(deviceObj: unknown): unknown {
3368
+ function _makeMockGPU(deviceObj: unknown): unknown {
4000
3369
  return {
4001
3370
  requestAdapter: async () => ({
4002
3371
  requestDevice: async () => deviceObj,
@@ -4007,7 +3376,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4007
3376
 
4008
3377
  const baseNavigator = { userAgent: 'mock-engine-test' } as unknown as Navigator;
4009
3378
 
4010
- function buildManifestDataUrl(): string {
3379
+ function _buildManifestDataUrl(): string {
4011
3380
  // feat-20260518-pbr-direct-lighting-mvp M5 / w22.9: createRenderer's
4012
3381
  // post-fallback path requires both pbr (`f_schlick(` marker) + unlit
4013
3382
  // entries; seed two minimal stubs (mock device's createShaderModule does
@@ -4064,8 +3433,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4064
3433
  '..',
4065
3434
  'render',
4066
3435
  'src',
4067
- 'renderer',
4068
- 'renderer-factory.ts',
3436
+ 'assembly',
3437
+ 'factory.ts',
4069
3438
  );
4070
3439
  const text = fs.readFileSync(rendererSrc, 'utf8');
4071
3440
  expect(text).not.toMatch(/interface RendererDrawTarget/);
@@ -4075,101 +3444,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4075
3444
  const mod = (await import(RENDERER)) as Record<string, unknown>;
4076
3445
  expect(mod.RendererDrawTarget).toBeUndefined();
4077
3446
  });
4078
-
4079
- it('Renderer.draw signature accepts a World instance, not a kind marker', async () => {
4080
- const { device } = makeMockGPUDevice();
4081
- vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
4082
- const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
4083
- const { createRenderer } = (await import(ENGINE)) as {
4084
- createRenderer: (
4085
- canvas: unknown,
4086
- opts?: unknown,
4087
- bundler?: unknown,
4088
- ) => Promise<{
4089
- draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
4090
- ready: Promise<void>;
4091
- }>;
4092
- };
4093
- const { World } = (await import('@forgeax/engine-ecs')) as { World: new () => unknown };
4094
- const renderer = await createRenderer(
4095
- canvas,
4096
- {},
4097
- { shaderManifestUrl: buildManifestDataUrl() },
4098
- );
4099
- await renderer.ready;
4100
- const world = new World();
4101
- const typedRenderer = renderer as unknown as RendererType;
4102
- const typedWorld = world as WorldType;
4103
- const attachment = typedRenderer.attachWorld(typedWorld);
4104
- if (!attachment.ok) throw attachment.error;
4105
- typedWorld.update().unwrap();
4106
- expect(typedRenderer.draw([typedWorld], { cameraOwner: 0, resourceOwner: 0 }).ok).toBe(true);
4107
- // Source-level guarantee: createRenderer.ts must use World as the draw
4108
- // parameter type after the K-4 rewrite (D-S2: RenderSystem walks the
4109
- // World query graph; charter proposition 5 single ECS-driven entry).
4110
- const fsId = 'node:fs';
4111
- const pathId = 'node:path';
4112
- const urlId = 'node:url';
4113
- const fs = (await import(/* @vite-ignore */ fsId)) as {
4114
- readFileSync: (p: string, enc: string) => string;
4115
- };
4116
- const path = (await import(/* @vite-ignore */ pathId)) as {
4117
- resolve: (...parts: string[]) => string;
4118
- dirname: (p: string) => string;
4119
- };
4120
- const url = (await import(/* @vite-ignore */ urlId)) as {
4121
- fileURLToPath: (u: string) => string;
4122
- };
4123
- const here = url.fileURLToPath(import.meta.url);
4124
- const createRendererSrc = path.resolve(
4125
- path.dirname(here),
4126
- '..',
4127
- '..',
4128
- '..',
4129
- 'render',
4130
- 'src',
4131
- 'renderer',
4132
- 'renderer-factory.ts',
4133
- );
4134
- const text = fs.readFileSync(createRendererSrc, 'utf8');
4135
- expect(text).toMatch(/draw\s*\(\s*\w+\s*:\s*readonly World\[\]/);
4136
- expect(text).not.toMatch(/draw\s*\(\s*\w+\s*:\s*RendererDrawTarget\b/);
4137
- });
4138
-
4139
- it('fires onError with rhi-not-available when draw(world) is called before ready settles', async () => {
4140
- const { device } = makeMockGPUDevice();
4141
- vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
4142
- const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
4143
- const { createRenderer } = (await import(ENGINE)) as {
4144
- createRenderer: (
4145
- canvas: unknown,
4146
- opts?: unknown,
4147
- bundler?: unknown,
4148
- ) => Promise<{
4149
- draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
4150
- ready: Promise<void>;
4151
- onError: (cb: (err: { code: string }) => void) => () => void;
4152
- }>;
4153
- };
4154
- const { World } = (await import('@forgeax/engine-ecs')) as { World: new () => unknown };
4155
-
4156
- const renderer = await createRenderer(
4157
- canvas,
4158
- {},
4159
- { shaderManifestUrl: buildManifestDataUrl() },
4160
- );
4161
- const world = new World();
4162
- const errors: { code: string }[] = [];
4163
- renderer.onError((e) => errors.push(e));
4164
-
4165
- // Call draw before awaiting ready - must fire onError + skip frame.
4166
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
4167
- throw new Error('World attachment failed');
4168
- }
4169
- (world as WorldType).update().unwrap();
4170
- renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 });
4171
- expect(errors.some((e) => e.code === 'rhi-not-available')).toBe(true);
4172
- });
4173
3447
  });
4174
3448
 
4175
3449
  // bug-20260519 AC-03: when a world *does* carry a `MeshRenderer` entity but
@@ -4185,132 +3459,11 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4185
3459
  // `baseColor` but zero passes — the extract stage
4186
3460
  // walks the parent chain (no parent), finds zero passes, and fires
4187
3461
  // `material-no-effective-pass` with a recovery hint for adding a pass.
4188
- describe('Renderer.draw(world) bug-20260519 zero-manifest mesh path (AC-03)', () => {
4189
- it('MeshRenderer + no shaderManifestUrl -> render-time fires material-no-effective-pass', async () => {
4190
- const { device } = makeMockGPUDevice();
4191
- vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
4192
- const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
4193
- const { createRenderer } = (await import(ENGINE)) as {
4194
- createRenderer: (
4195
- canvas: unknown,
4196
- opts?: unknown,
4197
- bundler?: unknown,
4198
- ) => Promise<{
4199
- draw: (worlds: unknown, opts: { cameraOwner: number; resourceOwner: number }) => void;
4200
- ready: Promise<unknown>;
4201
- onError: (cb: (err: { code: string; hint?: string }) => void) => () => void;
4202
- assets: {
4203
- register: (asset: unknown) => { unwrap: () => unknown };
4204
- };
4205
- }>;
4206
- };
4207
- const { World } = (await import('@forgeax/engine-ecs')) as { World: new () => unknown };
4208
- const { registerPropagateTransforms } = await import('@forgeax/engine-scene');
4209
- const components = {
4210
- ...(await import(new URL('../../../render/src/index.ts', import.meta.url).href)),
4211
- ...(await import('@forgeax/engine-scene')),
4212
- } as {
4213
- Transform: unknown;
4214
- Camera: unknown;
4215
- MeshFilter: unknown;
4216
- MeshRenderer: unknown;
4217
- };
4218
- // Zero-manifest path: explicit `shaderManifestUrl: undefined` opts into
4219
- // zero-entry mode. After feat-20260529 D-3, the material's zero-passes
4220
- // surface is caught at extract (not record):
4221
- // - `await renderer.ready` resolves Result.ok (Step 2 guard skips
4222
- // dual createShaderModule because `registry.entries().length === 0`)
4223
- // - on `renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 })` the per-entity loop in
4224
- // `render-system-extract.ts` walks the material parent chain (none),
4225
- // finds zero passes, and fires
4226
- // `material-no-effective-pass` (MaterialError).
4227
- const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
4228
- const ready = (await renderer.ready) as { ok: boolean };
4229
- expect(ready.ok).toBe(true);
4230
-
4231
- // Spawn a Camera (so `cameras.length === 1`) plus a single MeshRenderer
4232
- // entity (so `validated.length > 0` reaches the pipeline-pick branch).
4233
- const world = new World() as {
4234
- spawn: (...components: unknown[]) => { unwrap: () => unknown };
4235
- allocSharedRef: (target: string, payload: unknown) => number;
4236
- update: (delta: number) => { ok: boolean };
4237
- };
4238
- registerPropagateTransforms(world as never);
4239
-
4240
- // Mint a material with zero passes (no parent chain) as a user-tier column
4241
- // handle. The extract stage's material walk returns Err with
4242
- // `material-no-effective-pass`.
4243
- const matHandle = world.allocSharedRef('MaterialAsset', {
4244
- kind: 'material',
4245
- baseColor: [1, 0, 0, 1],
4246
- });
4247
- world
4248
- .spawn(
4249
- {
4250
- component: components.Transform,
4251
- data: {
4252
- pos: [0, 0, 5],
4253
- quat: [0, 0, 0, 1],
4254
- scale: [1, 1, 1],
4255
- },
4256
- },
4257
- {
4258
- component: components.Camera,
4259
- data: {
4260
- fov: Math.PI / 4,
4261
- aspect: 1,
4262
- near: 0.1,
4263
- far: 100,
4264
- projection: 0,
4265
- left: -1,
4266
- right: 1,
4267
- bottom: -1,
4268
- top: 1,
4269
- },
4270
- },
4271
- )
4272
- .unwrap();
4273
- world
4274
- .spawn(
4275
- {
4276
- component: components.Transform,
4277
- data: {
4278
- pos: [0, 0, 0],
4279
- quat: [0, 0, 0, 1],
4280
- scale: [1, 1, 1],
4281
- },
4282
- },
4283
- {
4284
- component: components.MeshFilter,
4285
- data: {
4286
- assetHandle: world.allocSharedRef(
4287
- 'MeshAsset',
4288
- createPrimitiveMesh('triangle').unwrap(),
4289
- ),
4290
- },
4291
- },
4292
- { component: components.MeshRenderer, data: { materials: [matHandle] } },
4293
- )
4294
- .unwrap();
4295
-
4296
- expect(world.update(1 / 60).ok).toBe(true);
4297
-
4298
- if (!(renderer as unknown as RendererType).attachWorld(world as WorldType).ok) {
4299
- throw new Error('World attachment failed');
4300
- }
4301
-
4302
- (world as WorldType).update().unwrap();
4303
-
4304
- // The host-owned draw boundary isolates the invalid material and keeps
4305
- // the call non-throwing; ECS no longer exposes a user error callback.
4306
- expect(() => renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 })).not.toThrow();
4307
- });
4308
- });
4309
3462
  }
4310
3463
 
4311
3464
  {
4312
3465
  // --- from renderer-input-snapshot.test.ts ---
4313
- const ENGINE = new URL('../../../render/src/renderer/renderer-factory.ts', import.meta.url).href;
3466
+ const _ENGINE = new URL('../../../render/src/assembly/factory.ts', import.meta.url).href;
4314
3467
 
4315
3468
  interface MockGL2Context {
4316
3469
  __mockTag: 'webgl2';
@@ -4328,7 +3481,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4328
3481
  };
4329
3482
  }
4330
3483
 
4331
- function makeMockCanvas(): HTMLCanvasElement {
3484
+ function _makeMockCanvas(): HTMLCanvasElement {
4332
3485
  const listeners = new Map<string, Set<(e: unknown) => void>>();
4333
3486
  const canvas = {
4334
3487
  width: 800,
@@ -4359,7 +3512,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4359
3512
  return canvas;
4360
3513
  }
4361
3514
 
4362
- function makeMockGPU(): unknown {
3515
+ function _makeMockGPU(): unknown {
4363
3516
  return {
4364
3517
  requestAdapter: async () => ({
4365
3518
  requestDevice: async () => ({
@@ -4391,88 +3544,11 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4391
3544
  getPreferredCanvasFormat: () => 'bgra8unorm',
4392
3545
  };
4393
3546
  }
4394
-
4395
- describe('renderer.input.snapshot(world) (V-2 first-class shim, D-2 + AC-09)', () => {
4396
- beforeEach(() => {
4397
- vi.stubGlobal('navigator', { gpu: makeMockGPU() });
4398
- });
4399
- afterEach(() => {
4400
- vi.unstubAllGlobals();
4401
- });
4402
-
4403
- it('exposes input.snapshot as a function on the Renderer surface', async () => {
4404
- const canvas = makeMockCanvas();
4405
- const { createRenderer } = (await import(ENGINE)) as {
4406
- createRenderer: (
4407
- canvas: unknown,
4408
- opts?: { shaderManifestUrl?: string | undefined },
4409
- bundler?: unknown,
4410
- ) => Promise<{ input: { snapshot: unknown } }>;
4411
- };
4412
- const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
4413
- expect(renderer.input).toBeDefined();
4414
- expect(typeof renderer.input.snapshot).toBe('function');
4415
- });
4416
-
4417
- it('returns the InputSnapshot Resource attached to the supplied World', async () => {
4418
- const canvas = makeMockCanvas();
4419
- const { createRenderer } = (await import(ENGINE)) as {
4420
- createRenderer: (
4421
- canvas: unknown,
4422
- opts?: { shaderManifestUrl?: string | undefined },
4423
- bundler?: unknown,
4424
- ) => Promise<{ input: { snapshot: (world: World) => InputSnapshot | undefined } }>;
4425
- };
4426
- const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
4427
- const world = new World();
4428
- const seeded = createInputSnapshot();
4429
- world.insertResource(INPUT_SNAPSHOT_RESOURCE_KEY, seeded);
4430
- const got: InputSnapshot | undefined = renderer.input.snapshot(world);
4431
- // Identity check: curried reader is a thin facade over getResource.
4432
- expect(got).toBe(seeded);
4433
- });
4434
-
4435
- it('returns undefined when the World has no InputSnapshot resource (P3 empty)', async () => {
4436
- const canvas = makeMockCanvas();
4437
- const { createRenderer } = (await import(ENGINE)) as {
4438
- createRenderer: (
4439
- canvas: unknown,
4440
- opts?: { shaderManifestUrl?: string | undefined },
4441
- bundler?: unknown,
4442
- ) => Promise<{ input: { snapshot: (world: World) => InputSnapshot | undefined } }>;
4443
- };
4444
- const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
4445
- const world = new World();
4446
- expect(renderer.input.snapshot(world)).toBeUndefined();
4447
- });
4448
-
4449
- it('does not persist World between calls (P5 producer/consumer split)', async () => {
4450
- const canvas = makeMockCanvas();
4451
- const { createRenderer } = (await import(ENGINE)) as {
4452
- createRenderer: (
4453
- canvas: unknown,
4454
- opts?: { shaderManifestUrl?: string | undefined },
4455
- bundler?: unknown,
4456
- ) => Promise<{ input: { snapshot: (world: World) => InputSnapshot | undefined } }>;
4457
- };
4458
- const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
4459
- // Two distinct worlds, two distinct snapshots; the curried reader picks
4460
- // the World argument verbatim instead of holding an internal reference.
4461
- const wA = new World();
4462
- const wB = new World();
4463
- const sA = createInputSnapshot();
4464
- const sB = createInputSnapshot();
4465
- wA.insertResource(INPUT_SNAPSHOT_RESOURCE_KEY, sA);
4466
- wB.insertResource(INPUT_SNAPSHOT_RESOURCE_KEY, sB);
4467
- expect(renderer.input.snapshot(wA)).toBe(sA);
4468
- expect(renderer.input.snapshot(wB)).toBe(sB);
4469
- });
4470
- });
4471
3547
  }
4472
3548
 
4473
3549
  {
4474
3550
  // --- from renderer-read-pixels.test.ts ---
4475
- const ENGINE = new URL('../../../render/src/renderer/renderer-factory.ts', import.meta.url).href;
3551
+ const ENGINE = new URL('../../../render/src/assembly/factory.ts', import.meta.url).href;
4476
3552
 
4477
3553
  interface CanvasOptions {
4478
3554
  webgl2: 'context' | 'null';
@@ -4482,7 +3558,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4482
3558
  const CANVAS_W = 8;
4483
3559
  const CANVAS_H = 8;
4484
3560
 
4485
- function makeMockCanvas(opts: CanvasOptions): HTMLCanvasElement {
3561
+ function _makeMockCanvas(opts: CanvasOptions): HTMLCanvasElement {
4486
3562
  const canvas = {
4487
3563
  width: CANVAS_W,
4488
3564
  height: CANVAS_H,
@@ -4567,7 +3643,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4567
3643
 
4568
3644
  const baseNavigator = { userAgent: 'mock-engine-test' } as unknown as Navigator;
4569
3645
 
4570
- function buildManifestDataUrl(): string {
3646
+ function _buildManifestDataUrl(): string {
4571
3647
  // feat-20260518-pbr-direct-lighting-mvp M5 / w22.9: createRenderer's
4572
3648
  // post-fallback path requires both pbr (`f_schlick(` marker) + unlit
4573
3649
  // entries; seed two minimal stubs (mock device's createShaderModule does
@@ -4603,7 +3679,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4603
3679
  getContext: (kind: string, opts?: unknown) => Ctx2dStub | null;
4604
3680
  }
4605
3681
 
4606
- function installBitmapStubs(opts: {
3682
+ function _installBitmapStubs(opts: {
4607
3683
  failBitmap?: false | Error;
4608
3684
  ctx2dReturn?: Ctx2dStub | null;
4609
3685
  }): {
@@ -4660,7 +3736,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4660
3736
  return (await import(ENGINE)) as never;
4661
3737
  }
4662
3738
 
4663
- async function setupWebGPU(): Promise<{
3739
+ async function _setupWebGPU(): Promise<{
4664
3740
  createRenderer: (
4665
3741
  canvas: unknown,
4666
3742
  opts?: unknown,
@@ -4671,86 +3747,11 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4671
3747
  vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
4672
3748
  return importEngine();
4673
3749
  }
4674
-
4675
- describe('Renderer.readPixels()', () => {
4676
- it('WebGPU happy path returns Result.ok(Uint8Array) of length canvas.width * canvas.height * 4', async () => {
4677
- const { createRenderer } = await setupWebGPU();
4678
- installBitmapStubs({
4679
- ctx2dReturn: {
4680
- drawImage: () => undefined,
4681
- getImageData: (_x, _y, w, h) => ({
4682
- data: new Uint8ClampedArray(w * h * 4).fill(128),
4683
- }),
4684
- },
4685
- });
4686
- const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
4687
- const renderer = await createRenderer(
4688
- canvas,
4689
- {},
4690
- { shaderManifestUrl: buildManifestDataUrl() },
4691
- );
4692
- await renderer.ready;
4693
-
4694
- const result = await renderer.readPixels();
4695
- expect(result.ok).toBe(true);
4696
- if (!result.ok) throw new Error('unreachable');
4697
- expect(result.value).toBeInstanceOf(Uint8Array);
4698
- expect(result.value.length).toBe(CANVAS_W * CANVAS_H * 4);
4699
- // Sample value is 128 (the fill we placed in the mock).
4700
- expect(result.value[0]).toBe(128);
4701
- });
4702
-
4703
- it('OffscreenCanvas 2D ctx unavailable returns webgpu-runtime-error with detail.error message', async () => {
4704
- const { createRenderer } = await setupWebGPU();
4705
- installBitmapStubs({ ctx2dReturn: null });
4706
- const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
4707
- const renderer = await createRenderer(
4708
- canvas,
4709
- {},
4710
- { shaderManifestUrl: buildManifestDataUrl() },
4711
- );
4712
- await renderer.ready;
4713
-
4714
- const result = await renderer.readPixels();
4715
- expect(result.ok).toBe(false);
4716
- if (result.ok) throw new Error('unreachable');
4717
- expect(result.error.code).toBe('webgpu-runtime-error');
4718
- const detailErr = result.error.detail?.error;
4719
- expect(detailErr).toBeDefined();
4720
- // detail.error is widened to RhiError | { code: string; message: string; name?: string; };
4721
- // Both branches carry .message (RhiError has it via Error base class).
4722
- if (detailErr && typeof detailErr === 'object') {
4723
- expect((detailErr as { message: string }).message).toContain('null');
4724
- }
4725
- });
4726
-
4727
- it('createImageBitmap throw surfaces as webgpu-runtime-error with thrown message in detail.error', async () => {
4728
- const { createRenderer } = await setupWebGPU();
4729
- installBitmapStubs({ failBitmap: new Error('mock: createImageBitmap rejected by canvas') });
4730
- const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
4731
- const renderer = await createRenderer(
4732
- canvas,
4733
- {},
4734
- { shaderManifestUrl: buildManifestDataUrl() },
4735
- );
4736
- await renderer.ready;
4737
-
4738
- const result = await renderer.readPixels();
4739
- expect(result.ok).toBe(false);
4740
- if (result.ok) throw new Error('unreachable');
4741
- expect(result.error.code).toBe('webgpu-runtime-error');
4742
- const detailErr = result.error.detail?.error;
4743
- expect(detailErr).toBeDefined();
4744
- if (detailErr && typeof detailErr === 'object') {
4745
- expect((detailErr as { message: string }).message).toContain('createImageBitmap rejected');
4746
- }
4747
- });
4748
- });
4749
3750
  }
4750
3751
 
4751
3752
  {
4752
3753
  // --- from renderer-ready.test.ts ---
4753
- const ENGINE = new URL('../../../render/src/renderer/renderer-factory.ts', import.meta.url).href;
3754
+ const _ENGINE = new URL('../../../render/src/assembly/factory.ts', import.meta.url).href;
4754
3755
 
4755
3756
  // ─── Mock helpers (mirrors createRenderer.test.ts shape) ────────────────────
4756
3757
 
@@ -4775,7 +3776,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4775
3776
  webgpu?: 'context' | 'null';
4776
3777
  }
4777
3778
 
4778
- function makeMockCanvas(opts: CanvasOptions): HTMLCanvasElement {
3779
+ function _makeMockCanvas(opts: CanvasOptions): HTMLCanvasElement {
4779
3780
  const canvas = {
4780
3781
  width: 800,
4781
3782
  height: 600,
@@ -4815,7 +3816,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4815
3816
  failWriteBuffer?: boolean;
4816
3817
  }
4817
3818
 
4818
- function makeMockGPUDevice(
3819
+ function _makeMockGPUDevice(
4819
3820
  log: DeviceCallLog,
4820
3821
  overrides: DeviceOverrides = {},
4821
3822
  ): {
@@ -4909,7 +3910,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4909
3910
  return { device };
4910
3911
  }
4911
3912
 
4912
- function makeMockGPU(deviceObj: unknown): unknown {
3913
+ function _makeMockGPU(deviceObj: unknown): unknown {
4913
3914
  return {
4914
3915
  requestAdapter: async () => ({
4915
3916
  requestDevice: async () => deviceObj,
@@ -4928,7 +3929,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4928
3929
  // createShaderModule does not parse WGSL syntax, the runtime test only
4929
3930
  // requires that two distinct entries with the right content markers
4930
3931
  // exist.
4931
- function buildManifestDataUrl(): string {
3932
+ function _buildManifestDataUrl(): string {
4932
3933
  const manifest = {
4933
3934
  schemaVersion: '1.0.0',
4934
3935
  entries: [
@@ -4965,150 +3966,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
4965
3966
 
4966
3967
  // ─── Tests ──────────────────────────────────────────────────────────────────
4967
3968
 
4968
- describe('Renderer.ready - WebGPU path three-step serial (D-S3)', () => {
4969
- it('exposes a readonly `ready: Promise<void>` property', async () => {
4970
- const log: DeviceCallLog = { order: [] };
4971
- const { device } = makeMockGPUDevice(log);
4972
- vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
4973
- const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
4974
- const { createRenderer } = (await import(ENGINE)) as {
4975
- createRenderer: (
4976
- canvas: unknown,
4977
- opts?: unknown,
4978
- bundler?: unknown,
4979
- ) => Promise<{ ready: Promise<void> }>;
4980
- };
4981
-
4982
- const renderer = await createRenderer(
4983
- canvas,
4984
- {},
4985
- { shaderManifestUrl: buildManifestDataUrl() },
4986
- );
4987
- expect(renderer.ready).toBeInstanceOf(Promise);
4988
- });
4989
-
4990
- it('resolves only after manifest load -> pipeline compile -> asset upload (in order)', async () => {
4991
- const log: DeviceCallLog = { order: [] };
4992
- const { device } = makeMockGPUDevice(log);
4993
- vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
4994
- const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
4995
- const { createRenderer } = (await import(ENGINE)) as {
4996
- createRenderer: (
4997
- canvas: unknown,
4998
- opts?: unknown,
4999
- bundler?: unknown,
5000
- ) => Promise<{ ready: Promise<void> }>;
5001
- };
5002
-
5003
- const renderer = await createRenderer(
5004
- canvas,
5005
- {},
5006
- { shaderManifestUrl: buildManifestDataUrl() },
5007
- );
5008
- await renderer.ready;
5009
-
5010
- // Order assertion: createShaderModule (manifest-driven pipeline step 1)
5011
- // must happen before createBuffer (asset upload step 3). Specifically,
5012
- // the very first create* call belongs to step 2 pipeline (after manifest
5013
- // load completes async), and createBuffer runs after every pipeline call.
5014
- const firstShader = log.order.indexOf('createShaderModule');
5015
- const firstBuffer = log.order.indexOf('createBuffer');
5016
- const firstWrite = log.order.indexOf('queue.writeBuffer');
5017
- expect(firstShader).toBeGreaterThanOrEqual(0);
5018
- expect(firstBuffer).toBeGreaterThan(firstShader);
5019
- expect(firstWrite).toBeGreaterThan(firstBuffer);
5020
-
5021
- // Three BindGroupLayouts must be created (view / material / mesh-array)
5022
- // before createPipelineLayout aggregates them. (bug-20260519 merge:
5023
- // feat-20260519-tonemap-reinhard-mvp added a second createPipelineLayout
5024
- // call for the post-process tonemap pipeline whose own BGL is built
5025
- // *after* the geometry pipeline-layout has already been aggregated, so
5026
- // `lastIndexOf('createBindGroupLayout')` may now sit between the two
5027
- // PLs. Constrain to the geometry chain only — slice the prefix up to
5028
- // the first PL and assert the count of BGLs in that prefix is >= 3.)
5029
- const bglCount = log.order.filter((c) => c === 'createBindGroupLayout').length;
5030
- expect(bglCount).toBeGreaterThanOrEqual(3);
5031
- const firstPipelineLayout = log.order.indexOf('createPipelineLayout');
5032
- const bglCountBeforeFirstPL = log.order
5033
- .slice(0, firstPipelineLayout)
5034
- .filter((c) => c === 'createBindGroupLayout').length;
5035
- expect(bglCountBeforeFirstPL).toBeGreaterThanOrEqual(3);
5036
- });
5037
-
5038
- it('settles with err shader-compile-failed when shader module creation fails (w24)', async () => {
5039
- const log: DeviceCallLog = { order: [] };
5040
- const { device } = makeMockGPUDevice(log, { failShaderModule: true });
5041
- vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
5042
- const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
5043
- const { createRenderer } = (await import(ENGINE)) as {
5044
- createRenderer: (
5045
- canvas: unknown,
5046
- opts?: unknown,
5047
- bundler?: unknown,
5048
- ) => Promise<{ ready: Promise<unknown> }>;
5049
- };
5050
-
5051
- const renderer = await createRenderer(
5052
- canvas,
5053
- {},
5054
- { shaderManifestUrl: buildManifestDataUrl() },
5055
- );
5056
- // w24 — Renderer.ready now resolves Result<void, RhiError>; AI users
5057
- // branch on `.ok` rather than try/catch the await.
5058
- const ready = (await renderer.ready) as { ok: boolean; error?: { code: string } };
5059
- expect(ready.ok).toBe(false);
5060
- expect(ready.error?.code).toMatch(
5061
- /shader-compile-failed|manifest-malformed|shader-not-found/,
5062
- );
5063
- });
5064
-
5065
- it('settles with err when render pipeline creation fails (w24)', async () => {
5066
- const log: DeviceCallLog = { order: [] };
5067
- const { device } = makeMockGPUDevice(log, { failRenderPipeline: true });
5068
- vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
5069
- const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
5070
- const { createRenderer } = (await import(ENGINE)) as {
5071
- createRenderer: (
5072
- canvas: unknown,
5073
- opts?: unknown,
5074
- bundler?: unknown,
5075
- ) => Promise<{ ready: Promise<unknown> }>;
5076
- };
5077
-
5078
- const renderer = await createRenderer(
5079
- canvas,
5080
- {},
5081
- { shaderManifestUrl: buildManifestDataUrl() },
5082
- );
5083
- const ready = (await renderer.ready) as { ok: boolean; error?: { code: string } };
5084
- expect(ready.ok).toBe(false);
5085
- expect(ready.error?.code).toBeDefined();
5086
- });
5087
-
5088
- it('settles with err when asset upload (createBuffer) fails (w24)', async () => {
5089
- const log: DeviceCallLog = { order: [] };
5090
- const { device } = makeMockGPUDevice(log, { failBuffer: true });
5091
- vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
5092
- const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
5093
- const { createRenderer } = (await import(ENGINE)) as {
5094
- createRenderer: (
5095
- canvas: unknown,
5096
- opts?: unknown,
5097
- bundler?: unknown,
5098
- ) => Promise<{ ready: Promise<unknown> }>;
5099
- };
5100
-
5101
- const renderer = await createRenderer(
5102
- canvas,
5103
- {},
5104
- { shaderManifestUrl: buildManifestDataUrl() },
5105
- );
5106
- const ready = (await renderer.ready) as { ok: boolean; error?: { code: string } };
5107
- expect(ready.ok).toBe(false);
5108
- expect(ready.error?.code).toBeDefined();
5109
- });
5110
- });
5111
-
5112
3969
  // bug-20260519 AC-02: Camera-only world / clear-pass-only path must NOT
5113
3970
  // force shader-compile when the caller does not pass `shaderManifestUrl`.
5114
3971
  // The fix lands in `createRenderer.ts` Step 2 guard + `RendererOptions`
@@ -5118,35 +3975,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
5118
3975
  // minimal LO 1.1 hello-window equivalent (Engine.create + clearColor +
5119
3976
  // no shader manifest) reach `Result.ok` without configuring vite-plugin-
5120
3977
  // shader. Plan-strategy D-1 + D-2 + D-3.
5121
- describe('Renderer.ready - bug-20260519 zero-manifest path (AC-02)', () => {
5122
- it('Camera-only world + no shaderManifestUrl -> ready ok + 0 createShaderModule', async () => {
5123
- const log: DeviceCallLog = { order: [] };
5124
- const { device } = makeMockGPUDevice(log);
5125
- vi.stubGlobal('navigator', { ...baseNavigator, gpu: makeMockGPU(device) });
5126
- const canvas = makeMockCanvas({ webgl2: 'context', webgpu: 'context' });
5127
- const { createRenderer } = (await import(ENGINE)) as {
5128
- createRenderer: (
5129
- canvas: unknown,
5130
- opts?: unknown,
5131
- bundler?: unknown,
5132
- ) => Promise<{ ready: Promise<unknown> }>;
5133
- };
5134
-
5135
- // Explicit `shaderManifestUrl: undefined` opts into zero-entry mode
5136
- // (D-7: the AC-02 case is the "no manifest configured" path).
5137
- // ShaderRegistry yields zero entries -> Step 2 guard skips
5138
- // createShaderModule entirely.
5139
- const renderer = await createRenderer(canvas, {}, { shaderManifestUrl: undefined });
5140
- const ready = (await renderer.ready) as { ok: boolean; error?: unknown };
5141
-
5142
- expect(ready.ok).toBe(true);
5143
- // Spy log: zero createShaderModule invocations -- the dual-compile
5144
- // (PBR + unlit) block in `buildReadyWebGPU` Step 2 must be gated
5145
- // behind `registry.entries().length > 0` so the zero-entry manifest
5146
- // skips it entirely (charter P3 + D-3 nullable PipelineState).
5147
- expect(log.order).not.toContain('createShaderModule');
5148
- });
5149
- });
5150
3978
  }
5151
3979
 
5152
3980
  {
@@ -5411,33 +4239,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
5411
4239
  expect(fragment.targets[0]?.blend).toEqual(blend);
5412
4240
  });
5413
4241
 
5414
- it('(d) AC-08: stencil reaches createRenderPipeline depthStencil descriptor', () => {
5415
- const mocks = makeMocks();
5416
- const ctx = makeMockContext(mocks);
5417
- const entry = makeMockEntry();
5418
-
5419
- const stencilFace: StencilFaceState = {
5420
- compare: 'always',
5421
- failOp: 'keep',
5422
- depthFailOp: 'keep',
5423
- passOp: 'replace',
5424
- };
5425
- const rs: MaterialRenderState = { stencil: stencilFace };
5426
- buildPipelineForMaterialShader('test::stencil', entry, ctx, rs);
5427
-
5428
- const desc = (mocks.createRenderPipeline.mock.calls as unknown[][])[0]?.[0] as Record<
5429
- string,
5430
- unknown
5431
- >;
5432
-
5433
- const depthStencil = desc.depthStencil as {
5434
- stencilFront?: typeof stencilFace;
5435
- stencilBack?: typeof stencilFace;
5436
- };
5437
- expect(depthStencil.stencilFront).toEqual(stencilFace);
5438
- expect(depthStencil.stencilBack).toEqual(stencilFace);
5439
- });
5440
-
5441
4242
  it('(d) AC-08: undefined renderState falls back to engine defaults', () => {
5442
4243
  const mocks = makeMocks();
5443
4244
  const ctx = makeMockContext(mocks);
@@ -5547,7 +4348,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
5547
4348
  }
5548
4349
 
5549
4350
  {
5550
- // --- from gpu-resource-store.test.ts (__tests__/) ---
4351
+ // --- from device/gpu-residency.test.ts (__tests__/) ---
5551
4352
  const okShim = <T>(v: T) => ({ ok: true as const, value: v });
5552
4353
 
5553
4354
  interface DeviceProbe {
@@ -5642,17 +4443,11 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
5642
4443
  }
5643
4444
 
5644
4445
  function meshPod(verts = 4): MeshAsset {
5645
- const attributes = {
5646
- position: new Float32Array(verts * 3),
5647
- normal: new Float32Array(verts * 3),
5648
- uv: new Float32Array(verts * 2),
5649
- tangent: new Float32Array(verts * 4),
5650
- };
5651
4446
  return {
5652
4447
  kind: 'mesh',
5653
4448
  vertices: new Float32Array(verts * 12),
5654
4449
  indices: new Uint16Array([0, 1, 2, 0, 2, 3]),
5655
- attributes,
4450
+ attributes: buildMeshAttributeMapForUvSets(1),
5656
4451
  aabb: new Float32Array(6),
5657
4452
  submeshes: [
5658
4453
  {
@@ -5677,13 +4472,13 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
5677
4472
  };
5678
4473
  }
5679
4474
 
5680
- function configured(probe: DeviceProbe): GpuResourceStore {
5681
- const store = new GpuResourceStore();
4475
+ function configured(probe: DeviceProbe): GpuResidencyCache {
4476
+ const store = new GpuResidencyCache();
5682
4477
  store.configureGpuDevice(makeMockDevice(probe), shaderFactory, makeRegisterCube(), mockCaps);
5683
4478
  return store;
5684
4479
  }
5685
4480
 
5686
- describe('GpuResourceStore residency', () => {
4481
+ describe('GpuResidencyCache residency', () => {
5687
4482
  it('(1)+(2) mesh ensureResident miss builds buffers, hit is O(1) (same buffers)', () => {
5688
4483
  const probe = freshProbe();
5689
4484
  const store = configured(probe);
@@ -5704,100 +4499,8 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
5704
4499
  }
5705
4500
  });
5706
4501
 
5707
- it('(3) texture ensureResident miss (mipmap=false) builds texture + view synchronously', () => {
5708
- const probe = freshProbe();
5709
- const store = configured(probe);
5710
- const handle = toShared<'TextureAsset'>(2048);
5711
- const pod = texturePod(false);
5712
-
5713
- const res = store.ensureResident(handle, pod);
5714
- expect(res.ok).toBe(true);
5715
- expect(probe.textures).toBe(1);
5716
- expect(store.getTextureGpuView(handle)).toBeDefined();
5717
-
5718
- // Hit: no second texture allocation.
5719
- const again = store.ensureResident(handle, pod);
5720
- expect(again.ok).toBe(true);
5721
- expect(probe.textures).toBe(1);
5722
- });
5723
-
5724
- it('(4) accessors return cached entry; miss returns undefined', () => {
5725
- const probe = freshProbe();
5726
- const store = configured(probe);
5727
- const meshHandle = toShared<'MeshAsset'>(1024);
5728
- const texHandle = toShared<'TextureAsset'>(2048);
5729
-
5730
- // Miss before residency.
5731
- expect(store.getMeshGpuHandles(meshHandle)).toBeUndefined();
5732
- expect(store.getTextureGpuView(texHandle)).toBeUndefined();
5733
- expect(store.getCubemapGpuView(toShared<'EquirectAsset'>(9))).toBeUndefined();
5734
- expect(store.getCubemapGpuTexture(toShared<'EquirectAsset'>(9))).toBeUndefined();
5735
- expect(store.getCubemapFaceViews(toShared<'EquirectAsset'>(9))).toBeUndefined();
5736
-
5737
- store.ensureResident(meshHandle, meshPod());
5738
- store.ensureResident(texHandle, texturePod(false));
5739
- expect(store.getMeshGpuHandles(meshHandle)).toBeDefined();
5740
- expect(store.getTextureGpuView(texHandle)).toBeDefined();
5741
- });
5742
-
5743
- it('(5) cubemapIdempotentMap: same source handle returns the same cube handle', async () => {
5744
- const probe = freshProbe();
5745
- const store = configured(probe);
5746
- const srcHandle = toShared<'EquirectAsset'>(2048);
5747
- const srcPod: EquirectAsset = {
5748
- kind: 'equirect',
5749
- width: 8,
5750
- height: 4,
5751
- format: 'rgba16float',
5752
- data: new Uint8Array(8 * 4 * 8),
5753
- colorSpace: 'linear',
5754
- };
5755
-
5756
- // biome-ignore lint/suspicious/noExplicitAny: package-internal projection reached via store cast
5757
- const r1 = await (store as any)._uploadCubemapFromEquirect(new World(), srcHandle, srcPod);
5758
- // biome-ignore lint/suspicious/noExplicitAny: package-internal projection reached via store cast
5759
- const r2 = await (store as any)._uploadCubemapFromEquirect(new World(), srcHandle, srcPod);
5760
- expect(r1.ok && r2.ok).toBe(true);
5761
- if (r1.ok && r2.ok) {
5762
- // Second call is the idempotent cache hit -> identical cube handle.
5763
- expect(JSON.stringify(r1.value)).toBe(JSON.stringify(r2.value));
5764
- // getCubemapGpuTexture resolves the cube handle (D-3 single-call contract).
5765
- expect(store.getCubemapGpuTexture(r1.value)).toBeDefined();
5766
- }
5767
- });
5768
-
5769
- it('(6) prewarmMipmapPipeline then mipmap=true texture ensureResident builds synchronously', async () => {
5770
- const probe = freshProbe();
5771
- const device = makeMockDevice(probe);
5772
- const store = new GpuResourceStore();
5773
- store.configureGpuDevice(device, shaderFactory, makeRegisterCube(), mockCaps);
5774
- // Prewarm so the sync blit reads the cached pipeline (no lazy await).
5775
- const prewarm = await store.prewarmMipmapPipeline(device, ['rgba8unorm-srgb']);
5776
- expect(prewarm.ok).toBe(true);
5777
-
5778
- const handle = toShared<'TextureAsset'>(2048);
5779
- const pod = texturePod(true, 'rgba8unorm-srgb');
5780
- const res = store.ensureResident(handle, pod);
5781
- expect(res.ok).toBe(true);
5782
- // The synchronous mipmap blit submitted a command buffer.
5783
- expect(probe.submits).toBeGreaterThanOrEqual(1);
5784
- });
5785
-
5786
- it('(7a) un-prewarmed mipmap format on the sync path returns structured RhiError (no lazy await)', () => {
5787
- const probe = freshProbe();
5788
- const store = configured(probe); // device wired, but NO prewarm
5789
- const handle = toShared<'TextureAsset'>(2048);
5790
- const pod = texturePod(true, 'rgba8unorm-srgb'); // mipmap=true, never prewarmed
5791
-
5792
- const res = store.ensureResident(handle, pod);
5793
- expect(res.ok).toBe(false);
5794
- if (!res.ok) {
5795
- expect(res.error.code).toBe('rhi-not-available');
5796
- }
5797
- });
5798
-
5799
4502
  it('(7b) no-device ensureResident returns a structured error (OOS-3 legacy degradation made explicit)', () => {
5800
- const store = new GpuResourceStore(); // configureGpuDevice never called
4503
+ const store = new GpuResidencyCache(); // configureGpuDevice never called
5801
4504
  const meshRes = store.ensureResident(toShared<'MeshAsset'>(1024), meshPod());
5802
4505
  expect(meshRes.ok).toBe(false);
5803
4506
  const texRes = store.ensureResident(toShared<'TextureAsset'>(2048), texturePod(false));
@@ -5816,21 +4519,12 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
5816
4519
  const COPY_DST = 0x2;
5817
4520
  const RENDER_ATTACHMENT = 0x10;
5818
4521
 
5819
- function canonicalAttributes(vertexCount: number): MeshAsset['attributes'] {
5820
- return {
5821
- position: new Float32Array(vertexCount * 3),
5822
- normal: new Float32Array(vertexCount * 3),
5823
- uv: new Float32Array(vertexCount * 2),
5824
- tangent: new Float32Array(vertexCount * 4),
5825
- };
5826
- }
5827
-
5828
4522
  function meshPod(overrides: Partial<MeshAsset> = {}): MeshAsset {
5829
4523
  return {
5830
4524
  kind: 'mesh',
5831
4525
  vertices: new Float32Array(4 * 12),
5832
4526
  indices: new Uint16Array([0, 1, 2, 0, 2, 3]),
5833
- attributes: canonicalAttributes(4),
4527
+ attributes: buildMeshAttributeMapForUvSets(1),
5834
4528
  aabb: new Float32Array(6),
5835
4529
  ...overrides,
5836
4530
  submeshes: [
@@ -5890,6 +4584,12 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
5890
4584
  expect(rd.indexCount).toBe(6);
5891
4585
  expect(rd.indexFormat).toBe('uint16');
5892
4586
  expect(rd.layoutProjection.arrayStride).toBe(48);
4587
+ expect(rd.layoutProjection.attributes.map((attribute) => attribute.key)).toEqual([
4588
+ 'position',
4589
+ 'normal',
4590
+ 'uv',
4591
+ 'tangent',
4592
+ ]);
5893
4593
  expect(rd.vertexUsage).toBe(GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST);
5894
4594
  expect(rd.indexUsage).toBe(GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST);
5895
4595
  });
@@ -6179,121 +4879,17 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
6179
4879
  }
6180
4880
 
6181
4881
  {
6182
- // --- from hdrp-caps-gate.test.ts ---
6183
- describe('HdrpCapsInsufficientError class shape (AC-17/AC-18/AC-20)', () => {
6184
- it('has .code === hdrp-caps-insufficient', async () => {
6185
- const { HdrpCapsInsufficientError } = await import('@forgeax/engine-render/internal');
6186
- const err = new HdrpCapsInsufficientError('maxStorageBuffersPerShaderStage', 2, 4);
6187
- expect(err.code).toBe('hdrp-caps-insufficient');
6188
- });
6189
-
6190
- it('.detail carries { capName, actual, required }', async () => {
6191
- const { HdrpCapsInsufficientError } = await import('@forgeax/engine-render/internal');
6192
- const err = new HdrpCapsInsufficientError('maxStorageBuffersPerShaderStage', 2, 4);
6193
- expect(err.detail).toBeDefined();
6194
- expect(err.detail.capName).toBe('maxStorageBuffersPerShaderStage');
6195
- expect(err.detail.actual).toBe(2);
6196
- expect(err.detail.required).toBe(4);
6197
- });
6198
-
6199
- it('.hint contains fall-back-to-URP substring (AC-18)', async () => {
6200
- const { HdrpCapsInsufficientError } = await import('@forgeax/engine-render/internal');
6201
- const err = new HdrpCapsInsufficientError('maxStorageBuffersPerShaderStage', 2, 4);
6202
- expect(err.hint).toBeDefined();
6203
- expect(typeof err.hint).toBe('string');
6204
- expect(err.hint.length).toBeGreaterThan(0);
6205
- expect(err.hint).toMatch(/fall back to URP|do not call installPipeline/i);
6206
- });
6207
-
6208
- it('.expected describes the capacity requirement', async () => {
6209
- const { HdrpCapsInsufficientError } = await import('@forgeax/engine-render/internal');
6210
- const err = new HdrpCapsInsufficientError('maxStorageBuffersPerShaderStage', 2, 4);
6211
- expect(err.expected).toBeDefined();
6212
- expect(typeof err.expected).toBe('string');
6213
- expect(err.expected.length).toBeGreaterThan(0);
6214
- });
6215
-
6216
- it('extends Error', async () => {
6217
- const { HdrpCapsInsufficientError } = await import('@forgeax/engine-render/internal');
6218
- const err = new HdrpCapsInsufficientError('maxStorageBuffersPerShaderStage', 2, 4);
6219
- expect(err).toBeInstanceOf(Error);
6220
- expect(err.name).toBe('HdrpCapsInsufficientError');
6221
- });
6222
- });
6223
-
6224
4882
  // ── Light budget error class shape ─────────────────────────────────────────────
6225
4883
 
6226
- describe('HdrpLightBudgetExceededError class shape (AC-07/AC-20)', () => {
6227
- it('has .code === hdrp-light-budget-exceeded', async () => {
6228
- const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
6229
- const err = new HdrpLightBudgetExceededError(257, 256);
6230
- expect(err.code).toBe('hdrp-light-budget-exceeded');
6231
- });
6232
-
6233
- it('.detail carries { actual, budget }', async () => {
6234
- const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
6235
- const err = new HdrpLightBudgetExceededError(257, 256);
6236
- expect(err.detail).toBeDefined();
6237
- expect(err.detail.actual).toBe(257);
6238
- expect(err.detail.budget).toBe(256);
6239
- });
6240
-
6241
- it('.hint is a non-empty actionable string', async () => {
6242
- const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
6243
- const err = new HdrpLightBudgetExceededError(257, 256);
6244
- expect(err.hint).toBeDefined();
6245
- expect(typeof err.hint).toBe('string');
6246
- expect(err.hint.length).toBeGreaterThan(0);
6247
- });
6248
-
6249
- it('extends Error', async () => {
6250
- const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
6251
- const err = new HdrpLightBudgetExceededError(257, 256);
6252
- expect(err).toBeInstanceOf(Error);
6253
- expect(err.name).toBe('HdrpLightBudgetExceededError');
6254
- });
6255
- });
6256
-
6257
4884
  // ── Index list overflow error class shape ─────────────────────────────────────
6258
4885
 
6259
- describe('HdrpIndexListOverflowError class shape (AC-24)', () => {
6260
- it('has .code === hdrp-index-list-overflow', async () => {
6261
- const { HdrpIndexListOverflowError } = await import('@forgeax/engine-render/internal');
6262
- const err = new HdrpIndexListOverflowError(65537, 65536);
6263
- expect(err.code).toBe('hdrp-index-list-overflow');
6264
- });
6265
-
6266
- it('.detail carries { actual, capacity }', async () => {
6267
- const { HdrpIndexListOverflowError } = await import('@forgeax/engine-render/internal');
6268
- const err = new HdrpIndexListOverflowError(65537, 65536);
6269
- expect(err.detail).toBeDefined();
6270
- expect(err.detail.actual).toBe(65537);
6271
- expect(err.detail.capacity).toBe(65536);
6272
- });
6273
-
6274
- it('.hint is a non-empty actionable string', async () => {
6275
- const { HdrpIndexListOverflowError } = await import('@forgeax/engine-render/internal');
6276
- const err = new HdrpIndexListOverflowError(65537, 65536);
6277
- expect(err.hint).toBeDefined();
6278
- expect(typeof err.hint).toBe('string');
6279
- expect(err.hint.length).toBeGreaterThan(0);
6280
- });
6281
-
6282
- it('extends Error', async () => {
6283
- const { HdrpIndexListOverflowError } = await import('@forgeax/engine-render/internal');
6284
- const err = new HdrpIndexListOverflowError(65537, 65536);
6285
- expect(err).toBeInstanceOf(Error);
6286
- expect(err.name).toBe('HdrpIndexListOverflowError');
6287
- });
6288
- });
6289
-
6290
4886
  // ── Caps gate 5 scenarios (AC-17) ─────────────────────────────────────────────
6291
4887
  // Note: the caps router lives in hdrp-pipeline.ts. These tests verify
6292
4888
  // the installPipeline behaviour by mocking device.caps.
6293
4889
 
6294
4890
  describe('HDRP caps gate 5 scenarios (AC-17)', () => {
6295
4891
  it('caps=0 (uniform fallback) yields ok(false)', async () => {
6296
- const { assertStorageBufferCap } = await import('@forgeax/engine-render/internal');
4892
+ const { assertStorageBufferCap } = await import('../../../render/src/light-buffer-layout');
6297
4893
  const result = assertStorageBufferCap(0);
6298
4894
  expect(result.ok).toBe(true);
6299
4895
  if (result.ok) {
@@ -6302,19 +4898,19 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
6302
4898
  });
6303
4899
 
6304
4900
  it('caps=1 (insufficient, partial storage) yields err', async () => {
6305
- const { assertStorageBufferCap } = await import('@forgeax/engine-render/internal');
4901
+ const { assertStorageBufferCap } = await import('../../../render/src/light-buffer-layout');
6306
4902
  const result = assertStorageBufferCap(1);
6307
4903
  expect(result.ok).toBe(false);
6308
4904
  });
6309
4905
 
6310
4906
  it('caps=3 (insufficient, below 4) yields err', async () => {
6311
- const { assertStorageBufferCap } = await import('@forgeax/engine-render/internal');
4907
+ const { assertStorageBufferCap } = await import('../../../render/src/light-buffer-layout');
6312
4908
  const result = assertStorageBufferCap(3);
6313
4909
  expect(result.ok).toBe(false);
6314
4910
  });
6315
4911
 
6316
4912
  it('caps=4 (sufficient) yields ok(true)', async () => {
6317
- const { assertStorageBufferCap } = await import('@forgeax/engine-render/internal');
4913
+ const { assertStorageBufferCap } = await import('../../../render/src/light-buffer-layout');
6318
4914
  const result = assertStorageBufferCap(4);
6319
4915
  expect(result.ok).toBe(true);
6320
4916
  if (result.ok) {
@@ -6323,7 +4919,7 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
6323
4919
  });
6324
4920
 
6325
4921
  it('caps=8 (sufficient, above minimum) yields ok(true)', async () => {
6326
- const { assertStorageBufferCap } = await import('@forgeax/engine-render/internal');
4922
+ const { assertStorageBufferCap } = await import('../../../render/src/light-buffer-layout');
6327
4923
  const result = assertStorageBufferCap(8);
6328
4924
  expect(result.ok).toBe(true);
6329
4925
  if (result.ok) {
@@ -6333,32 +4929,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
6333
4929
  });
6334
4930
 
6335
4931
  // ── RuntimeErrorCode union member count (12 -> 15) ───────────────────────────
6336
-
6337
- describe('RuntimeErrorCode union 12 -> 15 (D-4)', () => {
6338
- it('HdrpCapsInsufficientError .code is recognized as RuntimeErrorCode literal', async () => {
6339
- const { HdrpCapsInsufficientError } = await import('@forgeax/engine-render/internal');
6340
- const err = new HdrpCapsInsufficientError('maxStorageBuffersPerShaderStage', 2, 4);
6341
- const code: string = err.code;
6342
- // If hdrp-caps-insufficient is not in the RuntimeErrorCode union,
6343
- // assigning err.code to a typed RuntimeErrorCode variable would fail.
6344
- // This is a runtime smoke: assert the literal is kebab-case and hdrp-prefixed.
6345
- expect(code).toMatch(/^hdrp-/);
6346
- });
6347
-
6348
- it('HdrpLightBudgetExceededError .code is recognized as RuntimeErrorCode literal', async () => {
6349
- const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
6350
- const err = new HdrpLightBudgetExceededError(257, 256);
6351
- const code: string = err.code;
6352
- expect(code).toMatch(/^hdrp-/);
6353
- });
6354
-
6355
- it('HdrpIndexListOverflowError .code is recognized as RuntimeErrorCode literal', async () => {
6356
- const { HdrpIndexListOverflowError } = await import('@forgeax/engine-render/internal');
6357
- const err = new HdrpIndexListOverflowError(65537, 65536);
6358
- const code: string = err.code;
6359
- expect(code).toMatch(/^hdrp-/);
6360
- });
6361
- });
6362
4932
  }
6363
4933
 
6364
4934
  {
@@ -6506,18 +5076,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
6506
5076
 
6507
5077
  // ── hdrp-index-list-overflow RuntimeError class shape ────────────────────────
6508
5078
 
6509
- describe('hdrp-index-list-overflow RuntimeError class shape (AC-24)', () => {
6510
- it('HdrpIndexListOverflowError has the expected shape', async () => {
6511
- const { HdrpIndexListOverflowError } = await import('@forgeax/engine-render/internal');
6512
- const err = new HdrpIndexListOverflowError(70000, 65536);
6513
- expect(err.code).toBe('hdrp-index-list-overflow');
6514
- expect(err.detail.actual).toBe(70000);
6515
- expect(err.detail.capacity).toBe(65536);
6516
- expect(err.hint.length).toBeGreaterThan(0);
6517
- expect(err.expected.length).toBeGreaterThan(0);
6518
- });
6519
- });
6520
-
6521
5079
  // ── AC-03 guardrail: URP LIGHT_ARRAY_MAX_SLOTS=4 + 5 PointLights ─────────────
6522
5080
  //
6523
5081
  // This test verifies that the existing URP `render-system-multi-light` warn-once
@@ -6526,13 +5084,13 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
6526
5084
 
6527
5085
  describe('URP light budget warn-once guardrail (AC-03)', () => {
6528
5086
  it('LIGHT_ARRAY_MAX_SLOTS is still 4 (URP first-slice cap)', async () => {
6529
- const { LIGHT_ARRAY_MAX_SLOTS } = await import('@forgeax/engine-render/internal');
5087
+ const { LIGHT_ARRAY_MAX_SLOTS } = await import('../../../render/src/light-buffer-layout');
6530
5088
  expect(LIGHT_ARRAY_MAX_SLOTS).toBe(4);
6531
5089
  });
6532
5090
 
6533
5091
  it('POINT_LIGHT_STD430_BYTES and SPOT_LIGHT_STD430_BYTES still match URP specs', async () => {
6534
5092
  const { POINT_LIGHT_STD430_BYTES, SPOT_LIGHT_STD430_BYTES } = await import(
6535
- '@forgeax/engine-render/internal'
5093
+ '../../../render/src/light-buffer-layout'
6536
5094
  );
6537
5095
  expect(POINT_LIGHT_STD430_BYTES).toBe(32);
6538
5096
  // feat-20260625-spot-light-shadow-mapping M2 / w8 (D-4): SpotLight grew
@@ -6544,39 +5102,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
6544
5102
 
6545
5103
  {
6546
5104
  // --- from hdrp-light-budget.test.ts ---
6547
- describe('HdrpLightBudgetExceededError class shape (AC-07)', () => {
6548
- it('has .code === hdrp-light-budget-exceeded', async () => {
6549
- const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
6550
- const err = new HdrpLightBudgetExceededError(257, 256);
6551
- expect(err.code).toBe('hdrp-light-budget-exceeded');
6552
- });
6553
-
6554
- it('.detail carries { actual, budget } with correct values', async () => {
6555
- const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
6556
- const err = new HdrpLightBudgetExceededError(257, 256);
6557
- expect(err.detail.actual).toBe(257);
6558
- expect(err.detail.budget).toBe(256);
6559
- });
6560
-
6561
- it('.detail budget is 256 (the D-scope limit)', async () => {
6562
- const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
6563
- const err = new HdrpLightBudgetExceededError(300, 256);
6564
- expect(err.detail.budget).toBe(256);
6565
- });
6566
-
6567
- it('.expected describes the budget constraint', async () => {
6568
- const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
6569
- const err = new HdrpLightBudgetExceededError(257, 256);
6570
- expect(err.expected.length).toBeGreaterThan(0);
6571
- expect(err.expected).toMatch(/256/);
6572
- });
6573
-
6574
- it('.hint is a non-empty actionable string', async () => {
6575
- const { HdrpLightBudgetExceededError } = await import('@forgeax/engine-render/internal');
6576
- const err = new HdrpLightBudgetExceededError(257, 256);
6577
- expect(err.hint.length).toBeGreaterThan(0);
6578
- });
6579
- });
6580
5105
 
6581
5106
  // ── Budget gate unit logic (the pure gate w23 will embed) ─────────────────────
6582
5107
 
@@ -6869,9 +5394,9 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
6869
5394
  // --- from m7-hdrp-demo-shape.test.ts ---
6870
5395
  const HERE = dirname(fileURLToPath(import.meta.url));
6871
5396
  const REPO_ROOT = resolve(HERE, '..', '..', '..', '..');
6872
- const FIVE_METRIC_KINDS = ['bundle-size', 'fps', 'bench', 'gate', 'spike-report'] as const;
5397
+ const _FIVE_METRIC_KINDS = ['bundle-size', 'fps', 'bench', 'gate', 'spike-report'] as const;
6873
5398
 
6874
- function readPkg(rel: string): {
5399
+ function _readPkg(rel: string): {
6875
5400
  forgeax?: { metrics?: Record<string, unknown>; smokeInvocation?: string };
6876
5401
  name?: string;
6877
5402
  scripts?: Record<string, string>;
@@ -6887,70 +5412,6 @@ vi.mock('@forgeax/engine-rhi-wgpu', () => {
6887
5412
  return readFileSync(path, 'utf8');
6888
5413
  }
6889
5414
 
6890
- describe('M7 hello-hdrp-lighting demo shape (AC-21)', () => {
6891
- const pkgRel = 'apps/hello/hdrp-lighting/package.json';
6892
- const srcRel = 'apps/hello/hdrp-lighting/src/main.ts';
6893
-
6894
- it('package.json exists with @forgeax/hello-hdrp-lighting name', () => {
6895
- const pkg = readPkg(pkgRel);
6896
- expect(pkg.name).toBe('@forgeax/hello-hdrp-lighting');
6897
- });
6898
-
6899
- it('declares smoke invocation literal aligned with ci.yml', () => {
6900
- const pkg = readPkg(pkgRel);
6901
- expect(pkg.forgeax?.smokeInvocation).toBe('pnpm --filter @forgeax/hello-hdrp-lighting smoke');
6902
- });
6903
-
6904
- it('declares all 5 forgeax.metrics kinds (plan-strategy §5.6)', () => {
6905
- const pkg = readPkg(pkgRel);
6906
- const metrics = pkg.forgeax?.metrics ?? {};
6907
- for (const kind of FIVE_METRIC_KINDS) {
6908
- expect(metrics, `metric kind '${kind}' missing`).toHaveProperty(kind);
6909
- }
6910
- });
6911
-
6912
- it('exposes scripts.smoke = node scripts/smoke-dawn.mjs', () => {
6913
- const pkg = readPkg(pkgRel);
6914
- expect(pkg.scripts?.smoke).toBe('node scripts/smoke-dawn.mjs');
6915
- });
6916
-
6917
- it('src/main.ts wires HDRP install seam (charter P1: installPipeline + HDRP_PIPELINE_ID)', () => {
6918
- const src = readSource(srcRel);
6919
- // AC-06: installPipeline + HDRP_PIPELINE_ID + 256-light spawn signal
6920
- expect(src).toMatch(/installPipeline\s*\(/);
6921
- expect(src).toMatch(/HDRP_PIPELINE_ID|forgeax::hdrp/);
6922
- expect(src).toMatch(/PointLight/);
6923
- });
6924
- });
6925
-
6926
- describe('M7 parity-urp-vs-hdrp demo shape (AC-22)', () => {
6927
- const pkgRel = 'apps/parity/urp-vs-hdrp/package.json';
6928
- const srcRel = 'apps/parity/urp-vs-hdrp/src/main.ts';
6929
-
6930
- it('package.json exists with @forgeax/parity-urp-vs-hdrp name', () => {
6931
- const pkg = readPkg(pkgRel);
6932
- expect(pkg.name).toBe('@forgeax/parity-urp-vs-hdrp');
6933
- });
6934
-
6935
- it('declares all 5 forgeax.metrics kinds (plan-strategy §5.6)', () => {
6936
- const pkg = readPkg(pkgRel);
6937
- const metrics = pkg.forgeax?.metrics ?? {};
6938
- for (const kind of FIVE_METRIC_KINDS) {
6939
- expect(metrics, `metric kind '${kind}' missing`).toHaveProperty(kind);
6940
- }
6941
- });
6942
-
6943
- it('src/main.ts wires both URP and HDRP install seams (AC-22)', () => {
6944
- const src = readSource(srcRel);
6945
- expect(src).toMatch(/URP_PIPELINE_ID|forgeax::urp/);
6946
- expect(src).toMatch(/HDRP_PIPELINE_ID|forgeax::hdrp/);
6947
- // Side-by-side comparison: both __captureLeft and __captureRight hooks
6948
- // mirror the existing parity/forgeax + parity/threejs idiom for the
6949
- // bench's single-page dual-capture pipeline.
6950
- expect(src).toMatch(/__captureLeft|__captureRight/);
6951
- });
6952
- });
6953
-
6954
5415
  describe('M7 pixel-parity bench wires urp-vs-hdrp target', () => {
6955
5416
  it('scripts/bench/pixel-parity.mjs has urp-vs-hdrp target marker', () => {
6956
5417
  const src = readSource('scripts/bench/pixel-parity.mjs');