@forgeax/engine-runtime 0.1.21 → 0.1.24

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 (410) hide show
  1. package/README.md +18 -9
  2. package/dist/__tests__/advance-animation-player.cases.unit.test.d.ts +2 -0
  3. package/dist/__tests__/advance-animation-player.cases.unit.test.d.ts.map +1 -0
  4. package/dist/__tests__/asset-registry-aabb.cases.unit.test.d.ts +2 -0
  5. package/dist/__tests__/asset-registry-aabb.cases.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/asset-registry-bootstrap.cases.unit.test.d.ts +2 -0
  7. package/dist/__tests__/asset-registry-bootstrap.cases.unit.test.d.ts.map +1 -0
  8. package/dist/__tests__/asset-registry-builtin-nineslice.cases.unit.test.d.ts +2 -0
  9. package/dist/__tests__/asset-registry-builtin-nineslice.cases.unit.test.d.ts.map +1 -0
  10. package/dist/__tests__/asset-registry-d9-tile-sampler-soft-warn.cases.unit.test.d.ts +2 -0
  11. package/dist/__tests__/asset-registry-d9-tile-sampler-soft-warn.cases.unit.test.d.ts.map +1 -0
  12. package/dist/__tests__/asset-registry-guid.cases.unit.test.d.ts +2 -0
  13. package/dist/__tests__/asset-registry-guid.cases.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/asset-registry-material-validate.cases.unit.test.d.ts +2 -0
  15. package/dist/__tests__/asset-registry-material-validate.cases.unit.test.d.ts.map +1 -0
  16. package/dist/__tests__/asset-registry-mesh-fail-fast.cases.unit.test.d.ts +2 -0
  17. package/dist/__tests__/asset-registry-mesh-fail-fast.cases.unit.test.d.ts.map +1 -0
  18. package/dist/__tests__/asset-registry-scene.cases.unit.test.d.ts +2 -0
  19. package/dist/__tests__/asset-registry-scene.cases.unit.test.d.ts.map +1 -0
  20. package/dist/__tests__/asset-registry-sprite-slices-validate.cases.unit.test.d.ts +2 -0
  21. package/dist/__tests__/asset-registry-sprite-slices-validate.cases.unit.test.d.ts.map +1 -0
  22. package/dist/__tests__/asset-renderer-assembly.cases.unit.test.d.ts +2 -0
  23. package/dist/__tests__/asset-renderer-assembly.cases.unit.test.d.ts.map +1 -0
  24. package/dist/__tests__/asset.cases.unit.test.d.ts +2 -0
  25. package/dist/__tests__/asset.cases.unit.test.d.ts.map +1 -0
  26. package/dist/__tests__/bloom-gating.cases.unit.test.d.ts +2 -0
  27. package/dist/__tests__/bloom-gating.cases.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/builtin-guid-ssot.cases.unit.test.d.ts +2 -0
  29. package/dist/__tests__/builtin-guid-ssot.cases.unit.test.d.ts.map +1 -0
  30. package/dist/__tests__/builtin-pack.cases.unit.test.d.ts +2 -0
  31. package/dist/__tests__/builtin-pack.cases.unit.test.d.ts.map +1 -0
  32. package/dist/__tests__/camera-antialias.cases.unit.test.d.ts +2 -0
  33. package/dist/__tests__/camera-antialias.cases.unit.test.d.ts.map +1 -0
  34. package/dist/__tests__/camera-bloom.cases.unit.test.d.ts +2 -0
  35. package/dist/__tests__/camera-bloom.cases.unit.test.d.ts.map +1 -0
  36. package/dist/__tests__/camera-clear-schema.cases.unit.test.d.ts +2 -0
  37. package/dist/__tests__/camera-clear-schema.cases.unit.test.d.ts.map +1 -0
  38. package/dist/__tests__/camera-ortho.cases.unit.test.d.ts +2 -0
  39. package/dist/__tests__/camera-ortho.cases.unit.test.d.ts.map +1 -0
  40. package/dist/__tests__/cluster-binner.cases.unit.test.d.ts +2 -0
  41. package/dist/__tests__/cluster-binner.cases.unit.test.d.ts.map +1 -0
  42. package/dist/__tests__/create-renderer-fallback-shader-manifest.cases.unit.test.d.ts +2 -0
  43. package/dist/__tests__/create-renderer-fallback-shader-manifest.cases.unit.test.d.ts.map +1 -0
  44. package/dist/__tests__/create-renderer-uniform-fallback.cases.unit.test.d.ts +2 -0
  45. package/dist/__tests__/create-renderer-uniform-fallback.cases.unit.test.d.ts.map +1 -0
  46. package/dist/__tests__/cubemap-upload.cases.unit.test.d.ts +2 -0
  47. package/dist/__tests__/cubemap-upload.cases.unit.test.d.ts.map +1 -0
  48. package/dist/__tests__/dev-import-transport.cases.unit.test.d.ts +2 -0
  49. package/dist/__tests__/dev-import-transport.cases.unit.test.d.ts.map +1 -0
  50. package/dist/__tests__/engine-metrics.cases.unit.test.d.ts +2 -0
  51. package/dist/__tests__/engine-metrics.cases.unit.test.d.ts.map +1 -0
  52. package/dist/__tests__/equirect-projection-error.cases.unit.test.d.ts +2 -0
  53. package/dist/__tests__/equirect-projection-error.cases.unit.test.d.ts.map +1 -0
  54. package/dist/__tests__/font-asset-load.cases.unit.test.d.ts +2 -0
  55. package/dist/__tests__/font-asset-load.cases.unit.test.d.ts.map +1 -0
  56. package/dist/__tests__/fxaa-intermediate-texture.cases.unit.test.d.ts +2 -0
  57. package/dist/__tests__/fxaa-intermediate-texture.cases.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/fxaa-pipeline.cases.unit.test.d.ts +2 -0
  59. package/dist/__tests__/fxaa-pipeline.cases.unit.test.d.ts.map +1 -0
  60. package/dist/__tests__/gpu-residency.cases.unit.test.d.ts +2 -0
  61. package/dist/__tests__/gpu-residency.cases.unit.test.d.ts.map +1 -0
  62. package/dist/__tests__/gpu-resource-release.cases.unit.test.d.ts +2 -0
  63. package/dist/__tests__/gpu-resource-release.cases.unit.test.d.ts.map +1 -0
  64. package/dist/__tests__/handle-quad.cases.unit.test.d.ts +2 -0
  65. package/dist/__tests__/handle-quad.cases.unit.test.d.ts.map +1 -0
  66. package/dist/__tests__/hdrp-bgl-slots.cases.unit.test.d.ts +2 -0
  67. package/dist/__tests__/hdrp-bgl-slots.cases.unit.test.d.ts.map +1 -0
  68. package/dist/__tests__/hdrp-grid-invalid.cases.unit.test.d.ts +2 -0
  69. package/dist/__tests__/hdrp-grid-invalid.cases.unit.test.d.ts.map +1 -0
  70. package/dist/__tests__/hdrp-index-list-overflow-once.cases.unit.test.d.ts +2 -0
  71. package/dist/__tests__/hdrp-index-list-overflow-once.cases.unit.test.d.ts.map +1 -0
  72. package/dist/__tests__/hdrp-light-budget.cases.unit.test.d.ts +2 -0
  73. package/dist/__tests__/hdrp-light-budget.cases.unit.test.d.ts.map +1 -0
  74. package/dist/__tests__/hdrp-pipeline-asset-config.cases.unit.test.d.ts +2 -0
  75. package/dist/__tests__/hdrp-pipeline-asset-config.cases.unit.test.d.ts.map +1 -0
  76. package/dist/__tests__/helpers/register-runtime-components.d.ts.map +1 -1
  77. package/dist/__tests__/helpers/register-scene-components.d.ts.map +1 -1
  78. package/dist/__tests__/helpers/renderer-device-loss-dawn.d.ts +25 -0
  79. package/dist/__tests__/helpers/renderer-device-loss-dawn.d.ts.map +1 -0
  80. package/dist/__tests__/helpers/standard-material-manifest.d.ts +0 -1
  81. package/dist/__tests__/helpers/standard-material-manifest.d.ts.map +1 -1
  82. package/dist/__tests__/ibl-pipeline-cache.cases.unit.test.d.ts +2 -0
  83. package/dist/__tests__/ibl-pipeline-cache.cases.unit.test.d.ts.map +1 -0
  84. package/dist/__tests__/lazy-catalog.cases.unit.test.d.ts +2 -0
  85. package/dist/__tests__/lazy-catalog.cases.unit.test.d.ts.map +1 -0
  86. package/dist/__tests__/load-by-guid-hdr.cases.unit.test.d.ts +2 -0
  87. package/dist/__tests__/load-by-guid-hdr.cases.unit.test.d.ts.map +1 -0
  88. package/dist/__tests__/load-by-guid-prod-material-parent.cases.unit.test.d.ts +2 -0
  89. package/dist/__tests__/load-by-guid-prod-material-parent.cases.unit.test.d.ts.map +1 -0
  90. package/dist/__tests__/load-by-guid-prod.cases.unit.test.d.ts +2 -0
  91. package/dist/__tests__/load-by-guid-prod.cases.unit.test.d.ts.map +1 -0
  92. package/dist/__tests__/loader-registry.cases.unit.test.d.ts +2 -0
  93. package/dist/__tests__/loader-registry.cases.unit.test.d.ts.map +1 -0
  94. package/dist/__tests__/m7-hdrp-demo-shape.cases.unit.test.d.ts +2 -0
  95. package/dist/__tests__/m7-hdrp-demo-shape.cases.unit.test.d.ts.map +1 -0
  96. package/dist/__tests__/mipmap-formula.cases.unit.test.d.ts +2 -0
  97. package/dist/__tests__/mipmap-formula.cases.unit.test.d.ts.map +1 -0
  98. package/dist/__tests__/mipmap-pipeline-cache.cases.unit.test.d.ts +2 -0
  99. package/dist/__tests__/mipmap-pipeline-cache.cases.unit.test.d.ts.map +1 -0
  100. package/dist/__tests__/parse-asset-payload-material.cases.unit.test.d.ts +2 -0
  101. package/dist/__tests__/parse-asset-payload-material.cases.unit.test.d.ts.map +1 -0
  102. package/dist/__tests__/parse-asset-payload-texture.cases.unit.test.d.ts +2 -0
  103. package/dist/__tests__/parse-asset-payload-texture.cases.unit.test.d.ts.map +1 -0
  104. package/dist/__tests__/parse-scene-payload-refs.cases.unit.test.d.ts +2 -0
  105. package/dist/__tests__/parse-scene-payload-refs.cases.unit.test.d.ts.map +1 -0
  106. package/dist/__tests__/pass-selector.cases.unit.test.d.ts +2 -0
  107. package/dist/__tests__/pass-selector.cases.unit.test.d.ts.map +1 -0
  108. package/dist/__tests__/pipeline-builder.cases.unit.test.d.ts +2 -0
  109. package/dist/__tests__/pipeline-builder.cases.unit.test.d.ts.map +1 -0
  110. package/dist/__tests__/pipeline-cache-key-topology.cases.unit.test.d.ts +2 -0
  111. package/dist/__tests__/pipeline-cache-key-topology.cases.unit.test.d.ts.map +1 -0
  112. package/dist/__tests__/pipeline-vertex-stride-branch.cases.unit.test.d.ts +2 -0
  113. package/dist/__tests__/pipeline-vertex-stride-branch.cases.unit.test.d.ts.map +1 -0
  114. package/dist/__tests__/pipeline.cases.unit.test.d.ts +2 -0
  115. package/dist/__tests__/pipeline.cases.unit.test.d.ts.map +1 -0
  116. package/dist/__tests__/post-process-register.cases.unit.test.d.ts +2 -0
  117. package/dist/__tests__/post-process-register.cases.unit.test.d.ts.map +1 -0
  118. package/dist/__tests__/propagate-transforms.cases.unit.test.d.ts +2 -0
  119. package/dist/__tests__/propagate-transforms.cases.unit.test.d.ts.map +1 -0
  120. package/dist/__tests__/record-all-topology.cases.unit.test.d.ts +2 -0
  121. package/dist/__tests__/record-all-topology.cases.unit.test.d.ts.map +1 -0
  122. package/dist/__tests__/record-draw-branch.cases.unit.test.d.ts +2 -0
  123. package/dist/__tests__/record-draw-branch.cases.unit.test.d.ts.map +1 -0
  124. package/dist/__tests__/record-strip-index-format.cases.unit.test.d.ts +2 -0
  125. package/dist/__tests__/record-strip-index-format.cases.unit.test.d.ts.map +1 -0
  126. package/dist/__tests__/register-with-guid-rgba16float.cases.unit.test.d.ts +2 -0
  127. package/dist/__tests__/register-with-guid-rgba16float.cases.unit.test.d.ts.map +1 -0
  128. package/dist/__tests__/render-data.cases.unit.test.d.ts +2 -0
  129. package/dist/__tests__/render-data.cases.unit.test.d.ts.map +1 -0
  130. package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -1
  131. package/dist/__tests__/render-query-regression.cases.unit.test.d.ts +2 -0
  132. package/dist/__tests__/render-query-regression.cases.unit.test.d.ts.map +1 -0
  133. package/dist/__tests__/render-system-record-submesh.cases.unit.test.d.ts +2 -0
  134. package/dist/__tests__/render-system-record-submesh.cases.unit.test.d.ts.map +1 -0
  135. package/dist/__tests__/render-system-record-warn-once.cases.unit.test.d.ts +2 -0
  136. package/dist/__tests__/render-system-record-warn-once.cases.unit.test.d.ts.map +1 -0
  137. package/dist/__tests__/renderer-device-loss-multi-world.integration.test.d.ts +2 -0
  138. package/dist/__tests__/renderer-device-loss-multi-world.integration.test.d.ts.map +1 -0
  139. package/dist/__tests__/renderer-device-loss-provider-feasibility.dawn.test.d.ts +2 -0
  140. package/dist/__tests__/renderer-device-loss-provider-feasibility.dawn.test.d.ts.map +1 -0
  141. package/dist/__tests__/renderer-device-loss-recovery.dawn.test.d.ts +2 -0
  142. package/dist/__tests__/renderer-device-loss-recovery.dawn.test.d.ts.map +1 -0
  143. package/dist/__tests__/renderer-draw-world.cases.unit.test.d.ts +2 -0
  144. package/dist/__tests__/renderer-draw-world.cases.unit.test.d.ts.map +1 -0
  145. package/dist/__tests__/renderer-recovery-consumer.integration.test.d.ts +2 -0
  146. package/dist/__tests__/renderer-recovery-consumer.integration.test.d.ts.map +1 -0
  147. package/dist/__tests__/renderstate-pipeline-cache.cases.unit.test.d.ts +2 -0
  148. package/dist/__tests__/renderstate-pipeline-cache.cases.unit.test.d.ts.map +1 -0
  149. package/dist/__tests__/resolve-scene-guids.cases.unit.test.d.ts +2 -0
  150. package/dist/__tests__/resolve-scene-guids.cases.unit.test.d.ts.map +1 -0
  151. package/dist/__tests__/shadow-skip-non-triangle.cases.unit.test.d.ts +2 -0
  152. package/dist/__tests__/shadow-skip-non-triangle.cases.unit.test.d.ts.map +1 -0
  153. package/dist/__tests__/skin-errors-kebab-case.cases.unit.test.d.ts +2 -0
  154. package/dist/__tests__/skin-errors-kebab-case.cases.unit.test.d.ts.map +1 -0
  155. package/dist/__tests__/skin-instances-coexist.cases.unit.test.d.ts +2 -0
  156. package/dist/__tests__/skin-instances-coexist.cases.unit.test.d.ts.map +1 -0
  157. package/dist/__tests__/skin-m2-extract-happy.cases.unit.test.d.ts +2 -0
  158. package/dist/__tests__/skin-m2-extract-happy.cases.unit.test.d.ts.map +1 -0
  159. package/dist/__tests__/skin-m2-extract-joint-error.cases.unit.test.d.ts +2 -0
  160. package/dist/__tests__/skin-m2-extract-joint-error.cases.unit.test.d.ts.map +1 -0
  161. package/dist/__tests__/skin-m2-extract-null-assets.cases.unit.test.d.ts +2 -0
  162. package/dist/__tests__/skin-m2-extract-null-assets.cases.unit.test.d.ts.map +1 -0
  163. package/dist/__tests__/skin-m2-extract-skeleton-error.cases.unit.test.d.ts +2 -0
  164. package/dist/__tests__/skin-m2-extract-skeleton-error.cases.unit.test.d.ts.map +1 -0
  165. package/dist/__tests__/skin-m3-record-cache.cases.unit.test.d.ts +2 -0
  166. package/dist/__tests__/skin-m3-record-cache.cases.unit.test.d.ts.map +1 -0
  167. package/dist/__tests__/skin-palette-extract.cases.unit.test.d.ts +2 -0
  168. package/dist/__tests__/skin-palette-extract.cases.unit.test.d.ts.map +1 -0
  169. package/dist/__tests__/skin-pipeline-routing.cases.unit.test.d.ts +2 -0
  170. package/dist/__tests__/skin-pipeline-routing.cases.unit.test.d.ts.map +1 -0
  171. package/dist/__tests__/skin.cases.unit.test.d.ts +2 -0
  172. package/dist/__tests__/skin.cases.unit.test.d.ts.map +1 -0
  173. package/dist/__tests__/skybox-extract.cases.unit.test.d.ts +2 -0
  174. package/dist/__tests__/skybox-extract.cases.unit.test.d.ts.map +1 -0
  175. package/dist/__tests__/skybox-shader-compile.cases.unit.test.d.ts +2 -0
  176. package/dist/__tests__/skybox-shader-compile.cases.unit.test.d.ts.map +1 -0
  177. package/dist/__tests__/skylight-bind-group.cases.unit.test.d.ts +2 -0
  178. package/dist/__tests__/skylight-bind-group.cases.unit.test.d.ts.map +1 -0
  179. package/dist/__tests__/skylight-component.cases.unit.test.d.ts +2 -0
  180. package/dist/__tests__/skylight-component.cases.unit.test.d.ts.map +1 -0
  181. package/dist/__tests__/skylight-fallback-path.cases.unit.test.d.ts +2 -0
  182. package/dist/__tests__/skylight-fallback-path.cases.unit.test.d.ts.map +1 -0
  183. package/dist/__tests__/skylight-pipeline-layout.cases.unit.test.d.ts +2 -0
  184. package/dist/__tests__/skylight-pipeline-layout.cases.unit.test.d.ts.map +1 -0
  185. package/dist/__tests__/skylight-solid-color-extract.cases.unit.test.d.ts +2 -0
  186. package/dist/__tests__/skylight-solid-color-extract.cases.unit.test.d.ts.map +1 -0
  187. package/dist/__tests__/sprite-animation-tick-boundary.cases.unit.test.d.ts +2 -0
  188. package/dist/__tests__/sprite-animation-tick-boundary.cases.unit.test.d.ts.map +1 -0
  189. package/dist/__tests__/sprite-animation-tick-clamp.cases.unit.test.d.ts +2 -0
  190. package/dist/__tests__/sprite-animation-tick-clamp.cases.unit.test.d.ts.map +1 -0
  191. package/dist/__tests__/sprite-animation-tick-frame-duration-negative.cases.unit.test.d.ts +2 -0
  192. package/dist/__tests__/sprite-animation-tick-frame-duration-negative.cases.unit.test.d.ts.map +1 -0
  193. package/dist/__tests__/sprite-animation-tick-frame-duration-zero.cases.unit.test.d.ts +2 -0
  194. package/dist/__tests__/sprite-animation-tick-frame-duration-zero.cases.unit.test.d.ts.map +1 -0
  195. package/dist/__tests__/sprite-animation-tick-loop.cases.unit.test.d.ts +2 -0
  196. package/dist/__tests__/sprite-animation-tick-loop.cases.unit.test.d.ts.map +1 -0
  197. package/dist/__tests__/sprite-animation-tick-override-probe.cases.unit.test.d.ts +2 -0
  198. package/dist/__tests__/sprite-animation-tick-override-probe.cases.unit.test.d.ts.map +1 -0
  199. package/dist/__tests__/sprite-animation-tick-regions-mismatch.cases.unit.test.d.ts +2 -0
  200. package/dist/__tests__/sprite-animation-tick-regions-mismatch.cases.unit.test.d.ts.map +1 -0
  201. package/dist/__tests__/storage-buffer-caps.cases.unit.test.d.ts +2 -0
  202. package/dist/__tests__/storage-buffer-caps.cases.unit.test.d.ts.map +1 -0
  203. package/dist/__tests__/systems.cases.unit.test.d.ts +2 -0
  204. package/dist/__tests__/systems.cases.unit.test.d.ts.map +1 -0
  205. package/dist/__tests__/tonemap-hdr-target.cases.unit.test.d.ts +2 -0
  206. package/dist/__tests__/tonemap-hdr-target.cases.unit.test.d.ts.map +1 -0
  207. package/dist/__tests__/tonemap-pipeline-split.cases.unit.test.d.ts +2 -0
  208. package/dist/__tests__/tonemap-pipeline-split.cases.unit.test.d.ts.map +1 -0
  209. package/dist/__tests__/tonemap.cases.unit.test.d.ts +2 -0
  210. package/dist/__tests__/tonemap.cases.unit.test.d.ts.map +1 -0
  211. package/dist/__tests__/transparent-sort-config-get.cases.unit.test.d.ts +2 -0
  212. package/dist/__tests__/transparent-sort-config-get.cases.unit.test.d.ts.map +1 -0
  213. package/dist/__tests__/transparent-sort-config-set.cases.unit.test.d.ts +2 -0
  214. package/dist/__tests__/transparent-sort-config-set.cases.unit.test.d.ts.map +1 -0
  215. package/dist/__tests__/transparent-sort.cases.unit.test.d.ts +2 -0
  216. package/dist/__tests__/transparent-sort.cases.unit.test.d.ts.map +1 -0
  217. package/dist/__tests__/upload-texture-consistency.cases.unit.test.d.ts +2 -0
  218. package/dist/__tests__/upload-texture-consistency.cases.unit.test.d.ts.map +1 -0
  219. package/dist/__tests__/verify-revisions.cases.unit.test.d.ts +2 -0
  220. package/dist/__tests__/verify-revisions.cases.unit.test.d.ts.map +1 -0
  221. package/dist/__tests__/wire-default-loaders.cases.unit.test.d.ts +2 -0
  222. package/dist/__tests__/wire-default-loaders.cases.unit.test.d.ts.map +1 -0
  223. package/dist/__tests__/zero-camera-clear-fallback.cases.unit.test.d.ts +2 -0
  224. package/dist/__tests__/zero-camera-clear-fallback.cases.unit.test.d.ts.map +1 -0
  225. package/dist/createRenderer.d.ts.map +1 -1
  226. package/dist/dev-import-transport.d.ts.map +1 -1
  227. package/dist/index.mjs +4 -3
  228. package/dist/index.mjs.map +1 -1
  229. package/dist/sprite-param-values.d.ts +1 -1
  230. package/package.json +30 -30
  231. package/src/__tests__/advance-animation-player.cases.unit.test.ts +1666 -0
  232. package/src/__tests__/animation-graph-asset-union.test-d.ts +2 -0
  233. package/src/__tests__/asset-brand-and-name-retirement.test.ts +4 -3
  234. package/src/__tests__/asset-registry-aabb.cases.unit.test.ts +676 -0
  235. package/src/__tests__/asset-registry-bootstrap.cases.unit.test.ts +472 -0
  236. package/src/__tests__/asset-registry-builtin-nineslice.cases.unit.test.ts +481 -0
  237. package/src/__tests__/asset-registry-d9-tile-sampler-soft-warn.cases.unit.test.ts +568 -0
  238. package/src/__tests__/asset-registry-guid.cases.unit.test.ts +500 -0
  239. package/src/__tests__/asset-registry-material-validate.cases.unit.test.ts +756 -0
  240. package/src/__tests__/asset-registry-mesh-fail-fast.cases.unit.test.ts +591 -0
  241. package/src/__tests__/asset-registry-scene.cases.unit.test.ts +501 -0
  242. package/src/__tests__/asset-registry-scene.test-d.ts +1 -0
  243. package/src/__tests__/asset-registry-sprite-slices-validate.cases.unit.test.ts +536 -0
  244. package/src/__tests__/asset-renderer-assembly.cases.unit.test.ts +496 -0
  245. package/src/__tests__/asset.cases.unit.test.ts +439 -0
  246. package/src/__tests__/bind-group-cache-keying.test.ts +6 -2
  247. package/src/__tests__/bloom-gating.cases.unit.test.ts +428 -0
  248. package/src/__tests__/builtin-guid-ssot.cases.unit.test.ts +472 -0
  249. package/src/__tests__/builtin-pack.cases.unit.test.ts +639 -0
  250. package/src/__tests__/builtin-shader-register-e2e.test.ts +1 -1
  251. package/src/__tests__/camera-antialias.cases.unit.test.ts +372 -0
  252. package/src/__tests__/camera-bloom.cases.unit.test.ts +396 -0
  253. package/src/__tests__/camera-clear-schema.cases.unit.test.ts +391 -0
  254. package/src/__tests__/camera-ortho.cases.unit.test.ts +484 -0
  255. package/src/__tests__/cluster-binner.cases.unit.test.ts +817 -0
  256. package/src/__tests__/collect-scene-mount-collapse.test.ts +5 -1
  257. package/src/__tests__/collect-transient-roundtrip.test.ts +14 -3
  258. package/src/__tests__/components.test-d.ts +9 -6
  259. package/src/__tests__/components.unit.test.ts +23 -19
  260. package/src/__tests__/create-renderer-fallback-shader-manifest.cases.unit.test.ts +345 -0
  261. package/src/__tests__/create-renderer-uniform-fallback.cases.unit.test.ts +445 -0
  262. package/src/__tests__/cubemap-upload.cases.unit.test.ts +593 -0
  263. package/src/__tests__/dawn/ibl-precompute-readback.dawn.test.ts +184 -17
  264. package/src/__tests__/dawn/material-alpha.dawn.test.ts +17 -4
  265. package/src/__tests__/derive-bgl-integration.test.ts +8 -8
  266. package/src/__tests__/dev-import-transport.cases.unit.test.ts +503 -0
  267. package/src/__tests__/engine-metrics.cases.unit.test.ts +371 -0
  268. package/src/__tests__/equirect-projection-error.cases.unit.test.ts +416 -0
  269. package/src/__tests__/errors.unit.test.ts +3 -0
  270. package/src/__tests__/extract-frames-merge.test.ts +3 -0
  271. package/src/__tests__/font-asset-load.cases.unit.test.ts +510 -0
  272. package/src/__tests__/fxaa-intermediate-texture.cases.unit.test.ts +690 -0
  273. package/src/__tests__/fxaa-pipeline.cases.unit.test.ts +546 -0
  274. package/src/__tests__/gpu-residency.cases.unit.test.ts +479 -0
  275. package/src/__tests__/gpu-resource-release.cases.unit.test.ts +929 -0
  276. package/src/__tests__/handle-quad.cases.unit.test.ts +491 -0
  277. package/src/__tests__/hdrp-bgl-slots.cases.unit.test.ts +451 -0
  278. package/src/__tests__/hdrp-grid-invalid.cases.unit.test.ts +408 -0
  279. package/src/__tests__/hdrp-index-list-overflow-once.cases.unit.test.ts +405 -0
  280. package/src/__tests__/hdrp-light-budget.cases.unit.test.ts +483 -0
  281. package/src/__tests__/hdrp-pipeline-asset-config.cases.unit.test.ts +441 -0
  282. package/src/__tests__/helpers/register-runtime-components.ts +9 -1
  283. package/src/__tests__/helpers/register-scene-components.ts +9 -2
  284. package/src/__tests__/helpers/renderer-device-loss-dawn.ts +69 -0
  285. package/src/__tests__/helpers/standard-material-manifest.ts +11 -15
  286. package/src/__tests__/ibl-pipeline-cache.cases.unit.test.ts +415 -0
  287. package/src/__tests__/lazy-catalog.cases.unit.test.ts +848 -0
  288. package/src/__tests__/light-gpu-byte-neutral.unit.test.ts +14 -15
  289. package/src/__tests__/light-snapshot.test-d.ts +27 -1
  290. package/src/__tests__/lights-preservation.fixup.test.ts +19 -21
  291. package/src/__tests__/lights.unit.test.ts +2076 -1
  292. package/src/__tests__/load-by-guid-hdr.cases.unit.test.ts +582 -0
  293. package/src/__tests__/load-by-guid-prod-material-parent.cases.unit.test.ts +1514 -0
  294. package/src/__tests__/load-by-guid-prod.cases.unit.test.ts +557 -0
  295. package/src/__tests__/loader-name-survival.test.ts +8 -9
  296. package/src/__tests__/loader-registry.cases.unit.test.ts +767 -0
  297. package/src/__tests__/m7-hdrp-demo-shape.cases.unit.test.ts +361 -0
  298. package/src/__tests__/material-texture-uv-scale.unit.test.ts +2 -2
  299. package/src/__tests__/materials.unit.test.ts +29 -10
  300. package/src/__tests__/mipmap-formula.cases.unit.test.ts +434 -0
  301. package/src/__tests__/mipmap-pipeline-cache.cases.unit.test.ts +490 -0
  302. package/src/__tests__/package-promotion.test.ts +7 -11
  303. package/src/__tests__/parse-asset-payload-material.cases.unit.test.ts +565 -0
  304. package/src/__tests__/parse-asset-payload-texture.cases.unit.test.ts +720 -0
  305. package/src/__tests__/parse-scene-payload-refs.cases.unit.test.ts +767 -0
  306. package/src/__tests__/pass-selector.cases.unit.test.ts +376 -0
  307. package/src/__tests__/pbr-pipeline.unit.test.ts +39 -21
  308. package/src/__tests__/pipeline-builder.cases.unit.test.ts +624 -0
  309. package/src/__tests__/pipeline-cache-key-topology.cases.unit.test.ts +807 -0
  310. package/src/__tests__/pipeline-vertex-stride-branch.cases.unit.test.ts +379 -0
  311. package/src/__tests__/pipeline.cases.unit.test.ts +370 -0
  312. package/src/__tests__/point-light-shadow.browser.test.ts +1 -0
  313. package/src/__tests__/point-light-shadow.unit.test.ts +18 -98
  314. package/src/__tests__/post-process-register.cases.unit.test.ts +405 -0
  315. package/src/__tests__/propagate-transforms.cases.unit.test.ts +502 -0
  316. package/src/__tests__/propagate-transforms.dawn.test.ts +34 -18
  317. package/src/__tests__/record-all-topology.cases.unit.test.ts +608 -0
  318. package/src/__tests__/record-draw-branch.cases.unit.test.ts +621 -0
  319. package/src/__tests__/record-strip-index-format.cases.unit.test.ts +419 -0
  320. package/src/__tests__/record-worldid-isolation.test.ts +16 -32
  321. package/src/__tests__/register-with-guid-rgba16float.cases.unit.test.ts +469 -0
  322. package/src/__tests__/render-data.cases.unit.test.ts +553 -0
  323. package/src/__tests__/render-error-exhaustive.test-d.ts +11 -0
  324. package/src/__tests__/render-query-regression.cases.unit.test.ts +531 -0
  325. package/src/__tests__/render-system-extract.test.ts +5 -5
  326. package/src/__tests__/render-system-record-multi-material-textureview.test.ts +1 -0
  327. package/src/__tests__/render-system-record-per-submesh-transparency.test.ts +1 -0
  328. package/src/__tests__/render-system-record-submesh.cases.unit.test.ts +740 -0
  329. package/src/__tests__/render-system-record-warn-once.cases.unit.test.ts +418 -0
  330. package/src/__tests__/render-system-record.test.ts +15 -33
  331. package/src/__tests__/renderer-device-loss-multi-world.integration.test.ts +103 -0
  332. package/src/__tests__/renderer-device-loss-provider-feasibility.dawn.test.ts +124 -0
  333. package/src/__tests__/renderer-device-loss-recovery.dawn.test.ts +50 -0
  334. package/src/__tests__/renderer-draw-world.cases.unit.test.ts +514 -0
  335. package/src/__tests__/renderer-recovery-consumer.integration.test.ts +96 -0
  336. package/src/__tests__/renderer-surface.unit.test.ts +0 -2
  337. package/src/__tests__/renderstate-pipeline-cache.cases.unit.test.ts +631 -0
  338. package/src/__tests__/resolve-scene-guids.cases.unit.test.ts +1123 -0
  339. package/src/__tests__/resolveName-unified-entry.test.ts +6 -6
  340. package/src/__tests__/resolveName.test.ts +14 -2
  341. package/src/__tests__/shadow-caster-empty-schema.test.ts +1 -1
  342. package/src/__tests__/shadow-csm-cascade-loadop.test.ts +1 -0
  343. package/src/__tests__/shadow-csm-tile-consistency.test.ts +1 -0
  344. package/src/__tests__/shadow-csm-ubo.test.ts +3 -1
  345. package/src/__tests__/shadow-fields-observable.dawn.test.ts +15 -4
  346. package/src/__tests__/shadow-m1.dawn.test.ts +1 -1
  347. package/src/__tests__/shadow-skip-non-triangle.cases.unit.test.ts +681 -0
  348. package/src/__tests__/skin-errors-kebab-case.cases.unit.test.ts +475 -0
  349. package/src/__tests__/skin-extract-getarrayview-count.unit.test.ts +5 -4
  350. package/src/__tests__/skin-instances-coexist.cases.unit.test.ts +375 -0
  351. package/src/__tests__/skin-m2-extract-happy.cases.unit.test.ts +592 -0
  352. package/src/__tests__/skin-m2-extract-joint-error.cases.unit.test.ts +619 -0
  353. package/src/__tests__/skin-m2-extract-null-assets.cases.unit.test.ts +569 -0
  354. package/src/__tests__/skin-m2-extract-skeleton-error.cases.unit.test.ts +585 -0
  355. package/src/__tests__/skin-m3-record-cache.cases.unit.test.ts +457 -0
  356. package/src/__tests__/skin-palette-extract.cases.unit.test.ts +635 -0
  357. package/src/__tests__/skin-parented-double-transform.test.ts +5 -5
  358. package/src/__tests__/skin-pipeline-routing.cases.unit.test.ts +411 -0
  359. package/src/__tests__/skin.cases.unit.test.ts +421 -0
  360. package/src/__tests__/skinned-shadow-mixed.dawn.test.ts +1 -6
  361. package/src/__tests__/skybox-extract.cases.unit.test.ts +511 -0
  362. package/src/__tests__/skybox-shader-compile.cases.unit.test.ts +437 -0
  363. package/src/__tests__/skylight-bind-group.cases.unit.test.ts +642 -0
  364. package/src/__tests__/skylight-component.cases.unit.test.ts +386 -0
  365. package/src/__tests__/skylight-fallback-path.cases.unit.test.ts +566 -0
  366. package/src/__tests__/skylight-lazy-projection.test.ts +35 -20
  367. package/src/__tests__/skylight-pipeline-layout.cases.unit.test.ts +506 -0
  368. package/src/__tests__/skylight-solid-color-extract.cases.unit.test.ts +399 -0
  369. package/src/__tests__/sprite-animation-tick-boundary.cases.unit.test.ts +521 -0
  370. package/src/__tests__/sprite-animation-tick-clamp.cases.unit.test.ts +453 -0
  371. package/src/__tests__/sprite-animation-tick-frame-duration-negative.cases.unit.test.ts +447 -0
  372. package/src/__tests__/sprite-animation-tick-frame-duration-zero.cases.unit.test.ts +395 -0
  373. package/src/__tests__/sprite-animation-tick-loop.cases.unit.test.ts +494 -0
  374. package/src/__tests__/sprite-animation-tick-override-probe.cases.unit.test.ts +439 -0
  375. package/src/__tests__/sprite-animation-tick-regions-mismatch.cases.unit.test.ts +466 -0
  376. package/src/__tests__/sprite-lit-bgl-byte-identical.test.ts +16 -18
  377. package/src/__tests__/sprite-lit-pipeline-cache-key.test.ts +1 -1
  378. package/src/__tests__/ssao-bgl.test.ts +2 -1
  379. package/src/__tests__/ssao-pipelines.test.ts +1 -1
  380. package/src/__tests__/storage-buffer-caps.cases.unit.test.ts +373 -0
  381. package/src/__tests__/systems.cases.unit.test.ts +425 -0
  382. package/src/__tests__/template-game-default-api-contract.test.ts +45 -23
  383. package/src/__tests__/tilemap-chunk-extract.overhang-bounds.test.ts +1 -1
  384. package/src/__tests__/tilemap-chunk-extract.propagate-order.test.ts +10 -4
  385. package/src/__tests__/tonemap-hdr-target.cases.unit.test.ts +670 -0
  386. package/src/__tests__/tonemap-pipeline-split.cases.unit.test.ts +867 -0
  387. package/src/__tests__/tonemap.cases.unit.test.ts +446 -0
  388. package/src/__tests__/transform-hierarchy-pixel-diff.dawn.test.ts +2 -2
  389. package/src/__tests__/transparent-sort-config-get.cases.unit.test.ts +395 -0
  390. package/src/__tests__/transparent-sort-config-set.cases.unit.test.ts +424 -0
  391. package/src/__tests__/transparent-sort.cases.unit.test.ts +684 -0
  392. package/src/__tests__/upload-texture-consistency.cases.unit.test.ts +478 -0
  393. package/src/__tests__/verify-revisions.cases.unit.test.ts +619 -0
  394. package/src/__tests__/wire-default-loaders.cases.unit.test.ts +461 -0
  395. package/src/__tests__/zero-camera-clear-fallback.cases.unit.test.ts +374 -0
  396. package/src/collect-scene-asset.ts +1 -1
  397. package/src/components/__tests__/transform-vec-schema.test.ts +2 -2
  398. package/src/createRenderer.ts +1 -0
  399. package/src/dev-import-transport.ts +2 -1
  400. package/src/sprite-param-values.ts +1 -1
  401. package/dist/.tsbuildinfo +0 -1
  402. package/dist/__tests__/asset.unit.test.d.ts +0 -2
  403. package/dist/__tests__/asset.unit.test.d.ts.map +0 -1
  404. package/dist/__tests__/pipeline.unit.test.d.ts +0 -2
  405. package/dist/__tests__/pipeline.unit.test.d.ts.map +0 -1
  406. package/dist/__tests__/systems.unit.test.d.ts +0 -2
  407. package/dist/__tests__/systems.unit.test.d.ts.map +0 -1
  408. package/src/__tests__/asset.unit.test.ts +0 -7110
  409. package/src/__tests__/pipeline.unit.test.ts +0 -5283
  410. package/src/__tests__/systems.unit.test.ts +0 -9210
@@ -0,0 +1,929 @@
1
+ // @ts-nocheck — merged file: indexed-access checks cascade across noUncheckedIndexedAccess for blocks originally outside src/ rootDir
2
+ // Consolidated by feat-20260609-test-pool-startup-reduction-merge-tiny-test-files
3
+ // biome-ignore-all lint/complexity/noUselessLoneBlockStatements: block-scope isolation between merged source files (consolidation paradigm)
4
+ //
5
+ // Source files (N=44):
6
+ // - packages/runtime/__tests__/antialias-from-f32.test.ts
7
+ // - packages/runtime/src/__tests__/bloom-gating.test.ts
8
+ // - packages/runtime/src/__tests__/camera-antialias.test.ts
9
+ // - packages/runtime/src/__tests__/camera-bloom.test.ts
10
+ // - packages/runtime/src/__tests__/camera-clear-schema.test.ts
11
+ // - packages/runtime/src/__tests__/camera-ortho.test.ts
12
+ // - packages/runtime/src/__tests__/fxaa-intermediate-texture.test.ts
13
+ // - packages/runtime/src/__tests__/fxaa-pipeline.test.ts
14
+ // - packages/runtime/src/__tests__/ibl-pipeline-cache.test.ts
15
+ // - packages/runtime/src/__tests__/ibl-runtime-probe.test.ts
16
+ // - packages/runtime/src/__tests__/render-system-record-warn-once.test.ts
17
+ // - packages/runtime/src/__tests__/shadow-skip-non-triangle.test.ts
18
+ // - packages/runtime/src/__tests__/skin-errors-kebab-case.test.ts
19
+ // - packages/runtime/src/__tests__/skybox-error.test.ts
20
+ // - packages/runtime/src/__tests__/skybox-shader-compile.test.ts
21
+ // - packages/runtime/src/__tests__/skylight-bind-group.test.ts
22
+ // - packages/runtime/src/__tests__/skylight-component.test.ts
23
+ // - packages/runtime/src/__tests__/skylight-fallback-path.test.ts
24
+ // - packages/runtime/src/__tests__/skylight-pipeline-layout.test.ts
25
+ // - packages/runtime/src/__tests__/tonemap-hdr-target.test.ts
26
+ // - packages/runtime/src/__tests__/tonemap-pipeline-split.test.ts
27
+ // - packages/runtime/src/__tests__/zero-camera-clear-fallback.test.ts
28
+ // - packages/runtime/src/components/__tests__/skin.test.ts
29
+ // - packages/runtime/src/systems/__tests__/advance-animation-player.test.ts
30
+ // - packages/runtime/src/systems/__tests__/graph-skybox.test.ts
31
+ // - packages/runtime/src/systems/__tests__/propagate-transforms.test.ts
32
+ // - packages/runtime/src/systems/__tests__/skin-cap-gate.test.ts
33
+ // - packages/runtime/src/systems/__tests__/skin-instances-coexist.test.ts
34
+ // - packages/runtime/src/systems/__tests__/skin-palette-extract.test.ts
35
+ // - packages/runtime/src/systems/__tests__/skin-pipeline-routing.test.ts
36
+ // - packages/runtime/src/systems/__tests__/skybox-extract.test.ts
37
+ // - packages/runtime/src/systems/__tests__/sprite-animation-tick-boundary.test.ts
38
+ // - packages/runtime/src/systems/__tests__/sprite-animation-tick-clamp.test.ts
39
+ // - packages/runtime/src/systems/__tests__/sprite-animation-tick-frame-duration-negative.test.ts
40
+ // - packages/runtime/src/systems/__tests__/sprite-animation-tick-frame-duration-zero.test.ts
41
+ // - packages/runtime/src/systems/__tests__/sprite-animation-tick-loop.test.ts
42
+ // - packages/runtime/src/systems/__tests__/sprite-animation-tick-override-probe.test.ts
43
+ // - packages/runtime/src/systems/__tests__/sprite-animation-tick-regions-mismatch.test.ts
44
+ // - packages/runtime/src/systems/__tests__/tonemap.test.ts
45
+ // - packages/runtime/src/systems/__tests__/transparent-sort-config-get.test.ts
46
+ // - packages/runtime/src/systems/__tests__/transparent-sort-config-set.test.ts
47
+ // - packages/runtime/src/systems/__tests__/transparent-sort.test.ts
48
+ // - packages/runtime/src/__tests__/render-system-multi-material.test.ts
49
+ // - packages/runtime/src/__tests__/render-system-record-submesh.test.ts
50
+ //
51
+ // Paradigm: each block-scope wraps a source file. ancestorTitles[0] is the
52
+ // source-preserved inner describe (NOT the source filename for these 3 files
53
+ // — recovery path: vitest report ancestorTitles -> grep this file -> upstream
54
+ // `// ─── from <name>.test.ts ───` block separator -> source filename).
55
+ // Top-level imports merged + deduped.
56
+
57
+ import { readFileSync } from 'node:fs';
58
+ import { resolve } from 'node:path';
59
+ import { fileURLToPath } from 'node:url';
60
+ import { AnimationPlayer } from '@forgeax/engine-animation';
61
+ import type { AssetRuntimeErrorCode } from '@forgeax/engine-assets-runtime';
62
+ import { AssetRegistry } from '@forgeax/engine-assets-runtime';
63
+ import type { EntityHandle, World as WorldType } from '@forgeax/engine-ecs';
64
+ import { ENTITY_NULL_RAW, World } from '@forgeax/engine-ecs';
65
+ import { SpriteAnimationInvalidError } from '@forgeax/engine-ecs/projection';
66
+ import { mat4, vec3 } from '@forgeax/engine-math';
67
+ import type { RenderErrorCode, Renderer as RendererType } from '@forgeax/engine-render';
68
+ import {
69
+ ANTIALIAS_FXAA,
70
+ ANTIALIAS_NONE,
71
+ BLOOM_DISABLED,
72
+ BLOOM_ENABLED,
73
+ CAMERA_PROJECTION_ORTHOGRAPHIC,
74
+ CAMERA_PROJECTION_PERSPECTIVE,
75
+ Camera,
76
+ MeshFilter,
77
+ MeshRenderer,
78
+ SkyboxBackground,
79
+ Skylight,
80
+ } from '@forgeax/engine-render';
81
+ import type { BindGroupEntry, Buffer, Sampler, Texture, TextureView } from '@forgeax/engine-rhi';
82
+ import { ok as rhiOk } from '@forgeax/engine-rhi';
83
+ import { ChildOf, Name, propagateTransforms, Transform } from '@forgeax/engine-scene';
84
+ import { TONEMAP_LUMINANCE_EPSILON } from '@forgeax/engine-shader';
85
+ import type { SkinErrorCode } from '@forgeax/engine-skinning';
86
+ import {
87
+ Skin,
88
+ SkinInstancesCoexistForbiddenError,
89
+ SkinJointCountExceededError,
90
+ SkinJointDespawnedError,
91
+ SkinJointPathUnresolvedError,
92
+ } from '@forgeax/engine-skinning';
93
+ import type {
94
+ Handle,
95
+ MaterialAsset,
96
+ MeshAsset,
97
+ SkeletonAsset,
98
+ TextureFormat,
99
+ } from '@forgeax/engine-types';
100
+ import { toShared } from '@forgeax/engine-types';
101
+ import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
102
+ import { worldInternal } from '../../../ecs/src/world-internal';
103
+ import {
104
+ SPRITE_PLAYBACK_MODE_CLAMP,
105
+ SPRITE_PLAYBACK_MODE_LOOP,
106
+ SpriteAnimation,
107
+ SpriteRegionOverride,
108
+ } from '../../../render/src/components';
109
+ import {
110
+ type CameraProjection,
111
+ cameraProjectionFromF32,
112
+ } from '../../../render/src/components/camera';
113
+ import {
114
+ assembleMaterialWithSkylightEntries,
115
+ createSkylightFallback,
116
+ mergeSkylightIntoMaterialBgl,
117
+ } from '../../../render/src/ibl/skylight-bind-group';
118
+ import { buildPbrPipelineLayouts, buildUnlitMaterialBgl } from '../../../render/src/pbr-pipeline';
119
+ import { INSTANCE_STORAGE_STRIDE_FLOATS } from '../../../render/src/record/mesh-ssbo';
120
+ import { selectSwapChainFormat } from '../../../render/src/render-system';
121
+ import { createSkinPaletteAllocator } from '../../../render/src/systems/skin-palette-allocator';
122
+ import type { TransparentEntry } from '../../../render/src/systems/transparent-sort-config';
123
+ import { standardMaterialShaderVariants } from './helpers/standard-material-manifest';
124
+ import { drawWithOwners } from './renderer-test-utils';
125
+
126
+ function componentFieldType(component: { fields: Record<string, { type: string }> }, name: string) {
127
+ return component.fields[name]?.type;
128
+ }
129
+
130
+ function componentFieldDefault(
131
+ component: { fields: Record<string, { default?: unknown }> },
132
+ name: string,
133
+ ) {
134
+ return component.fields[name]?.default;
135
+ }
136
+
137
+ function componentSchemaTypes(component: { fields: Record<string, { type: string }> }) {
138
+ return Object.fromEntries(
139
+ Object.entries(component.fields).map(([name, field]) => [name, field.type]),
140
+ );
141
+ }
142
+
143
+ afterEach(() => vi.restoreAllMocks());
144
+
145
+ type RendererErrorObservation = {
146
+ readonly code: string;
147
+ readonly detail?: unknown;
148
+ readonly hint?: string;
149
+ };
150
+
151
+ function unwrapRendererError(value: unknown): RendererErrorObservation {
152
+ let current = value as RendererErrorObservation;
153
+ while (
154
+ current.detail !== undefined &&
155
+ typeof current.detail === 'object' &&
156
+ current.detail !== null
157
+ ) {
158
+ const cause = (current.detail as { cause?: unknown }).cause;
159
+ if (
160
+ cause === undefined ||
161
+ typeof cause !== 'object' ||
162
+ cause === null ||
163
+ typeof (cause as { code?: unknown }).code !== 'string'
164
+ ) {
165
+ break;
166
+ }
167
+ current = cause as RendererErrorObservation;
168
+ }
169
+ return current;
170
+ }
171
+
172
+ function subscribeRendererErrors(
173
+ renderer: RendererType,
174
+ listener: (error: RendererErrorObservation) => void,
175
+ ): () => void {
176
+ return renderer.subscribe((event) => {
177
+ if (event.kind === 'error') listener(unwrapRendererError(event.error));
178
+ });
179
+ }
180
+
181
+ function drawPublished(renderer: RendererType, world: WorldType) {
182
+ const attached = renderer.attach(world);
183
+ if (!attached.ok) throw attached.error;
184
+ world.update().unwrap();
185
+ return drawWithOwners(renderer, world);
186
+ }
187
+
188
+ // feat-20260704-runtime-tier1-decomposition M2 / w12: reconstitute the
189
+ // eliminated top-level RuntimeErrorCode aggregate union (D-3) as a test-local
190
+ // alias so the exhaustive-switch bodies below stay byte-identical (AC-09).
191
+ type RuntimeLayerErrorCode = RenderErrorCode | AssetRuntimeErrorCode | SkinErrorCode;
192
+
193
+ import {
194
+ AnimationTargetId,
195
+ bindAnimationTargets,
196
+ advanceAnimationPlayer as canonicalAdvanceAnimationPlayer,
197
+ } from '@forgeax/engine-animation';
198
+ import { deriveAnimationTargetId } from '@forgeax/engine-animation/target-id';
199
+ import { DeviceScope } from '../../../render/src/device/device-scope';
200
+ import { GpuBuffer } from '../../../render/src/gpu-resource';
201
+ import { getOrCreateIblCache, hasIblCache } from '../../../render/src/ibl/IblPipelineCache';
202
+ import {
203
+ disposeInstanceBuffers,
204
+ type InstanceBufferCacheEntry,
205
+ } from '../../../render/src/instance-buffer-cache';
206
+ import { standardPipeline as urpPipeline } from '../../../render/src/pipeline/standard-pipeline';
207
+ import { ZERO_CAMERA_CLEAR_FALLBACK } from '../../../render/src/record/frame-snapshot';
208
+ import {
209
+ warnMultiLightDirectional,
210
+ warnMultiLightPoint,
211
+ warnMultiLightSpot,
212
+ } from '../../../render/src/record/helpers';
213
+ import type { CameraSnapshot } from '../../../render/src/render-contract';
214
+ import {
215
+ type ExtractedLights,
216
+ extractFrame,
217
+ extractFrames,
218
+ prepareExtractContext,
219
+ } from '../../../render/src/render-system-extract';
220
+ import {
221
+ getTransparentSortConfig,
222
+ setTransparentSortConfig,
223
+ TRANSPARENT_SORT_CONFIG_KEY,
224
+ TRANSPARENT_SORT_MODE_LAYER_Y,
225
+ TRANSPARENT_SORT_MODE_LAYER_YZ,
226
+ TRANSPARENT_SORT_MODE_LAYER_Z,
227
+ } from '../../../render/src/systems/transparent-sort-config';
228
+ import { spriteAnimationTickSystem } from '../systems/sprite-animation-tick';
229
+ import { REC709_LUMA_WEIGHTS, tonemapReinhardLuminance } from '../systems/tonemap';
230
+ import { transparentSortEntries } from '../systems/transparent-sort';
231
+ import { makeMockShaderRegistry } from './helpers/mock-shader-registry';
232
+
233
+ void [
234
+ ANTIALIAS_FXAA,
235
+ ANTIALIAS_NONE,
236
+ AnimationPlayer,
237
+ AnimationTargetId,
238
+ AssetRegistry,
239
+ BLOOM_DISABLED,
240
+ BLOOM_ENABLED,
241
+ CAMERA_PROJECTION_ORTHOGRAPHIC,
242
+ CAMERA_PROJECTION_PERSPECTIVE,
243
+ Camera,
244
+ ChildOf,
245
+ DeviceScope,
246
+ ENTITY_NULL_RAW,
247
+ GpuBuffer,
248
+ INSTANCE_STORAGE_STRIDE_FLOATS,
249
+ MeshFilter,
250
+ MeshRenderer,
251
+ Name,
252
+ REC709_LUMA_WEIGHTS,
253
+ SPRITE_PLAYBACK_MODE_CLAMP,
254
+ SPRITE_PLAYBACK_MODE_LOOP,
255
+ Skin,
256
+ SkinInstancesCoexistForbiddenError,
257
+ SkinJointCountExceededError,
258
+ SkinJointDespawnedError,
259
+ SkinJointPathUnresolvedError,
260
+ SkyboxBackground,
261
+ Skylight,
262
+ SpriteAnimation,
263
+ SpriteAnimationInvalidError,
264
+ SpriteRegionOverride,
265
+ TONEMAP_LUMINANCE_EPSILON,
266
+ TRANSPARENT_SORT_CONFIG_KEY,
267
+ TRANSPARENT_SORT_MODE_LAYER_Y,
268
+ TRANSPARENT_SORT_MODE_LAYER_YZ,
269
+ TRANSPARENT_SORT_MODE_LAYER_Z,
270
+ Transform,
271
+ World,
272
+ ZERO_CAMERA_CLEAR_FALLBACK,
273
+ afterEach,
274
+ assembleMaterialWithSkylightEntries,
275
+ beforeAll,
276
+ beforeEach,
277
+ bindAnimationTargets,
278
+ buildPbrPipelineLayouts,
279
+ buildUnlitMaterialBgl,
280
+ cameraProjectionFromF32,
281
+ canonicalAdvanceAnimationPlayer,
282
+ componentFieldDefault,
283
+ componentFieldType,
284
+ componentSchemaTypes,
285
+ createSkinPaletteAllocator,
286
+ createSkylightFallback,
287
+ deriveAnimationTargetId,
288
+ describe,
289
+ disposeInstanceBuffers,
290
+ drawPublished,
291
+ drawWithOwners,
292
+ expect,
293
+ extractFrame,
294
+ extractFrames,
295
+ fileURLToPath,
296
+ getOrCreateIblCache,
297
+ getTransparentSortConfig,
298
+ hasIblCache,
299
+ it,
300
+ makeMockShaderRegistry,
301
+ mat4,
302
+ mergeSkylightIntoMaterialBgl,
303
+ prepareExtractContext,
304
+ propagateTransforms,
305
+ readFileSync,
306
+ resolve,
307
+ rhiOk,
308
+ selectSwapChainFormat,
309
+ setTransparentSortConfig,
310
+ spriteAnimationTickSystem,
311
+ standardMaterialShaderVariants,
312
+ subscribeRendererErrors,
313
+ toShared,
314
+ tonemapReinhardLuminance,
315
+ transparentSortEntries,
316
+ unwrapRendererError,
317
+ urpPipeline,
318
+ vec3,
319
+ vi,
320
+ warnMultiLightDirectional,
321
+ warnMultiLightPoint,
322
+ warnMultiLightSpot,
323
+ worldInternal,
324
+ ];
325
+ type __MergedKeep =
326
+ | AssetRuntimeErrorCode
327
+ | BindGroupEntry
328
+ | Buffer
329
+ | CameraProjection
330
+ | CameraSnapshot
331
+ | EntityHandle
332
+ | ExtractedLights
333
+ | Handle
334
+ | InstanceBufferCacheEntry
335
+ | MaterialAsset
336
+ | MeshAsset
337
+ | RenderErrorCode
338
+ | RendererErrorObservation
339
+ | RendererType
340
+ | RuntimeLayerErrorCode
341
+ | Sampler
342
+ | SkeletonAsset
343
+ | SkinErrorCode
344
+ | Texture
345
+ | TextureFormat
346
+ | TextureView
347
+ | TransparentEntry
348
+ | WorldType;
349
+
350
+ // Split source block: GPU resource symmetric release contract.
351
+ // ── feat-20260612-skin-palette-per-frame-upload M2: extractFrame hasSkin ──
352
+ // Helpers shared by m2-1..m2-4 tests covering the T-21 placeholder retirement
353
+ // (real allocator wiring + 3 new SkinExtractErrorCode routings + assets===null
354
+ // bind-pose equivalence).
355
+
356
+ {
357
+ // ── Helpers ──
358
+
359
+ async function _loadM4Libs() {
360
+ const { err: m4err, ok: m4ok, RhiError } = await import('@forgeax/engine-rhi');
361
+ return { m4err, m4ok, RhiError, GpuBuffer };
362
+ }
363
+
364
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic-import return types are opaque
365
+ function _mkBufDevice(m4err: any, m4ok: any, RhiError: any) {
366
+ const destroyedSet = new WeakSet<object>();
367
+ const dev = {
368
+ destroyBuffer(buf: object) {
369
+ if (destroyedSet.has(buf))
370
+ return m4err(new RhiError({ code: 'destroy-after-destroy', expected: '', hint: '' }));
371
+ destroyedSet.add(buf);
372
+ return m4ok(undefined);
373
+ },
374
+ };
375
+ return dev as never;
376
+ }
377
+
378
+ // biome-ignore lint/suspicious/noExplicitAny: dynamic-import return type is opaque
379
+ function _mkBufEntry(GpuBuffer: any, dev: never, bl: number, av: number) {
380
+ return {
381
+ buffer: new GpuBuffer(dev, {} as never),
382
+ uploadedByteLength: bl,
383
+ uploadedArchVersion: av,
384
+ };
385
+ }
386
+
387
+ // ── createRenderer integration harness (drives the real record passes) ──
388
+ //
389
+ // A WebGPU-shaped mock device whose createBuffer hands back raw handles
390
+ // that record their own .destroy() invocations. The runtime wraps each in a
391
+ // GpuBuffer; the F12 set-before-destroy path calls GpuBuffer.destroy() ->
392
+ // rhi-webgpu destroyBuffer -> rawBuf.destroy(), landing in `destroyed`.
393
+
394
+ interface IntegrationBufLog {
395
+ created: Array<{ handle: object; size: number; usage: number }>;
396
+ destroyed: object[];
397
+ }
398
+
399
+ // The per-entity instance-transform buffer is the only buffer created with
400
+ // usage STORAGE|COPY_DST (128|8 = 136) at the packed storage instance byte
401
+ // size. Storage carries current and previous mat4 values, so each instance
402
+ // occupies `INSTANCE_STORAGE_STRIDE_FLOATS` f32 values. Matching BOTH usage
403
+ // and size pins the F12 destroy to the instance buffer, not an incidental
404
+ // same-sized transient / uniform buffer elsewhere in the frame.
405
+ const INSTANCE_USAGE = 128 | 8;
406
+ const INSTANCE_BYTES = (instanceCount: number): number =>
407
+ instanceCount * INSTANCE_STORAGE_STRIDE_FLOATS * 4;
408
+
409
+ function latestInstanceBuffer(log: IntegrationBufLog, instanceCount: number): object {
410
+ const created = log.created.filter(
411
+ (c) => c.usage === INSTANCE_USAGE && c.size === INSTANCE_BYTES(instanceCount),
412
+ );
413
+ const latest = created[created.length - 1];
414
+ if (latest === undefined) throw new Error('instance buffer was not created');
415
+ return latest.handle;
416
+ }
417
+
418
+ function makeIntegrationCanvas(): HTMLCanvasElement {
419
+ const canvas = {
420
+ width: 800,
421
+ height: 600,
422
+ getContext(kind: string): unknown {
423
+ if (kind === 'webgl2') {
424
+ return {
425
+ __mockTag: 'webgl2',
426
+ getExtension: () => null,
427
+ getParameter: () => 1,
428
+ isContextLost: () => false,
429
+ };
430
+ }
431
+ if (kind === 'webgpu') {
432
+ return {
433
+ __mockTag: 'webgpu-canvas-context',
434
+ configure: () => undefined,
435
+ unconfigure: () => undefined,
436
+ getCurrentTexture: () => ({ createView: () => ({}) }),
437
+ };
438
+ }
439
+ return null;
440
+ },
441
+ addEventListener: () => undefined,
442
+ removeEventListener: () => undefined,
443
+ };
444
+ return canvas as Partial<HTMLCanvasElement> as HTMLCanvasElement;
445
+ }
446
+
447
+ function makeIntegrationDevice(
448
+ log: IntegrationBufLog,
449
+ opts?: { destroyThrows?: boolean },
450
+ ): unknown {
451
+ const lost = new Promise<unknown>(() => undefined);
452
+ const destroyedSet = new WeakSet<object>();
453
+ return {
454
+ __mockTag: 'gpu-device',
455
+ lost,
456
+ features: new Set(),
457
+ // maxStorageBuffersPerShaderStage > 0 => rhi-webgpu caps.storageBuffer
458
+ // true (device.ts:406) => the instance buffer takes the STORAGE path
459
+ // (usage 136), distinguishing it from uniform buffers in the frame.
460
+ limits: {
461
+ maxStorageBufferBindingSize: 1024 * 1024 * 1024,
462
+ maxStorageBuffersPerShaderStage: 8,
463
+ },
464
+ queue: {
465
+ submit: () => undefined,
466
+ onSubmittedWorkDone: () => Promise.resolve(undefined),
467
+ writeBuffer: () => undefined,
468
+ writeTexture: () => undefined,
469
+ },
470
+ createShaderModule: () => ({ getCompilationInfo: async () => ({ messages: [] }) }),
471
+ createBindGroupLayout: () => ({}),
472
+ createPipelineLayout: () => ({}),
473
+ createRenderPipeline: () => ({}),
474
+ createBindGroup: () => ({}),
475
+ createBuffer: (desc: { size?: number; usage?: number }) => {
476
+ const raw = {
477
+ __role: `buffer-${log.created.length}`,
478
+ destroy: () => {
479
+ if (opts?.destroyThrows) throw new Error('mock destroy failure');
480
+ if (destroyedSet.has(raw)) return;
481
+ destroyedSet.add(raw);
482
+ log.destroyed.push(raw);
483
+ },
484
+ getMappedRange: () => new ArrayBuffer(desc?.size ?? 64),
485
+ unmap: () => undefined,
486
+ };
487
+ log.created.push({ handle: raw, size: desc?.size ?? 0, usage: desc?.usage ?? 0 });
488
+ return raw;
489
+ },
490
+ createCommandEncoder: () => ({
491
+ beginRenderPass: () => ({
492
+ setPipeline: () => undefined,
493
+ setVertexBuffer: () => undefined,
494
+ setIndexBuffer: () => undefined,
495
+ setBindGroup: () => undefined,
496
+ setViewport: () => undefined,
497
+ setStencilReference: () => undefined,
498
+ draw: () => undefined,
499
+ drawIndexed: () => undefined,
500
+ end: () => undefined,
501
+ }),
502
+ finish: () => ({}),
503
+ }),
504
+ createTexture: () => ({ createView: () => ({}) }),
505
+ createSampler: () => ({}),
506
+ destroy: () => undefined,
507
+ };
508
+ }
509
+
510
+ function makeIntegrationGPU(device: unknown): unknown {
511
+ return {
512
+ requestAdapter: async () => ({ requestDevice: async () => device }),
513
+ getPreferredCanvasFormat: () => 'bgra8unorm',
514
+ };
515
+ }
516
+
517
+ const integrationNavigator = {
518
+ userAgent: 'mock-engine-test',
519
+ } as Partial<Navigator> as Navigator;
520
+
521
+ // The shadow-caster shader registration gates recordShadowPass: with it,
522
+ // the shadow pass records instances BEFORE the main pass each frame and so
523
+ // owns the F12 set-before-destroy (render-system-record.ts:3323); without
524
+ // it recordShadowPass early-exits (shadow PSO null) and the main pass owns
525
+ // the F12 destroy (:4521). Each w13 sub-test selects the manifest that
526
+ // isolates the production site it falsifies, so disabling that exact line
527
+ // turns exactly that sub-test red.
528
+ function buildIntegrationManifestUrl(withShadowCaster: boolean): string {
529
+ const materialShaderStub = (identifier: string) => ({
530
+ identifier,
531
+ sourcePath: `${identifier}.wgsl`,
532
+ composedWgsl: '/* stub */',
533
+ paramSchema: '[]',
534
+ variants:
535
+ identifier === 'forgeax::default-standard-pbr' ? standardMaterialShaderVariants() : [],
536
+ });
537
+ const entries: Array<{ hash: string; wgsl: string; glsl: string; bindings: string }> = [
538
+ { hash: 'pbr00000', wgsl: '/* pbr stub - calls f_schlick( */', glsl: '', bindings: '' },
539
+ { hash: 'unlit000', wgsl: '/* unlit stub */', glsl: '', bindings: '' },
540
+ {
541
+ hash: 'tonemap0',
542
+ wgsl: '/* tonemap stub - struct TonemapParams { exposure: f32 }; */',
543
+ glsl: '',
544
+ bindings: '',
545
+ },
546
+ ];
547
+ const materialShaders = [
548
+ materialShaderStub('forgeax::default-standard-pbr'),
549
+ materialShaderStub('forgeax::default-unlit'),
550
+ ];
551
+ if (withShadowCaster) {
552
+ materialShaders.push(
553
+ materialShaderStub(
554
+ 'forgeax::default-shadow-caster',
555
+ '/* reserved shadow caster - @location(0) position vertex-only */',
556
+ ),
557
+ );
558
+ }
559
+ const manifest = {
560
+ schemaVersion: '1.0.0',
561
+ entries,
562
+ materialShaders,
563
+ };
564
+ return `data:application/json,${encodeURIComponent(JSON.stringify(manifest))}`;
565
+ }
566
+
567
+ type IntegrationRenderer = RendererType;
568
+
569
+ interface IntegrationWorld {
570
+ spawn: (...a: unknown[]) => { unwrap: () => unknown };
571
+ set: (...a: unknown[]) => unknown;
572
+ }
573
+
574
+ interface IntegrationComponents {
575
+ Transform: unknown;
576
+ MeshFilter: unknown;
577
+ MeshRenderer: unknown;
578
+ Camera: unknown;
579
+ Instances: unknown;
580
+ DirectionalLight: unknown;
581
+ HANDLE_CUBE: unknown;
582
+ }
583
+
584
+ async function bootIntegrationRenderer(
585
+ device: unknown,
586
+ withShadowCaster = false,
587
+ ): Promise<{
588
+ renderer: IntegrationRenderer;
589
+ world: IntegrationWorld;
590
+ C: IntegrationComponents;
591
+ errors: { code: string }[];
592
+ }> {
593
+ vi.stubGlobal('navigator', { ...integrationNavigator, gpu: makeIntegrationGPU(device) });
594
+ const { createRenderer } = (await import('../createRenderer')) as {
595
+ createRenderer: (...args: unknown[]) => Promise<{ unwrap(): IntegrationRenderer }>;
596
+ };
597
+ const renderer = (
598
+ await createRenderer(
599
+ makeIntegrationCanvas(),
600
+ {},
601
+ { shaderManifestUrl: buildIntegrationManifestUrl(withShadowCaster) },
602
+ )
603
+ ).unwrap();
604
+ const { World: WorldCtor } = (await import('@forgeax/engine-ecs')) as unknown as {
605
+ World: new () => IntegrationWorld;
606
+ };
607
+ const C = {
608
+ ...(await import('@forgeax/engine-render')),
609
+ ...(await import('@forgeax/engine-scene')),
610
+ ...(await import('@forgeax/engine-assets-runtime')),
611
+ } as unknown as IntegrationComponents;
612
+ const errors: { code: string }[] = [];
613
+ subscribeRendererErrors(renderer, (e) => errors.push(e));
614
+ return { renderer, world: new WorldCtor(), C, errors };
615
+ }
616
+
617
+ function spawnCamera(world: IntegrationWorld, C: IntegrationComponents): void {
618
+ world.spawn(
619
+ {
620
+ component: C.Camera,
621
+ data: {
622
+ fov: Math.PI / 4,
623
+ aspect: 16 / 9,
624
+ near: 0.1,
625
+ far: 100,
626
+ projection: 0,
627
+ left: -1,
628
+ right: 1,
629
+ bottom: -1,
630
+ top: 1,
631
+ },
632
+ },
633
+ {
634
+ component: C.Transform,
635
+ data: {
636
+ pos: [0, 0, 5],
637
+ quat: [0, 0, 0, 1],
638
+ scale: [1, 1, 1],
639
+ },
640
+ },
641
+ );
642
+ }
643
+
644
+ function spawnInstancedCube(
645
+ world: IntegrationWorld,
646
+ C: IntegrationComponents,
647
+ instanceCount: number,
648
+ ): unknown {
649
+ return world
650
+ .spawn(
651
+ { component: C.MeshFilter, data: { assetHandle: C.HANDLE_CUBE } },
652
+ { component: C.MeshRenderer, data: {} },
653
+ {
654
+ component: C.Transform,
655
+ data: {
656
+ pos: [0, 0, 0],
657
+ quat: [0, 0, 0, 1],
658
+ scale: [1, 1, 1],
659
+ },
660
+ },
661
+ { component: C.Instances, data: { transforms: new Float32Array(instanceCount * 16) } },
662
+ )
663
+ .unwrap();
664
+ }
665
+
666
+ // ── w14: AC-07 B-family error strategy ──
667
+ //
668
+ // dispose-path sub-cases drive the real disposeInstanceBuffers (already
669
+ // correct round 1); the F12 sub-case drives the real record pass with a
670
+ // destroy that fails (rawBuf.destroy throws -> rhi-webgpu surfaces
671
+ // webgpu-runtime-error -> the F12 production fires errorRegistry + sweeps on).
672
+
673
+ describe('instance buffer error strategy (AC-07) [w14]', () => {
674
+ afterEach(() => {
675
+ vi.unstubAllGlobals();
676
+ });
677
+
678
+ it('disposeInstanceBuffers: destroy clears map, isDestroyed gate skips pre-destroyed entries', async () => {
679
+ const { m4err, m4ok, RhiError, GpuBuffer } = await _loadM4Libs();
680
+ const dev = _mkBufDevice(m4err, m4ok, RhiError);
681
+
682
+ const e1 = _mkBufEntry(GpuBuffer, dev, 256, 1);
683
+ const e2 = _mkBufEntry(GpuBuffer, dev, 512, 2);
684
+ e2.buffer.destroy(); // pre-destroy
685
+ const map = new Map<number, ReturnType<typeof _mkBufEntry>>();
686
+ map.set(1, e1);
687
+ map.set(2, e2);
688
+
689
+ const fires: unknown[] = [];
690
+ disposeInstanceBuffers(map, {
691
+ fire: (e: unknown) => {
692
+ fires.push(e);
693
+ },
694
+ });
695
+
696
+ expect(e1.buffer.isDestroyed).toBe(true);
697
+ expect(e2.buffer.isDestroyed).toBe(true);
698
+ expect(map.size).toBe(0);
699
+ expect(fires).toHaveLength(0);
700
+ });
701
+
702
+ it('disposeInstanceBuffers: sweep continues, all non-pre-destroyed entries destroyed', async () => {
703
+ const { m4err, m4ok, RhiError, GpuBuffer } = await _loadM4Libs();
704
+ const dev = _mkBufDevice(m4err, m4ok, RhiError);
705
+
706
+ const e1 = _mkBufEntry(GpuBuffer, dev, 256, 1);
707
+ const e2 = _mkBufEntry(GpuBuffer, dev, 512, 2);
708
+ const e3 = _mkBufEntry(GpuBuffer, dev, 768, 3);
709
+ e2.buffer.destroy(); // pre-destroy
710
+ const map = new Map<number, ReturnType<typeof _mkBufEntry>>();
711
+ map.set(1, e1);
712
+ map.set(2, e2);
713
+ map.set(3, e3);
714
+
715
+ const fires: unknown[] = [];
716
+ disposeInstanceBuffers(map, {
717
+ fire: (e: unknown) => {
718
+ fires.push(e);
719
+ },
720
+ });
721
+
722
+ expect(e1.buffer.isDestroyed).toBe(true);
723
+ expect(e3.buffer.isDestroyed).toBe(true);
724
+ expect(map.size).toBe(0);
725
+ expect(fires).toHaveLength(0);
726
+ });
727
+
728
+ it('disposeInstanceBuffers: without errorRegistry parameter, no fire (no crash)', async () => {
729
+ const { m4err, m4ok, RhiError, GpuBuffer } = await _loadM4Libs();
730
+ const dev = _mkBufDevice(m4err, m4ok, RhiError);
731
+
732
+ const e1 = _mkBufEntry(GpuBuffer, dev, 256, 1);
733
+ const map = new Map<number, ReturnType<typeof _mkBufEntry>>();
734
+ map.set(1, e1);
735
+
736
+ // Call without errorRegistry — should not throw.
737
+ disposeInstanceBuffers(map);
738
+
739
+ expect(e1.buffer.isDestroyed).toBe(true);
740
+ expect(map.size).toBe(0);
741
+ });
742
+
743
+ it('F12 set-before-destroy failure fires errorRegistry + sweep continues (real record pass)', async () => {
744
+ const log: IntegrationBufLog = { created: [], destroyed: [] };
745
+ const device = makeIntegrationDevice(log, { destroyThrows: true });
746
+ const { renderer, world, C, errors } = await bootIntegrationRenderer(device);
747
+
748
+ spawnCamera(world, C);
749
+ const cube = spawnInstancedCube(world, C, 2);
750
+
751
+ // Frame 1: allocate the instance buffer (fingerprint = 2 instances).
752
+ drawPublished(renderer, world as WorldType);
753
+
754
+ // Frame 2: fingerprint mismatch -> F12 destroys the old buffer, whose
755
+ // raw .destroy() throws -> rhi-webgpu webgpu-runtime-error -> the F12
756
+ // production fires errorRegistry and continues to set the new buffer.
757
+ world.set(cube, C.Instances, { transforms: new Float32Array(3 * 16) });
758
+ drawPublished(renderer, world as WorldType);
759
+
760
+ // Sweep continued: a fresh (larger) instance buffer was still allocated
761
+ // after the failed destroy. The failure surfaced as a fired error.
762
+ expect(errors.some((e) => e.code === 'webgpu-runtime-error')).toBe(true);
763
+ expect(log.created.length).toBeGreaterThan(0);
764
+ });
765
+ });
766
+
767
+ // ── w13: AC-06 B-family F12 set-before-destroy (real recordMainPass + recordShadowPass) ──
768
+
769
+ describe('instance buffer F12 set-before-destroy (AC-06) [w13]', () => {
770
+ afterEach(() => {
771
+ vi.unstubAllGlobals();
772
+ });
773
+
774
+ it('main pass: fingerprint change destroys the old cached buffer before set', async () => {
775
+ const log: IntegrationBufLog = { created: [], destroyed: [] };
776
+ const device = makeIntegrationDevice(log);
777
+ const { renderer, world, C, errors } = await bootIntegrationRenderer(device);
778
+
779
+ spawnCamera(world, C);
780
+ const cube = spawnInstancedCube(world, C, 2);
781
+
782
+ // Frame 1: cold allocate the 2-instance buffer (256 B).
783
+ drawPublished(renderer, world as WorldType);
784
+ expect(errors).toHaveLength(0);
785
+ const createdAfterF1 = log.created.length;
786
+ expect(createdAfterF1).toBeGreaterThan(0);
787
+ expect(log.destroyed).toHaveLength(0);
788
+ const oldInstanceBuffer = latestInstanceBuffer(log, 2);
789
+
790
+ // Frame 2: 2 -> 3 instances => byteLength fingerprint mismatch => the
791
+ // main-pass F12 path (render-system-record.ts:4521) destroys the old
792
+ // 256 B buffer, then sets a fresh 384 B one. No shadow-caster shader is
793
+ // registered, so recordShadowPass early-exits and the main pass is the
794
+ // sole F12 owner this frame.
795
+ world.set(cube, C.Instances, { transforms: new Float32Array(3 * 16) });
796
+ drawPublished(renderer, world as WorldType);
797
+
798
+ // The destroyed buffer is specifically the old instance buffer (STORAGE
799
+ // usage, 256 B), not an incidental destroy elsewhere. Disabling :4521
800
+ // drops it.
801
+ expect(log.destroyed.includes(oldInstanceBuffer)).toBe(true);
802
+ // A new (larger) 384 B instance buffer replaced the destroyed one.
803
+ expect(
804
+ log.created.some((c) => c.usage === INSTANCE_USAGE && c.size === INSTANCE_BYTES(3)),
805
+ ).toBe(true);
806
+ });
807
+
808
+ it('shadow pass: fingerprint change destroys the old cached buffer before set', async () => {
809
+ const log: IntegrationBufLog = { created: [], destroyed: [] };
810
+ const device = makeIntegrationDevice(log);
811
+ // withShadowCaster=true registers forgeax::default-shadow-caster so
812
+ // recordShadowPass runs.
813
+ const { renderer, world, C } = await bootIntegrationRenderer(device, true);
814
+
815
+ spawnCamera(world, C);
816
+ // DirectionalLight with castShadow => recordShadowPass runs and
817
+ // records the instance entity BEFORE the main pass, so the shadow F12
818
+ // path (render-system-record.ts:3323) owns the destroy this frame.
819
+ world.spawn({
820
+ component: C.DirectionalLight,
821
+ data: {
822
+ direction: [-0.5, -1, -0.3],
823
+ color: [1, 1, 1],
824
+ intensity: 1,
825
+ cascadeCount: 1,
826
+ mapSize: 1024,
827
+ },
828
+ });
829
+ const cube = spawnInstancedCube(world, C, 2);
830
+
831
+ drawPublished(renderer, world as WorldType);
832
+ expect(log.destroyed).toHaveLength(0);
833
+ const oldInstanceBuffer = latestInstanceBuffer(log, 2);
834
+
835
+ world.set(cube, C.Instances, { transforms: new Float32Array(3 * 16) });
836
+ drawPublished(renderer, world as WorldType);
837
+
838
+ // The shadow pass records the instance entity before the main pass, so
839
+ // it owns the F12 destroy of the old (STORAGE, 256 B) instance buffer
840
+ // this frame. Disabling :3323 drops this assertion (main reuses the
841
+ // already-updated entry and never re-destroys).
842
+ expect(log.destroyed.includes(oldInstanceBuffer)).toBe(true);
843
+ });
844
+ });
845
+
846
+ // ── w15: AC-10 WeakMap chain behavior invariants ──
847
+ //
848
+ // feat-20260622-handle-to-id-allocator-elimination: the old
849
+ // getOrAssignHandleId is gone; the old numeric counter is removed. WeakMap chain
850
+ // determinism replaces it — same handle object identity → same
851
+ // leaf BG, different handles → different leaf.
852
+
853
+ describe('WeakMap chain behavior invariants [w15]', () => {
854
+ it('same handle object in chain → same leaf BindGroup (deterministic)', () => {
855
+ const root = new WeakMap<object, unknown>();
856
+ const h1 = {};
857
+ const h2 = {};
858
+
859
+ // We simulate the chain by building two levels manually for the test
860
+ const inner = new WeakMap<object, unknown>();
861
+ root.set(h1, inner);
862
+ const leaf = new Map<string, unknown>();
863
+ inner.set(h2, leaf);
864
+ const bg1 = { __label: 'bg-1' };
865
+ leaf.set('variant-a', bg1);
866
+
867
+ // Same handle path → same leaf entry
868
+ const innerCheck = root.get(h1) as WeakMap<object, unknown>;
869
+ expect(innerCheck).toBeDefined();
870
+ const leafCheck = innerCheck.get(h2) as Map<string, unknown>;
871
+ expect(leafCheck).toBeDefined();
872
+ expect(leafCheck.get('variant-a')).toBe(bg1);
873
+ expect(leafCheck.get('variant-a')).toBe(bg1);
874
+
875
+ // Different variant on same chain → different leaf entry
876
+ const bg2 = { __label: 'bg-2' };
877
+ leaf.set('variant-b', bg2);
878
+ expect(leaf.get('variant-a')).toBe(bg1);
879
+ expect(leaf.get('variant-b')).toBe(bg2);
880
+ expect(bg1).not.toBe(bg2);
881
+ });
882
+
883
+ it('different handle objects → different chain position → different leaf', () => {
884
+ const root = new WeakMap<object, unknown>();
885
+ const hA = {};
886
+ const hB = {};
887
+ const innerA = new WeakMap<object, unknown>();
888
+ const innerB = new WeakMap<object, unknown>();
889
+ const leafA = new Map<string, unknown>();
890
+ const leafB = new Map<string, unknown>();
891
+ innerA.set({}, leafA);
892
+ innerB.set({}, leafB);
893
+ root.set(hA, innerA);
894
+ root.set(hB, innerB);
895
+
896
+ // Different root-level key → completely independent chains
897
+ const chainA = root.get(hA) as WeakMap<object, unknown>;
898
+ const chainB = root.get(hB) as WeakMap<object, unknown>;
899
+ expect(chainA).not.toBe(chainB);
900
+
901
+ // No shared leaf — hA's chain entries don't hit hB's chain
902
+ leafA.set('v', 'from-A');
903
+ leafB.set('v', 'from-B');
904
+ expect(
905
+ (root.get(hA) as WeakMap<object, unknown>).get({}) as Map<string, unknown>,
906
+ ).toBeUndefined();
907
+ });
908
+
909
+ it('grow miss: new inner buffer is new WeakMap key → cache miss (AC-07)', () => {
910
+ // AC-07: when mesh SSBO grows, the inner buffer object is replaced.
911
+ // The old inner buffer was a WeakMap key in the chain; the new one is
912
+ // a different object, so chain lookup naturally misses.
913
+ const root = new WeakMap<object, unknown>();
914
+ const oldBuf = {};
915
+ const inner = new WeakMap<object, unknown>();
916
+ const leaf = new Map<string, unknown>();
917
+ inner.set({}, leaf);
918
+ root.set(oldBuf, inner);
919
+
920
+ // Old buffer hits
921
+ expect(root.has(oldBuf)).toBe(true);
922
+
923
+ // New buffer (grow replacement) misses
924
+ const newBuf = {};
925
+ expect(root.has(newBuf)).toBe(false);
926
+ expect(oldBuf).not.toBe(newBuf);
927
+ });
928
+ });
929
+ }