@forgeax/engine-runtime 0.1.4 → 0.1.7

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 +65 -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 +7 -4
  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 +20 -82
  173. package/dist/index.d.ts.map +1 -1
  174. package/dist/index.mjs +107 -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 +608 -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 +24 -9
  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 +1227 -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 +31 -511
  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 +21 -25
  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 +117 -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 +65 -559
  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 +61 -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 +56 -260
  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 +15 -14
  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 +68 -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 +35 -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 +51 -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
@@ -0,0 +1,1397 @@
1
+ // asset-registry.recursive.spec — M1/SceneAsset recursive loadByGuid (TDD red
2
+ // phase for AC-01 / AC-06 / AC-07 / AC-08). The test file compiles and the
3
+ // "red" stage is intentional (type=test task).
4
+ //
5
+ // Coverage:
6
+ // AC-01 — scene + N material + 1 mesh: recursive loadByGuid<SceneAsset>
7
+ // registers all transitive sub-assets
8
+ // AC-06 — idempotency: second loadByGuid(g) returns same handle, zero fetch
9
+ // AC-07 — transitive failure: missing sub-asset → err with breadcrumb hint
10
+ // AC-08 — in-flight dedup + cycle: concurrent Promise.all 3-way dedup;
11
+ // cycle A→B→A no stack overflow
12
+
13
+ import { AssetRegistry } from '@forgeax/engine-assets-runtime';
14
+ import { type Component, defineComponent } from '@forgeax/engine-ecs';
15
+ import { AssetGuid } from '@forgeax/engine-pack/guid';
16
+ import type {
17
+ AssetError,
18
+ LocalEntityId,
19
+ MaterialAsset,
20
+ SceneAsset,
21
+ MeshAsset as TypesMeshAsset,
22
+ } from '@forgeax/engine-types';
23
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
24
+ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
25
+
26
+ // ── test GUIDs ──────────────────────────────────────────────────────────────
27
+ const SCENE_GUID = 'a0000000-0000-4000-a000-000000000001';
28
+ const MATERIAL_A_GUID = 'a0000000-0000-4000-a000-000000000002';
29
+ const MATERIAL_B_GUID = 'a0000000-0000-4000-a000-000000000003';
30
+ const MESH_GUID = 'a0000000-0000-4000-a000-000000000004';
31
+ const MISSING_SUB_GUID = 'a0000000-0000-4000-a000-000000009999';
32
+ const CYCLE_A_GUID = 'b0000000-0000-4000-b000-000000000001';
33
+ const CYCLE_B_GUID = 'b0000000-0000-4000-b000-000000000002';
34
+ const WRONG_DEFAULT_KIND_GUID = 'a0000000-0000-4000-a000-000000000005';
35
+
36
+ // ECS component reflection is World/owner-local after the ECS architecture
37
+ // update. The recursive prod-path breadcrumb still needs the scene schema, so
38
+ // inject the test vocabulary into AssetRegistry explicitly.
39
+ const BreadcrumbTransform = defineComponent('Transform', { pos: 'array<f32, 3>' });
40
+ const BreadcrumbMeshFilter = defineComponent('MeshFilter', {
41
+ assetHandle: 'shared<MeshAsset>',
42
+ });
43
+ const BreadcrumbMeshRenderer = defineComponent('MeshRenderer', {
44
+ materials: 'array<shared<MaterialAsset>>',
45
+ });
46
+
47
+ function parseGuid(s: string): AssetGuid {
48
+ const r = AssetGuid.parse(s);
49
+ if (!r.ok) throw new Error(`invalid test GUID: ${s}`);
50
+ return r.value;
51
+ }
52
+
53
+ function localId(n: number): LocalEntityId {
54
+ return n as LocalEntityId;
55
+ }
56
+
57
+ // ── helpers ─────────────────────────────────────────────────────────────────
58
+
59
+ function makeMesh(): TypesMeshAsset {
60
+ return {
61
+ kind: 'mesh',
62
+ vertices: new Float32Array([
63
+ -0.5, 0, 0.5, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0.5, 0, 0.5, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0.5, 0, -0.5,
64
+ 0, 1, 0, 1, 1, 1, 0, 0, 1,
65
+ ]),
66
+ indices: new Uint16Array([0, 1, 2]),
67
+ attributes: {},
68
+ submeshes: [
69
+ { indexOffset: 0, indexCount: 3, vertexCount: 3, topology: 'triangle-list', materialSlot: 0 },
70
+ ],
71
+
72
+ materialSlots: [{ slotName: 'Default' }],
73
+ };
74
+ }
75
+
76
+ function makeMaterialAsset(): MaterialAsset {
77
+ return {
78
+ kind: 'material',
79
+ passes: [
80
+ {
81
+ name: 'forward',
82
+ program: { module: 'test::dummy' },
83
+ renderState: { tags: { LightMode: 'Forward' } },
84
+ },
85
+ ],
86
+ values: {},
87
+ };
88
+ }
89
+
90
+ function makeTestSceneAsset(subRefs: { meshGuid: string; materialGuids: string[] }): SceneAsset {
91
+ return {
92
+ kind: 'scene',
93
+ entities: [
94
+ {
95
+ localId: localId(0),
96
+ components: {
97
+ Transform: { pos: [0, 0, 0] },
98
+ MeshFilter: { assetHandle: subRefs.meshGuid },
99
+ MeshRenderer: { materials: subRefs.materialGuids },
100
+ },
101
+ },
102
+ ],
103
+ };
104
+ }
105
+
106
+ // ── registry setup ──────────────────────────────────────────────────────────
107
+
108
+ function makeRegistry(): AssetRegistry {
109
+ return new AssetRegistry(
110
+ makeMockShaderRegistry(),
111
+ undefined,
112
+ undefined,
113
+ undefined,
114
+ undefined,
115
+ new Map<string, Component>([
116
+ [BreadcrumbTransform.name, BreadcrumbTransform],
117
+ [BreadcrumbMeshFilter.name, BreadcrumbMeshFilter],
118
+ [BreadcrumbMeshRenderer.name, BreadcrumbMeshRenderer],
119
+ ]),
120
+ );
121
+ }
122
+
123
+ function preregisterMesh(reg: AssetRegistry): void {
124
+ reg.catalog(parseGuid(MESH_GUID), makeMesh());
125
+ }
126
+
127
+ function preregisterMaterial(reg: AssetRegistry, guidStr: string): void {
128
+ reg.catalog(parseGuid(guidStr), makeMaterialAsset());
129
+ }
130
+
131
+ // ── AC-01: scene + N material + 1 mesh recursive resolution ────────────────
132
+
133
+ describe('AC-01 — scene recursive loadByGuid', () => {
134
+ it('loadByGuid<SceneAsset> in dev mode returns ok when sub-assets pre-registered', async () => {
135
+ defineComponent('Transform', { pos: 'array<f32, 3>' });
136
+ defineComponent('MeshFilter', { assetHandle: 'shared<MeshAsset>' });
137
+ defineComponent('MeshRenderer', { materials: 'array<shared<MaterialAsset>>' });
138
+
139
+ const reg = makeRegistry();
140
+ preregisterMesh(reg);
141
+ preregisterMaterial(reg, MATERIAL_A_GUID);
142
+ preregisterMaterial(reg, MATERIAL_B_GUID);
143
+
144
+ const scene = makeTestSceneAsset({
145
+ meshGuid: MESH_GUID,
146
+ materialGuids: [MATERIAL_A_GUID, MATERIAL_B_GUID],
147
+ });
148
+ reg.catalog(parseGuid(SCENE_GUID), scene);
149
+
150
+ const result = await reg.loadByGuid<SceneAsset>(parseGuid(SCENE_GUID));
151
+ expect(result.ok).toBe(true);
152
+ if (result.ok) {
153
+ expect(result.value.kind).toBe('scene');
154
+ }
155
+ });
156
+ });
157
+
158
+ // ── AC-06: idempotency ─────────────────────────────────────────────────────
159
+
160
+ describe('AC-06 — idempotency', () => {
161
+ it('two sequential loadByGuid(g) return the same payload in dev mode', async () => {
162
+ const reg = makeRegistry();
163
+ const meshGuid = parseGuid(MESH_GUID);
164
+ const mesh = makeMesh();
165
+ const cat1 = reg.catalog(meshGuid, mesh);
166
+ expect(cat1.ok).toBe(true);
167
+
168
+ const r1 = await reg.loadByGuid<TypesMeshAsset>(meshGuid);
169
+ expect(r1.ok).toBe(true);
170
+ const r2 = await reg.loadByGuid<TypesMeshAsset>(meshGuid);
171
+ expect(r2.ok).toBe(true);
172
+
173
+ if (r1.ok && r2.ok && cat1.ok) {
174
+ expect(r1.value).toBe(r2.value);
175
+ expect(r1.value).toBe(cat1.value);
176
+ }
177
+ });
178
+
179
+ it('loadByGuid returns the same payload for a pre-registered scene', async () => {
180
+ defineComponent('Transform', { pos: 'array<f32, 3>' });
181
+ defineComponent('MeshFilter', { assetHandle: 'shared<MeshAsset>' });
182
+ defineComponent('MeshRenderer', { materials: 'array<shared<MaterialAsset>>' });
183
+
184
+ const reg = makeRegistry();
185
+ preregisterMesh(reg);
186
+ preregisterMaterial(reg, MATERIAL_A_GUID);
187
+
188
+ const scene = makeTestSceneAsset({
189
+ meshGuid: MESH_GUID,
190
+ materialGuids: [MATERIAL_A_GUID],
191
+ });
192
+ reg.catalog(parseGuid(SCENE_GUID), scene);
193
+
194
+ const r1 = await reg.loadByGuid<SceneAsset>(parseGuid(SCENE_GUID));
195
+ const r2 = await reg.loadByGuid<SceneAsset>(parseGuid(SCENE_GUID));
196
+
197
+ expect(r1.ok).toBe(true);
198
+ expect(r2.ok).toBe(true);
199
+ if (r1.ok && r2.ok) {
200
+ expect(r1.value).toBe(r2.value);
201
+ }
202
+ });
203
+ });
204
+
205
+ // ── AC-07: transitive failure attribution ───────────────────────────────────
206
+
207
+ describe('AC-07 — transitive failure attribution', () => {
208
+ it('dev-mode loadByGuid returns asset-not-found for missing GUID', async () => {
209
+ const reg = makeRegistry();
210
+ const rMissing = await reg.loadByGuid<TypesMeshAsset>(parseGuid(MISSING_SUB_GUID));
211
+ expect(rMissing.ok).toBe(false);
212
+ if (!rMissing.ok) {
213
+ const err = rMissing.error as AssetError;
214
+ expect(err.code).toBe('asset-not-found');
215
+ expect(typeof err.hint).toBe('string');
216
+ }
217
+ });
218
+
219
+ it('prod-path recursive error includes sub-asset GUID + parent scene GUID in hint', async () => {
220
+ // Construct a catalog where the scene pack and mesh pack exist but the
221
+ // material sub-ref GUID is missing. After M1 recursion is wired in ddcLoad,
222
+ // loadByGuid<SceneAsset> should fail with a breadcrumb hint containing both
223
+ // the missing GUID and the parent scene GUID.
224
+ //
225
+ // Pack fixture format matches the existing load-by-guid-prod fixtures
226
+ // (pack-index is a flat array, pack file uses schemaVersion + assets[]).
227
+
228
+ defineComponent('Transform', { pos: 'array<f32, 3>' });
229
+ defineComponent('MeshFilter', { assetHandle: 'shared<MeshAsset>' });
230
+ defineComponent('MeshRenderer', { materials: 'array<shared<MaterialAsset>>' });
231
+
232
+ const reg = makeRegistry();
233
+
234
+ // Pack-index: scene + mesh present, material missing.
235
+ const packIndex = [
236
+ { guid: SCENE_GUID, packageUrl: '/packs/scene.pack.json', kind: 'scene' },
237
+ {
238
+ guid: MESH_GUID,
239
+ packageUrl: '/packs/mesh.pack.json',
240
+ kind: 'mesh',
241
+ materialSlots: [{ slotName: 'Default' }],
242
+ },
243
+ // MISSING_SUB_GUID intentionally absent
244
+ ];
245
+
246
+ // Scene pack payload references MESH_GUID + MISSING_SUB_GUID.
247
+ // feat-20260622 M4 / w14: refs[] is now the recursion SSOT (D-5). Handle
248
+ // fields carry refs[] indices (resolved by parseScenePayload), and the
249
+ // scene envelope's refs[] (GUID-string projection) drives the unified
250
+ // recursive for-loop. MESH_GUID -> refs[0], MISSING_SUB_GUID -> refs[1].
251
+ const scenePack = {
252
+ schemaVersion: '2.0.0',
253
+ kind: 'internal-text-package',
254
+ assets: [
255
+ {
256
+ guid: SCENE_GUID,
257
+ kind: 'scene',
258
+ payload: {
259
+ entities: [
260
+ {
261
+ localId: 0,
262
+ components: {
263
+ Transform: { pos: [0, 0, 0] },
264
+ MeshFilter: { assetHandle: 0 },
265
+ MeshRenderer: { materials: [1] },
266
+ },
267
+ },
268
+ ],
269
+ },
270
+ refs: [MESH_GUID, MISSING_SUB_GUID],
271
+ },
272
+ ],
273
+ };
274
+
275
+ const meshPack = {
276
+ schemaVersion: '2.0.0',
277
+ kind: 'internal-text-package',
278
+ assets: [
279
+ {
280
+ guid: MESH_GUID,
281
+ kind: 'mesh',
282
+ payload: {
283
+ vertices: [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1],
284
+ indices: [0],
285
+ attributes: {},
286
+ },
287
+ refs: [],
288
+ submeshes: [
289
+ {
290
+ indexOffset: 0,
291
+ indexCount: 0,
292
+ vertexCount: 0,
293
+ topology: 'triangle-list',
294
+ materialSlot: 0,
295
+ },
296
+ ],
297
+
298
+ materialSlots: [{ slotName: 'Default' }],
299
+ },
300
+ ],
301
+ };
302
+
303
+ reg.configurePackIndex(`/pack-index.json`);
304
+
305
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
306
+ if (url === `/pack-index.json`) {
307
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(packIndex) });
308
+ }
309
+ if (url === '/packs/scene.pack.json') {
310
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(scenePack) });
311
+ }
312
+ if (url === '/packs/mesh.pack.json') {
313
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(meshPack) });
314
+ }
315
+ return Promise.resolve({ ok: false, status: 404 });
316
+ });
317
+ globalThis.fetch = fetchMock as typeof globalThis.fetch;
318
+
319
+ try {
320
+ const result = await reg.loadByGuid<SceneAsset>(parseGuid(SCENE_GUID));
321
+
322
+ // After M1 impl, this should fail because MISSING_SUB_GUID is not in the
323
+ // catalog. The error hint should contain the missing GUID and the parent
324
+ // scene GUID as a breadcrumb.
325
+ expect(result.ok).toBe(false);
326
+ if (!result.ok) {
327
+ const err = result.error as AssetError;
328
+ expect(
329
+ ['asset-not-found', 'asset-fetch-failed', 'asset-not-imported'].includes(err.code),
330
+ ).toBe(true);
331
+ expect(typeof err.hint).toBe('string');
332
+ // Breadcrumb (D-7 / B-8): hint contains the missing sub-asset GUID,
333
+ // the parent scene GUID, and the entity localId + component.field path
334
+ // (4-segment chain: scene → entity → component.field → sub-asset).
335
+ expect(err.hint).toContain(MISSING_SUB_GUID);
336
+ expect(err.hint).toContain(SCENE_GUID);
337
+ expect(err.hint).toContain('entity 0');
338
+ expect(err.hint).toContain('MeshRenderer.materials');
339
+ // feat-20260622 verify r1: the breadcrumb provenance is ALSO exposed in
340
+ // structured form on `.detail` so an AI user locates the broken edge by
341
+ // property access, not by parsing the hint (charter P3, requirements
342
+ // error-self-recovery). Only asserted when the propagated error did not
343
+ // carry a more-specific detail of its own.
344
+ const detail = err.detail as
345
+ | {
346
+ referencedByGuid?: string;
347
+ subAssetGuid?: string;
348
+ sceneEntityId?: number;
349
+ sourceField?: { componentName?: string; fieldName?: string; arrayIndex?: number };
350
+ }
351
+ | undefined;
352
+ // Unconditional (verify r2): the structured breadcrumb MUST be delivered
353
+ // on this scene->missing-sub-asset prod path — a guarded assertion would
354
+ // pass vacuously if the provenance silently regressed to hint-only.
355
+ expect(detail?.referencedByGuid?.toLowerCase()).toBe(SCENE_GUID.toLowerCase());
356
+ expect(detail?.subAssetGuid?.toLowerCase()).toBe(MISSING_SUB_GUID.toLowerCase());
357
+ expect(detail?.sceneEntityId).toBe(0);
358
+ expect(detail?.sourceField?.componentName).toBe('MeshRenderer');
359
+ expect(detail?.sourceField?.fieldName).toBe('materials');
360
+ }
361
+ } finally {
362
+ // biome-ignore lint/suspicious/noExplicitAny: test teardown
363
+ delete (globalThis as any).fetch;
364
+ }
365
+ });
366
+ });
367
+
368
+ describe('mesh default material ready boundary', () => {
369
+ it.each([
370
+ ['empty refs', []],
371
+ ['unrelated refs', [WRONG_DEFAULT_KIND_GUID]],
372
+ ])('rejects a payload default omitted from %s before ready', async (_label, refs) => {
373
+ const reg = makeRegistry();
374
+ if (refs.length > 0) {
375
+ reg.catalog(parseGuid(WRONG_DEFAULT_KIND_GUID), {
376
+ kind: 'texture' as const,
377
+ width: 1,
378
+ height: 1,
379
+ format: 'rgba8unorm' as const,
380
+ data: new Uint8Array([255, 255, 255, 255]),
381
+ colorSpace: 'srgb' as const,
382
+ mipmap: false,
383
+ });
384
+ }
385
+ const omittedDefaultGuid = 'b0000000-0000-4000-b000-000000000099';
386
+ reg.configurePackIndex('/pack-index.json');
387
+ globalThis.fetch = vi.fn().mockImplementation((url: string) => {
388
+ if (url === '/pack-index.json') {
389
+ return Promise.resolve({
390
+ ok: true,
391
+ json: () =>
392
+ Promise.resolve([
393
+ { guid: MESH_GUID, packageUrl: '/packs/missing-edge.pack.json', kind: 'mesh' },
394
+ ]),
395
+ });
396
+ }
397
+ if (url === '/packs/missing-edge.pack.json') {
398
+ return Promise.resolve({
399
+ ok: true,
400
+ json: () =>
401
+ Promise.resolve({
402
+ schemaVersion: '2.0.0',
403
+ kind: 'internal-text-package',
404
+ assets: [
405
+ {
406
+ guid: MESH_GUID,
407
+ kind: 'mesh',
408
+ refs,
409
+ payload: {
410
+ vertices: [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1],
411
+ indices: [0, 0, 0],
412
+ attributes: {},
413
+ submeshes: [
414
+ {
415
+ indexOffset: 0,
416
+ indexCount: 3,
417
+ vertexCount: 1,
418
+ topology: 'triangle-list',
419
+ materialSlot: 0,
420
+ },
421
+ ],
422
+ materialSlots: [{ slotName: 'Body', defaultMaterial: omittedDefaultGuid }],
423
+ },
424
+ },
425
+ ],
426
+ }),
427
+ });
428
+ }
429
+ return Promise.resolve({ ok: false, status: 404 });
430
+ }) as typeof globalThis.fetch;
431
+
432
+ const result = await reg.loadByGuid<TypesMeshAsset>(parseGuid(MESH_GUID));
433
+ expect(result.ok).toBe(false);
434
+ if (!result.ok) {
435
+ expect((result.error as AssetError).detail).toMatchObject({
436
+ meshAssetGuid: MESH_GUID,
437
+ defaultMaterialGuid: omittedDefaultGuid,
438
+ actualKind: 'missing-ref-edge',
439
+ });
440
+ }
441
+ expect(reg.inspect().assets.some((entry) => entry.guid === MESH_GUID)).toBe(false);
442
+ });
443
+
444
+ it('rejects a loaded non-material default before the Mesh becomes ready', async () => {
445
+ const reg = makeRegistry();
446
+ reg.catalog(parseGuid(WRONG_DEFAULT_KIND_GUID), {
447
+ kind: 'texture' as const,
448
+ width: 1,
449
+ height: 1,
450
+ format: 'rgba8unorm' as const,
451
+ data: new Uint8Array([255, 255, 255, 255]),
452
+ colorSpace: 'srgb' as const,
453
+ mipmap: false,
454
+ });
455
+
456
+ const packIndex = [
457
+ { guid: MESH_GUID, packageUrl: '/packs/wrong-default.pack.json', kind: 'mesh' },
458
+ ];
459
+ const meshPack = {
460
+ schemaVersion: '2.0.0',
461
+ kind: 'internal-text-package',
462
+ assets: [
463
+ {
464
+ guid: MESH_GUID,
465
+ kind: 'mesh',
466
+ payload: {
467
+ vertices: [
468
+ -0.5, 0, 0.5, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0.5, 0, 0.5, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0.5,
469
+ 0, -0.5, 0, 1, 0, 1, 1, 1, 0, 0, 1,
470
+ ],
471
+ indices: [0, 1, 2],
472
+ attributes: {},
473
+ submeshes: [
474
+ {
475
+ indexOffset: 0,
476
+ indexCount: 3,
477
+ vertexCount: 3,
478
+ topology: 'triangle-list',
479
+ materialSlot: 0,
480
+ },
481
+ ],
482
+ materialSlots: [
483
+ {
484
+ slotName: 'Body',
485
+ sourceKey: 'gltf:material:0',
486
+ defaultMaterial: WRONG_DEFAULT_KIND_GUID,
487
+ },
488
+ ],
489
+ },
490
+ refs: [WRONG_DEFAULT_KIND_GUID],
491
+ },
492
+ ],
493
+ };
494
+
495
+ reg.configurePackIndex('/pack-index.json');
496
+ globalThis.fetch = vi.fn().mockImplementation((url: string) => {
497
+ if (url === '/pack-index.json') {
498
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(packIndex) });
499
+ }
500
+ if (url === '/packs/wrong-default.pack.json') {
501
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(meshPack) });
502
+ }
503
+ return Promise.resolve({ ok: false, status: 404 });
504
+ }) as typeof globalThis.fetch;
505
+
506
+ const result = await reg.loadByGuid<TypesMeshAsset>(parseGuid(MESH_GUID));
507
+ expect(result.ok).toBe(false);
508
+ if (!result.ok) {
509
+ const error = result.error as AssetError;
510
+ expect(error.code).toBe('asset-parse-failed');
511
+ expect(error.detail).toEqual({
512
+ meshAssetGuid: MESH_GUID,
513
+ slotIndex: 0,
514
+ slotName: 'Body',
515
+ defaultMaterialGuid: WRONG_DEFAULT_KIND_GUID,
516
+ actualKind: 'texture',
517
+ });
518
+ }
519
+ expect(reg.inspect().assets.some((entry) => entry.guid === MESH_GUID)).toBe(false);
520
+ });
521
+ });
522
+
523
+ // ── M2 GUIDs ───────────────────────────────────────────────────────────────
524
+ const TEXTURE_A_GUID = 'c0000000-0000-4000-c000-000000000001';
525
+ const TEXTURE_B_GUID = 'c0000000-0000-4000-c000-000000000002';
526
+ const TEXTURE_C_GUID = 'c0000000-0000-4000-c000-000000000003';
527
+ const MATERIAL_PARENT_GUID = 'c0000000-0000-4000-c000-000000000004';
528
+ const SKELETON_GUID = 'd0000000-0000-4000-d000-000000000001';
529
+ const SKIN_GUID = 'd0000000-0000-4000-d000-000000000002';
530
+ const SCENE_GLTF_GUID = 'e0000000-0000-4000-e000-000000000001';
531
+
532
+ // ── AC-08: in-flight dedup + cycle ─────────────────────────────────────────
533
+
534
+ describe('AC-08 — in-flight dedup + cycle', () => {
535
+ let originalFetch: typeof globalThis.fetch | undefined;
536
+
537
+ beforeEach(() => {
538
+ originalFetch = globalThis.fetch;
539
+ });
540
+
541
+ afterEach(() => {
542
+ if (originalFetch !== undefined) {
543
+ globalThis.fetch = originalFetch;
544
+ } else {
545
+ // biome-ignore lint/suspicious/noExplicitAny: test teardown
546
+ delete (globalThis as any).fetch;
547
+ }
548
+ });
549
+
550
+ it('concurrent loadByGuid(g) 3-way shares in-flight promise — only 1 pack fetch', async () => {
551
+ const reg = makeRegistry();
552
+
553
+ const packIndex = [
554
+ {
555
+ guid: MESH_GUID,
556
+ packageUrl: '/packs/mesh.pack.json',
557
+ kind: 'mesh',
558
+ materialSlots: [{ slotName: 'Default' }],
559
+ },
560
+ ];
561
+
562
+ const meshPack = {
563
+ schemaVersion: '2.0.0',
564
+ kind: 'internal-text-package',
565
+ assets: [
566
+ {
567
+ guid: MESH_GUID,
568
+ kind: 'mesh',
569
+ payload: {
570
+ vertices: [
571
+ -0.5, 0, 0.5, 0, 1, 0, 0, 0, 1, 0, 0, 1, 0.5, 0, 0.5, 0, 1, 0, 1, 0, 1, 0, 0, 1, 0.5,
572
+ 0, -0.5, 0, 1, 0, 1, 1, 1, 0, 0, 1,
573
+ ],
574
+ indices: [0, 1, 2],
575
+ attributes: {},
576
+ },
577
+ refs: [],
578
+ submeshes: [
579
+ {
580
+ indexOffset: 0,
581
+ indexCount: 3,
582
+ vertexCount: 3,
583
+ topology: 'triangle-list',
584
+ materialSlot: 0,
585
+ },
586
+ ],
587
+
588
+ materialSlots: [{ slotName: 'Default' }],
589
+ },
590
+ ],
591
+ };
592
+
593
+ reg.configurePackIndex(`/pack-index.json`);
594
+
595
+ let packFetchCount = 0;
596
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
597
+ if (url === `/pack-index.json`) {
598
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(packIndex) });
599
+ }
600
+ if (url === '/packs/mesh.pack.json') {
601
+ packFetchCount++;
602
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(meshPack) });
603
+ }
604
+ return Promise.resolve({ ok: false, status: 404 });
605
+ });
606
+ globalThis.fetch = fetchMock as typeof globalThis.fetch;
607
+
608
+ try {
609
+ const meshGuid = parseGuid(MESH_GUID);
610
+ const [r1, r2, r3] = await Promise.all([
611
+ reg.loadByGuid<TypesMeshAsset>(meshGuid),
612
+ reg.loadByGuid<TypesMeshAsset>(meshGuid),
613
+ reg.loadByGuid<TypesMeshAsset>(meshGuid),
614
+ ]);
615
+
616
+ expect(r1.ok).toBe(true);
617
+ expect(r2.ok).toBe(true);
618
+ expect(r3.ok).toBe(true);
619
+
620
+ if (r1.ok && r2.ok && r3.ok) {
621
+ expect(r1.value).toBe(r2.value);
622
+ expect(r2.value).toBe(r3.value);
623
+
624
+ // In-flight dedup: only 1 pack-index fetch + 1 pack fetch = 2 total
625
+ // (pack-index might be fetched N times since fetchMock is per-call,
626
+ // but packFetchCount should be 1 thanks to inFlight dedup)
627
+ expect(packFetchCount).toBe(1);
628
+ }
629
+ } finally {
630
+ // biome-ignore lint/suspicious/noExplicitAny: test teardown
631
+ delete (globalThis as any).fetch;
632
+ }
633
+ });
634
+
635
+ it('cycle A→B→A no stack overflow, both registered', async () => {
636
+ defineComponent('Transform', { pos: 'array<f32, 3>' });
637
+ defineComponent('SceneCycler', { refScene: 'shared<unknown>' });
638
+
639
+ const reg = makeRegistry();
640
+
641
+ // Two SceneAssets that reference each other via a refs[] edge.
642
+ // feat-20260622 M4 / w14: the recursion source is envelope.refs (D-5).
643
+ // A's envelope.refs = [CYCLE_B_GUID], B's = [CYCLE_A_GUID].
644
+ // With register-before-recurse + inFlight dedup: A registers then recurses
645
+ // into B; B registers then recurses into A; A is already catalogued
646
+ // (fast-path hit) / inFlight Promise satisfies it → no stack overflow.
647
+
648
+ const packIndex = [
649
+ { guid: CYCLE_A_GUID, packageUrl: '/packs/a.pack.json', kind: 'scene' },
650
+ { guid: CYCLE_B_GUID, packageUrl: '/packs/b.pack.json', kind: 'scene' },
651
+ ];
652
+
653
+ const sceneAPack = {
654
+ schemaVersion: '2.0.0',
655
+ kind: 'internal-text-package',
656
+ assets: [
657
+ {
658
+ guid: CYCLE_A_GUID,
659
+ kind: 'scene',
660
+ payload: {
661
+ entities: [
662
+ {
663
+ localId: 0,
664
+ components: {
665
+ Transform: { pos: [0, 0, 0] },
666
+ SceneCycler: { refScene: 0 },
667
+ },
668
+ },
669
+ ],
670
+ },
671
+ refs: [CYCLE_B_GUID],
672
+ },
673
+ ],
674
+ };
675
+
676
+ const sceneBPack = {
677
+ schemaVersion: '2.0.0',
678
+ kind: 'internal-text-package',
679
+ assets: [
680
+ {
681
+ guid: CYCLE_B_GUID,
682
+ kind: 'scene',
683
+ payload: {
684
+ entities: [
685
+ {
686
+ localId: 0,
687
+ components: {
688
+ Transform: { pos: [0, 0, 0] },
689
+ SceneCycler: { refScene: 0 },
690
+ },
691
+ },
692
+ ],
693
+ },
694
+ refs: [CYCLE_A_GUID],
695
+ },
696
+ ],
697
+ };
698
+
699
+ reg.configurePackIndex(`/pack-index.json`);
700
+
701
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
702
+ if (url === `/pack-index.json`) {
703
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(packIndex) });
704
+ }
705
+ if (url === '/packs/a.pack.json') {
706
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(sceneAPack) });
707
+ }
708
+ if (url === '/packs/b.pack.json') {
709
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(sceneBPack) });
710
+ }
711
+ return Promise.resolve({ ok: false, status: 404 });
712
+ });
713
+ globalThis.fetch = fetchMock as typeof globalThis.fetch;
714
+
715
+ try {
716
+ const result = await reg.loadByGuid<SceneAsset>(parseGuid(CYCLE_A_GUID));
717
+ expect(result.ok).toBe(true);
718
+
719
+ const resB = reg.lookup(parseGuid(CYCLE_B_GUID));
720
+ expect(resB).not.toBe(undefined);
721
+ } finally {
722
+ // biome-ignore lint/suspicious/noExplicitAny: test teardown
723
+ delete (globalThis as any).fetch;
724
+ }
725
+ });
726
+ });
727
+
728
+ // ── AC-02: material recursive (D-8 — values string-only walker) ─────────
729
+
730
+ describe('AC-02 — material recursive loadByGuid', () => {
731
+ it('loadByGuid<MaterialAsset> recurses into texture sub-assets pre-registered in dev mode', async () => {
732
+ // Verify that the recursive loadByGuid<MaterialAsset> walk reaches
733
+ // texture GUIDs from values. The textures are pre-registered in dev
734
+ // mode (fast-path hit), simulating the typical dev workflow where leaf
735
+ // assets are registered before the material. The material itself is
736
+ // loaded via the prod path, and the recursive walk triggers loadByGuid
737
+ // for each texture GUID — each hitting the fast-path cache.
738
+ const reg = makeRegistry();
739
+
740
+ // Pre-register textures in dev mode (fast-path compatible)
741
+ reg.catalog(parseGuid(TEXTURE_A_GUID), {
742
+ kind: 'texture' as const,
743
+ width: 4,
744
+ height: 4,
745
+ format: 'rgba8unorm' as const,
746
+ data: new Uint8Array(64),
747
+ colorSpace: 'srgb' as const,
748
+ mipmap: false,
749
+ });
750
+ reg.catalog(parseGuid(TEXTURE_B_GUID), {
751
+ kind: 'texture' as const,
752
+ width: 4,
753
+ height: 4,
754
+ format: 'rgba8unorm' as const,
755
+ data: new Uint8Array(64),
756
+ colorSpace: 'srgb' as const,
757
+ mipmap: false,
758
+ });
759
+
760
+ const packIndex = [
761
+ { guid: MATERIAL_PARENT_GUID, packageUrl: '/packs/material.pack.json', kind: 'material' },
762
+ ];
763
+
764
+ const materialPack = {
765
+ schemaVersion: '2.0.0',
766
+ kind: 'internal-text-package',
767
+ assets: [
768
+ {
769
+ guid: MATERIAL_PARENT_GUID,
770
+ kind: 'material',
771
+ payload: {
772
+ passes: [
773
+ {
774
+ name: 'forward',
775
+ program: { module: 'test::dummy' },
776
+ renderState: { tags: { LightMode: 'Forward' } },
777
+ },
778
+ ],
779
+ values: {
780
+ u_albedoMap: TEXTURE_A_GUID,
781
+ u_normalMap: TEXTURE_B_GUID,
782
+ },
783
+ },
784
+ refs: [],
785
+ },
786
+ ],
787
+ };
788
+
789
+ reg.configurePackIndex(`/pack-index.json`);
790
+
791
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
792
+ if (url === `/pack-index.json`) {
793
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(packIndex) });
794
+ }
795
+ if (url === '/packs/material.pack.json') {
796
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(materialPack) });
797
+ }
798
+ return Promise.resolve({ ok: false, status: 404 });
799
+ });
800
+ globalThis.fetch = fetchMock as typeof globalThis.fetch;
801
+
802
+ try {
803
+ const result = await reg.loadByGuid<MaterialAsset>(parseGuid(MATERIAL_PARENT_GUID));
804
+
805
+ expect(result.ok).toBe(true);
806
+ if (result.ok) {
807
+ // Both texture GUIDs should be resolvable (fast-path hit from dev pre-registration)
808
+ expect(reg.lookup(parseGuid(TEXTURE_A_GUID))).not.toBe(undefined);
809
+ expect(reg.lookup(parseGuid(TEXTURE_B_GUID))).not.toBe(undefined);
810
+ }
811
+ } finally {
812
+ // biome-ignore lint/suspicious/noExplicitAny: test teardown
813
+ delete (globalThis as any).fetch;
814
+ }
815
+ });
816
+ });
817
+
818
+ // ── AC-04: skin recursive (D-10 — single skeletonGuid) ─────────────────────-
819
+
820
+ describe('AC-04 — skin recursive loadByGuid', () => {
821
+ it('loadByGuid<SkinAsset> registers skeleton sub-asset via recursive walk', async () => {
822
+ const reg = makeRegistry();
823
+
824
+ // Pre-register skeleton in dev mode (fast-path compatible); the skeleton
825
+ // loader requires Float32Array which does not survive JSON round-trip.
826
+ reg.catalog(parseGuid(SKELETON_GUID), {
827
+ kind: 'skeleton',
828
+ inverseBindMatrices: new Float32Array(32),
829
+ jointCount: 2,
830
+ });
831
+
832
+ const packIndex = [{ guid: SKIN_GUID, packageUrl: '/packs/skin.pack.json', kind: 'skin' }];
833
+
834
+ const skinPack = {
835
+ schemaVersion: '2.0.0',
836
+ kind: 'internal-text-package',
837
+ assets: [
838
+ {
839
+ guid: SKIN_GUID,
840
+ kind: 'skin',
841
+ payload: {
842
+ jointPaths: ['Root', 'Root/Spine'],
843
+ skeletonGuid: SKELETON_GUID,
844
+ },
845
+ refs: [],
846
+ },
847
+ ],
848
+ };
849
+
850
+ reg.configurePackIndex(`/pack-index.json`);
851
+
852
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
853
+ if (url === `/pack-index.json`) {
854
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(packIndex) });
855
+ }
856
+ if (url === '/packs/skin.pack.json') {
857
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(skinPack) });
858
+ }
859
+ return Promise.resolve({ ok: false, status: 404 });
860
+ });
861
+ globalThis.fetch = fetchMock as typeof globalThis.fetch;
862
+
863
+ try {
864
+ const result = await reg.loadByGuid<import('@forgeax/engine-types').SkinAsset>(
865
+ parseGuid(SKIN_GUID),
866
+ );
867
+
868
+ expect(result.ok).toBe(true);
869
+ if (result.ok) {
870
+ // Skeleton GUID should be resolvable (fast-path hit from dev pre-registration)
871
+ expect(reg.lookup(parseGuid(SKELETON_GUID))).not.toBe(undefined);
872
+ }
873
+ } finally {
874
+ // biome-ignore lint/suspicious/noExplicitAny: test teardown
875
+ delete (globalThis as any).fetch;
876
+ }
877
+ });
878
+ });
879
+
880
+ // ── AC-03: gltf-shaped scene composite (D-9 — via SceneAsset entry) ─────────
881
+
882
+ describe('AC-03 — gltf-shaped scene composite (via SceneAsset, D-9)', () => {
883
+ it('loadByGuid<SceneAsset> registers mesh + material + texture-by-material chain', async () => {
884
+ // D-9: runtime Asset union contains no GltfAsset POD; gltf importer
885
+ // output (mesh + material + texture) is covered via SceneAsset recursion.
886
+ // This fixture mirrors that output: a scene whose entities reference mesh
887
+ // + material, and the material's values reference a texture GUID.
888
+ // The recursive walk must reach all three kinds.
889
+ //
890
+ // Texture is pre-registered in dev mode (fast-path compatible); the
891
+ // material envelope's refs[] carries the texture GUIDs from values,
892
+ // and the recursive loadByGuid hits the dev fast-path.
893
+
894
+ defineComponent('Transform', { pos: 'array<f32, 3>' });
895
+ defineComponent('MeshFilter', { assetHandle: 'shared<MeshAsset>' });
896
+ defineComponent('MeshRenderer', { materials: 'array<shared<MaterialAsset>>' });
897
+
898
+ const reg = makeRegistry();
899
+
900
+ // Pre-register texture in dev mode.
901
+ reg.catalog(parseGuid(TEXTURE_C_GUID), {
902
+ kind: 'texture' as const,
903
+ width: 4,
904
+ height: 4,
905
+ format: 'rgba8unorm' as const,
906
+ data: new Uint8Array(64),
907
+ colorSpace: 'srgb' as const,
908
+ mipmap: false,
909
+ });
910
+
911
+ const packIndex = [
912
+ { guid: SCENE_GLTF_GUID, packageUrl: '/packs/scene-gltf.pack.json', kind: 'scene' },
913
+ {
914
+ guid: MESH_GUID,
915
+ packageUrl: '/packs/mesh.pack.json',
916
+ kind: 'mesh',
917
+ materialSlots: [{ slotName: 'Default' }],
918
+ },
919
+ {
920
+ guid: MATERIAL_A_GUID,
921
+ packageUrl: '/packs/mat.pack.json',
922
+ kind: 'material',
923
+ },
924
+ ];
925
+
926
+ const scenePack = {
927
+ schemaVersion: '2.0.0',
928
+ kind: 'internal-text-package',
929
+ assets: [
930
+ {
931
+ guid: SCENE_GLTF_GUID,
932
+ kind: 'scene',
933
+ payload: {
934
+ entities: [
935
+ {
936
+ localId: 0,
937
+ components: {
938
+ Transform: { pos: [0, 0, 0] },
939
+ MeshFilter: { assetHandle: 0 },
940
+ MeshRenderer: { materials: [1] },
941
+ },
942
+ },
943
+ ],
944
+ },
945
+ // feat-20260622 M4 / w14: refs[] is the recursion SSOT (D-5).
946
+ // MESH_GUID -> refs[0], MATERIAL_A_GUID -> refs[1].
947
+ refs: [MESH_GUID, MATERIAL_A_GUID],
948
+ },
949
+ ],
950
+ };
951
+
952
+ const meshPack = {
953
+ schemaVersion: '2.0.0',
954
+ kind: 'internal-text-package',
955
+ assets: [
956
+ {
957
+ guid: MESH_GUID,
958
+ kind: 'mesh',
959
+ payload: {
960
+ vertices: [0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 1],
961
+ indices: [0],
962
+ attributes: {},
963
+ },
964
+ refs: [],
965
+ submeshes: [
966
+ {
967
+ indexOffset: 0,
968
+ indexCount: 0,
969
+ vertexCount: 0,
970
+ topology: 'triangle-list',
971
+ materialSlot: 0,
972
+ },
973
+ ],
974
+
975
+ materialSlots: [{ slotName: 'Default' }],
976
+ },
977
+ ],
978
+ };
979
+
980
+ const materialPack = {
981
+ schemaVersion: '2.0.0',
982
+ kind: 'internal-text-package',
983
+ assets: [
984
+ {
985
+ guid: MATERIAL_A_GUID,
986
+ kind: 'material',
987
+ payload: {
988
+ passes: [
989
+ {
990
+ name: 'forward',
991
+ program: { module: 'test::dummy' },
992
+ renderState: { tags: { LightMode: 'Forward' } },
993
+ },
994
+ ],
995
+ // feat-20260622 M4 / w14: texture handle fields carry refs[] indices
996
+ // (resolved by materialLoader); the texture edge rides material
997
+ // refs[] so the unified for-loop recurses into it.
998
+ values: {
999
+ u_baseColorMap: 0,
1000
+ u_normalMap: 0, // same texture via two params (diamond)
1001
+ u_metallic: 0.5, // non-GUID value → skipped
1002
+ },
1003
+ },
1004
+ refs: [TEXTURE_C_GUID],
1005
+ },
1006
+ ],
1007
+ };
1008
+
1009
+ reg.configurePackIndex(`/pack-index.json`);
1010
+
1011
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
1012
+ if (url === `/pack-index.json`) {
1013
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(packIndex) });
1014
+ }
1015
+ if (url === '/packs/scene-gltf.pack.json') {
1016
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(scenePack) });
1017
+ }
1018
+ if (url === '/packs/mesh.pack.json') {
1019
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(meshPack) });
1020
+ }
1021
+ if (url === '/packs/mat.pack.json') {
1022
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(materialPack) });
1023
+ }
1024
+ return Promise.resolve({ ok: false, status: 404 });
1025
+ });
1026
+ globalThis.fetch = fetchMock as typeof globalThis.fetch;
1027
+
1028
+ try {
1029
+ const result = await reg.loadByGuid<SceneAsset>(parseGuid(SCENE_GLTF_GUID));
1030
+
1031
+ expect(result.ok).toBe(true);
1032
+ if (result.ok) {
1033
+ // All three kinds should be registered:
1034
+ // - mesh (via scene entity MeshFilter, prod path)
1035
+ // - material (via scene entity MeshRenderer, prod path)
1036
+ // - texture (via material values, dev fast-path hit)
1037
+ expect(reg.lookup(parseGuid(MESH_GUID))).not.toBe(undefined);
1038
+ expect(reg.lookup(parseGuid(MATERIAL_A_GUID))).not.toBe(undefined);
1039
+ expect(reg.lookup(parseGuid(TEXTURE_C_GUID))).not.toBe(undefined);
1040
+ }
1041
+ } finally {
1042
+ // biome-ignore lint/suspicious/noExplicitAny: test teardown
1043
+ delete (globalThis as any).fetch;
1044
+ }
1045
+ });
1046
+ });
1047
+
1048
+ // ── M3: leaf coverage (AC-05) ──────────────────────────────────────────────
1049
+ //
1050
+ // The inline-payload leaf kinds in the closed Asset union carry no sub-asset
1051
+ // edges; the importer fills envelope.refs = [] for them. The spec-enumerated
1052
+ // leaves (mesh / texture / animation-clip / audio / font) plus extras
1053
+ // (sampler / shader / skeleton / render-pipeline) -- tested together for union
1054
+ // completeness. (equirect rides the Pack v2 artifact path, covered by
1055
+ // asset-registry-hdr-equirect.test.ts, not the inline-payload roundtrip here.)
1056
+ //
1057
+ // AC-05 per-leaf assertions:
1058
+ // loadByGuid (fast-path) returns the catalogued payload
1059
+ // lookup after catalog returns the payload
1060
+ // inspect().assets count incremented by 1 from catalog (zero extra mutations)
1061
+
1062
+ const LEAF_TEST_GUIDS: Record<string, string> = {
1063
+ mesh: 'f0000000-0000-4000-f000-000000000001',
1064
+ texture: 'f0000000-0000-4000-f000-000000000002',
1065
+ sampler: 'f0000000-0000-4000-f000-000000000004',
1066
+ program: 'f0000000-0000-4000-f000-000000000005',
1067
+ skeleton: 'f0000000-0000-4000-f000-000000000006',
1068
+ 'animation-clip': 'f0000000-0000-4000-f000-000000000007',
1069
+ audio: 'f0000000-0000-4000-f000-000000000008',
1070
+ font: 'f0000000-0000-4000-f000-000000000009',
1071
+ 'render-pipeline': 'f0000000-0000-4000-f000-00000000000a',
1072
+ };
1073
+
1074
+ interface LeafFixture {
1075
+ readonly label: string;
1076
+ readonly kind: string;
1077
+ readonly makeAsset: () => import('@forgeax/engine-types').Asset;
1078
+ }
1079
+
1080
+ const LEAF_FIXTURES: readonly LeafFixture[] = [
1081
+ {
1082
+ label: 'mesh',
1083
+ kind: 'mesh',
1084
+ makeAsset: () => ({
1085
+ kind: 'mesh' as const,
1086
+ vertices: new Float32Array(12),
1087
+ indices: new Uint16Array([0]),
1088
+ attributes: {},
1089
+ submeshes: [
1090
+ {
1091
+ indexOffset: 0,
1092
+ indexCount: 0,
1093
+ vertexCount: 0,
1094
+ topology: 'triangle-list' as const,
1095
+ materialSlot: 0,
1096
+ },
1097
+ ],
1098
+ materialSlots: [{ slotName: 'Default' }],
1099
+ }),
1100
+ },
1101
+ {
1102
+ label: 'texture',
1103
+ kind: 'texture',
1104
+ makeAsset: () => ({
1105
+ kind: 'texture' as const,
1106
+ width: 4,
1107
+ height: 4,
1108
+ format: 'rgba8unorm' as const,
1109
+ data: new Uint8Array(64),
1110
+ colorSpace: 'srgb' as const,
1111
+ mipmap: false,
1112
+ }),
1113
+ },
1114
+ {
1115
+ label: 'sampler',
1116
+ kind: 'sampler',
1117
+ makeAsset: () => ({
1118
+ kind: 'sampler' as const,
1119
+ magFilter: 'linear' as const,
1120
+ minFilter: 'linear' as const,
1121
+ }),
1122
+ },
1123
+ {
1124
+ label: 'skeleton',
1125
+ kind: 'skeleton',
1126
+ makeAsset: () => ({
1127
+ kind: 'skeleton' as const,
1128
+ inverseBindMatrices: new Float32Array(16),
1129
+ jointCount: 1,
1130
+ }),
1131
+ },
1132
+ {
1133
+ label: 'animation-clip',
1134
+ kind: 'animation-clip',
1135
+ makeAsset: () => ({
1136
+ kind: 'animation-clip' as const,
1137
+ duration: 1.0,
1138
+ channels: [],
1139
+ }),
1140
+ },
1141
+ {
1142
+ label: 'audio',
1143
+ kind: 'audio',
1144
+ makeAsset: () => ({
1145
+ kind: 'audio' as const,
1146
+ sourceKey: 'fixture-audio',
1147
+ mediaType: 'audio/ogg' as const,
1148
+ bytes: new Uint8Array(),
1149
+ }),
1150
+ },
1151
+ {
1152
+ label: 'font',
1153
+ kind: 'font',
1154
+ makeAsset: () => ({
1155
+ kind: 'font' as const,
1156
+ // D-19: FontAsset.atlas/sampler are embedded AssetGuids (not handles).
1157
+ atlas: parseGuid('a0000000-0000-4000-a000-00000000f001'),
1158
+ sampler: parseGuid('a0000000-0000-4000-a000-00000000f002'),
1159
+ glyphs: {},
1160
+ common: {
1161
+ lineHeight: 0,
1162
+ base: 0,
1163
+ distanceRange: 0,
1164
+ pxRange: 0,
1165
+ atlasWidth: 0,
1166
+ atlasHeight: 0,
1167
+ },
1168
+ }),
1169
+ },
1170
+ {
1171
+ label: 'render-pipeline',
1172
+ kind: 'render-pipeline',
1173
+ makeAsset: () => ({
1174
+ kind: 'render-pipeline' as const,
1175
+ pipelineId: 'forgeax::urp' as const,
1176
+ }),
1177
+ },
1178
+ ];
1179
+
1180
+ describe('AC-05 -- leaf assets (no sub-asset edges)', () => {
1181
+ // Leaf kinds carry no recursion edges; the importer fills envelope.refs = []
1182
+ // and loadByGuid does not recurse for them. These tests confirm the registry
1183
+ // lifecycle (catalog -> fast-path loadByGuid -> lookup) per leaf kind.
1184
+
1185
+ it.each(
1186
+ LEAF_FIXTURES.map((f) => [f.label, f]),
1187
+ )('loadByGuid<%s> returns ok and asset is catalogued via fast-path', async (_label, fixture) => {
1188
+ const reg = makeRegistry();
1189
+ const guidStr = LEAF_TEST_GUIDS[fixture.label];
1190
+ if (guidStr === undefined) throw new Error(`missing leaf test GUID for ${fixture.label}`);
1191
+ const guid = parseGuid(guidStr);
1192
+ const asset = fixture.makeAsset();
1193
+
1194
+ // Record pre-catalogue state
1195
+ const assetsBefore = reg.inspect().assets.length;
1196
+
1197
+ // Catalogue the leaf
1198
+ const cat = reg.catalog(guid, asset);
1199
+ expect(cat.ok).toBe(true);
1200
+
1201
+ // Verify catalog added exactly one entry
1202
+ const assetsAfterCatalog = reg.inspect().assets.length;
1203
+ expect(assetsAfterCatalog).toBe(assetsBefore + 1);
1204
+
1205
+ // loadByGuid fast-path (already catalogued) returns the payload
1206
+ const result = await reg.loadByGuid(guid);
1207
+
1208
+ expect(result.ok).toBe(true);
1209
+ if (result.ok && cat.ok) {
1210
+ expect(result.value).toBe(cat.value);
1211
+ }
1212
+
1213
+ // No extra registry mutations from loadByGuid
1214
+ const assetsAfterLoad = reg.inspect().assets.length;
1215
+ expect(assetsAfterLoad).toBe(assetsBefore + 1);
1216
+
1217
+ // lookup confirms catalogue
1218
+ expect(reg.lookup(guid)).not.toBe(undefined);
1219
+ });
1220
+ });
1221
+
1222
+ // ── feat-20260612 M2 fixup: SceneAsset.skinGuids browser-async-load chain ──
1223
+ //
1224
+ // REGRESSION COVERAGE: before the fix, browser-path `loadByGuid<SceneAsset>`
1225
+ // recursively followed entity-component handle fields (mesh / material /
1226
+ // texture / skeleton) but had no cross-edge to SkinAssets -- SkinAsset is a
1227
+ // sibling identified by matching `skeletonGuid`, not by any handle field on a
1228
+ // SceneEntity component. After loadByGuid<SceneAsset> resolved, instantiate()
1229
+ // invoked `postSpawnResolveJoints` which silently `continue`d when
1230
+ // `resolver.resolveSkinAsset` returned undefined. The result: `Skin.joints`
1231
+ // stayed length=0, and the M2-introduced `JointCountMismatchError` fail-fast
1232
+ // in render-system-extract triggered every frame on the browser path. dawn
1233
+ // smoke missed it because dawn pre-registers every sub-asset before
1234
+ // instantiate (sync register-handle path), bypassing the recursive walk.
1235
+ //
1236
+ // The fix:
1237
+ // 1. SceneAsset POD now carries `skinGuids?: readonly string[]` (a list of
1238
+ // SkinAsset GUIDs the scene's skinned entities reference). gltfImporter
1239
+ // populates it; on-disk pack JSON stores it as either string GUIDs or
1240
+ // refs[] indices (parseScenePayload accepts both).
1241
+ // 2. The scene envelope's `refs[]` includes each SkinAsset GUID so the
1242
+ // recursive `loadByGuid` walk pulls every SkinAsset.
1243
+ // 3. `postSpawnResolveJoints` upgraded its silent `continue` on missing
1244
+ // SkinAsset to a fail-fast `skin-asset-unresolved` error, so a future
1245
+ // regression of the cross-edge wiring surfaces immediately rather than
1246
+ // hiding behind a frame-1 JointCountMismatchError.
1247
+ const SKIN_FIXUP_SCENE_GUID = 'f0000000-0000-4000-f000-000000000001';
1248
+ const SKIN_FIXUP_SKIN_GUID = 'f0000000-0000-4000-f000-000000000002';
1249
+ const SKIN_FIXUP_SKELETON_GUID = 'f0000000-0000-4000-f000-000000000003';
1250
+
1251
+ describe('feat-20260612 M2 fixup — SceneAsset.skinGuids cross-edge', () => {
1252
+ let originalFetch: typeof globalThis.fetch | undefined;
1253
+
1254
+ beforeEach(() => {
1255
+ originalFetch = globalThis.fetch;
1256
+ });
1257
+
1258
+ afterEach(() => {
1259
+ if (originalFetch !== undefined) {
1260
+ globalThis.fetch = originalFetch;
1261
+ } else {
1262
+ // biome-ignore lint/suspicious/noExplicitAny: test teardown
1263
+ delete (globalThis as any).fetch;
1264
+ }
1265
+ });
1266
+
1267
+ it('loadByGuid<SceneAsset> recursively loads SkinAsset via skinGuids cross-edge (browser pack-fetch path)', async () => {
1268
+ // Reproduces the browser-async-pack-fetch path: SkinAsset is NOT
1269
+ // pre-registered; loadByGuid<SceneAsset> must reach it through the
1270
+ // skinGuids cross-edge encoded in the scene pack body. Without the fix,
1271
+ // the SkinAsset is never loaded and no resolveGuid lookup succeeds.
1272
+
1273
+ defineComponent('Transform', { pos: 'array<f32, 3>' });
1274
+ defineComponent('Skin', {
1275
+ skeleton: 'shared<SkeletonAsset>',
1276
+ joints: 'array<entity>',
1277
+ });
1278
+
1279
+ const reg = makeRegistry();
1280
+
1281
+ // Pre-register Skeleton in dev mode (Float32Array does not survive JSON
1282
+ // round-trip; the skeleton-loader's dual contract handles array form,
1283
+ // but this fixture keeps the focus on skin cross-edge wiring).
1284
+ reg.catalog(parseGuid(SKIN_FIXUP_SKELETON_GUID), {
1285
+ kind: 'skeleton',
1286
+ inverseBindMatrices: new Float32Array(64),
1287
+ jointCount: 1,
1288
+ });
1289
+
1290
+ const packIndex = [
1291
+ { guid: SKIN_FIXUP_SCENE_GUID, packageUrl: '/packs/scene.pack.json', kind: 'scene' },
1292
+ { guid: SKIN_FIXUP_SKIN_GUID, packageUrl: '/packs/skin.pack.json', kind: 'skin' },
1293
+ ];
1294
+
1295
+ const scenePack = {
1296
+ schemaVersion: '2.0.0',
1297
+ kind: 'internal-text-package',
1298
+ assets: [
1299
+ {
1300
+ guid: SKIN_FIXUP_SCENE_GUID,
1301
+ kind: 'scene',
1302
+ payload: {
1303
+ entities: [
1304
+ {
1305
+ localId: 0,
1306
+ components: {
1307
+ Transform: { pos: [0, 0, 0] },
1308
+ Skin: { skeleton: 0 },
1309
+ },
1310
+ },
1311
+ ],
1312
+ // refs[]-index form (browser JSON-roundtrip shape)
1313
+ skinGuids: [1],
1314
+ },
1315
+ // refs ordering matches indices used by both the Skin.skeleton
1316
+ // handle field (resolved via HANDLE_FIELD_NAMES) and the
1317
+ // skinGuids[] integer form.
1318
+ refs: [SKIN_FIXUP_SKELETON_GUID, SKIN_FIXUP_SKIN_GUID],
1319
+ },
1320
+ ],
1321
+ };
1322
+
1323
+ const skinPack = {
1324
+ schemaVersion: '2.0.0',
1325
+ kind: 'internal-text-package',
1326
+ assets: [
1327
+ {
1328
+ guid: SKIN_FIXUP_SKIN_GUID,
1329
+ kind: 'skin',
1330
+ payload: {
1331
+ jointPaths: ['Root'],
1332
+ skeletonGuid: SKIN_FIXUP_SKELETON_GUID,
1333
+ },
1334
+ refs: [SKIN_FIXUP_SKELETON_GUID],
1335
+ },
1336
+ ],
1337
+ };
1338
+
1339
+ reg.configurePackIndex(`/pack-index.json`);
1340
+
1341
+ const fetchMock = vi.fn().mockImplementation((url: string) => {
1342
+ if (url === `/pack-index.json`) {
1343
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(packIndex) });
1344
+ }
1345
+ if (url === '/packs/scene.pack.json') {
1346
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(scenePack) });
1347
+ }
1348
+ if (url === '/packs/skin.pack.json') {
1349
+ return Promise.resolve({ ok: true, json: () => Promise.resolve(skinPack) });
1350
+ }
1351
+ return Promise.resolve({ ok: false, status: 404 });
1352
+ });
1353
+ globalThis.fetch = fetchMock as typeof globalThis.fetch;
1354
+
1355
+ // BEFORE fix: SkinAsset not catalogued -> lookup fails.
1356
+ // AFTER fix: the scene envelope's refs[] carries the SkinAsset GUID ->
1357
+ // loadByGuid recursion pulls it -> lookup succeeds.
1358
+ const result = await reg.loadByGuid<SceneAsset>(parseGuid(SKIN_FIXUP_SCENE_GUID));
1359
+ expect(result.ok).toBe(true);
1360
+
1361
+ expect(reg.lookup(parseGuid(SKIN_FIXUP_SKIN_GUID))).not.toBe(undefined);
1362
+ });
1363
+
1364
+ it('parseScenePayload accepts skinGuids as inline strings (in-memory form)', async () => {
1365
+ // dawn-smoke / direct register path stores skinGuids as raw GUID strings
1366
+ // (not refs[] indices). parseScenePayload's resolveSkinGuids must accept
1367
+ // both shapes for round-trip symmetry with the browser pack-fetch path.
1368
+
1369
+ defineComponent('Transform', { pos: 'array<f32, 3>' });
1370
+
1371
+ const reg = makeRegistry();
1372
+ reg.catalog(parseGuid(SKIN_FIXUP_SKELETON_GUID), {
1373
+ kind: 'skeleton',
1374
+ inverseBindMatrices: new Float32Array(64),
1375
+ jointCount: 1,
1376
+ });
1377
+ reg.catalog(parseGuid(SKIN_FIXUP_SKIN_GUID), {
1378
+ kind: 'skin',
1379
+ jointPaths: ['Root'],
1380
+ skeletonGuid: SKIN_FIXUP_SKELETON_GUID,
1381
+ });
1382
+
1383
+ // Build a SceneAsset POD with inline skinGuids (no refs[] indices).
1384
+ const scene: SceneAsset = {
1385
+ kind: 'scene',
1386
+ entities: [{ localId: localId(0), components: { Transform: { pos: [0, 0, 0] } } }],
1387
+ skinGuids: [SKIN_FIXUP_SKIN_GUID],
1388
+ };
1389
+ reg.catalog(parseGuid(SKIN_FIXUP_SCENE_GUID), scene);
1390
+
1391
+ const result = await reg.loadByGuid<SceneAsset>(parseGuid(SKIN_FIXUP_SCENE_GUID));
1392
+ expect(result.ok).toBe(true);
1393
+
1394
+ // Inline-string skinGuids form resolves through the recursive walk too.
1395
+ expect(reg.lookup(parseGuid(SKIN_FIXUP_SKIN_GUID))).not.toBe(undefined);
1396
+ });
1397
+ });