@forgeax/engine-render 0.0.0-dev.8d955ade1c79

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (891) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +647 -0
  3. package/dist/.tsbuildinfo +1 -0
  4. package/dist/__tests__/authoring-path.unit.test.d.ts +2 -0
  5. package/dist/__tests__/authoring-path.unit.test.d.ts.map +1 -0
  6. package/dist/__tests__/backend-selection-rhi-capture.test-d.d.ts +2 -0
  7. package/dist/__tests__/backend-selection-rhi-capture.test-d.d.ts.map +1 -0
  8. package/dist/__tests__/callback-inference.test-d.d.ts +2 -0
  9. package/dist/__tests__/callback-inference.test-d.d.ts.map +1 -0
  10. package/dist/__tests__/cluster-binner.test.d.ts +2 -0
  11. package/dist/__tests__/cluster-binner.test.d.ts.map +1 -0
  12. package/dist/__tests__/cluster-grid-default-surface.unit.test.d.ts +2 -0
  13. package/dist/__tests__/cluster-grid-default-surface.unit.test.d.ts.map +1 -0
  14. package/dist/__tests__/construct-renderer-identity.unit.test.d.ts +2 -0
  15. package/dist/__tests__/construct-renderer-identity.unit.test.d.ts.map +1 -0
  16. package/dist/__tests__/device-scope-lifecycle.unit.test.d.ts +2 -0
  17. package/dist/__tests__/device-scope-lifecycle.unit.test.d.ts.map +1 -0
  18. package/dist/__tests__/device-scope-stale-matrix.unit.test.d.ts +2 -0
  19. package/dist/__tests__/device-scope-stale-matrix.unit.test.d.ts.map +1 -0
  20. package/dist/__tests__/direct-extract-consumer-contract.test-d.d.ts +2 -0
  21. package/dist/__tests__/direct-extract-consumer-contract.test-d.d.ts.map +1 -0
  22. package/dist/__tests__/direct-light-snapshot.unit.test.d.ts +2 -0
  23. package/dist/__tests__/direct-light-snapshot.unit.test.d.ts.map +1 -0
  24. package/dist/__tests__/factory-contract.integration.test.d.ts +2 -0
  25. package/dist/__tests__/factory-contract.integration.test.d.ts.map +1 -0
  26. package/dist/__tests__/fallback-row-stride-surface.unit.test.d.ts +2 -0
  27. package/dist/__tests__/fallback-row-stride-surface.unit.test.d.ts.map +1 -0
  28. package/dist/__tests__/frame-observation.contract.test.d.ts +2 -0
  29. package/dist/__tests__/frame-observation.contract.test.d.ts.map +1 -0
  30. package/dist/__tests__/fullscreen-feature-plan.unit.test.d.ts +2 -0
  31. package/dist/__tests__/fullscreen-feature-plan.unit.test.d.ts.map +1 -0
  32. package/dist/__tests__/glyph-text-layout-recovery.unit.test.d.ts +2 -0
  33. package/dist/__tests__/glyph-text-layout-recovery.unit.test.d.ts.map +1 -0
  34. package/dist/__tests__/gpu-driven-indirect-raster-evidence.d.ts +6 -0
  35. package/dist/__tests__/gpu-driven-indirect-raster-evidence.d.ts.map +1 -0
  36. package/dist/__tests__/gpu-driven-indirect-raster.browser.test.d.ts +2 -0
  37. package/dist/__tests__/gpu-driven-indirect-raster.browser.test.d.ts.map +1 -0
  38. package/dist/__tests__/gpu-driven-indirect-raster.dawn.test.d.ts +2 -0
  39. package/dist/__tests__/gpu-driven-indirect-raster.dawn.test.d.ts.map +1 -0
  40. package/dist/__tests__/gpu-driven-production.integration.test.d.ts +2 -0
  41. package/dist/__tests__/gpu-driven-production.integration.test.d.ts.map +1 -0
  42. package/dist/__tests__/gpu-driven-scaling.unit.test.d.ts +2 -0
  43. package/dist/__tests__/gpu-driven-scaling.unit.test.d.ts.map +1 -0
  44. package/dist/__tests__/gpu-driven-view-gpu-evidence.d.ts +18 -0
  45. package/dist/__tests__/gpu-driven-view-gpu-evidence.d.ts.map +1 -0
  46. package/dist/__tests__/gpu-driven-view-graph.integration.test.d.ts +2 -0
  47. package/dist/__tests__/gpu-driven-view-graph.integration.test.d.ts.map +1 -0
  48. package/dist/__tests__/gpu-driven-view-reference.d.ts +15 -0
  49. package/dist/__tests__/gpu-driven-view-reference.d.ts.map +1 -0
  50. package/dist/__tests__/gpu-driven-view.browser.test.d.ts +2 -0
  51. package/dist/__tests__/gpu-driven-view.browser.test.d.ts.map +1 -0
  52. package/dist/__tests__/gpu-driven-view.dawn.test.d.ts +2 -0
  53. package/dist/__tests__/gpu-driven-view.dawn.test.d.ts.map +1 -0
  54. package/dist/__tests__/gpu-driven-view.unit.test.d.ts +2 -0
  55. package/dist/__tests__/gpu-driven-view.unit.test.d.ts.map +1 -0
  56. package/dist/__tests__/gpu-resource-store-stride.unit.test.d.ts +2 -0
  57. package/dist/__tests__/gpu-resource-store-stride.unit.test.d.ts.map +1 -0
  58. package/dist/__tests__/gpu-scene-render-graph-gpu.d.ts +7 -0
  59. package/dist/__tests__/gpu-scene-render-graph-gpu.d.ts.map +1 -0
  60. package/dist/__tests__/gpu-scene-render-graph.browser.test.d.ts +2 -0
  61. package/dist/__tests__/gpu-scene-render-graph.browser.test.d.ts.map +1 -0
  62. package/dist/__tests__/gpu-scene-render-graph.dawn.test.d.ts +2 -0
  63. package/dist/__tests__/gpu-scene-render-graph.dawn.test.d.ts.map +1 -0
  64. package/dist/__tests__/gpu-scene-schema.unit.test.d.ts +2 -0
  65. package/dist/__tests__/gpu-scene-schema.unit.test.d.ts.map +1 -0
  66. package/dist/__tests__/gpu-scene.dawn.test.d.ts +2 -0
  67. package/dist/__tests__/gpu-scene.dawn.test.d.ts.map +1 -0
  68. package/dist/__tests__/gpu-scene.unit.test.d.ts +2 -0
  69. package/dist/__tests__/gpu-scene.unit.test.d.ts.map +1 -0
  70. package/dist/__tests__/ibl/face-uniforms.unit.test.d.ts +2 -0
  71. package/dist/__tests__/ibl/face-uniforms.unit.test.d.ts.map +1 -0
  72. package/dist/__tests__/infinite-grid-no-vertex-probe.unit.test.d.ts +2 -0
  73. package/dist/__tests__/infinite-grid-no-vertex-probe.unit.test.d.ts.map +1 -0
  74. package/dist/__tests__/injection-kind-owner.test-d.d.ts +2 -0
  75. package/dist/__tests__/injection-kind-owner.test-d.d.ts.map +1 -0
  76. package/dist/__tests__/ktx2-basis-gpu-consumer.dawn.test.d.ts +2 -0
  77. package/dist/__tests__/ktx2-basis-gpu-consumer.dawn.test.d.ts.map +1 -0
  78. package/dist/__tests__/light-buffer-layout.unit.test.d.ts +2 -0
  79. package/dist/__tests__/light-buffer-layout.unit.test.d.ts.map +1 -0
  80. package/dist/__tests__/material-contract-inventory.unit.test.d.ts +2 -0
  81. package/dist/__tests__/material-contract-inventory.unit.test.d.ts.map +1 -0
  82. package/dist/__tests__/material-cooked-projection.unit.test.d.ts +2 -0
  83. package/dist/__tests__/material-cooked-projection.unit.test.d.ts.map +1 -0
  84. package/dist/__tests__/material-snapshot-mutation.integration.test.d.ts +2 -0
  85. package/dist/__tests__/material-snapshot-mutation.integration.test.d.ts.map +1 -0
  86. package/dist/__tests__/material-static-dynamic-route.unit.test.d.ts +2 -0
  87. package/dist/__tests__/material-static-dynamic-route.unit.test.d.ts.map +1 -0
  88. package/dist/__tests__/material-stride-surface.unit.test.d.ts +2 -0
  89. package/dist/__tests__/material-stride-surface.unit.test.d.ts.map +1 -0
  90. package/dist/__tests__/mesh-buffer-usage-surface.unit.test.d.ts +2 -0
  91. package/dist/__tests__/mesh-buffer-usage-surface.unit.test.d.ts.map +1 -0
  92. package/dist/__tests__/mesh-material-bindings.unit.test.d.ts +2 -0
  93. package/dist/__tests__/mesh-material-bindings.unit.test.d.ts.map +1 -0
  94. package/dist/__tests__/mesh-stride-surface.unit.test.d.ts +2 -0
  95. package/dist/__tests__/mesh-stride-surface.unit.test.d.ts.map +1 -0
  96. package/dist/__tests__/morph-culling-reentry.dawn.test.d.ts +2 -0
  97. package/dist/__tests__/morph-culling-reentry.dawn.test.d.ts.map +1 -0
  98. package/dist/__tests__/morph-feature-gpu-work.integration.test.d.ts +2 -0
  99. package/dist/__tests__/morph-feature-gpu-work.integration.test.d.ts.map +1 -0
  100. package/dist/__tests__/morph-feature-plan.unit.test.d.ts +2 -0
  101. package/dist/__tests__/morph-feature-plan.unit.test.d.ts.map +1 -0
  102. package/dist/__tests__/morph-visual-evidence.integration.test.d.ts +2 -0
  103. package/dist/__tests__/morph-visual-evidence.integration.test.d.ts.map +1 -0
  104. package/dist/__tests__/nineslice-handle-surface.unit.test.d.ts +2 -0
  105. package/dist/__tests__/nineslice-handle-surface.unit.test.d.ts.map +1 -0
  106. package/dist/__tests__/post-process-error-policy-owner.unit.test.d.ts +2 -0
  107. package/dist/__tests__/post-process-error-policy-owner.unit.test.d.ts.map +1 -0
  108. package/dist/__tests__/prepared-buffer-kind-owner.test-d.d.ts +2 -0
  109. package/dist/__tests__/prepared-buffer-kind-owner.test-d.d.ts.map +1 -0
  110. package/dist/__tests__/prepared-graphics-capability.unit.test.d.ts +2 -0
  111. package/dist/__tests__/prepared-graphics-capability.unit.test.d.ts.map +1 -0
  112. package/dist/__tests__/prepared-graphics-generation.unit.test.d.ts +2 -0
  113. package/dist/__tests__/prepared-graphics-generation.unit.test.d.ts.map +1 -0
  114. package/dist/__tests__/prepared-graphics-lifecycle.unit.test.d.ts +2 -0
  115. package/dist/__tests__/prepared-graphics-lifecycle.unit.test.d.ts.map +1 -0
  116. package/dist/__tests__/prepared-graphics-pipeline-warmup.unit.test.d.ts +2 -0
  117. package/dist/__tests__/prepared-graphics-pipeline-warmup.unit.test.d.ts.map +1 -0
  118. package/dist/__tests__/prepared-graphics-resolver.unit.test.d.ts +2 -0
  119. package/dist/__tests__/prepared-graphics-resolver.unit.test.d.ts.map +1 -0
  120. package/dist/__tests__/prepared-graphics-snapshot.unit.test.d.ts +2 -0
  121. package/dist/__tests__/prepared-graphics-snapshot.unit.test.d.ts.map +1 -0
  122. package/dist/__tests__/prepared-graphics-store.unit.test.d.ts +2 -0
  123. package/dist/__tests__/prepared-graphics-store.unit.test.d.ts.map +1 -0
  124. package/dist/__tests__/prepared-graphics-validation.unit.test.d.ts +2 -0
  125. package/dist/__tests__/prepared-graphics-validation.unit.test.d.ts.map +1 -0
  126. package/dist/__tests__/profiler-default-off.test.d.ts +2 -0
  127. package/dist/__tests__/profiler-default-off.test.d.ts.map +1 -0
  128. package/dist/__tests__/profiler-phase-catalog.test.d.ts +2 -0
  129. package/dist/__tests__/profiler-phase-catalog.test.d.ts.map +1 -0
  130. package/dist/__tests__/profiler-phase-integration.test.d.ts +2 -0
  131. package/dist/__tests__/profiler-phase-integration.test.d.ts.map +1 -0
  132. package/dist/__tests__/profiler-phase-token.test.d.ts +2 -0
  133. package/dist/__tests__/profiler-phase-token.test.d.ts.map +1 -0
  134. package/dist/__tests__/public-surface.test-d.d.ts +2 -0
  135. package/dist/__tests__/public-surface.test-d.d.ts.map +1 -0
  136. package/dist/__tests__/recover-error-policy-owner.unit.test.d.ts +2 -0
  137. package/dist/__tests__/recover-error-policy-owner.unit.test.d.ts.map +1 -0
  138. package/dist/__tests__/render-error-code-owner.test-d.d.ts +2 -0
  139. package/dist/__tests__/render-error-code-owner.test-d.d.ts.map +1 -0
  140. package/dist/__tests__/render-error-detail-ssot.test-d.d.ts +2 -0
  141. package/dist/__tests__/render-error-detail-ssot.test-d.d.ts.map +1 -0
  142. package/dist/__tests__/render-error-exhaustive.test-d.d.ts +2 -0
  143. package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +1 -0
  144. package/dist/__tests__/render-feature-assembly.test-d.d.ts +2 -0
  145. package/dist/__tests__/render-feature-assembly.test-d.d.ts.map +1 -0
  146. package/dist/__tests__/render-feature-context.test-d.d.ts +2 -0
  147. package/dist/__tests__/render-feature-context.test-d.d.ts.map +1 -0
  148. package/dist/__tests__/render-feature-contract.test-d.d.ts +2 -0
  149. package/dist/__tests__/render-feature-contract.test-d.d.ts.map +1 -0
  150. package/dist/__tests__/render-feature-diagnostics.unit.test.d.ts +2 -0
  151. package/dist/__tests__/render-feature-diagnostics.unit.test.d.ts.map +1 -0
  152. package/dist/__tests__/render-feature-docs.unit.test.d.ts +2 -0
  153. package/dist/__tests__/render-feature-docs.unit.test.d.ts.map +1 -0
  154. package/dist/__tests__/render-feature-errors.unit.test.d.ts +2 -0
  155. package/dist/__tests__/render-feature-errors.unit.test.d.ts.map +1 -0
  156. package/dist/__tests__/render-feature-gpu-work.integration.test.d.ts +2 -0
  157. package/dist/__tests__/render-feature-gpu-work.integration.test.d.ts.map +1 -0
  158. package/dist/__tests__/render-feature-plan.test-d.d.ts +2 -0
  159. package/dist/__tests__/render-feature-plan.test-d.d.ts.map +1 -0
  160. package/dist/__tests__/render-feature-plan.unit.test.d.ts +2 -0
  161. package/dist/__tests__/render-feature-plan.unit.test.d.ts.map +1 -0
  162. package/dist/__tests__/render-feature-prepared-diagnostics.unit.test.d.ts +2 -0
  163. package/dist/__tests__/render-feature-prepared-diagnostics.unit.test.d.ts.map +1 -0
  164. package/dist/__tests__/render-feature-prepared-errors.test-d.d.ts +2 -0
  165. package/dist/__tests__/render-feature-prepared-errors.test-d.d.ts.map +1 -0
  166. package/dist/__tests__/render-feature-prepared-graphics-docs.unit.test.d.ts +2 -0
  167. package/dist/__tests__/render-feature-prepared-graphics-docs.unit.test.d.ts.map +1 -0
  168. package/dist/__tests__/render-feature-prepared-graphics-isolation.unit.test.d.ts +2 -0
  169. package/dist/__tests__/render-feature-prepared-graphics-isolation.unit.test.d.ts.map +1 -0
  170. package/dist/__tests__/render-feature-prepared-graphics-negative.test-d.d.ts +2 -0
  171. package/dist/__tests__/render-feature-prepared-graphics-negative.test-d.d.ts.map +1 -0
  172. package/dist/__tests__/render-feature-prepared-graphics.test-d.d.ts +2 -0
  173. package/dist/__tests__/render-feature-prepared-graphics.test-d.d.ts.map +1 -0
  174. package/dist/__tests__/render-feature-prepared-graphics.unit.test.d.ts +2 -0
  175. package/dist/__tests__/render-feature-prepared-graphics.unit.test.d.ts.map +1 -0
  176. package/dist/__tests__/render-feature-public-surface.test-d.d.ts +2 -0
  177. package/dist/__tests__/render-feature-public-surface.test-d.d.ts.map +1 -0
  178. package/dist/__tests__/render-feature-retry.unit.test.d.ts +2 -0
  179. package/dist/__tests__/render-feature-retry.unit.test.d.ts.map +1 -0
  180. package/dist/__tests__/render-feature-scheduling.unit.test.d.ts +2 -0
  181. package/dist/__tests__/render-feature-scheduling.unit.test.d.ts.map +1 -0
  182. package/dist/__tests__/render-feature-stage-failed-policy-owner.unit.test.d.ts +2 -0
  183. package/dist/__tests__/render-feature-stage-failed-policy-owner.unit.test.d.ts.map +1 -0
  184. package/dist/__tests__/render-feature-stage-fault-recovery.unit.test.d.ts +2 -0
  185. package/dist/__tests__/render-feature-stage-fault-recovery.unit.test.d.ts.map +1 -0
  186. package/dist/__tests__/render-feature-visibility-context.test-d.d.ts +2 -0
  187. package/dist/__tests__/render-feature-visibility-context.test-d.d.ts.map +1 -0
  188. package/dist/__tests__/render-feature-visibility-context.unit.test.d.ts +2 -0
  189. package/dist/__tests__/render-feature-visibility-context.unit.test.d.ts.map +1 -0
  190. package/dist/__tests__/render-feature-zero-work.unit.test.d.ts +2 -0
  191. package/dist/__tests__/render-feature-zero-work.unit.test.d.ts.map +1 -0
  192. package/dist/__tests__/render-graph-compute-raster-gpu.d.ts +7 -0
  193. package/dist/__tests__/render-graph-compute-raster-gpu.d.ts.map +1 -0
  194. package/dist/__tests__/render-graph-compute-raster.browser.test.d.ts +2 -0
  195. package/dist/__tests__/render-graph-compute-raster.browser.test.d.ts.map +1 -0
  196. package/dist/__tests__/render-graph-compute-raster.dawn.test.d.ts +2 -0
  197. package/dist/__tests__/render-graph-compute-raster.dawn.test.d.ts.map +1 -0
  198. package/dist/__tests__/render-graph-hzb-gpu.d.ts +7 -0
  199. package/dist/__tests__/render-graph-hzb-gpu.d.ts.map +1 -0
  200. package/dist/__tests__/render-graph-hzb.browser.test.d.ts +2 -0
  201. package/dist/__tests__/render-graph-hzb.browser.test.d.ts.map +1 -0
  202. package/dist/__tests__/render-graph-hzb.dawn.test.d.ts +2 -0
  203. package/dist/__tests__/render-graph-hzb.dawn.test.d.ts.map +1 -0
  204. package/dist/__tests__/render-graph-primitives.unit.test.d.ts +2 -0
  205. package/dist/__tests__/render-graph-primitives.unit.test.d.ts.map +1 -0
  206. package/dist/__tests__/render-graph-storage-indirect-gpu.d.ts +12 -0
  207. package/dist/__tests__/render-graph-storage-indirect-gpu.d.ts.map +1 -0
  208. package/dist/__tests__/render-graph-storage-indirect.browser.test.d.ts +2 -0
  209. package/dist/__tests__/render-graph-storage-indirect.browser.test.d.ts.map +1 -0
  210. package/dist/__tests__/render-graph-storage-indirect.dawn.test.d.ts +2 -0
  211. package/dist/__tests__/render-graph-storage-indirect.dawn.test.d.ts.map +1 -0
  212. package/dist/__tests__/render-scene-projection.unit.test.d.ts +2 -0
  213. package/dist/__tests__/render-scene-projection.unit.test.d.ts.map +1 -0
  214. package/dist/__tests__/renderer-factory-material-contract.unit.test.d.ts +2 -0
  215. package/dist/__tests__/renderer-factory-material-contract.unit.test.d.ts.map +1 -0
  216. package/dist/__tests__/renderer-lifecycle.integration.test.d.ts +2 -0
  217. package/dist/__tests__/renderer-lifecycle.integration.test.d.ts.map +1 -0
  218. package/dist/__tests__/rhi-backend-package-boundary.unit.test.d.ts +2 -0
  219. package/dist/__tests__/rhi-backend-package-boundary.unit.test.d.ts.map +1 -0
  220. package/dist/__tests__/scriptable-pack-consumer.test-d.d.ts +2 -0
  221. package/dist/__tests__/scriptable-pack-consumer.test-d.d.ts.map +1 -0
  222. package/dist/__tests__/shader-stage-surface.unit.test.d.ts +2 -0
  223. package/dist/__tests__/shader-stage-surface.unit.test.d.ts.map +1 -0
  224. package/dist/__tests__/ssao-config.unit.test.d.ts +2 -0
  225. package/dist/__tests__/ssao-config.unit.test.d.ts.map +1 -0
  226. package/dist/__tests__/ssao-kernel-surface.unit.test.d.ts +2 -0
  227. package/dist/__tests__/ssao-kernel-surface.unit.test.d.ts.map +1 -0
  228. package/dist/__tests__/standard-clustered-pipeline.unit.test.d.ts +2 -0
  229. package/dist/__tests__/standard-clustered-pipeline.unit.test.d.ts.map +1 -0
  230. package/dist/__tests__/standard-descriptors.test-d.d.ts +2 -0
  231. package/dist/__tests__/standard-descriptors.test-d.d.ts.map +1 -0
  232. package/dist/__tests__/standard-pipeline.dawn.test.d.ts +2 -0
  233. package/dist/__tests__/standard-pipeline.dawn.test.d.ts.map +1 -0
  234. package/dist/__tests__/standard-pipeline.integration.test.d.ts +2 -0
  235. package/dist/__tests__/standard-pipeline.integration.test.d.ts.map +1 -0
  236. package/dist/__tests__/standard-profile-oracle.unit.test.d.ts +2 -0
  237. package/dist/__tests__/standard-profile-oracle.unit.test.d.ts.map +1 -0
  238. package/dist/__tests__/surface-configuration.unit.test.d.ts +2 -0
  239. package/dist/__tests__/surface-configuration.unit.test.d.ts.map +1 -0
  240. package/dist/__tests__/texture-usage-surface.unit.test.d.ts +2 -0
  241. package/dist/__tests__/texture-usage-surface.unit.test.d.ts.map +1 -0
  242. package/dist/__tests__/typed-pipeline-topology.unit.test.d.ts +2 -0
  243. package/dist/__tests__/typed-pipeline-topology.unit.test.d.ts.map +1 -0
  244. package/dist/__tests__/urp-pipeline-targets.unit.test.d.ts +2 -0
  245. package/dist/__tests__/urp-pipeline-targets.unit.test.d.ts.map +1 -0
  246. package/dist/__tests__/urp-pipeline.unit.test.d.ts +2 -0
  247. package/dist/__tests__/urp-pipeline.unit.test.d.ts.map +1 -0
  248. package/dist/__tests__/vertex-color-pipeline.unit.test.d.ts +2 -0
  249. package/dist/__tests__/vertex-color-pipeline.unit.test.d.ts.map +1 -0
  250. package/dist/__tests__/vertex-layout-consumer.unit.test.d.ts +2 -0
  251. package/dist/__tests__/vertex-layout-consumer.unit.test.d.ts.map +1 -0
  252. package/dist/__tests__/visibility-diagnostics.integration.test.d.ts +2 -0
  253. package/dist/__tests__/visibility-diagnostics.integration.test.d.ts.map +1 -0
  254. package/dist/__tests__/visibility-extract.integration.test.d.ts +2 -0
  255. package/dist/__tests__/visibility-extract.integration.test.d.ts.map +1 -0
  256. package/dist/__tests__/visibility-extract.perf.test.d.ts +2 -0
  257. package/dist/__tests__/visibility-extract.perf.test.d.ts.map +1 -0
  258. package/dist/__tests__/visibility-feature-dedup.integration.test.d.ts +2 -0
  259. package/dist/__tests__/visibility-feature-dedup.integration.test.d.ts.map +1 -0
  260. package/dist/__tests__/visibility-hidden-report.unit.test.d.ts +2 -0
  261. package/dist/__tests__/visibility-hidden-report.unit.test.d.ts.map +1 -0
  262. package/dist/__tests__/visibility-instances.integration.test.d.ts +2 -0
  263. package/dist/__tests__/visibility-instances.integration.test.d.ts.map +1 -0
  264. package/dist/__tests__/visibility-multi-world.integration.test.d.ts +2 -0
  265. package/dist/__tests__/visibility-multi-world.integration.test.d.ts.map +1 -0
  266. package/dist/__tests__/visibility-producer-matrix.integration.test.d.ts +2 -0
  267. package/dist/__tests__/visibility-producer-matrix.integration.test.d.ts.map +1 -0
  268. package/dist/__tests__/visibility-producer-matrix.unit.test.d.ts +2 -0
  269. package/dist/__tests__/visibility-producer-matrix.unit.test.d.ts.map +1 -0
  270. package/dist/__tests__/visibility-public-surface.test-d.d.ts +2 -0
  271. package/dist/__tests__/visibility-public-surface.test-d.d.ts.map +1 -0
  272. package/dist/__tests__/visibility-resolver.unit.test.d.ts +2 -0
  273. package/dist/__tests__/visibility-resolver.unit.test.d.ts.map +1 -0
  274. package/dist/__tests__/visibility-resource-short-circuit.unit.test.d.ts +2 -0
  275. package/dist/__tests__/visibility-resource-short-circuit.unit.test.d.ts.map +1 -0
  276. package/dist/__tests__/visibility-snapshot-contract.unit.test.d.ts +2 -0
  277. package/dist/__tests__/visibility-snapshot-contract.unit.test.d.ts.map +1 -0
  278. package/dist/__tests__/vocabulary.unit.test.d.ts +2 -0
  279. package/dist/__tests__/vocabulary.unit.test.d.ts.map +1 -0
  280. package/dist/assembly/backend-contract.d.ts +37 -0
  281. package/dist/assembly/backend-contract.d.ts.map +1 -0
  282. package/dist/assembly/factory.d.ts +207 -0
  283. package/dist/assembly/factory.d.ts.map +1 -0
  284. package/dist/assembly/host-contract.d.ts +84 -0
  285. package/dist/assembly/host-contract.d.ts.map +1 -0
  286. package/dist/assembly/material/assembly.d.ts +21 -0
  287. package/dist/assembly/material/assembly.d.ts.map +1 -0
  288. package/dist/assembly/material/pipeline-projection.d.ts +30 -0
  289. package/dist/assembly/material/pipeline-projection.d.ts.map +1 -0
  290. package/dist/assets/asset-decoders.d.ts +5 -0
  291. package/dist/assets/asset-decoders.d.ts.map +1 -0
  292. package/dist/assets/render-asset-port.d.ts +17 -0
  293. package/dist/assets/render-asset-port.d.ts.map +1 -0
  294. package/dist/assets/resolve-render-asset.d.ts +6 -0
  295. package/dist/assets/resolve-render-asset.d.ts.map +1 -0
  296. package/dist/authoring.d.ts +12 -0
  297. package/dist/authoring.d.ts.map +1 -0
  298. package/dist/authoring.mjs +27 -0
  299. package/dist/authoring.mjs.map +1 -0
  300. package/dist/chunk-CQXZC5XI.mjs +182 -0
  301. package/dist/chunk-CQXZC5XI.mjs.map +1 -0
  302. package/dist/chunk-HCOGPHKS.mjs +93 -0
  303. package/dist/chunk-HCOGPHKS.mjs.map +1 -0
  304. package/dist/chunk-YEJIUW7D.mjs +1152 -0
  305. package/dist/chunk-YEJIUW7D.mjs.map +1 -0
  306. package/dist/cluster-binner.d.ts +152 -0
  307. package/dist/cluster-binner.d.ts.map +1 -0
  308. package/dist/components/__tests__/points-lines-admission.unit.test.d.ts +2 -0
  309. package/dist/components/__tests__/points-lines-admission.unit.test.d.ts.map +1 -0
  310. package/dist/components/__tests__/points-lines-schema.unit.test.d.ts +2 -0
  311. package/dist/components/__tests__/points-lines-schema.unit.test.d.ts.map +1 -0
  312. package/dist/components/__tests__/schema-contract.unit.test.d.ts +2 -0
  313. package/dist/components/__tests__/schema-contract.unit.test.d.ts.map +1 -0
  314. package/dist/components/__tests__/schema-parity.unit.test.d.ts +2 -0
  315. package/dist/components/__tests__/schema-parity.unit.test.d.ts.map +1 -0
  316. package/dist/components/__tests__/visibility-schema.unit.test.d.ts +2 -0
  317. package/dist/components/__tests__/visibility-schema.unit.test.d.ts.map +1 -0
  318. package/dist/components/camera.d.ts +311 -0
  319. package/dist/components/camera.d.ts.map +1 -0
  320. package/dist/components/directional-light.d.ts +76 -0
  321. package/dist/components/directional-light.d.ts.map +1 -0
  322. package/dist/components/glyph-text.d.ts +26 -0
  323. package/dist/components/glyph-text.d.ts.map +1 -0
  324. package/dist/components/index.d.ts +24 -0
  325. package/dist/components/index.d.ts.map +1 -0
  326. package/dist/components/instances.d.ts +41 -0
  327. package/dist/components/instances.d.ts.map +1 -0
  328. package/dist/components/layer.d.ts +34 -0
  329. package/dist/components/layer.d.ts.map +1 -0
  330. package/dist/components/light-helpers.d.ts +53 -0
  331. package/dist/components/light-helpers.d.ts.map +1 -0
  332. package/dist/components/lines.d.ts +5 -0
  333. package/dist/components/lines.d.ts.map +1 -0
  334. package/dist/components/mesh-filter.d.ts +24 -0
  335. package/dist/components/mesh-filter.d.ts.map +1 -0
  336. package/dist/components/mesh-renderer.d.ts +33 -0
  337. package/dist/components/mesh-renderer.d.ts.map +1 -0
  338. package/dist/components/point-light-shadow.d.ts +27 -0
  339. package/dist/components/point-light-shadow.d.ts.map +1 -0
  340. package/dist/components/point-light.d.ts +35 -0
  341. package/dist/components/point-light.d.ts.map +1 -0
  342. package/dist/components/points.d.ts +15 -0
  343. package/dist/components/points.d.ts.map +1 -0
  344. package/dist/components/post-process-params.d.ts +5 -0
  345. package/dist/components/post-process-params.d.ts.map +1 -0
  346. package/dist/components/scene-instance.d.ts +140 -0
  347. package/dist/components/scene-instance.d.ts.map +1 -0
  348. package/dist/components/skybox-background.d.ts +56 -0
  349. package/dist/components/skybox-background.d.ts.map +1 -0
  350. package/dist/components/skylight.d.ts +55 -0
  351. package/dist/components/skylight.d.ts.map +1 -0
  352. package/dist/components/sort-key.d.ts +44 -0
  353. package/dist/components/sort-key.d.ts.map +1 -0
  354. package/dist/components/spot-light.d.ts +81 -0
  355. package/dist/components/spot-light.d.ts.map +1 -0
  356. package/dist/components/sprite-animation.d.ts +74 -0
  357. package/dist/components/sprite-animation.d.ts.map +1 -0
  358. package/dist/components/sprite-instances.d.ts +51 -0
  359. package/dist/components/sprite-instances.d.ts.map +1 -0
  360. package/dist/components/sprite-playback-mode.d.ts +42 -0
  361. package/dist/components/sprite-playback-mode.d.ts.map +1 -0
  362. package/dist/components/sprite-region-override.d.ts +46 -0
  363. package/dist/components/sprite-region-override.d.ts.map +1 -0
  364. package/dist/components/tile-layer.d.ts +106 -0
  365. package/dist/components/tile-layer.d.ts.map +1 -0
  366. package/dist/components/tilemap.d.ts +25 -0
  367. package/dist/components/tilemap.d.ts.map +1 -0
  368. package/dist/components/visibility.d.ts +22 -0
  369. package/dist/components/visibility.d.ts.map +1 -0
  370. package/dist/construct-renderer.d.ts +19 -0
  371. package/dist/construct-renderer.d.ts.map +1 -0
  372. package/dist/construct-renderer.mjs +27462 -0
  373. package/dist/construct-renderer.mjs.map +1 -0
  374. package/dist/debug-draw-glue.d.ts +5 -0
  375. package/dist/debug-draw-glue.d.ts.map +1 -0
  376. package/dist/device/device-scope.d.ts +83 -0
  377. package/dist/device/device-scope.d.ts.map +1 -0
  378. package/dist/device/gpu-residency.d.ts +337 -0
  379. package/dist/device/gpu-residency.d.ts.map +1 -0
  380. package/dist/device/resource-types.d.ts +8 -0
  381. package/dist/device/resource-types.d.ts.map +1 -0
  382. package/dist/engine-metrics.d.ts +23 -0
  383. package/dist/engine-metrics.d.ts.map +1 -0
  384. package/dist/errors/ecs-validation.d.ts +3 -0
  385. package/dist/errors/ecs-validation.d.ts.map +1 -0
  386. package/dist/errors/index.d.ts +4 -0
  387. package/dist/errors/index.d.ts.map +1 -0
  388. package/dist/errors/recover.d.ts +105 -0
  389. package/dist/errors/recover.d.ts.map +1 -0
  390. package/dist/errors/render.d.ts +705 -0
  391. package/dist/errors/render.d.ts.map +1 -0
  392. package/dist/extract/visibility.d.ts +26 -0
  393. package/dist/extract/visibility.d.ts.map +1 -0
  394. package/dist/features/fullscreen.d.ts +19 -0
  395. package/dist/features/fullscreen.d.ts.map +1 -0
  396. package/dist/features/host.d.ts +106 -0
  397. package/dist/features/host.d.ts.map +1 -0
  398. package/dist/features/morph/morph-feature.d.ts +79 -0
  399. package/dist/features/morph/morph-feature.d.ts.map +1 -0
  400. package/dist/features/plan.d.ts +182 -0
  401. package/dist/features/plan.d.ts.map +1 -0
  402. package/dist/features/prepared-gpu-work.d.ts +117 -0
  403. package/dist/features/prepared-gpu-work.d.ts.map +1 -0
  404. package/dist/features/prepared-graphics-store.d.ts +66 -0
  405. package/dist/features/prepared-graphics-store.d.ts.map +1 -0
  406. package/dist/features/prepared-graphics.d.ts +215 -0
  407. package/dist/features/prepared-graphics.d.ts.map +1 -0
  408. package/dist/features/render-graph-compute.d.ts +13 -0
  409. package/dist/features/render-graph-compute.d.ts.map +1 -0
  410. package/dist/features/render-graph-contribution.d.ts +12 -0
  411. package/dist/features/render-graph-contribution.d.ts.map +1 -0
  412. package/dist/features/render-graph-raster.d.ts +38 -0
  413. package/dist/features/render-graph-raster.d.ts.map +1 -0
  414. package/dist/features/render-graph-resources.d.ts +11 -0
  415. package/dist/features/render-graph-resources.d.ts.map +1 -0
  416. package/dist/features/targets.d.ts +32 -0
  417. package/dist/features/targets.d.ts.map +1 -0
  418. package/dist/features/types.d.ts +68 -0
  419. package/dist/features/types.d.ts.map +1 -0
  420. package/dist/features/vocabulary.d.ts +14 -0
  421. package/dist/features/vocabulary.d.ts.map +1 -0
  422. package/dist/fullscreen-post-process-pass.d.ts +157 -0
  423. package/dist/fullscreen-post-process-pass.d.ts.map +1 -0
  424. package/dist/glyph-text-layout-system.d.ts +19 -0
  425. package/dist/glyph-text-layout-system.d.ts.map +1 -0
  426. package/dist/gpu-driven/batch-topology.d.ts +55 -0
  427. package/dist/gpu-driven/batch-topology.d.ts.map +1 -0
  428. package/dist/gpu-driven/production-raster.d.ts +51 -0
  429. package/dist/gpu-driven/production-raster.d.ts.map +1 -0
  430. package/dist/gpu-driven/view-gpu.d.ts +85 -0
  431. package/dist/gpu-driven/view-gpu.d.ts.map +1 -0
  432. package/dist/gpu-resource.d.ts +73 -0
  433. package/dist/gpu-resource.d.ts.map +1 -0
  434. package/dist/gpu-scene-schema.d.ts +78 -0
  435. package/dist/gpu-scene-schema.d.ts.map +1 -0
  436. package/dist/gpu-scene.d.ts +55 -0
  437. package/dist/gpu-scene.d.ts.map +1 -0
  438. package/dist/gpu-stage.d.ts +4 -0
  439. package/dist/gpu-stage.d.ts.map +1 -0
  440. package/dist/gpu-texture-usage.d.ts +6 -0
  441. package/dist/gpu-texture-usage.d.ts.map +1 -0
  442. package/dist/gpu-usage.d.ts +9 -0
  443. package/dist/gpu-usage.d.ts.map +1 -0
  444. package/dist/hdrp-buffers.d.ts +163 -0
  445. package/dist/hdrp-buffers.d.ts.map +1 -0
  446. package/dist/ibl/IblPipelineCache.d.ts +168 -0
  447. package/dist/ibl/IblPipelineCache.d.ts.map +1 -0
  448. package/dist/ibl/skylight-bind-group.d.ts +130 -0
  449. package/dist/ibl/skylight-bind-group.d.ts.map +1 -0
  450. package/dist/index.d.ts +31 -0
  451. package/dist/index.d.ts.map +1 -0
  452. package/dist/index.mjs +169 -0
  453. package/dist/index.mjs.map +1 -0
  454. package/dist/inspection-types.d.ts +72 -0
  455. package/dist/inspection-types.d.ts.map +1 -0
  456. package/dist/instance-buffer-cache.d.ts +56 -0
  457. package/dist/instance-buffer-cache.d.ts.map +1 -0
  458. package/dist/internal.d.ts +2 -0
  459. package/dist/internal.d.ts.map +1 -0
  460. package/dist/internal.mjs +3 -0
  461. package/dist/internal.mjs.map +1 -0
  462. package/dist/lifecycle.d.ts +62 -0
  463. package/dist/lifecycle.d.ts.map +1 -0
  464. package/dist/light-buffer-layout.d.ts +164 -0
  465. package/dist/light-buffer-layout.d.ts.map +1 -0
  466. package/dist/materials.d.ts +47 -0
  467. package/dist/materials.d.ts.map +1 -0
  468. package/dist/mesh-material-bindings.d.ts +43 -0
  469. package/dist/mesh-material-bindings.d.ts.map +1 -0
  470. package/dist/pbr-pipeline.d.ts +301 -0
  471. package/dist/pbr-pipeline.d.ts.map +1 -0
  472. package/dist/pipeline/standard-forward-lane.d.ts +5 -0
  473. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -0
  474. package/dist/pipeline/standard-pipeline.d.ts +31 -0
  475. package/dist/pipeline/standard-pipeline.d.ts.map +1 -0
  476. package/dist/pipeline/standard-profile.d.ts +40 -0
  477. package/dist/pipeline/standard-profile.d.ts.map +1 -0
  478. package/dist/pipeline-builder.d.ts +122 -0
  479. package/dist/pipeline-builder.d.ts.map +1 -0
  480. package/dist/pipeline-spec-types.d.ts +24 -0
  481. package/dist/pipeline-spec-types.d.ts.map +1 -0
  482. package/dist/pipeline-spec.d.ts +367 -0
  483. package/dist/pipeline-spec.d.ts.map +1 -0
  484. package/dist/plugin.d.ts +4 -0
  485. package/dist/plugin.d.ts.map +1 -0
  486. package/dist/points-lines/__tests__/bounds.unit.test.d.ts +2 -0
  487. package/dist/points-lines/__tests__/bounds.unit.test.d.ts.map +1 -0
  488. package/dist/points-lines/__tests__/cache-recovery.unit.test.d.ts +2 -0
  489. package/dist/points-lines/__tests__/cache-recovery.unit.test.d.ts.map +1 -0
  490. package/dist/points-lines/__tests__/inspection-lanes.integration.test.d.ts +2 -0
  491. package/dist/points-lines/__tests__/inspection-lanes.integration.test.d.ts.map +1 -0
  492. package/dist/points-lines/__tests__/inspection.unit.test.d.ts +2 -0
  493. package/dist/points-lines/__tests__/inspection.unit.test.d.ts.map +1 -0
  494. package/dist/points-lines/__tests__/lane-structural.integration.test.d.ts +2 -0
  495. package/dist/points-lines/__tests__/lane-structural.integration.test.d.ts.map +1 -0
  496. package/dist/points-lines/__tests__/points-lines.bench.d.ts +2 -0
  497. package/dist/points-lines/__tests__/points-lines.bench.d.ts.map +1 -0
  498. package/dist/points-lines/__tests__/raster-lane.integration.test.d.ts +2 -0
  499. package/dist/points-lines/__tests__/raster-lane.integration.test.d.ts.map +1 -0
  500. package/dist/points-lines/__tests__/snapshot.unit.test.d.ts +2 -0
  501. package/dist/points-lines/__tests__/snapshot.unit.test.d.ts.map +1 -0
  502. package/dist/points-lines/__tests__/view-ubo.unit.test.d.ts +2 -0
  503. package/dist/points-lines/__tests__/view-ubo.unit.test.d.ts.map +1 -0
  504. package/dist/points-lines/admission.d.ts +36 -0
  505. package/dist/points-lines/admission.d.ts.map +1 -0
  506. package/dist/points-lines/bounds.d.ts +8 -0
  507. package/dist/points-lines/bounds.d.ts.map +1 -0
  508. package/dist/points-lines/expansion-cache.d.ts +28 -0
  509. package/dist/points-lines/expansion-cache.d.ts.map +1 -0
  510. package/dist/points-lines/inspection.d.ts +53 -0
  511. package/dist/points-lines/inspection.d.ts.map +1 -0
  512. package/dist/points-lines/prepare.d.ts +67 -0
  513. package/dist/points-lines/prepare.d.ts.map +1 -0
  514. package/dist/points-lines/record.d.ts +60 -0
  515. package/dist/points-lines/record.d.ts.map +1 -0
  516. package/dist/points-lines/snapshot.d.ts +49 -0
  517. package/dist/points-lines/snapshot.d.ts.map +1 -0
  518. package/dist/post-process-errors.d.ts +182 -0
  519. package/dist/post-process-errors.d.ts.map +1 -0
  520. package/dist/prepare/prepared-graphics-resolver.d.ts +59 -0
  521. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -0
  522. package/dist/record/__tests__/material-bg-cache.unit.test.d.ts +2 -0
  523. package/dist/record/__tests__/material-bg-cache.unit.test.d.ts.map +1 -0
  524. package/dist/record/__tests__/material-slot-plan.unit.test.d.ts +2 -0
  525. package/dist/record/__tests__/material-slot-plan.unit.test.d.ts.map +1 -0
  526. package/dist/record/__tests__/typed-frame-graph-plan.unit.test.d.ts +2 -0
  527. package/dist/record/__tests__/typed-frame-graph-plan.unit.test.d.ts.map +1 -0
  528. package/dist/record/__tests__/video-texture-view.unit.test.d.ts +2 -0
  529. package/dist/record/__tests__/video-texture-view.unit.test.d.ts.map +1 -0
  530. package/dist/record/frame-lighting.d.ts +121 -0
  531. package/dist/record/frame-lighting.d.ts.map +1 -0
  532. package/dist/record/frame-snapshot.d.ts +429 -0
  533. package/dist/record/frame-snapshot.d.ts.map +1 -0
  534. package/dist/record/frame-targets.d.ts +20 -0
  535. package/dist/record/frame-targets.d.ts.map +1 -0
  536. package/dist/record/frame.d.ts +71 -0
  537. package/dist/record/frame.d.ts.map +1 -0
  538. package/dist/record/helpers.d.ts +102 -0
  539. package/dist/record/helpers.d.ts.map +1 -0
  540. package/dist/record/main-pass-geometry.d.ts +28 -0
  541. package/dist/record/main-pass-geometry.d.ts.map +1 -0
  542. package/dist/record/main-pass-material.d.ts +204 -0
  543. package/dist/record/main-pass-material.d.ts.map +1 -0
  544. package/dist/record/main-pass-sprite-draws.d.ts +43 -0
  545. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -0
  546. package/dist/record/main-pass.d.ts +20 -0
  547. package/dist/record/main-pass.d.ts.map +1 -0
  548. package/dist/record/mesh-ssbo.d.ts +446 -0
  549. package/dist/record/mesh-ssbo.d.ts.map +1 -0
  550. package/dist/record/render-context.d.ts +252 -0
  551. package/dist/record/render-context.d.ts.map +1 -0
  552. package/dist/record/shadow-pass.d.ts +52 -0
  553. package/dist/record/shadow-pass.d.ts.map +1 -0
  554. package/dist/record/skybox-post-pass.d.ts +33 -0
  555. package/dist/record/skybox-post-pass.d.ts.map +1 -0
  556. package/dist/record/typed-frame-graph.d.ts +35 -0
  557. package/dist/record/typed-frame-graph.d.ts.map +1 -0
  558. package/dist/record/view-ubo.d.ts +45 -0
  559. package/dist/record/view-ubo.d.ts.map +1 -0
  560. package/dist/render-contract.d.ts +424 -0
  561. package/dist/render-contract.d.ts.map +1 -0
  562. package/dist/render-data.d.ts +143 -0
  563. package/dist/render-data.d.ts.map +1 -0
  564. package/dist/render-graph-primitives.d.ts +76 -0
  565. package/dist/render-graph-primitives.d.ts.map +1 -0
  566. package/dist/render-pipeline.d.ts +110 -0
  567. package/dist/render-pipeline.d.ts.map +1 -0
  568. package/dist/render-system-extract.d.ts +868 -0
  569. package/dist/render-system-extract.d.ts.map +1 -0
  570. package/dist/render-system.d.ts +125 -0
  571. package/dist/render-system.d.ts.map +1 -0
  572. package/dist/scene/render-scene-types.d.ts +61 -0
  573. package/dist/scene/render-scene-types.d.ts.map +1 -0
  574. package/dist/scene/render-scene.d.ts +112 -0
  575. package/dist/scene/render-scene.d.ts.map +1 -0
  576. package/dist/scene-instances/post-spawn-resolve-joints.d.ts +30 -0
  577. package/dist/scene-instances/post-spawn-resolve-joints.d.ts.map +1 -0
  578. package/dist/shadow-atlas.d.ts +117 -0
  579. package/dist/shadow-atlas.d.ts.map +1 -0
  580. package/dist/ssao-buffers.d.ts +50 -0
  581. package/dist/ssao-buffers.d.ts.map +1 -0
  582. package/dist/ssao-config.d.ts +28 -0
  583. package/dist/ssao-config.d.ts.map +1 -0
  584. package/dist/systems/active-camera.d.ts +53 -0
  585. package/dist/systems/active-camera.d.ts.map +1 -0
  586. package/dist/systems/pass-selector.d.ts +24 -0
  587. package/dist/systems/pass-selector.d.ts.map +1 -0
  588. package/dist/systems/skin-palette-allocator.d.ts +49 -0
  589. package/dist/systems/skin-palette-allocator.d.ts.map +1 -0
  590. package/dist/systems/skin-palette-types.d.ts +14 -0
  591. package/dist/systems/skin-palette-types.d.ts.map +1 -0
  592. package/dist/systems/transparent-sort-config.d.ts +112 -0
  593. package/dist/systems/transparent-sort-config.d.ts.map +1 -0
  594. package/dist/tilemap-chunk-extract-system.d.ts +143 -0
  595. package/dist/tilemap-chunk-extract-system.d.ts.map +1 -0
  596. package/dist/typed-render-graph-primitives.d.ts +57 -0
  597. package/dist/typed-render-graph-primitives.d.ts.map +1 -0
  598. package/dist/typed-shadow-passes.d.ts +11 -0
  599. package/dist/typed-shadow-passes.d.ts.map +1 -0
  600. package/package.json +94 -0
  601. package/src/__tests__/authoring-path.unit.test.ts +23 -0
  602. package/src/__tests__/backend-selection-rhi-capture.test-d.ts +21 -0
  603. package/src/__tests__/callback-inference.test-d.ts +8 -0
  604. package/src/__tests__/cluster-binner.test.ts +272 -0
  605. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +20 -0
  606. package/src/__tests__/construct-renderer-identity.unit.test.ts +23 -0
  607. package/src/__tests__/device-scope-lifecycle.unit.test.ts +151 -0
  608. package/src/__tests__/device-scope-stale-matrix.unit.test.ts +42 -0
  609. package/src/__tests__/direct-extract-consumer-contract.test-d.ts +23 -0
  610. package/src/__tests__/direct-light-snapshot.unit.test.ts +61 -0
  611. package/src/__tests__/factory-contract.integration.test.ts +280 -0
  612. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +15 -0
  613. package/src/__tests__/frame-observation.contract.test.ts +112 -0
  614. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +32 -0
  615. package/src/__tests__/glyph-text-layout-recovery.unit.test.ts +167 -0
  616. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +350 -0
  617. package/src/__tests__/gpu-driven-indirect-raster.browser.test.ts +12 -0
  618. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +16 -0
  619. package/src/__tests__/gpu-driven-production.integration.test.ts +436 -0
  620. package/src/__tests__/gpu-driven-scaling.unit.test.ts +87 -0
  621. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +271 -0
  622. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +290 -0
  623. package/src/__tests__/gpu-driven-view-reference.ts +111 -0
  624. package/src/__tests__/gpu-driven-view.browser.test.ts +30 -0
  625. package/src/__tests__/gpu-driven-view.dawn.test.ts +28 -0
  626. package/src/__tests__/gpu-driven-view.unit.test.ts +199 -0
  627. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +59 -0
  628. package/src/__tests__/gpu-scene-render-graph-gpu.ts +308 -0
  629. package/src/__tests__/gpu-scene-render-graph.browser.test.ts +19 -0
  630. package/src/__tests__/gpu-scene-render-graph.dawn.test.ts +19 -0
  631. package/src/__tests__/gpu-scene-schema.unit.test.ts +16 -0
  632. package/src/__tests__/gpu-scene.dawn.test.ts +205 -0
  633. package/src/__tests__/gpu-scene.unit.test.ts +147 -0
  634. package/src/__tests__/ibl/face-uniforms.unit.test.ts +46 -0
  635. package/src/__tests__/infinite-grid-no-vertex-probe.unit.test.ts +92 -0
  636. package/src/__tests__/injection-kind-owner.test-d.ts +14 -0
  637. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +728 -0
  638. package/src/__tests__/light-buffer-layout.unit.test.ts +40 -0
  639. package/src/__tests__/material-contract-inventory.unit.test.ts +24 -0
  640. package/src/__tests__/material-cooked-projection.unit.test.ts +150 -0
  641. package/src/__tests__/material-snapshot-mutation.integration.test.ts +53 -0
  642. package/src/__tests__/material-static-dynamic-route.unit.test.ts +94 -0
  643. package/src/__tests__/material-stride-surface.unit.test.ts +32 -0
  644. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +106 -0
  645. package/src/__tests__/mesh-material-bindings.unit.test.ts +89 -0
  646. package/src/__tests__/mesh-stride-surface.unit.test.ts +16 -0
  647. package/src/__tests__/morph-culling-reentry.dawn.test.ts +157 -0
  648. package/src/__tests__/morph-feature-gpu-work.integration.test.ts +80 -0
  649. package/src/__tests__/morph-feature-plan.unit.test.ts +53 -0
  650. package/src/__tests__/morph-visual-evidence.integration.test.ts +80 -0
  651. package/src/__tests__/nineslice-handle-surface.unit.test.ts +19 -0
  652. package/src/__tests__/post-process-error-policy-owner.unit.test.ts +201 -0
  653. package/src/__tests__/prepared-buffer-kind-owner.test-d.ts +29 -0
  654. package/src/__tests__/prepared-graphics-capability.unit.test.ts +65 -0
  655. package/src/__tests__/prepared-graphics-generation.unit.test.ts +161 -0
  656. package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +138 -0
  657. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +29 -0
  658. package/src/__tests__/prepared-graphics-resolver.unit.test.ts +285 -0
  659. package/src/__tests__/prepared-graphics-snapshot.unit.test.ts +105 -0
  660. package/src/__tests__/prepared-graphics-store.unit.test.ts +154 -0
  661. package/src/__tests__/prepared-graphics-validation.unit.test.ts +176 -0
  662. package/src/__tests__/profiler-default-off.test.ts +22 -0
  663. package/src/__tests__/profiler-phase-catalog.test.ts +66 -0
  664. package/src/__tests__/profiler-phase-integration.test.ts +62 -0
  665. package/src/__tests__/profiler-phase-token.test.ts +79 -0
  666. package/src/__tests__/public-surface.test-d.ts +94 -0
  667. package/src/__tests__/recover-error-policy-owner.unit.test.ts +107 -0
  668. package/src/__tests__/render-error-code-owner.test-d.ts +116 -0
  669. package/src/__tests__/render-error-detail-ssot.test-d.ts +139 -0
  670. package/src/__tests__/render-error-exhaustive.test-d.ts +110 -0
  671. package/src/__tests__/render-feature-assembly.test-d.ts +43 -0
  672. package/src/__tests__/render-feature-context.test-d.ts +57 -0
  673. package/src/__tests__/render-feature-contract.test-d.ts +49 -0
  674. package/src/__tests__/render-feature-diagnostics.unit.test.ts +149 -0
  675. package/src/__tests__/render-feature-docs.unit.test.ts +89 -0
  676. package/src/__tests__/render-feature-errors.unit.test.ts +47 -0
  677. package/src/__tests__/render-feature-gpu-work.integration.test.ts +316 -0
  678. package/src/__tests__/render-feature-plan.test-d.ts +161 -0
  679. package/src/__tests__/render-feature-plan.unit.test.ts +170 -0
  680. package/src/__tests__/render-feature-prepared-diagnostics.unit.test.ts +90 -0
  681. package/src/__tests__/render-feature-prepared-errors.test-d.ts +79 -0
  682. package/src/__tests__/render-feature-prepared-graphics-docs.unit.test.ts +80 -0
  683. package/src/__tests__/render-feature-prepared-graphics-isolation.unit.test.ts +72 -0
  684. package/src/__tests__/render-feature-prepared-graphics-negative.test-d.ts +36 -0
  685. package/src/__tests__/render-feature-prepared-graphics.test-d.ts +74 -0
  686. package/src/__tests__/render-feature-prepared-graphics.unit.test.ts +121 -0
  687. package/src/__tests__/render-feature-public-surface.test-d.ts +57 -0
  688. package/src/__tests__/render-feature-retry.unit.test.ts +87 -0
  689. package/src/__tests__/render-feature-scheduling.unit.test.ts +110 -0
  690. package/src/__tests__/render-feature-stage-failed-policy-owner.unit.test.ts +137 -0
  691. package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +146 -0
  692. package/src/__tests__/render-feature-visibility-context.test-d.ts +33 -0
  693. package/src/__tests__/render-feature-visibility-context.unit.test.ts +37 -0
  694. package/src/__tests__/render-feature-zero-work.unit.test.ts +76 -0
  695. package/src/__tests__/render-graph-compute-raster-gpu.ts +342 -0
  696. package/src/__tests__/render-graph-compute-raster.browser.test.ts +16 -0
  697. package/src/__tests__/render-graph-compute-raster.dawn.test.ts +22 -0
  698. package/src/__tests__/render-graph-hzb-gpu.ts +239 -0
  699. package/src/__tests__/render-graph-hzb.browser.test.ts +16 -0
  700. package/src/__tests__/render-graph-hzb.dawn.test.ts +23 -0
  701. package/src/__tests__/render-graph-primitives.unit.test.ts +22 -0
  702. package/src/__tests__/render-graph-storage-indirect-gpu.ts +162 -0
  703. package/src/__tests__/render-graph-storage-indirect.browser.test.ts +21 -0
  704. package/src/__tests__/render-graph-storage-indirect.dawn.test.ts +28 -0
  705. package/src/__tests__/render-scene-projection.unit.test.ts +168 -0
  706. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +121 -0
  707. package/src/__tests__/renderer-lifecycle.integration.test.ts +58 -0
  708. package/src/__tests__/rhi-backend-package-boundary.unit.test.ts +11 -0
  709. package/src/__tests__/scriptable-pack-consumer.test-d.ts +14 -0
  710. package/src/__tests__/shader-stage-surface.unit.test.ts +43 -0
  711. package/src/__tests__/ssao-config.unit.test.ts +36 -0
  712. package/src/__tests__/ssao-kernel-surface.unit.test.ts +14 -0
  713. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +28 -0
  714. package/src/__tests__/standard-descriptors.test-d.ts +22 -0
  715. package/src/__tests__/standard-pipeline.dawn.test.ts +61 -0
  716. package/src/__tests__/standard-pipeline.integration.test.ts +124 -0
  717. package/src/__tests__/standard-profile-oracle.unit.test.ts +62 -0
  718. package/src/__tests__/surface-configuration.unit.test.ts +91 -0
  719. package/src/__tests__/texture-usage-surface.unit.test.ts +58 -0
  720. package/src/__tests__/typed-pipeline-topology.unit.test.ts +223 -0
  721. package/src/__tests__/urp-pipeline-targets.unit.test.ts +42 -0
  722. package/src/__tests__/urp-pipeline.unit.test.ts +14 -0
  723. package/src/__tests__/vertex-color-pipeline.unit.test.ts +132 -0
  724. package/src/__tests__/vertex-layout-consumer.unit.test.ts +25 -0
  725. package/src/__tests__/visibility-diagnostics.integration.test.ts +62 -0
  726. package/src/__tests__/visibility-extract.integration.test.ts +98 -0
  727. package/src/__tests__/visibility-extract.perf.test.ts +130 -0
  728. package/src/__tests__/visibility-feature-dedup.integration.test.ts +55 -0
  729. package/src/__tests__/visibility-hidden-report.unit.test.ts +36 -0
  730. package/src/__tests__/visibility-instances.integration.test.ts +46 -0
  731. package/src/__tests__/visibility-multi-world.integration.test.ts +86 -0
  732. package/src/__tests__/visibility-producer-matrix.integration.test.ts +168 -0
  733. package/src/__tests__/visibility-producer-matrix.unit.test.ts +96 -0
  734. package/src/__tests__/visibility-public-surface.test-d.ts +27 -0
  735. package/src/__tests__/visibility-resolver.unit.test.ts +85 -0
  736. package/src/__tests__/visibility-resource-short-circuit.unit.test.ts +80 -0
  737. package/src/__tests__/visibility-snapshot-contract.unit.test.ts +60 -0
  738. package/src/__tests__/vocabulary.unit.test.ts +21 -0
  739. package/src/assembly/backend-contract.ts +57 -0
  740. package/src/assembly/factory.ts +8130 -0
  741. package/src/assembly/host-contract.ts +101 -0
  742. package/src/assembly/material/assembly.ts +48 -0
  743. package/src/assembly/material/pipeline-projection.ts +63 -0
  744. package/src/assets/asset-decoders.ts +158 -0
  745. package/src/assets/render-asset-port.ts +44 -0
  746. package/src/assets/resolve-render-asset.ts +13 -0
  747. package/src/authoring.ts +17 -0
  748. package/src/cluster-binner.ts +700 -0
  749. package/src/components/__tests__/points-lines-admission.unit.test.ts +219 -0
  750. package/src/components/__tests__/points-lines-schema.unit.test.ts +56 -0
  751. package/src/components/__tests__/schema-contract.unit.test.ts +10 -0
  752. package/src/components/__tests__/schema-parity.unit.test.ts +25 -0
  753. package/src/components/__tests__/visibility-schema.unit.test.ts +35 -0
  754. package/src/components/camera.ts +512 -0
  755. package/src/components/directional-light.ts +120 -0
  756. package/src/components/glyph-text.ts +64 -0
  757. package/src/components/index.ts +54 -0
  758. package/src/components/instances.ts +206 -0
  759. package/src/components/layer.ts +69 -0
  760. package/src/components/light-helpers.ts +188 -0
  761. package/src/components/lines.ts +18 -0
  762. package/src/components/mesh-filter.ts +60 -0
  763. package/src/components/mesh-renderer.ts +61 -0
  764. package/src/components/point-light-shadow.ts +43 -0
  765. package/src/components/point-light.ts +65 -0
  766. package/src/components/points.ts +40 -0
  767. package/src/components/post-process-params.ts +35 -0
  768. package/src/components/scene-instance.ts +193 -0
  769. package/src/components/skybox-background.ts +103 -0
  770. package/src/components/skylight.ts +97 -0
  771. package/src/components/sort-key.ts +75 -0
  772. package/src/components/spot-light.ts +124 -0
  773. package/src/components/sprite-animation.ts +168 -0
  774. package/src/components/sprite-instances.ts +179 -0
  775. package/src/components/sprite-playback-mode.ts +79 -0
  776. package/src/components/sprite-region-override.ts +105 -0
  777. package/src/components/tile-layer.ts +167 -0
  778. package/src/components/tilemap.ts +48 -0
  779. package/src/components/visibility.ts +53 -0
  780. package/src/construct-renderer.ts +67 -0
  781. package/src/debug-draw-glue.ts +48 -0
  782. package/src/device/device-scope.ts +306 -0
  783. package/src/device/gpu-residency.ts +1822 -0
  784. package/src/device/resource-types.ts +20 -0
  785. package/src/engine-metrics.ts +82 -0
  786. package/src/errors/ecs-validation.ts +16 -0
  787. package/src/errors/index.ts +3 -0
  788. package/src/errors/recover.ts +161 -0
  789. package/src/errors/render.ts +1284 -0
  790. package/src/extract/visibility.ts +120 -0
  791. package/src/features/fullscreen.ts +46 -0
  792. package/src/features/host.ts +1363 -0
  793. package/src/features/morph/morph-feature.ts +377 -0
  794. package/src/features/plan.ts +447 -0
  795. package/src/features/prepared-gpu-work.ts +959 -0
  796. package/src/features/prepared-graphics-store.ts +449 -0
  797. package/src/features/prepared-graphics.ts +420 -0
  798. package/src/features/render-graph-compute.ts +69 -0
  799. package/src/features/render-graph-contribution.ts +75 -0
  800. package/src/features/render-graph-raster.ts +332 -0
  801. package/src/features/render-graph-resources.ts +38 -0
  802. package/src/features/targets.ts +70 -0
  803. package/src/features/types.ts +103 -0
  804. package/src/features/vocabulary.ts +24 -0
  805. package/src/fullscreen-post-process-pass.ts +363 -0
  806. package/src/glyph-text-layout-system.ts +421 -0
  807. package/src/gpu-driven/batch-topology.ts +245 -0
  808. package/src/gpu-driven/production-raster.ts +695 -0
  809. package/src/gpu-driven/view-gpu.ts +703 -0
  810. package/src/gpu-resource.ts +159 -0
  811. package/src/gpu-scene-schema.ts +136 -0
  812. package/src/gpu-scene.ts +722 -0
  813. package/src/gpu-stage.ts +9 -0
  814. package/src/gpu-texture-usage.ts +12 -0
  815. package/src/gpu-usage.ts +14 -0
  816. package/src/hdrp-buffers.ts +480 -0
  817. package/src/ibl/IblPipelineCache.ts +1006 -0
  818. package/src/ibl/skylight-bind-group.ts +513 -0
  819. package/src/index.ts +129 -0
  820. package/src/inspection-types.ts +80 -0
  821. package/src/instance-buffer-cache.ts +95 -0
  822. package/src/internal.ts +9 -0
  823. package/src/lifecycle.ts +168 -0
  824. package/src/light-buffer-layout.ts +346 -0
  825. package/src/materials.ts +214 -0
  826. package/src/mesh-material-bindings.ts +103 -0
  827. package/src/pbr-pipeline.ts +1137 -0
  828. package/src/pipeline/standard-forward-lane.ts +229 -0
  829. package/src/pipeline/standard-pipeline.ts +555 -0
  830. package/src/pipeline/standard-profile.ts +65 -0
  831. package/src/pipeline-builder.ts +382 -0
  832. package/src/pipeline-spec-types.ts +28 -0
  833. package/src/pipeline-spec.ts +1231 -0
  834. package/src/plugin.ts +69 -0
  835. package/src/points-lines/__tests__/bounds.unit.test.ts +21 -0
  836. package/src/points-lines/__tests__/cache-recovery.unit.test.ts +157 -0
  837. package/src/points-lines/__tests__/inspection-lanes.integration.test.ts +146 -0
  838. package/src/points-lines/__tests__/inspection.unit.test.ts +173 -0
  839. package/src/points-lines/__tests__/lane-structural.integration.test.ts +122 -0
  840. package/src/points-lines/__tests__/points-lines.bench.ts +400 -0
  841. package/src/points-lines/__tests__/raster-lane.integration.test.ts +44 -0
  842. package/src/points-lines/__tests__/snapshot.unit.test.ts +101 -0
  843. package/src/points-lines/__tests__/view-ubo.unit.test.ts +73 -0
  844. package/src/points-lines/admission.ts +238 -0
  845. package/src/points-lines/bounds.ts +23 -0
  846. package/src/points-lines/expansion-cache.ts +215 -0
  847. package/src/points-lines/inspection.ts +97 -0
  848. package/src/points-lines/prepare.ts +177 -0
  849. package/src/points-lines/record.ts +125 -0
  850. package/src/points-lines/snapshot.ts +130 -0
  851. package/src/post-process-errors.ts +302 -0
  852. package/src/prepare/prepared-graphics-resolver.ts +456 -0
  853. package/src/record/__tests__/material-bg-cache.unit.test.ts +41 -0
  854. package/src/record/__tests__/material-slot-plan.unit.test.ts +44 -0
  855. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +41 -0
  856. package/src/record/__tests__/video-texture-view.unit.test.ts +90 -0
  857. package/src/record/frame-lighting.ts +1074 -0
  858. package/src/record/frame-snapshot.ts +500 -0
  859. package/src/record/frame-targets.ts +137 -0
  860. package/src/record/frame.ts +1598 -0
  861. package/src/record/helpers.ts +284 -0
  862. package/src/record/main-pass-geometry.ts +1065 -0
  863. package/src/record/main-pass-material.ts +1043 -0
  864. package/src/record/main-pass-sprite-draws.ts +1137 -0
  865. package/src/record/main-pass.ts +610 -0
  866. package/src/record/mesh-ssbo.ts +885 -0
  867. package/src/record/render-context.ts +384 -0
  868. package/src/record/shadow-pass.ts +698 -0
  869. package/src/record/skybox-post-pass.ts +593 -0
  870. package/src/record/typed-frame-graph.ts +461 -0
  871. package/src/record/view-ubo.ts +222 -0
  872. package/src/render-contract.ts +582 -0
  873. package/src/render-data.ts +385 -0
  874. package/src/render-graph-primitives.ts +811 -0
  875. package/src/render-pipeline.ts +259 -0
  876. package/src/render-system-extract.ts +4585 -0
  877. package/src/render-system.ts +1933 -0
  878. package/src/scene/render-scene-types.ts +67 -0
  879. package/src/scene/render-scene.ts +1087 -0
  880. package/src/scene-instances/post-spawn-resolve-joints.ts +165 -0
  881. package/src/shadow-atlas.ts +244 -0
  882. package/src/ssao-buffers.ts +334 -0
  883. package/src/ssao-config.ts +61 -0
  884. package/src/systems/active-camera.ts +116 -0
  885. package/src/systems/pass-selector.ts +49 -0
  886. package/src/systems/skin-palette-allocator.ts +251 -0
  887. package/src/systems/skin-palette-types.ts +14 -0
  888. package/src/systems/transparent-sort-config.ts +233 -0
  889. package/src/tilemap-chunk-extract-system.ts +1290 -0
  890. package/src/typed-render-graph-primitives.ts +541 -0
  891. package/src/typed-shadow-passes.ts +165 -0
@@ -0,0 +1,1598 @@
1
+ // @forgeax/engine-runtime - RenderSystem record stage: frame.
2
+ // Extracted from render-system-record.ts (feat-20260704 M3/w17, pure move).
3
+
4
+ import { HANDLE_NINESLICE_QUAD, resolveAssetHandle } from '@forgeax/engine-assets-runtime';
5
+ import type { World } from '@forgeax/engine-ecs';
6
+ import type { RenderReadLease } from '@forgeax/engine-ecs/projection';
7
+ import {
8
+ type CurrentFrameObservationLease,
9
+ createCurrentFrameObservationLease,
10
+ } from '@forgeax/engine-render-graph';
11
+ import {
12
+ err,
13
+ ok,
14
+ type Result,
15
+ type RhiCommandEncoder,
16
+ RhiError,
17
+ type TextureFormat,
18
+ type TextureView,
19
+ } from '@forgeax/engine-rhi';
20
+ import type { MaterialRenderState, MeshAsset } from '@forgeax/engine-types';
21
+ import { handleSlot, toShared } from '@forgeax/engine-types';
22
+ import type { MeshGpuHandles } from '../device/gpu-residency';
23
+ import { ObservationUnavailableError, type ObservationUnavailableReason } from '../errors/render';
24
+ import type { GpuDrivenProduction, PreparedGpuDrivenFrame } from '../gpu-driven/production-raster';
25
+ import { GpuBuffer } from '../gpu-resource';
26
+ import { GPU_BUFFER_USAGE_COPY_DST, GPU_BUFFER_USAGE_VERTEX } from '../gpu-usage';
27
+ import { disposeTransientInstanceBuffers } from '../instance-buffer-cache';
28
+ import type { CameraSnapshot, RenderRecordPhase } from '../render-contract';
29
+ import type {
30
+ DispatchEntry,
31
+ ExtractedLights,
32
+ MaterialSnapshot,
33
+ RenderableSnapshot,
34
+ SkyboxSnapshot,
35
+ SkylightSnapshot,
36
+ } from '../render-system-extract';
37
+ import type { PersistentGpuDrivenState } from '../scene/render-scene';
38
+ import {
39
+ getTransparentSortConfig,
40
+ TRANSPARENT_SORT_MODE_LAYER_Y,
41
+ TRANSPARENT_SORT_MODE_LAYER_Z,
42
+ } from '../systems/transparent-sort-config';
43
+ import {
44
+ buildPerFrameBindGroups,
45
+ prepareFrameLighting,
46
+ resolveSkyboxActive,
47
+ warnZeroLightStandard,
48
+ writeHdrpClusterAndSsaoBuffers,
49
+ writePointSpotLightBuffers,
50
+ writeShadowParamsBuffer,
51
+ } from './frame-lighting';
52
+ import {
53
+ type BindGroupCounts,
54
+ type DirectionalShadowCache,
55
+ type DirectionalShadowWorldState,
56
+ type DispatchCounts,
57
+ type FrameObservationSource,
58
+ makeZeroCameraFallbackSnapshot,
59
+ type RenderFrameState,
60
+ type ValidatedRenderable,
61
+ worldEntityKey,
62
+ } from './frame-snapshot';
63
+ // feat-20260622-chunk-gpu-instancing-sprite-tilemap M1 / w4 (D-1 record-stage
64
+ // fold operator). Pure linear-scan helper that groups transparent-dispatch
65
+ // entries with equal (Layer.value, sortKey, materialHandle) into FoldBucket
66
+ // descriptors. The drawIndexed swap (1 instanced draw per bucket vs N
67
+ // per-entity draws) hooks into the sprite-pass dispatch loop below
68
+ // (w4-record-swap), using {@link buildFoldDispatchPlan} to translate
69
+ // renderableIndex-keyed buckets into validatedOrdered-index-keyed
70
+ // head/skip maps.
71
+ import {
72
+ buildFoldDispatchPlan,
73
+ evaluateFoldBucketUniformCap,
74
+ type FoldBucket,
75
+ type FoldDispatchPlan,
76
+ foldDispatchBuckets,
77
+ incrementFoldedDrawsMetric,
78
+ } from './mesh-ssbo';
79
+ import type {
80
+ _InternalRenderPipelineContext,
81
+ PipelineState,
82
+ RenderSystemInternals,
83
+ } from './render-context';
84
+
85
+ export type { FrameObservationSource } from './frame-snapshot';
86
+
87
+ export type FrameObservationReadback = (
88
+ lease: CurrentFrameObservationLease,
89
+ ) => Promise<Result<Uint8Array, Error>>;
90
+
91
+ export interface FrameObservationOptions {
92
+ readonly semantic: 'linear-hdr';
93
+ readonly readback: FrameObservationReadback;
94
+ }
95
+
96
+ export interface FrameObservationMetadata {
97
+ readonly format: TextureFormat;
98
+ readonly size: { readonly width: number; readonly height: number };
99
+ readonly usage: number;
100
+ readonly sample: number;
101
+ readonly frameId: number;
102
+ readonly lifetime: { readonly frameId: number; readonly state: 'active' | 'retired' };
103
+ readonly pipelineId: 'forgeax::standard';
104
+ readonly backendId: string;
105
+ }
106
+
107
+ export interface FrameObservation {
108
+ readonly bytes: Uint8Array;
109
+ readonly metadata: FrameObservationMetadata;
110
+ }
111
+
112
+ function unavailable(
113
+ reason: ObservationUnavailableReason,
114
+ hint: string,
115
+ ): Result<never, ObservationUnavailableError> {
116
+ return err(new ObservationUnavailableError(reason, hint));
117
+ }
118
+
119
+ function mapLeaseFailure(code: string, hint: string): Result<never, ObservationUnavailableError> {
120
+ let reason: ObservationUnavailableReason = 'resource';
121
+ if (code === 'observation-stale') reason = 'stale';
122
+ else if (code === 'observation-invalid-format') reason = 'format';
123
+ else if (code === 'observation-missing-copy-src') reason = 'copy-src';
124
+ return unavailable(reason, hint);
125
+ }
126
+
127
+ export async function observeCurrentFrame(
128
+ options: FrameObservationOptions,
129
+ source: FrameObservationSource | undefined,
130
+ currentFrameId: number,
131
+ ): Promise<Result<FrameObservation, ObservationUnavailableError>> {
132
+ if (options.semantic !== 'linear-hdr') {
133
+ return unavailable('identity', 'request the producer-owned linear-hdr semantic');
134
+ }
135
+ if (source === undefined) {
136
+ return unavailable(
137
+ 'no-frame',
138
+ 'draw a frame through the Standard producer before requesting observation',
139
+ );
140
+ }
141
+ if (source.pipelineId !== 'forgeax::standard' || source.backendId.length === 0) {
142
+ return unavailable(
143
+ 'identity',
144
+ 'observe a producer with explicit pipeline and backend identity',
145
+ );
146
+ }
147
+
148
+ const leaseResult = createCurrentFrameObservationLease(
149
+ { ...source.descriptor, texture: source.texture, frameId: source.frameId },
150
+ currentFrameId,
151
+ );
152
+ if (!leaseResult.ok) {
153
+ return mapLeaseFailure(leaseResult.error.code, leaseResult.error.hint);
154
+ }
155
+
156
+ let bytesResult: Result<Uint8Array, Error>;
157
+ try {
158
+ bytesResult = await options.readback(leaseResult.value);
159
+ } catch (cause) {
160
+ const message = cause instanceof Error ? cause.message : String(cause);
161
+ return unavailable('readback-failed', `retry the current-frame readback after: ${message}`);
162
+ }
163
+ if (!bytesResult.ok) {
164
+ return unavailable('readback-failed', bytesResult.error.message);
165
+ }
166
+
167
+ const activeResult = leaseResult.value.beginReadback();
168
+ if (!activeResult.ok) {
169
+ return mapLeaseFailure(activeResult.error.code, activeResult.error.hint);
170
+ }
171
+
172
+ return ok({
173
+ bytes: bytesResult.value,
174
+ metadata: {
175
+ format: leaseResult.value.descriptor.format,
176
+ size: leaseResult.value.descriptor.size,
177
+ usage: leaseResult.value.descriptor.usage,
178
+ sample: source.descriptor.sample,
179
+ frameId: source.frameId,
180
+ lifetime: leaseResult.value.lifetime,
181
+ pipelineId: source.pipelineId,
182
+ backendId: source.backendId,
183
+ },
184
+ });
185
+ }
186
+
187
+ import {
188
+ acquireSwapChainTarget,
189
+ graphExecutionPhase,
190
+ resolveShadowMapSize,
191
+ resolveSpotShadowMapSize,
192
+ } from './frame-targets';
193
+ import {
194
+ computeViewMatrix,
195
+ driveLazyEquirectProjection,
196
+ selectLazyEquirectHandle,
197
+ variantSetFromDefines,
198
+ warnMultiSkybox,
199
+ warnMultiSkylight,
200
+ } from './helpers';
201
+ import { computeSplitLdrSprite } from './main-pass-sprite-draws';
202
+ import { cleanPerEntityCache, ensureMeshSsboCapacity, uploadMeshSsboBatch } from './mesh-ssbo';
203
+ import { writeShadowCasterUniforms } from './shadow-pass';
204
+ import {
205
+ ensureCompiledFrameGraph,
206
+ executeCompiledFrameGraph,
207
+ type RenderFeatureGraphCandidate,
208
+ } from './typed-frame-graph';
209
+ import { writePointsLinesViewUbo, writeViewUbo } from './view-ubo';
210
+
211
+ /**
212
+ * A frame-local material table. Repeated snapshot objects share one UBO slot;
213
+ * each renderable keeps only the slot index used by each authored material.
214
+ */
215
+ export interface MaterialSlotPlan<T extends object> {
216
+ readonly slotIndices: readonly (readonly number[])[];
217
+ readonly slots: readonly T[];
218
+ /** Index of the first renderable that referenced each slot. */
219
+ readonly slotOwners: readonly number[];
220
+ }
221
+
222
+ /**
223
+ * Intern identical material snapshots into one frame-local slot table.
224
+ * Snapshot identity is the extract layer's invalidation token, so this keeps
225
+ * the same correctness boundary while avoiding duplicate payload assembly and
226
+ * upload for repeated scene instances.
227
+ */
228
+ export function buildMaterialSlotPlan<T extends object>(
229
+ materialGroups: readonly (readonly T[])[],
230
+ ): MaterialSlotPlan<T> {
231
+ const slotByMaterial = new Map<T, number>();
232
+ const slots: T[] = [];
233
+ const slotOwners: number[] = [];
234
+ const slotIndices = materialGroups.map((materials, ownerIndex) =>
235
+ materials.map((material) => {
236
+ const cached = slotByMaterial.get(material);
237
+ if (cached !== undefined) return cached;
238
+ const slot = slots.length;
239
+ slots.push(material);
240
+ slotOwners.push(ownerIndex);
241
+ slotByMaterial.set(material, slot);
242
+ return slot;
243
+ }),
244
+ );
245
+ return { slotIndices, slots, slotOwners };
246
+ }
247
+
248
+ /**
249
+ * Return the largest complete renderable prefix supported by a shared
250
+ * mesh/material capacity. Mesh rows consume one slot per renderable; material
251
+ * rows use the deduplicated slot indices assigned in first-seen order.
252
+ */
253
+ export function findRenderablePrefixForSlotCapacity(
254
+ materialSlotIndices: readonly (readonly number[])[],
255
+ slotCapacity: number,
256
+ ): number {
257
+ const capacity = Math.max(0, Math.floor(slotCapacity));
258
+ const maxRenderableCount = Math.min(materialSlotIndices.length, capacity);
259
+ let renderableCount = 0;
260
+ while (renderableCount < maxRenderableCount) {
261
+ const slots = materialSlotIndices[renderableCount] ?? [];
262
+ if (slots.some((slot) => slot >= capacity)) break;
263
+ renderableCount += 1;
264
+ }
265
+ return renderableCount;
266
+ }
267
+
268
+ /** Count material slots reachable from a prefix of a first-seen slot plan. */
269
+ export function materialSlotCountForPrefix(
270
+ materialSlotIndices: readonly (readonly number[])[],
271
+ renderableCount: number,
272
+ ): number {
273
+ let maxSlot = -1;
274
+ for (let index = 0; index < renderableCount; index += 1) {
275
+ for (const slot of materialSlotIndices[index] ?? []) maxSlot = Math.max(maxSlot, slot);
276
+ }
277
+ return maxSlot + 1;
278
+ }
279
+
280
+ export type RecordProfileRunner = <T>(phase: RenderRecordPhase, action: () => T) => T;
281
+
282
+ const EMPTY_ENTITY_KEYS: ReadonlySet<number> = new Set<number>();
283
+
284
+ function runRecordProfilePhase<T>(
285
+ runner: RecordProfileRunner | undefined,
286
+ phase: RenderRecordPhase,
287
+ action: () => T,
288
+ ): T {
289
+ return runner === undefined ? action() : runner(phase, action);
290
+ }
291
+
292
+ export function recordFrame(
293
+ internals: RenderSystemInternals,
294
+ world: World,
295
+ cameras: CameraSnapshot[],
296
+ lights: ExtractedLights,
297
+ renderables: RenderableSnapshot[],
298
+ transparentDispatch: readonly DispatchEntry[],
299
+ frameState: RenderFrameState,
300
+ dispatchCounts: DispatchCounts,
301
+ bindGroupCounts: BindGroupCounts,
302
+ skylight: SkylightSnapshot | undefined,
303
+ skylightCount: number,
304
+ skybox: SkyboxSnapshot | undefined,
305
+ skyboxCount: number,
306
+ postProcessParams: ReadonlyMap<string, Uint8Array>,
307
+ // feat-20260709-editor-world-partition ENGINE-fix-round2 (defect 2): the
308
+ // full worlds[] list passed to `renderer.draw`, indexed by
309
+ // RenderableSnapshot.worldId so the record stage resolves each renderable's
310
+ // mesh + material textures against the world it was extracted from (mirroring
311
+ // the per-world extract stage). Optional + defaulting to `[world]` keeps the
312
+ // pre-split single-world identity path (worldId always 0) byte-for-byte and
313
+ // the existing unit-test callers (which pass a single mock world) valid.
314
+ worlds: readonly World[] = [world],
315
+ profilePhase?: RecordProfileRunner,
316
+ gpuDriven?: {
317
+ readonly owner: GpuDrivenProduction;
318
+ readonly scene: PersistentGpuDrivenState | undefined;
319
+ readonly onCoverage: (ownsAll: boolean) => void;
320
+ },
321
+ renderReadLeases?: readonly RenderReadLease[],
322
+ featureGraphCandidate?: RenderFeatureGraphCandidate,
323
+ pointsLinesOwner?: import('./render-context').PointsLinesRecordOwner,
324
+ ): boolean {
325
+ // The `try / finally` wrapper advances `frameState.frameNumber` exactly
326
+ // once per `recordFrame` invocation regardless of which early-return
327
+ // branch is taken (camera missing, cap exceeded, pipeline pending,
328
+ // swap-chain unavailable, etc).
329
+ // The boolean result reports whether the shared frame encoder reached queue
330
+ // submission; prepared graphics uses it to distinguish in-flight from
331
+ // unsubmitted buffer leases.
332
+ // Case B: 0 Camera => fire onError diagnostic. After
333
+ // feat-20260608-create-app-param-surface-trim / M1 / D-8, the frame
334
+ // is NOT skipped: a synthetic CameraSnapshot is injected so the
335
+ // downstream clear-pass-only path (Case E softening, D-Q7) still
336
+ // paints the swap-chain with the `ZERO_CAMERA_CLEAR_FALLBACK` color
337
+ // (`[0, 0, 0, 1]` opaque black; AC-05). The synthetic camera carries
338
+ // identity-shaped projection / view inputs (fov=PI/4, aspect=1,
339
+ // near=0.1, far=100) so the existing matrix math stays numerically
340
+ // stable; no MeshRenderer entity will pass validation in this state
341
+ // (no Camera = no scene), so geometry submission is a no-op and only
342
+ // the swap-chain clear lands.
343
+ let activeCameras = cameras;
344
+ if (activeCameras.length === 0) {
345
+ internals.errorRegistry.fire(
346
+ new RhiError({
347
+ code: 'render-system-no-camera',
348
+ expected: 'world has at least one entity with Transform + Camera',
349
+ hint: 'world.spawn({ component: Transform, data: { pos: [x, y, z], quat: [x, y, z, w], scale: [x, y, z] } }, { component: Camera, data: { fov, aspect, near, far, clearColor: [r, g, b, a] } }) before renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 })',
350
+ }),
351
+ );
352
+ activeCameras = [makeZeroCameraFallbackSnapshot()];
353
+ }
354
+ try {
355
+ // Case D: N>1 Camera => fire onError, use first hit (D-S7).
356
+ if (activeCameras.length > 1) {
357
+ internals.errorRegistry.fire(
358
+ new RhiError({
359
+ code: 'render-system-multi-camera',
360
+ expected: 'world has exactly one entity with Transform + Camera',
361
+ hint: 'remove duplicate Camera entities or wait for feat-future-multi-viewport',
362
+ }),
363
+ );
364
+ }
365
+ const camera = activeCameras[0];
366
+ if (!camera) return false;
367
+ const pipelineState = internals.getPipelineState();
368
+ if (pipelineState === null) return false;
369
+ frameState.currentFrameObservationSource = undefined;
370
+ frameState.currentDirectionalShadowView = null;
371
+ frameState.currentSpotShadowView = null;
372
+
373
+ // Record-stage fold operator linear scan (groups transparent-sort entries
374
+ // into fold buckets + records the fold-eligible count metric). Extracted to
375
+ // computeFoldBuckets (M3/w18).
376
+ const { foldBuckets, light } = runRecordProfilePhase(profilePhase, 'record/scene-state', () => {
377
+ const foldBuckets = runRecordProfilePhase(
378
+ profilePhase,
379
+ 'record/scene-state/fold-buckets',
380
+ () => computeFoldBuckets(world, frameState, transparentDispatch, renderables),
381
+ );
382
+
383
+ // Multi-light warnings + point/spot shadow-snapshot pin + point-shadow atlas
384
+ // ensure + ExtractedLights three-arm destructure (directional fallback,
385
+ // point/spot arrays, totalLightCount). Extracted to prepareFrameLighting
386
+ // (M3/w18) so recordFrame stays a skeleton.
387
+ const { light, pointLights, spotLights, totalLightCount } = runRecordProfilePhase(
388
+ profilePhase,
389
+ 'record/scene-state/lighting-prep',
390
+ () => prepareFrameLighting(internals, frameState, lights),
391
+ );
392
+
393
+ runRecordProfilePhase(profilePhase, 'record/scene-state/ambient-resolution', () => {
394
+ // feat-20260520-skylight-ibl-cubemap M4 / t27 (AC-10 + F-4 nit):
395
+ // 0-light three-condition conjunction (plan-strategy D-5):
396
+ // no Skylight (skylight === undefined)
397
+ // AND 0 direct light (totalLightCount === 0)
398
+ // AND StandardMaterial (renderables.some materialShaderId !== 'forgeax::default-unlit')
399
+ // All three true -> black + warn. A single false -> no warn.
400
+ //
401
+ // Multi-Skylight warn (F-4 nit + feat-20260630 M3 / w19): >1 Skylight
402
+ // entity -> warn ONCE per RenderSystem lifetime (not per frame), naming the
403
+ // winning entity handle so the scene author can tell which Skylight is used
404
+ // (F-8: warn carries conflicting entity info). First Skylight (by archetype
405
+ // order) wins.
406
+ warnMultiSkylight(frameState, skylightCount, skylight?.entityHandle ?? 0);
407
+
408
+ // Multi-SkyboxBackground warn (feat-20260630 M3 / w19): mirror the Skylight
409
+ // once-warn + winning-entity-handle pattern. First SkyboxBackground (by
410
+ // archetype order) wins.
411
+ warnMultiSkybox(frameState, skyboxCount, skybox?.entityHandle ?? 0);
412
+
413
+ // feat-20260630-equirect-kind-internalized-ibl-declarative-skyligh M3 / w18:
414
+ // lazy equirect-to-cubemap projection trigger (the single per-frame driver;
415
+ // plan-strategy D-4 + sequence diagram). The Skylight (or, when present
416
+ // without one, the SkyboxBackground) supplies the equirect handle; both
417
+ // reuse the same handle so a single projection serves IBL ambient + skybox.
418
+ // - handle 0 -> no equirect (solid-color ambient); skip
419
+ // - caps.rgba16float
420
+ // Renderable false -> permanent white fallback; never project (AC-06,
421
+ // the only IBL gate; no UA guard)
422
+ // - status undefined -> first sight: resolve POD + fire-and-forget launch
423
+ // (does NOT await; the store writes status:'pending'
424
+ // synchronously so this launches exactly once)
425
+ // - status pending -> projection in flight; white fallback this frame
426
+ // (normal transition, not an error -- no fire)
427
+ // - status ready -> real IBL bound by the recordMainPass cache check
428
+ // - status failed -> fire EquirectProjectionFailedError ONCE per
429
+ // handle (R-2/AC-09: store records failed
430
+ // permanently and never retries; the latch keeps
431
+ // the channel from flooding)
432
+ const lazyEquirectHandle = selectLazyEquirectHandle(skylight, skybox);
433
+ if (lazyEquirectHandle !== 0) {
434
+ driveLazyEquirectProjection(internals, world, frameState, lazyEquirectHandle);
435
+ }
436
+ });
437
+
438
+ // feat-20260608-cluster-lighting M5 / w21 + M6 / w23: HDRP per-frame CPU
439
+ // cluster binning + light-data / cluster-grid / SSAO buffer uploads. Runs
440
+ // only when the HDRP pipeline is active and there is at least one punctual
441
+ // light. Extracted to a sub-function (M3/w18) so recordFrame stays a
442
+ // skeleton; fail-soft error semantics are preserved verbatim.
443
+ runRecordProfilePhase(profilePhase, 'record/scene-state/hdrp-cluster', () => {
444
+ writeHdrpClusterAndSsaoBuffers(
445
+ internals,
446
+ frameState,
447
+ camera,
448
+ pointLights,
449
+ spotLights,
450
+ profilePhase,
451
+ pipelineState.hdrpClusterMembershipPipeline !== null &&
452
+ internals.device.caps?.compute === true,
453
+ pipelineState.hdrpClusterMembershipBindGroupLayout,
454
+ );
455
+ });
456
+
457
+ // Zero-light standard-material once-warn (no Skylight + 0 direct light +
458
+ // >=1 lit material -> black). Extracted to warnZeroLightStandard (M3/w18).
459
+ warnZeroLightStandard(frameState, renderables, skylight, totalLightCount);
460
+
461
+ return { foldBuckets, light };
462
+ });
463
+
464
+ // Case E (this commit): 0 renderables = legitimate scene (LO §1.1
465
+ // hello-window minimum semantic). Mirrors the Case C softening for
466
+ // 0 DirectionalLight (line 134 above; D-Q7). recordFrame() falls
467
+ // through to encode + submit a clear-pass-only render pass so the
468
+ // canvas is painted with `clearColor` even when no entity carries
469
+ // MeshFilter + MeshRenderer. Geometry submission (mat4 uploads,
470
+ // bind-group construction, vertex/index binding, drawIndexed) is
471
+ // conditional on `validatedOrdered.length > 0` further down.
472
+
473
+ // Point-shadow params UBO write (per-layer near/far/invSpan). Extracted to
474
+ // writeShadowParamsBuffer (M3/w18).
475
+ writeShadowParamsBuffer(internals, frameState, pipelineState);
476
+
477
+ // feat-20260625-spot-light-shadow-mapping w25 (scope-amend webkit-fallback):
478
+ // the per-spot perspective `lightViewProj` matrices fold into the View UBO
479
+ // tail (`view.spotLightViewProj`, floats 132..195 / bytes 528..784) and are
480
+ // written as part of the per-frame viewPayload below — no standalone binding
481
+ // 9 uniform buffer (it overflowed the WebGL2 fallback fragment uniform-buffer
482
+ // budget). See the viewPayload construction (VIEW_PAYLOAD_FLOATS = 196).
483
+
484
+ const effectiveShadowMapSize = resolveShadowMapSize(internals, lights);
485
+ const effectiveSpotShadowMapSize = resolveSpotShadowMapSize(internals, lights);
486
+ const graphShadowMapSize = effectiveShadowMapSize ?? effectiveSpotShadowMapSize;
487
+
488
+ let preparedGpuDriven: PreparedGpuDrivenFrame | undefined;
489
+ if (gpuDriven !== undefined) {
490
+ const prepared = gpuDriven.owner.prepare({
491
+ scene: gpuDriven.scene,
492
+ camera,
493
+ meshes: pipelineState.meshes,
494
+ viewBindGroupLayout: pipelineState.viewBindGroupLayout,
495
+ meshResidencyEpoch: internals.gpuStore.meshResidencyEpoch,
496
+ hdrp: frameState.isHdrpActive,
497
+ });
498
+ if (prepared.ok) preparedGpuDriven = prepared.value;
499
+ else if (prepared.error.code !== 'rhi-not-available')
500
+ internals.errorRegistry.fire(prepared.error);
501
+ }
502
+
503
+ // The GPU lane owns only the primary opaque raster today. Directional,
504
+ // point, and spot shadow views still consume the CPU-validated draw plan.
505
+ const cpuShadowCastersActive =
506
+ (effectiveShadowMapSize ?? 0) > 0 ||
507
+ lights.pointShadow.length > 0 ||
508
+ lights.spot.some((spot) => spot.castShadow);
509
+ const gpuOwnsAllPrimaryRaster = preparedGpuDriven?.ownsAllRenderables === true;
510
+ gpuDriven?.onCoverage(gpuOwnsAllPrimaryRaster && !cpuShadowCastersActive);
511
+
512
+ const preflightGraph = runRecordProfilePhase(profilePhase, 'record/render-graph', () =>
513
+ ensureCompiledFrameGraph(
514
+ internals,
515
+ frameState,
516
+ pipelineState,
517
+ camera,
518
+ lights,
519
+ Math.max(1, internals.canvas.width),
520
+ Math.max(1, internals.canvas.height),
521
+ graphShadowMapSize,
522
+ preparedGpuDriven,
523
+ featureGraphCandidate,
524
+ ),
525
+ );
526
+ if (preflightGraph === null) return false;
527
+
528
+ // Acquire the swap-chain texture + colour view + target dimensions (with
529
+ // one reconfigure-and-retry on surface-outdated). Extracted to
530
+ // acquireSwapChainTarget (M3/w18); returns null on unrecoverable failure
531
+ // (context null / double getCurrentTexture fail / view creation fail), in
532
+ // which case recordFrame bails after the finally-block frame advance.
533
+ const swapTarget = runRecordProfilePhase(profilePhase, 'record/swapchain', () =>
534
+ acquireSwapChainTarget(internals, pipelineState),
535
+ );
536
+ if (swapTarget === null) return false;
537
+ const currentTexture = swapTarget.currentTexture;
538
+ const view = swapTarget.view;
539
+ const targetW = swapTarget.targetW;
540
+ const targetH = swapTarget.targetH;
541
+
542
+ const depthView: TextureView | null = null;
543
+ // feat-20260709 M3 / D-3: clear-color read from the active CameraSnapshot's
544
+ // single `clearColor` array field (first-archetype-hit per OOS-2). When the
545
+ // world had zero Camera entities, `camera` here is the synthetic fallback
546
+ // snapshot built above (Case B), which carries
547
+ // `ZERO_CAMERA_CLEAR_FALLBACK = [0, 0, 0, 1]`.
548
+ const clear: readonly [number, number, number, number] = [
549
+ camera.clearColor[0],
550
+ camera.clearColor[1],
551
+ camera.clearColor[2],
552
+ camera.clearColor[3],
553
+ ];
554
+
555
+ pipelineState.perPassResources.shadowMapSize = graphShadowMapSize ?? 0;
556
+ pipelineState.perPassResources.shadowCascadeCount = lights.cascadeCount ?? 0;
557
+ const firstDirectionalShadowMatrix = lights.lightViewProj?.[0];
558
+ pipelineState.perPassResources.shadowLightSpaceMatrix =
559
+ firstDirectionalShadowMatrix === undefined
560
+ ? null
561
+ : new Float32Array(firstDirectionalShadowMatrix);
562
+ if (lights.lightViewProj === undefined) {
563
+ pipelineState.perPassResources.shadowCsmLightViewProj = null;
564
+ pipelineState.perPassResources.shadowCsmSelection = null;
565
+ } else {
566
+ const csmMatrices = new Float32Array(64);
567
+ for (let cascade = 0; cascade < 4; cascade += 1) {
568
+ const matrix = lights.lightViewProj[cascade];
569
+ if (matrix !== undefined) csmMatrices.set(matrix, cascade * 16);
570
+ }
571
+ pipelineState.perPassResources.shadowCsmLightViewProj = csmMatrices;
572
+ pipelineState.perPassResources.shadowCsmSelection =
573
+ lights.splitPlanes === undefined
574
+ ? null
575
+ : {
576
+ viewMatrix: new Float32Array(computeViewMatrix(camera)),
577
+ splitPlanes: new Float32Array(lights.splitPlanes),
578
+ };
579
+ }
580
+
581
+ // Tonemap + skybox-active resolution (skybox requires tonemap HDR target +
582
+ // a resident cubemap view). Extracted to resolveSkyboxActive (M3/w18).
583
+ const { tonemapActive, skyboxActive } = resolveSkyboxActive(
584
+ internals,
585
+ frameState,
586
+ camera,
587
+ skybox,
588
+ );
589
+
590
+ // feat-20260604-learn-render-4.10-anti-aliasing-msaa M2 / w9 (D-6, C-9):
591
+ // MSAA is a per-Camera switch derived from `camera.antialias`, never
592
+ // stored separately. When active the geometry pass writes a count=4
593
+ // multisample colour target and resolves to a single-sample output; the
594
+ // record stage selects the `*Msaa` pipeline variants and the geometry
595
+ // pass attaches the resolve target. When inactive every attachment +
596
+ // pipeline stays single-sample (the pre-MSAA path is byte-for-byte
597
+ // unchanged).
598
+ // Geometry colour / depth / resolve / sprite-split target view resolution
599
+ // (MSAA + tonemap routing) + MSAA writeback to perPassResources. Extracted
600
+ // to resolveGeometryTargetViews (M3/w18) so recordFrame stays a skeleton.
601
+ const msaaActive =
602
+ camera.antialias === 'msaa' && internals.device.caps.backendKind !== 'wgpu-webgl2';
603
+ const geometryColorView = view;
604
+ const geometryDepthView = depthView;
605
+ const geometryDepthKey: string | null = null;
606
+ const geometryColorResolveView: TextureView | null = null;
607
+ const ldrSpriteColorView: TextureView | null = null;
608
+
609
+ // Validate renderable handles + collect render plan. Empty `renderables`
610
+ // input or all-unregistered handles both produce `validatedOrdered.length === 0`,
611
+ // which is the Case E (clear-pass-only) path.
612
+ //
613
+ // feat-20260520-2d-sprite-layer-mvp M-3 / w25 (@new-surface): per-entry
614
+ // `renderableIndex` tracks the position into the original renderables[]
615
+ // array so the bucket-aware reordering below can correlate
616
+ // transparent-sort output (TransparentEntry.renderableIndex) back to
617
+ // the validated row.
618
+ // bug-20260527-renderstate-pipeline-dispatch-gap D-4:
619
+ // build a renderableIndex -> renderState map from dispatch entries
620
+ // so each ValidatedRenderable carries its per-material renderState
621
+ // override without an O(n^2) back-scan in the draw loop.
622
+ const gpuOwnedEntityKeys = preparedGpuDriven?.entityKeys ?? EMPTY_ENTITY_KEYS;
623
+ const validated = runRecordProfilePhase(profilePhase, 'record/validation', () =>
624
+ gpuOwnsAllPrimaryRaster && !cpuShadowCastersActive
625
+ ? []
626
+ : validateRenderables(
627
+ internals,
628
+ world,
629
+ worlds,
630
+ pipelineState,
631
+ frameState,
632
+ renderables,
633
+ transparentDispatch,
634
+ cpuShadowCastersActive ? EMPTY_ENTITY_KEYS : gpuOwnedEntityKeys,
635
+ ),
636
+ );
637
+ gpuDriven?.owner.recordCpuValidation(validated, gpuOwnedEntityKeys);
638
+
639
+ // Per-frame cache clean-up: drop per-entity BG cache entries + instance
640
+ // buffers whose entityKey is absent from the current validated set (despawn
641
+ // eviction). Extracted to cleanPerFrameCaches (M3/w18).
642
+ cleanPerFrameCaches(internals, frameState, validated, gpuOwnedEntityKeys);
643
+
644
+ // Dispatch-ordered render plan: reorder validated renderables to dispatch
645
+ // order, run the mesh-SSBO capacity gate (graceful truncation), and build
646
+ // the fold dispatch plan. Extracted to buildDispatchPlan (M3/w18).
647
+ const dispatchPlan = runRecordProfilePhase(profilePhase, 'record/dispatch-plan', () =>
648
+ buildDispatchPlan(internals, validated, transparentDispatch, foldBuckets),
649
+ );
650
+ const validatedOrdered = dispatchPlan.validatedOrdered;
651
+ const foldDispatchPlan = dispatchPlan.foldDispatchPlan;
652
+ const materialSlotIndices = dispatchPlan.materialSlotIndices;
653
+ const materialSlots = dispatchPlan.materialSlots;
654
+ const materialSlotOwners = dispatchPlan.materialSlotOwners;
655
+ const materialSlotCount = dispatchPlan.materialSlotCount;
656
+
657
+ // D-2 (bug-20260527): LDR sprite pass split, generalised feat-20260625
658
+ // M2 / w7 via {@link computeSplitLdrSprite}; M3 w13 finalised by deleting
659
+ // the legacy shadingModel arm — transparent is the single SSOT. AC-05
660
+ // (non-sprite shader carrying transparent:true) trips the split too;
661
+ // the unit suite render-system-record.test.ts 'transparent decouples
662
+ // from sprite shader' locks the contract.
663
+ //
664
+ // When the LDR path (tonemapActive=false) has transparent entities in
665
+ // the validated draw list, the render is split into two serial passes
666
+ // sharing one color attachment. The transparent pass format is resolved
667
+ // by transparentPassColorFormat: native linear-LDR frames use the
668
+ // graph-owned `ldrColor` attachment (which may be rgba16float), while
669
+ // the swap-chain fallback uses its raw storage view. The encoder and
670
+ // sprite PSO must use the same resolved format because WebGPU requires
671
+ // attachment and pipeline target formats to match.
672
+ const splitLdrSprite = computeSplitLdrSprite(validatedOrdered, tonemapActive);
673
+ let ldrSpritePassView: TextureView | null = null;
674
+ if (splitLdrSprite) {
675
+ const unormViewRes = internals.device.createTextureView(currentTexture, {});
676
+ if (!unormViewRes.ok) {
677
+ internals.errorRegistry.fire(unormViewRes.error);
678
+ return false;
679
+ }
680
+ ldrSpritePassView = unormViewRes.value;
681
+ }
682
+
683
+ // View / mesh uniform uploads are only needed when geometry will be drawn,
684
+ // OR when a skybox is active (skybox pass reads inverseViewProj from
685
+ // the View UBO). Skip the writeBuffer round-trips on the Case E
686
+ // (clear-pass-only) path only when neither condition is met.
687
+ // feat-20260531-skybox-env-background M2 / w6: gate relaxed from
688
+ // `validatedOrdered.length > 0` to include skybox-only frames
689
+ // (plan-strategy D-3, R-3).
690
+ if (validatedOrdered.length > 0 || skyboxActive) {
691
+ runRecordProfilePhase(profilePhase, 'record/uploads', () => {
692
+ // View UBO + CSM/spot-shadow matrix pack: assembled + uploaded in one
693
+ // queue.writeBuffer round-trip. Extracted to view-ubo.ts (M3/w18) so
694
+ // recordFrame stays an orchestration skeleton (D-2).
695
+ writeViewUbo(
696
+ internals.device.queue,
697
+ pipelineState.viewUniformBuffer,
698
+ camera,
699
+ light,
700
+ lights,
701
+ frameState.spotShadowSnapshots,
702
+ );
703
+ if (pipelineState.pointsLinesViewBuffer !== undefined) {
704
+ writePointsLinesViewUbo(
705
+ internals.device.queue,
706
+ pipelineState.pointsLinesViewBuffer,
707
+ camera,
708
+ targetW,
709
+ targetH,
710
+ );
711
+ }
712
+ writeShadowCasterUniforms(
713
+ internals.device.queue,
714
+ pipelineState.shadowCasterCascadeBuffer,
715
+ lights,
716
+ );
717
+
718
+ // Per-frame full rewrite of the PointLight + SpotLight std430 storage
719
+ // buffers (header + first-slice cap N=4 slots). Extracted to
720
+ // writePointSpotLightBuffers (M3/w18).
721
+ writePointSpotLightBuffers(internals, pipelineState, lights);
722
+
723
+ // Per-renderable entity_world upload (batched): all N mat4+normalMatrix
724
+ // slots assembled into a single contiguous scratch buffer, then flushed
725
+ // as one writeBuffer call. Extracted to mesh-ssbo.ts (M3/w21) so its
726
+ // module-scoped scratch buffer co-locates with the other mesh-SSBO lets.
727
+ uploadMeshSsboBatch(
728
+ internals.device.queue,
729
+ pipelineState.meshStorageBuffer,
730
+ validatedOrdered,
731
+ foldDispatchPlan,
732
+ );
733
+ });
734
+ }
735
+
736
+ const directionalShadowCache = runRecordProfilePhase(
737
+ profilePhase,
738
+ 'record/scene-state/directional-shadow-cache',
739
+ () =>
740
+ prepareDirectionalShadowCache(
741
+ internals,
742
+ frameState,
743
+ worlds,
744
+ renderReadLeases,
745
+ lights,
746
+ effectiveShadowMapSize,
747
+ validatedOrdered,
748
+ ),
749
+ );
750
+ frameState.directionalShadowCacheRecorded = false;
751
+
752
+ const encoderResult = internals.device.createCommandEncoder({ label: 'render-system-frame' });
753
+ if (!encoderResult.ok) {
754
+ internals.errorRegistry.fire(encoderResult.error);
755
+ return false;
756
+ }
757
+ const encoder: RhiCommandEncoder = encoderResult.value;
758
+
759
+ // Per-frame bind group cache resolution (view / mesh / HDRP-cluster).
760
+ // Extracted to buildPerFrameBindGroups (M3/w18) so recordFrame stays a
761
+ // skeleton; returns null groups on the Case E (0-validated) path.
762
+ const { viewBindGroup, meshBindGroup, hdrpClusterBindGroup, hdrpClusterMembershipBindGroup } =
763
+ runRecordProfilePhase(profilePhase, 'record/bind-groups', () =>
764
+ buildPerFrameBindGroups(
765
+ internals,
766
+ frameState,
767
+ pipelineState,
768
+ validated.length > 0 || preparedGpuDriven !== undefined,
769
+ bindGroupCounts,
770
+ undefined,
771
+ false,
772
+ ),
773
+ );
774
+
775
+ // ── feat-20260529-rendergraph-pass-abstraction M4 / w13b ───────────
776
+ // The compiled typed graph is the sole per-frame pass owner.
777
+ //
778
+ // Every pass records into the shared encoder. The graph owns ordering and
779
+ // the renderer finishes and submits exactly once after graph execution.
780
+ // feat-20260601 M2 / w12: the per-frame shared state is the clean
781
+ // `RenderPipelineContext` - `internals` is replaced by the named `assets`
782
+ // (CPU POD) / `store` (GPU residency) / `pipelineState` / `runtime` (device +
783
+ // errorRegistry + shader-cache lookups) surfaces (`internals` itself satisfies
784
+ // `RenderSystemRuntime` so `runtime: internals` is a zero-cost reference). The
785
+ // 0-consumed `skyboxCount` residual is dropped.
786
+ const passCtx: _InternalRenderPipelineContext = {
787
+ assets: internals.assets,
788
+ world,
789
+ gpuDrivenEntityKeys: gpuOwnedEntityKeys,
790
+ store: internals.gpuStore,
791
+ pipelineState,
792
+ runtime: internals,
793
+ encoder,
794
+ view,
795
+ clear,
796
+ targetW,
797
+ targetH,
798
+ currentTexture,
799
+ camera,
800
+ tonemapActive,
801
+ geometryColorView,
802
+ geometryDepthView,
803
+ geometryDepthKey,
804
+ validated,
805
+ validatedOrdered,
806
+ viewBindGroup,
807
+ meshBindGroup,
808
+ frameState,
809
+ dispatchCounts,
810
+ bindGroupCounts,
811
+ skylight,
812
+ skylightCount,
813
+ skyboxActive,
814
+ skybox,
815
+ splitLdrSprite,
816
+ ldrSpritePassView,
817
+ msaaActive,
818
+ geometryColorResolveView,
819
+ ldrSpriteColorView,
820
+ postProcessParams,
821
+ dispatch: transparentDispatch,
822
+ hdrpClusterBindGroup,
823
+ hdrpClusterMembershipBindGroup,
824
+ foldDispatchPlan,
825
+ materialSlotIndices,
826
+ materialSlots,
827
+ materialSlotOwners,
828
+ materialSlotCount,
829
+ pointsLines: pointsLinesOwner,
830
+ materialBgAssemblyCache: frameState.materialBgAssemblyCache,
831
+ directionalShadowCacheReuse: directionalShadowCache.reuse,
832
+ ...(profilePhase !== undefined ? { profilePhase } : {}),
833
+ };
834
+ const submitted = runRecordProfilePhase(profilePhase, 'record/graph-execute', () =>
835
+ executeCompiledFrameGraph(
836
+ internals,
837
+ frameState,
838
+ passCtx,
839
+ encoder,
840
+ profilePhase === undefined
841
+ ? undefined
842
+ : (pass, encode) => profilePhase(graphExecutionPhase(pass.name), encode),
843
+ ),
844
+ );
845
+ if (submitted && !directionalShadowCache.reuse) {
846
+ frameState.directionalShadowCache = frameState.directionalShadowCacheRecorded
847
+ ? directionalShadowCache.next
848
+ : null;
849
+ }
850
+ return submitted;
851
+ } finally {
852
+ frameState.frameNumber += 1;
853
+ // feat-20260608-cluster-lighting M5 / w22: clear HDRP once-per-frame fired
854
+ // set so the next frame re-fires if the condition persists.
855
+ frameState.hdrpOncePerFrameFired.clear();
856
+ }
857
+ }
858
+
859
+ interface DirectionalShadowCacheDecision {
860
+ readonly reuse: boolean;
861
+ readonly next: DirectionalShadowCache | null;
862
+ }
863
+
864
+ function readWorldState(lease: RenderReadLease): DirectionalShadowWorldState {
865
+ return {
866
+ worldIdentity: lease.worldIdentity,
867
+ changeCursor: lease.inspectCursor(),
868
+ };
869
+ }
870
+
871
+ function sameWorldState(
872
+ cached: DirectionalShadowCache,
873
+ worlds: readonly World[],
874
+ leases: readonly RenderReadLease[] | undefined,
875
+ ): boolean {
876
+ if (leases === undefined || leases.length !== worlds.length) return false;
877
+ if (cached.worlds.length !== worlds.length || cached.worldStateTokens.length !== leases.length) {
878
+ return false;
879
+ }
880
+ for (let index = 0; index < worlds.length; index += 1) {
881
+ const world = worlds[index];
882
+ const lease = leases[index];
883
+ if (world === undefined || lease === undefined || cached.worlds[index] !== world) return false;
884
+ const cachedState = cached.worldStateTokens[index];
885
+ if (cachedState === undefined) return false;
886
+ if (cachedState.worldIdentity !== lease.worldIdentity) return false;
887
+ let changes: ReturnType<RenderReadLease['readChanges']>;
888
+ try {
889
+ changes = lease.readChanges(cachedState.changeCursor);
890
+ } catch {
891
+ // A lease may have evicted an old cursor after another owner consumed
892
+ // its bounded cursor window. Treat that as stale render evidence and
893
+ // rebuild the atlas from the current lease rather than reaching into
894
+ // World for a second mutation clock.
895
+ return false;
896
+ }
897
+ if (changes.status !== 'ok') return false;
898
+ if (changes.world.records.length !== 0 || changes.sharedRefs.records.length !== 0) {
899
+ return false;
900
+ }
901
+ }
902
+ return true;
903
+ }
904
+
905
+ function sameLightViewProj(
906
+ cached: DirectionalShadowCache,
907
+ current: readonly Float32Array[],
908
+ ): boolean {
909
+ if (cached.lightViewProj.length !== current.length) return false;
910
+ for (let matrixIndex = 0; matrixIndex < current.length; matrixIndex += 1) {
911
+ const previous = cached.lightViewProj[matrixIndex];
912
+ const next = current[matrixIndex];
913
+ if (previous === undefined || next === undefined || previous.length !== next.length) {
914
+ return false;
915
+ }
916
+ for (let valueIndex = 0; valueIndex < next.length; valueIndex += 1) {
917
+ if (previous[valueIndex] !== next[valueIndex]) return false;
918
+ }
919
+ }
920
+ return true;
921
+ }
922
+
923
+ function prepareDirectionalShadowCache(
924
+ internals: RenderSystemInternals,
925
+ frameState: RenderFrameState,
926
+ worlds: readonly World[],
927
+ leases: readonly RenderReadLease[] | undefined,
928
+ lights: ExtractedLights,
929
+ shadowMapSize: number | undefined,
930
+ validatedOrdered: readonly ValidatedRenderable[],
931
+ ): DirectionalShadowCacheDecision {
932
+ const cascadeCount = lights.cascadeCount;
933
+ const lightViewProj = lights.lightViewProj;
934
+ if (
935
+ frameState.compiledFrameGraph === null ||
936
+ frameState.compiledFrameGraphTopologyKey === null ||
937
+ shadowMapSize === undefined ||
938
+ shadowMapSize <= 0 ||
939
+ cascadeCount === undefined ||
940
+ cascadeCount <= 0 ||
941
+ lightViewProj === undefined ||
942
+ validatedOrdered.length === 0
943
+ ) {
944
+ return { reuse: false, next: null };
945
+ }
946
+
947
+ const cached = frameState.directionalShadowCache;
948
+ const assetCatalogEpoch = internals.assets.catalogEpoch;
949
+ const meshResidencyEpoch = internals.gpuStore.meshResidencyEpoch;
950
+ if (
951
+ cached !== null &&
952
+ cached.shadowMapSize === shadowMapSize &&
953
+ cached.cascadeCount === cascadeCount &&
954
+ cached.pipelineHandle === frameState.installedPipelineHandle &&
955
+ cached.graphTopologyKey === frameState.compiledFrameGraphTopologyKey &&
956
+ cached.assetCatalogEpoch === assetCatalogEpoch &&
957
+ cached.meshResidencyEpoch === meshResidencyEpoch &&
958
+ sameWorldState(cached, worlds, leases) &&
959
+ sameLightViewProj(cached, lightViewProj)
960
+ ) {
961
+ return { reuse: true, next: null };
962
+ }
963
+
964
+ if (leases === undefined || leases.length !== worlds.length) {
965
+ // Direct record-stage test harnesses may omit the host-owned leases. They
966
+ // still exercise the real shadow encoder, but without the ECS read owner
967
+ // they must conservatively rebuild rather than inspect World internals.
968
+ return { reuse: false, next: null };
969
+ }
970
+ const worldStateTokens = leases.map(readWorldState);
971
+
972
+ return {
973
+ reuse: false,
974
+ next: {
975
+ worlds: worlds.slice(),
976
+ worldStateTokens: worldStateTokens as DirectionalShadowWorldState[],
977
+ assetCatalogEpoch,
978
+ pipelineHandle: frameState.installedPipelineHandle,
979
+ graphTopologyKey: frameState.compiledFrameGraphTopologyKey,
980
+ shadowMapSize,
981
+ cascadeCount,
982
+ lightViewProj: lightViewProj.map((matrix) => new Float32Array(matrix)),
983
+ meshResidencyEpoch,
984
+ },
985
+ };
986
+ }
987
+
988
+ /**
989
+ * feat-20260704 M3/w18: validate renderable handles + collect the render plan,
990
+ * extracted verbatim from `recordFrame`. Empty `renderables` input or all-
991
+ * unregistered handles both yield an empty result (the Case E clear-pass-only
992
+ * path). Builds per-entry renderState / stencilReference overlays from the
993
+ * transparent-dispatch entries, resolves each MeshFilter.assetHandle through
994
+ * the AssetRegistry + GPU store (with the sprite 9-slice mesh swap), and fires
995
+ * structured `asset-not-registered` errors for handles that fail to resolve.
996
+ *
997
+ * @internal
998
+ */
999
+ function validateRenderables(
1000
+ internals: RenderSystemInternals,
1001
+ world: World,
1002
+ // feat-20260709-editor-world-partition ENGINE-fix-round2 (defect 2): the full
1003
+ // worlds[] list. Each renderable's mesh handle is a user-tier slot in its OWN
1004
+ // world's sharedRefs; resolving against a foreign world either misses
1005
+ // (asset-not-registered) or resolves the wrong slot payload. `world` (the
1006
+ // resource-owner) is retained as the fallback for renderables whose worldId
1007
+ // is out of range (defensive; extractFrames always stamps a valid index).
1008
+ worlds: readonly World[],
1009
+ pipelineState: PipelineState,
1010
+ frameState: RenderFrameState,
1011
+ renderables: readonly RenderableSnapshot[],
1012
+ transparentDispatch: readonly DispatchEntry[],
1013
+ excludedEntityKeys: ReadonlySet<number> = new Set<number>(),
1014
+ ): ValidatedRenderable[] {
1015
+ // bug-20260527-renderstate-pipeline-dispatch-gap D-4:
1016
+ // build a renderableIndex -> renderState map from dispatch entries
1017
+ // so each ValidatedRenderable carries its per-material renderState
1018
+ // override without an O(n^2) back-scan in the draw loop.
1019
+ const renderStateByRenderableIdx = new Map<number, MaterialRenderState | undefined>();
1020
+ // w10: also build a renderableIndex -> stencilReference map from
1021
+ // dispatch entries for per-draw setStencilReference calls.
1022
+ const stencilRefByRenderableIdx = new Map<number, number | undefined>();
1023
+ const variantSetByRenderableIdx = new Map<number, string | undefined>();
1024
+ // Keep these three independent maps because each overlay has its own
1025
+ // consumer and the last dispatch entry remains authoritative. Populate
1026
+ // them in one pass: the old code walked the same dispatch list three times
1027
+ // and repeated the same renderableIndex branch / hash writes.
1028
+ for (const de of transparentDispatch) {
1029
+ const renderableIndex = de.renderableIndex;
1030
+ if (renderableIndex === undefined) continue;
1031
+ renderStateByRenderableIdx.set(renderableIndex, de.renderState);
1032
+ stencilRefByRenderableIdx.set(renderableIndex, de.stencilReference);
1033
+ variantSetByRenderableIdx.set(renderableIndex, variantSetFromDefines(de.defines));
1034
+ }
1035
+ const validated: ValidatedRenderable[] = [];
1036
+ for (let rIdx = 0; rIdx < renderables.length; rIdx++) {
1037
+ const r = renderables[rIdx];
1038
+ if (r === undefined) continue;
1039
+ if (excludedEntityKeys.has(worldEntityKey(r.worldId, r.entityKey))) continue;
1040
+ // feat-20260709-editor-world-partition ENGINE-fix-round2 (defect 2): resolve
1041
+ // this renderable's mesh against the world it was EXTRACTED from
1042
+ // (worlds[r.worldId]) — NOT the single resource-owner `world`. Builtin mesh
1043
+ // slots (< BUILTIN_BASE) resolve process-statically regardless of world, so
1044
+ // the per-world pick only matters for user-tier handles, but selecting it
1045
+ // unconditionally keeps a single code path. Falls back to the resource-owner
1046
+ // world if worldId is out of range (defensive; extractFrames always stamps
1047
+ // a valid index into worlds[]).
1048
+ const renderableWorld = worlds[r.worldId] ?? world;
1049
+ const assetRes = resolveAssetHandle<MeshAsset>(
1050
+ renderableWorld,
1051
+ toShared<'MeshAsset'>(r.assetHandle),
1052
+ );
1053
+ if (!assetRes.ok) {
1054
+ internals.errorRegistry.fire(
1055
+ new RhiError({
1056
+ code: 'asset-not-registered',
1057
+ expected: 'MeshFilter.assetHandle in AssetRegistry',
1058
+ hint: 'use HANDLE_CUBE / HANDLE_TRIANGLE imports; custom mesh register path: feat-future-asset-system',
1059
+ detail: { assetHandle: r.assetHandle },
1060
+ }),
1061
+ );
1062
+ continue;
1063
+ }
1064
+ // feat-20260601-device/gpu-residency-extraction M1 (D-1): builtin meshes
1065
+ // (slots through HANDLE_NINESLICE_QUAD) keep the createRenderer step-3
1066
+ // direct-upload + `pipelineState.meshes` path -- they are NOT routed
1067
+ // through `ensureResident`. User-registered meshes pull through the store
1068
+ // on first access (the register->upload push was severed in this M1);
1069
+ // the POD fetched above (assetRes.value) is passed in, store holds no
1070
+ // registry ref (D-2). A first-access miss builds the GPU buffers; later
1071
+ // frames hit the O(1) cache.
1072
+ const meshAssetHandle = toShared<'MeshAsset'>(r.assetHandle);
1073
+ let meshHandles = internals.gpuStore.getMeshGpuHandles(meshAssetHandle, renderableWorld);
1074
+ if (meshHandles === undefined && r.assetHandle > handleSlot(HANDLE_NINESLICE_QUAD)) {
1075
+ const residentRes = internals.gpuStore.ensureResident(
1076
+ meshAssetHandle,
1077
+ assetRes.value,
1078
+ renderableWorld,
1079
+ );
1080
+ if (residentRes.ok) {
1081
+ meshHandles = residentRes.value;
1082
+ } else if (residentRes.error instanceof RhiError) {
1083
+ internals.errorRegistry.fire(residentRes.error);
1084
+ }
1085
+ }
1086
+ meshHandles = meshHandles ?? pipelineState.meshes.get(r.assetHandle);
1087
+ if (meshHandles === undefined) {
1088
+ internals.errorRegistry.fire(
1089
+ new RhiError({
1090
+ code: 'asset-not-registered',
1091
+ expected: 'GPU mesh buffers uploaded for assetHandle',
1092
+ hint: 'await renderer.initialization before draw([world], { cameraOwner: 0, resourceOwner: 0 }); ensure AssetRegistry.configureGpuDevice ran so user meshes are uploaded',
1093
+ detail: { assetHandle: r.assetHandle },
1094
+ }),
1095
+ );
1096
+ continue;
1097
+ }
1098
+ // feat-20260527-sprite-nineslice M2 / w11 (plan-strategy section D-2):
1099
+ // sprite branch with non-zero `slicesAndMode` (post-w12 paramSnapshot
1100
+ // entry name) overrides the user-supplied mesh handle (typically
1101
+ // HANDLE_QUAD = 3) with the 16-vertex / 54-index HANDLE_NINESLICE_QUAD
1102
+ // (id=5) topology so the vertex shader sees the 4x4 grid required for
1103
+ // 9-region anchor mapping. Default slicesAndMode ([0, 0, 0, 0]) keeps
1104
+ // the legacy HANDLE_QUAD path; a flip from zero to non-zero on the
1105
+ // same entity routes here per-frame so AI users can toggle 9-slice
1106
+ // on the fly without re-spawning the entity (charter F1 minimum
1107
+ // surface). The HANDLE_NINESLICE_QUAD GPU buffers are seeded by
1108
+ // createRenderer step-3.
1109
+ //
1110
+ // feat-20260625-refactor-sprite-as-transparent-mesh M3 / w13: judgement
1111
+ // key migrated from `shadingModel === 'sprite'` to
1112
+ // `materialShaderId === 'forgeax::sprite'` (plan-strategy D-10); slices
1113
+ // sourced from `paramSnapshot.slicesAndMode` (post-w12 UBO-aligned
1114
+ // overlay path).
1115
+ //
1116
+ // feat-20260624-sprite-lit-shading-model-pure-2d-lighting M1' / t7:
1117
+ // sprite-lit shares the sprite paramSchema (5 fields, t4 mirror) so
1118
+ // the 9-slices mesh swap applies identically.
1119
+ let effectiveMeshHandles = meshHandles;
1120
+ if (
1121
+ r.material.materialShaderId === 'forgeax::sprite' ||
1122
+ r.material.materialShaderId === 'forgeax::sprite-lit'
1123
+ ) {
1124
+ const slicesArr = r.material.paramSnapshot?.slicesAndMode as readonly number[] | undefined;
1125
+ if (
1126
+ slicesArr !== undefined &&
1127
+ slicesArr.length >= 4 &&
1128
+ (slicesArr[0] !== 0 || slicesArr[1] !== 0 || slicesArr[2] !== 0 || slicesArr[3] !== 0)
1129
+ ) {
1130
+ const nineSliceHandles = pipelineState.meshes.get(handleSlot(HANDLE_NINESLICE_QUAD));
1131
+ if (nineSliceHandles !== undefined) {
1132
+ effectiveMeshHandles = nineSliceHandles;
1133
+ }
1134
+ }
1135
+ }
1136
+ if (r.morph !== undefined) {
1137
+ const morphed = prepareMorphMesh(
1138
+ internals,
1139
+ frameState,
1140
+ worldEntityKey(r.worldId, r.entityKey),
1141
+ effectiveMeshHandles,
1142
+ assetRes.value,
1143
+ r.morph,
1144
+ );
1145
+ if (morphed !== undefined) effectiveMeshHandles = morphed;
1146
+ }
1147
+ validated.push({
1148
+ source: r,
1149
+ world: renderableWorld,
1150
+ mesh: effectiveMeshHandles,
1151
+ renderableIndex: rIdx,
1152
+ renderState: renderStateByRenderableIdx.get(rIdx),
1153
+ variantSet: variantSetByRenderableIdx.get(rIdx),
1154
+ stencilReference: stencilRefByRenderableIdx.get(rIdx),
1155
+ });
1156
+ }
1157
+ return validated;
1158
+ }
1159
+
1160
+ function prepareMorphMesh(
1161
+ internals: RenderSystemInternals,
1162
+ frameState: RenderFrameState,
1163
+ cacheKey: number,
1164
+ base: MeshGpuHandles,
1165
+ mesh: MeshAsset,
1166
+ morph: RenderableSnapshot['morph'],
1167
+ ): MeshGpuHandles | undefined {
1168
+ if (morph === undefined || frameState.morphBuffers === undefined) return undefined;
1169
+ const targets = mesh.morphTargets;
1170
+ if (targets === undefined || targets.length !== morph.targetCount) return undefined;
1171
+ if (base.layoutProjection.arrayStride !== 48 || base.vertexCount <= 0) return undefined;
1172
+ const stride = mesh.vertices.length / base.vertexCount;
1173
+ if (!Number.isInteger(stride) || stride < 3) return undefined;
1174
+
1175
+ let active = false;
1176
+ for (const weight of morph.weights) {
1177
+ if (weight !== 0) {
1178
+ active = true;
1179
+ break;
1180
+ }
1181
+ }
1182
+ if (!active) {
1183
+ frameState.morphBuffers.get(cacheKey)?.buffer.destroy();
1184
+ frameState.morphBuffers.delete(cacheKey);
1185
+ return undefined;
1186
+ }
1187
+
1188
+ const vertices = new Float32Array(mesh.vertices);
1189
+ for (let targetIndex = 0; targetIndex < targets.length; targetIndex += 1) {
1190
+ const weight = morph.weights[targetIndex] ?? 0;
1191
+ const positions = targets[targetIndex]?.position;
1192
+ if (positions === undefined || positions.length !== base.vertexCount * 3) return undefined;
1193
+ for (let vertex = 0; vertex < base.vertexCount; vertex += 1) {
1194
+ const vertexBase = vertex * stride;
1195
+ const positionBase = vertex * 3;
1196
+ vertices[vertexBase] = (vertices[vertexBase] ?? 0) + (positions[positionBase] ?? 0) * weight;
1197
+ vertices[vertexBase + 1] =
1198
+ (vertices[vertexBase + 1] ?? 0) + (positions[positionBase + 1] ?? 0) * weight;
1199
+ vertices[vertexBase + 2] =
1200
+ (vertices[vertexBase + 2] ?? 0) + (positions[positionBase + 2] ?? 0) * weight;
1201
+ }
1202
+ }
1203
+
1204
+ let cached = frameState.morphBuffers.get(cacheKey);
1205
+ if (cached === undefined || cached.byteLength !== vertices.byteLength) {
1206
+ if (cached !== undefined && !cached.buffer.isDestroyed) {
1207
+ const destroyed = cached.buffer.destroy();
1208
+ if (!destroyed.ok) internals.errorRegistry.fire(destroyed.error);
1209
+ }
1210
+ const created = internals.device.createBuffer({
1211
+ label: `morph-${cacheKey}-vbo`,
1212
+ size: vertices.byteLength,
1213
+ usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
1214
+ mappedAtCreation: false,
1215
+ });
1216
+ if (!created.ok) {
1217
+ internals.errorRegistry.fire(created.error);
1218
+ frameState.morphBuffers.delete(cacheKey);
1219
+ return undefined;
1220
+ }
1221
+ cached = {
1222
+ buffer: new GpuBuffer(internals.device, created.value),
1223
+ byteLength: vertices.byteLength,
1224
+ };
1225
+ frameState.morphBuffers.set(cacheKey, cached);
1226
+ }
1227
+ const written = internals.device.queue.writeBuffer(cached.buffer.handle, 0, vertices);
1228
+ if (!written.ok) {
1229
+ internals.errorRegistry.fire(written.error);
1230
+ return undefined;
1231
+ }
1232
+ return { ...base, vertexBuffer: cached.buffer };
1233
+ }
1234
+
1235
+ /**
1236
+ * feat-20260704 M3/w18: build the dispatch-ordered render plan, extracted
1237
+ * verbatim from `recordFrame`. (1) M3/w26 dispatch-ordered reorder: reorder
1238
+ * `validated` to follow the transparent-dispatch order (extract-order fallback
1239
+ * for unmatched entries). (2) feat-20260608 mesh-SSBO capacity gate: size the
1240
+ * mesh-SSBO + material-UBO pair to the larger of entity vs cumulative
1241
+ * material-slot count, truncating on ceiling (graceful degradation). (3)
1242
+ * feat-20260622 fold dispatch plan: build + apply the WebGL2 uniform-cap
1243
+ * fallback + bump the folded-draws metric.
1244
+ *
1245
+ * @internal
1246
+ */
1247
+ function buildDispatchPlan(
1248
+ internals: RenderSystemInternals,
1249
+ validated: readonly ValidatedRenderable[],
1250
+ transparentDispatch: readonly DispatchEntry[],
1251
+ foldBuckets: readonly FoldBucket[],
1252
+ ): {
1253
+ validatedOrdered: readonly ValidatedRenderable[];
1254
+ foldDispatchPlan: FoldDispatchPlan | null;
1255
+ materialSlotIndices: readonly (readonly number[])[];
1256
+ materialSlots: readonly MaterialSnapshot[];
1257
+ materialSlotOwners: readonly number[];
1258
+ materialSlotCount: number;
1259
+ } {
1260
+ // M3 / w26: dispatch-ordered render. The dispatch list is pre-sorted
1261
+ // by queue (ascending, stable) by the extract stage per plan-strategy D-3.
1262
+ // Reorder validated renderables to follow the dispatch order, falling
1263
+ // back to extract order for renderables with no matching dispatch entry.
1264
+ let dispatchOrderMatchesValidated = transparentDispatch.length === validated.length;
1265
+ if (dispatchOrderMatchesValidated) {
1266
+ for (let i = 0; i < validated.length; i++) {
1267
+ if (transparentDispatch[i]?.renderableIndex !== validated[i]?.renderableIndex) {
1268
+ dispatchOrderMatchesValidated = false;
1269
+ break;
1270
+ }
1271
+ }
1272
+ }
1273
+ let validatedOrdered: readonly ValidatedRenderable[] = dispatchOrderMatchesValidated
1274
+ ? validated
1275
+ : [...validated];
1276
+ if (transparentDispatch.length > 0 && !dispatchOrderMatchesValidated) {
1277
+ const validatedByRenderableIdx = new Map<number, ValidatedRenderable>();
1278
+ const seen = new Set<number>();
1279
+ for (const v of validated) {
1280
+ validatedByRenderableIdx.set(v.renderableIndex, v);
1281
+ }
1282
+ const ordered: ValidatedRenderable[] = [];
1283
+ for (const de of transparentDispatch) {
1284
+ if (de.renderableIndex === undefined) continue;
1285
+ const v = validatedByRenderableIdx.get(de.renderableIndex);
1286
+ if (v !== undefined && !seen.has(de.renderableIndex)) {
1287
+ seen.add(de.renderableIndex);
1288
+ ordered.push(v);
1289
+ }
1290
+ }
1291
+ // Append renderables not in the dispatch list (e.g. default-material entities)
1292
+ for (const v of validated) {
1293
+ if (!seen.has(v.renderableIndex)) {
1294
+ ordered.push(v);
1295
+ }
1296
+ }
1297
+ validatedOrdered = ordered;
1298
+ }
1299
+
1300
+ const materialGroups = validatedOrdered.map((entry): readonly MaterialSnapshot[] => {
1301
+ const shaderId = entry.source.material.materialShaderId;
1302
+ if (shaderId === 'forgeax::sprite' || shaderId === 'forgeax::sprite-lit') {
1303
+ return [entry.source.material];
1304
+ }
1305
+ return entry.source.materials.length > 0 ? entry.source.materials : [entry.source.material];
1306
+ });
1307
+ const materialSlotPlan = buildMaterialSlotPlan(materialGroups);
1308
+ let materialSlotIndices = materialSlotPlan.slotIndices;
1309
+ let materialSlots = materialSlotPlan.slots;
1310
+ let materialSlotOwners = materialSlotPlan.slotOwners;
1311
+ let materialSlotCount = materialSlots.length;
1312
+
1313
+ // feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M3 / T-M3-04:
1314
+ // ensure the mesh-SSBO + material-UBO buffer pair is large enough to hold
1315
+ // the render plan BEFORE the first per-entity writeBuffer.
1316
+ // On `ok:false` the controller has already fired a structured RuntimeError
1317
+ // (`mesh-ssbo-ceiling-reached` / `mesh-ssbo-capacity-exceeded`); we truncate
1318
+ // the draw list to the largest complete entity prefix whose cumulative
1319
+ // material slots fit `degradedToSlotCount` (graceful degradation per
1320
+ // plan-strategy D-2): render the subset that fits, discard overflow, no
1321
+ // black frame. The helper is idempotent across same-frame re-calls (AC-09)
1322
+ // and short-circuits on length=0 / length<=slotCount (boundary table).
1323
+ //
1324
+ // bug-20260609: feat-20260608 M5 amend made the material UBO indexed by
1325
+ // cumulative *material-slot* count (one slot per submesh material),
1326
+ // which is >= entity count once an entity carries `materials.length>1`.
1327
+ // The mesh + material buffer pair share `slotCount` (single allocator),
1328
+ // so we size against the larger of the two requirements: entity count
1329
+ // (mesh-SSBO consumer) vs cumulative material-slot count (material-UBO
1330
+ // consumer). Sprite entities collapse to 1 slot in the material table,
1331
+ // mirroring the same rule (sprite per-submesh OOS-1;
1332
+ // post-w13 judgement key migrated to materialShaderId).
1333
+ //
1334
+ // feat-20260624 M1' / t7: sprite-lit treated identically to sprite
1335
+ // for material-slot accounting (paramSchema mirror, t4).
1336
+ const neededSlots = Math.max(validatedOrdered.length, materialSlotCount);
1337
+ const meshSsboCapResult = ensureMeshSsboCapacity(internals, neededSlots);
1338
+ if (!meshSsboCapResult.ok) {
1339
+ // Graceful degradation: the controller reports slots, but this stage
1340
+ // consumes entities. Find a complete prefix instead of slicing at the
1341
+ // numeric slot count; a multi-material entity can consume several slots.
1342
+ const degradedRenderableCount = findRenderablePrefixForSlotCapacity(
1343
+ materialSlotIndices,
1344
+ meshSsboCapResult.degradedToSlotCount,
1345
+ );
1346
+ materialSlotCount = materialSlotCountForPrefix(materialSlotIndices, degradedRenderableCount);
1347
+ materialSlotIndices = materialSlotIndices.slice(0, degradedRenderableCount);
1348
+ materialSlots = materialSlots.slice(0, materialSlotCount);
1349
+ materialSlotOwners = materialSlotOwners.slice(0, materialSlotCount);
1350
+ validatedOrdered = validatedOrdered.slice(0, degradedRenderableCount);
1351
+ }
1352
+
1353
+ // feat-20260622-chunk-gpu-instancing-sprite-tilemap M1 / w4-record-swap
1354
+ // (D-1): build the fold dispatch plan once `validatedOrdered` is final
1355
+ // (post truncation by mesh-SSBO capacity gate). The plan re-keys each
1356
+ // non-singleton bucket from `renderableIndex` to the validated-ordered
1357
+ // index `i` consumed by the dispatch loops; the loops use it to skip
1358
+ // non-head bucket members and emit one instanced drawIndexed per
1359
+ // bucket head. Empty plan (no fold-eligible buckets) is a byte-
1360
+ // identical no-op for the dispatch loops below (charter P3: silent
1361
+ // pass-through, no error path).
1362
+ let renderableToValidatedIdx: Map<number, number> | null = null;
1363
+ let foldDispatchPlan: FoldDispatchPlan | null = null;
1364
+ if (foldBuckets.length > 0) {
1365
+ renderableToValidatedIdx = new Map<number, number>();
1366
+ for (let i = 0; i < validatedOrdered.length; i++) {
1367
+ const e = validatedOrdered[i];
1368
+ if (e === undefined) continue;
1369
+ renderableToValidatedIdx.set(e.renderableIndex, i);
1370
+ }
1371
+ foldDispatchPlan = buildFoldDispatchPlan(foldBuckets, renderableToValidatedIdx);
1372
+
1373
+ // feat-20260622 M2 / w11 (D-2 + D-9 + AC-05): WebGL2 uniform-fallback
1374
+ // per-bucket instance-count cap. When caps.storageBuffer===false AND
1375
+ // a fold bucket carries more than FOLD_UNIFORM_INSTANCE_CAP (128)
1376
+ // instances, fire RhiError({code:'instancing-exceeds-uniform-cap'})
1377
+ // AND remove the bucket from the dispatch plan so its members fall
1378
+ // through to the per-entity drawIndexed exit (the same exit the
1379
+ // mode-gate bypass uses — D-9 "shared fallback exit"). The frame
1380
+ // stays visually correct (charter proposition 9 graceful
1381
+ // degradation: no identity-collapse / black screen) while the cap
1382
+ // event surfaces structurally for AI users (proposition 4 explicit
1383
+ // failure on .code).
1384
+ //
1385
+ // Scope discrimination: tilemap-chunk-extract-system encodes
1386
+ // Layer.value = (layerOrder<<20) | (chunkIndex & 0xfffff), so a
1387
+ // bucket whose head entry carries non-zero low-20-bits is
1388
+ // definitively a tilemap-chunk dispatch site. Plain sprite buckets
1389
+ // use SPRITE_LAYER_VALUE = layerOrder<<20 (low-20 zero) by the
1390
+ // documented convention (apps/hello/asi-world main.ts pattern).
1391
+ // The chunkIndex===0 edge case maps to 'sprite' (the helper's
1392
+ // default branch) — a one-bucket ambiguity per layerOrder that is
1393
+ // acceptable for the AI-user affordance level (the error semantics
1394
+ // — "this bucket exceeded the cap" — is the actionable signal;
1395
+ // scope=sprite vs tilemap-chunk only refines the recovery hint).
1396
+ if (foldDispatchPlan.headBuckets.size > 0 && !internals.device.caps.storageBuffer) {
1397
+ const filteredHeads = new Map<number, FoldBucket>(foldDispatchPlan.headBuckets);
1398
+ const filteredSkips = new Set<number>(foldDispatchPlan.skipIndices);
1399
+ let filteredCount = foldDispatchPlan.foldedBucketCount;
1400
+ for (const [headIdx, bucket] of foldDispatchPlan.headBuckets) {
1401
+ const scope: 'sprite' | 'tilemap-chunk' =
1402
+ (bucket.layer & 0xfffff) !== 0 ? 'tilemap-chunk' : 'sprite';
1403
+ const decision = evaluateFoldBucketUniformCap(bucket, internals.device.caps, scope);
1404
+ if (decision.fallback && decision.error !== undefined) {
1405
+ internals.errorRegistry.fire(decision.error);
1406
+ filteredHeads.delete(headIdx);
1407
+ filteredCount -= 1;
1408
+ for (let j = 1; j < bucket.entries.length; j++) {
1409
+ const memberEntry = bucket.entries[j];
1410
+ if (memberEntry === undefined) continue;
1411
+ const memberValidatedIdx = renderableToValidatedIdx.get(memberEntry.renderableIndex);
1412
+ if (memberValidatedIdx !== undefined) {
1413
+ filteredSkips.delete(memberValidatedIdx);
1414
+ }
1415
+ }
1416
+ }
1417
+ }
1418
+ if (filteredCount !== foldDispatchPlan.foldedBucketCount) {
1419
+ foldDispatchPlan = {
1420
+ headBuckets: filteredHeads,
1421
+ skipIndices: filteredSkips,
1422
+ foldedBucketCount: filteredCount,
1423
+ };
1424
+ }
1425
+ }
1426
+
1427
+ // feat-20260622-chunk-gpu-instancing-sprite-tilemap M3 / w13 (D-3 +
1428
+ // AC-06): increment `render.instancing.foldedDraws` once per fold-
1429
+ // eligible head bucket retained after the cap-fallback filter above.
1430
+ // The metric tracks instanced drawIndexed call count for this frame
1431
+ // — cap-overrun buckets routed through the per-entity fallback exit
1432
+ // are removed from `foldDispatchPlan` and therefore not counted, by
1433
+ // construction (M2 / w11 cap-fallback + plan-strategy D-3 semantics).
1434
+ // Singleton buckets (mode-bypass under D-5, or non-foldable under
1435
+ // mode 0) carry `bucketSize === 1` and never enter `headBuckets`, so
1436
+ // the per-entity drawIndexed path correctly does not count.
1437
+ // SSOT helper lives in this record owner — engine code never
1438
+ // hardcodes the metric key string.
1439
+ incrementFoldedDrawsMetric(foldDispatchPlan, internals.metrics);
1440
+ }
1441
+
1442
+ return {
1443
+ validatedOrdered,
1444
+ foldDispatchPlan,
1445
+ materialSlotIndices,
1446
+ materialSlots,
1447
+ materialSlotOwners,
1448
+ materialSlotCount,
1449
+ };
1450
+ }
1451
+
1452
+ /**
1453
+ * feat-20260704 M3/w18: per-frame cache clean-up (despawn eviction), extracted
1454
+ * verbatim from `recordFrame`.
1455
+ *
1456
+ * feat-20260531-per-frame-bind-group-cache M4 / w14 (D-5): drop per-entity BG
1457
+ * cache entries (materialBgPerEntity / instancesBgPerEntity) + instance buffers
1458
+ * whose outer-Map entityKey (packed Entity u32) is absent from the current
1459
+ * validated set, preventing unbounded growth after entity despawn. view + mesh
1460
+ * caches are frame-shared (keyed by GPU resource handle objects, WeakMap chains
1461
+ * naturally bounded) and are not touched. feat-20260619 M4 / F11: destroy the
1462
+ * GPU instance buffer before Map.delete (D-6 symmetric release); failure fires
1463
+ * errorRegistry + continues the sweep.
1464
+ *
1465
+ * @internal
1466
+ */
1467
+ function cleanPerFrameCaches(
1468
+ internals: RenderSystemInternals,
1469
+ frameState: RenderFrameState,
1470
+ validated: readonly ValidatedRenderable[],
1471
+ retainedEntityKeys: ReadonlySet<number> = new Set<number>(),
1472
+ ): void {
1473
+ if (frameState.transientInstanceBuffers !== undefined) {
1474
+ disposeTransientInstanceBuffers(frameState.transientInstanceBuffers, internals.errorRegistry);
1475
+ }
1476
+ // Build a Set<number> of worldEntityKey composites from the validated
1477
+ // renderables. D-1a #4: validatedEntityKeys are worldEntityKey(worldId, entityKey)
1478
+ // composites matching the write-side keys of #1-#3 — cross-world false eviction
1479
+ // is prevented because worldEntityKey(0, k) !== worldEntityKey(1, k).
1480
+ const validatedEntityKeys = new Set<number>(retainedEntityKeys);
1481
+ for (const v of validated) {
1482
+ validatedEntityKeys.add(worldEntityKey(v.source.worldId, v.source.entityKey));
1483
+ }
1484
+
1485
+ // Clean per-entity material BG cache: drop outer-Map entries whose
1486
+ // entityKey is absent from the current validated set. The shared and
1487
+ // singleton material caches have no entityKey and are not touched here.
1488
+ cleanPerEntityCache(frameState.materialBgPerEntity, validatedEntityKeys);
1489
+
1490
+ // Clean per-entity instances BG cache.
1491
+ cleanPerEntityCache(frameState.instancesBgPerEntity, validatedEntityKeys);
1492
+
1493
+ // D-5 retrofit: instanceBuffers clean-up. The instanceBuffers Map is
1494
+ // keyed by cacheKey (packed Entity u32, same as entityKey on
1495
+ // RenderableSnapshot). Drop entries whose key is no longer in the
1496
+ // validated set (OQ-3 / R-4). feat-20260619 M4 / F11: destroy the GPU
1497
+ // buffer before Map.delete so despawned entities release their
1498
+ // instance-buffer backing memory symmetrically (D-6).
1499
+ //
1500
+ // D-1a #1: instanceBuffers keys on the positive half (>= 0) are
1501
+ // worldEntityKey composites matching the write side. Negative half
1502
+ // fold-bucket keys (< 0) are NOT worldEntityKey; they are
1503
+ // material-handle-based and cross-world collision is semantically
1504
+ // correct (same material renders in same fold bucket).
1505
+ for (const [key, entry] of frameState.instanceBuffers.entries()) {
1506
+ if (!validatedEntityKeys.has(key)) {
1507
+ if (!entry.buffer.isDestroyed) {
1508
+ const r = entry.buffer.destroy();
1509
+ if (!r.ok) internals.errorRegistry.fire(r.error);
1510
+ }
1511
+ frameState.instanceBuffers.delete(key);
1512
+ }
1513
+ }
1514
+
1515
+ if (frameState.morphBuffers !== undefined) {
1516
+ for (const [key, entry] of frameState.morphBuffers.entries()) {
1517
+ if (validatedEntityKeys.has(key)) continue;
1518
+ if (!entry.buffer.isDestroyed) {
1519
+ const result = entry.buffer.destroy();
1520
+ if (!result.ok) internals.errorRegistry.fire(result.error);
1521
+ }
1522
+ frameState.morphBuffers.delete(key);
1523
+ }
1524
+ }
1525
+ }
1526
+
1527
+ /**
1528
+ * feat-20260704 M3/w18: record-stage fold operator linear scan, extracted
1529
+ * verbatim from `recordFrame`.
1530
+ *
1531
+ * feat-20260622-chunk-gpu-instancing-sprite-tilemap M1 / w4 + w5 (D-1, D-5):
1532
+ * groups transparent-sort-ordered dispatch entries with equal (Layer.value,
1533
+ * sortKey, materialHandle) into fold buckets. Mode-gate (D-5 extended): modes 0
1534
+ * (LAYER_Z) and 1 (LAYER_Y) fold using the pos z/y lanes; modes 2/3 bypass per-entity
1535
+ * (each entry a singleton bucket). Records `frameState.lastFoldBucketCount` =
1536
+ * fold-eligible buckets (bucketSize > 1) for the AC-06 metric. Empty dispatch
1537
+ * short-circuits (test fixtures pass null world).
1538
+ *
1539
+ * @internal
1540
+ */
1541
+ function computeFoldBuckets(
1542
+ world: World,
1543
+ frameState: RenderFrameState,
1544
+ transparentDispatch: readonly DispatchEntry[],
1545
+ renderables: readonly RenderableSnapshot[],
1546
+ ): readonly FoldBucket[] {
1547
+ if (transparentDispatch.length === 0) {
1548
+ frameState.lastFoldBucketCount = 0;
1549
+ return [];
1550
+ }
1551
+ const transparentSortCfg = getTransparentSortConfig(world);
1552
+
1553
+ // Only layer-Z / layer-Y modes can produce non-singleton fold buckets.
1554
+ // The other modes make one inert singleton per entry, which is discarded by
1555
+ // buildFoldDispatchPlan. Returning the same empty plan here avoids allocating
1556
+ // a transform matrix for every renderable when folding is disabled by mode.
1557
+ if (
1558
+ transparentSortCfg.mode !== TRANSPARENT_SORT_MODE_LAYER_Z &&
1559
+ transparentSortCfg.mode !== TRANSPARENT_SORT_MODE_LAYER_Y
1560
+ ) {
1561
+ frameState.lastFoldBucketCount = 0;
1562
+ return [];
1563
+ }
1564
+
1565
+ // `transparentDispatch` is the legacy name for the complete sorted dispatch
1566
+ // list. Opaque entries can never participate in the transparent-only fold
1567
+ // plan, yet foldDispatchBuckets must preserve its general helper contract and
1568
+ // therefore materializes an inert singleton (including a Float32Array(16))
1569
+ // for each one. Filter only at this private production call site so the fold
1570
+ // helper keeps its testable semantics while record avoids work whose result
1571
+ // is provably discarded.
1572
+ let transparentCount = 0;
1573
+ for (const entry of transparentDispatch) {
1574
+ if (renderables[entry.renderableIndex]?.material.transparent === true) {
1575
+ transparentCount += 1;
1576
+ }
1577
+ }
1578
+ if (transparentCount === 0) {
1579
+ frameState.lastFoldBucketCount = 0;
1580
+ return [];
1581
+ }
1582
+ const foldCandidates =
1583
+ transparentCount === transparentDispatch.length
1584
+ ? transparentDispatch
1585
+ : transparentDispatch.filter(
1586
+ (entry) => renderables[entry.renderableIndex]?.material.transparent === true,
1587
+ );
1588
+ const foldBuckets = foldDispatchBuckets(foldCandidates, transparentSortCfg.mode, renderables);
1589
+ // Count only fold-eligible buckets (bucketSize > 1) so the metric
1590
+ // surfaces fold actually reducing draws — singleton buckets under
1591
+ // mode bypass do not change draw count, so they do not contribute.
1592
+ let foldEligibleCount = 0;
1593
+ for (const b of foldBuckets) {
1594
+ if (b.bucketSize > 1) foldEligibleCount += 1;
1595
+ }
1596
+ frameState.lastFoldBucketCount = foldEligibleCount;
1597
+ return foldBuckets;
1598
+ }