@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,1290 @@
1
+ // @forgeax/engine-runtime - tilemap-chunk-extract-system.
2
+ //
3
+ // Walks every TileLayer attached to a Tilemap via ChildOf; when a layer is
4
+ // dirty (or has never been extracted), purges its previously-spawned derived
5
+ // per-cell entities and re-spawns one ECS entity per non-zero cell. Each
6
+ // derived entity carries Transform + MeshFilter (HANDLE_QUAD) + MeshRenderer
7
+ // + Layer + ChildOf { parent: layerEntity }. The ChildOf edge lets
8
+ // `world.despawn(tilemapEntity)` cascade-despawn the entire subtree
9
+ // (Tilemap -> TileLayer -> derived render entities) via the engine's
10
+ // default `linkedSpawn: true` (tweak-20260714 M2, requirements AC-02/03/04).
11
+ //
12
+ // M0 baseline: unit-cell 1x1 form, single-atlas, no UV inset.
13
+ // M2 extension (plan-strategy §D-2 + §D-7 step 3):
14
+ // M3 extension (plan-strategy §D-7 step 2 + §D-12):
15
+ // - resolveTilesetMaterial walks the 3-hop chain
16
+ // `tile.regionIndex -> regions[i].atlasIndex ?? 0 -> atlases[idx]`
17
+ // so multi-atlas tilesets route each region to the correct GPU
18
+ // texture handle (requirements AC-04 / AC-11). Cache key stays
19
+ // binary `(atlasHandle, regionIndex)` -- atlasHandle already
20
+ // encodes the atlas pick.
21
+ // - spawnDerivedRenderEntities scales the quad by widthCells x
22
+ // heightCells (defaults 1 x 1) and offsets the centre so the pivot
23
+ // lands at the (pivotX, pivotY) location inside the anchor cell.
24
+ // - basePivotForX = D ? pivotY : pivotX (and dually for Y), so the
25
+ // 90deg CW z-rotation correctly swaps which atlas-axis pivot maps
26
+ // to which world axis.
27
+ // - effectivePivotX = H ? (1 - basePivotForX) : basePivotForX (and
28
+ // dually for Y). The anchor pivot under H flip lands at the
29
+ // mirrored cell position (charter P4 - same semantics as Tiled's
30
+ // "pivot stays in atlas-local coords, flips with the texel grid").
31
+ // - posX = (cellX + effectivePivotX + (0.5 - effectivePivotX) * widthCells)
32
+ // * tileSizeX
33
+ // posY analogous with effectivePivotY + heightCells.
34
+ // - scaleX/scaleY: signed by H/V flip; magnitude is widthCells *
35
+ // tileSizeX / heightCells * tileSizeY (multi-cell scale).
36
+ // - resolveTilesetMaterial inset-shrinks the region UV rectangle by
37
+ // half a texel on every edge so GPU bilinear filtering at the
38
+ // atlas-tile boundary never bleeds into the adjacent tile
39
+ // (charter P3 - default behaviour avoids visual defects).
40
+ //
41
+ // M3 boundary: per-entity sort key with effectivePivotY is wired in
42
+ // render-system-extract (m3-t5), not here.
43
+ //
44
+ // Cache key for resolveTilesetMaterial is BINARY: (atlasHandle, regionIndex).
45
+ // AI users widthCells / pivot variations share the same material handle
46
+ // (charter P4 consistent abstraction; plan-strategy §D-9 / §D-12).
47
+ //
48
+ // Anchors: plan-tasks m0-t10 / m2-t2 / m2-t4; plan-strategy §D-1 +
49
+ // §D-2 (multi-cell + flip x pivot) + §D-5 (M0 baseline file-by-file
50
+ // ECS API adaptation) + §D-7 step 3 (half-texel UV inset).
51
+
52
+ import {
53
+ HANDLE_QUAD,
54
+ resolveTilesetRuntime,
55
+ type TilesetAtlasLookup,
56
+ } from '@forgeax/engine-assets-runtime';
57
+ import type { EntityHandle, World } from '@forgeax/engine-ecs';
58
+ import { decodeTileBits } from '@forgeax/engine-graphics-extras';
59
+ import { type box3, frustum, mat4 } from '@forgeax/engine-math';
60
+ import { ChildOf, Children, Transform } from '@forgeax/engine-scene';
61
+ import {
62
+ type Handle,
63
+ type MaterialAsset,
64
+ type TilesetAsset,
65
+ type TilesetRegion,
66
+ type TilesetTileEntry,
67
+ toShared,
68
+ unwrapHandle,
69
+ } from '@forgeax/engine-types';
70
+ import {
71
+ CAMERA_PROJECTION_ORTHOGRAPHIC,
72
+ Camera,
73
+ decodeSortScope,
74
+ Layer,
75
+ MeshFilter,
76
+ MeshRenderer,
77
+ type SortScope,
78
+ SpriteInstances,
79
+ TileLayer,
80
+ Tilemap,
81
+ } from './components';
82
+ import { SPRITE_PREMULTIPLIED_ALPHA_BLEND } from './materials';
83
+
84
+ // Module-scoped caches (charter P5 — engine-side memoisation; AI users
85
+ // never reach in). Test harness can flush them via the reset helpers.
86
+ //
87
+ // Two material caches coexist because the extract system produces two
88
+ // kinds of derived entities, each with a different material-key granularity:
89
+ //
90
+ // 1. `atlasMaterialCache` — key `${atlasId}|${regionIndex}` → per-region
91
+ // material with the UV rectangle baked into `values.region`.
92
+ // Used by the per-cell entity path (sortScope='per-cell' object
93
+ // layers). Each tile graphic gets a distinct MaterialAsset.
94
+ //
95
+ // 2. `atlasOnlyMaterialCache` — key `${atlasId}` → per-atlas material
96
+ // with `values.region: [0,0,1,1]` placeholder. Used by the
97
+ // SpriteInstances batched path (sortScope='layer' terrain layers).
98
+ // The shader's PER_INSTANCE_REGION=true variant reads per-instance
99
+ // UV from the instance buffer, not from the material UBO, so the
100
+ // placeholder is ignored at draw time.
101
+ let atlasMaterialCache = new WeakMap<World, Map<string, Handle<'MaterialAsset', 'shared'>>>();
102
+ let atlasOnlyMaterialCache = new WeakMap<World, Map<string, Handle<'MaterialAsset', 'shared'>>>();
103
+ // tweak-20260714 M3 (plan-strategy §2 D-3): the terrain (sortScope='layer')
104
+ // derived-entity tracker Map + first-frame-heuristic Set that used to live
105
+ // alongside these material caches were retired once every derived entity
106
+ // was attached as `ChildOf` child of its TileLayer (M2). The reverse
107
+ // mirror `Children.entities` on the TileLayer is now the SSOT for
108
+ // "which derived entities belong to this layer" — engine-maintained on
109
+ // `world.addComponent(ChildOf) / world.despawn`, deriving-not-duplicating
110
+ // (architecture-principles §2). `purgeDerivedEntities` reads that mirror
111
+ // directly; the "already built" gate uses `Children.entities.length > 0`,
112
+ // which naturally handles both first frames (empty ⇒ rebuild) and steady
113
+ // state (populated ⇒ skip) without a second parallel ledger.
114
+
115
+ // ─── Chunk-streaming state for sortScope='per-cell' (object) layers ───────
116
+ //
117
+ // Object layers stream per-chunk: only chunks whose world AABB intersects the
118
+ // camera frustum have per-cell entities alive in the ECS world. This keeps
119
+ // extractFrame's entity count proportional to visible tile count rather than
120
+ // total map tile count (charter P5: engine-side memoisation / streaming).
121
+ //
122
+ // Design: one WeakMap entry per World owns three bounded maps:
123
+ // layers : layerKey → pre-bucketed specs by chunkIndex (built once
124
+ // from bucketTileLayer, rebuilt on dirty). Avoids re-reading
125
+ // the full tileset and re-materialising every frame.
126
+ // chunkEntities : "${layerKey}:${chunkIdx}" → spawned entity ids
127
+ // for that chunk (purged when chunk leaves frustum).
128
+ // activeChunks : layerKey → Set<chunkIdx> currently spawned.
129
+ //
130
+ // bug-20260703-tilemap-chunk-stale-frustum-and-cull-overhang (D2): each entry
131
+ // stores the ACTUAL world-space bounding box (union of every tile's post-TRS
132
+ // footprint from `computeTileTrs`) rather than a tile-aligned grid box.
133
+ // Multi-cell tiles (`widthCells` / `heightCells > 1`) and non-central pivots
134
+ // let a tile extend beyond its anchor chunk's grid; the grid box would
135
+ // despawn the anchor chunk while overhanging tiles were still on screen,
136
+ // causing edge-of-chunk flicker as the camera pans. Bounds are computed once
137
+ // per (dirty | first-time) rebuild, so the per-frame visibility test stays a
138
+ // single `frustum.intersectsBox` call (plan-strategy §2 D-3).
139
+ interface ChunkStreamEntry {
140
+ readonly specs: readonly DerivedSpawnSpec[];
141
+ readonly bounds: box3.Box3Like;
142
+ }
143
+ interface StreamLayerCache {
144
+ readonly byChunk: ReadonlyMap<number, ChunkStreamEntry>;
145
+ readonly tilemap: {
146
+ readonly cols: number;
147
+ readonly rows: number;
148
+ readonly tileSize: ArrayLike<number>;
149
+ readonly chunkSize: number;
150
+ };
151
+ readonly layerOrder: number;
152
+ readonly sortScope: SortScope;
153
+ }
154
+ interface PerWorldStreamingCache {
155
+ readonly layers: Map<string, StreamLayerCache>;
156
+ readonly chunkEntities: Map<string, EntityHandle[]>;
157
+ readonly activeChunks: Map<string, Set<number>>;
158
+ }
159
+
160
+ let streamingCacheByWorld = new WeakMap<World, PerWorldStreamingCache>();
161
+
162
+ function streamingCache(world: World): PerWorldStreamingCache {
163
+ let cache = streamingCacheByWorld.get(world);
164
+ if (cache === undefined) {
165
+ cache = {
166
+ layers: new Map(),
167
+ chunkEntities: new Map(),
168
+ activeChunks: new Map(),
169
+ };
170
+ streamingCacheByWorld.set(world, cache);
171
+ }
172
+ return cache;
173
+ }
174
+
175
+ /**
176
+ * Flush both material caches. Useful in test harnesses + after a
177
+ * TilesetAsset reload.
178
+ */
179
+ export function resetTilemapChunkExtractCache(): void {
180
+ atlasMaterialCache = new WeakMap<World, Map<string, Handle<'MaterialAsset', 'shared'>>>();
181
+ atlasOnlyMaterialCache = new WeakMap<World, Map<string, Handle<'MaterialAsset', 'shared'>>>();
182
+ }
183
+
184
+ /**
185
+ * Flush the per-cell streaming caches. Useful in test harnesses + when
186
+ * the World is re-created.
187
+ *
188
+ * Signature preserved (AC-08 hard constraint) after tweak-20260714 M3
189
+ * retired the terrain-side tracker Map + first-frame heuristic Set:
190
+ * terrain layers now derive their "already-built" state from
191
+ * `Children.entities` on the TileLayer (mirror of ChildOf, engine-
192
+ * maintained), so the tracker only needs to clear the 3 streaming
193
+ * caches (plan-strategy §2 D-3 + §2 D-5).
194
+ */
195
+ export function resetTilemapDerivedEntityTracker(): void {
196
+ streamingCacheByWorld = new WeakMap<World, PerWorldStreamingCache>();
197
+ }
198
+
199
+ /**
200
+ * @internal Test-only helper for AC-11 (tweak-20260714 M4). Returns the
201
+ * union of `layerKey`s currently referenced by the three per-cell
202
+ * streaming caches. Post-diff-cleanup, an evicted layerKey MUST NOT
203
+ * appear here; a slot subsequently reused for a fresh TileLayer sees
204
+ * empty caches and rebuilds from zero (plan-strategy §2 D-4).
205
+ *
206
+ * Parsing the per-World `chunkEntities` key format `${entity}:${chunkIdx}`
207
+ * is intentional so callers observe cleanup on all three maps without
208
+ * depending on the invariant `activeSet ↔ chunkEntities
209
+ * keys are paired` — the test then also cross-checks that invariant.
210
+ */
211
+ export function _peekPerCellStreamingLayerKeys(world: World): readonly number[] {
212
+ const cache = streamingCacheByWorld.get(world);
213
+ if (cache === undefined) return [];
214
+ const out = new Set<number>();
215
+ for (const layerKey of cache.layers.keys()) {
216
+ if (Number.isFinite(Number(layerKey))) out.add(Number(layerKey));
217
+ }
218
+ for (const layerKey of cache.activeChunks.keys()) {
219
+ if (Number.isFinite(Number(layerKey))) out.add(Number(layerKey));
220
+ }
221
+ for (const key of cache.chunkEntities.keys()) {
222
+ const parts = key.split(':');
223
+ const entity = parts[0];
224
+ if (entity !== undefined && Number.isFinite(Number(entity))) out.add(Number(entity));
225
+ }
226
+ return Array.from(out);
227
+ }
228
+
229
+ /**
230
+ * Compute the per-layer / per-chunk packed value carried in `Layer.value`
231
+ * on derived entities. `sortScope` is the closed string union from
232
+ * `TileLayer.sortScope`:
233
+ * - `'layer'` (default): `(layerOrder << 20) | (chunkIndex & 0xFFFFF)` —
234
+ * terrain semantics, layerOrder dominates with chunkIndex
235
+ * tiebreak within the layer.
236
+ * - `'per-cell'`: returns `(layerOrder << 20)` (chunkIndex folded to 0)
237
+ * so every derived entity in the layer shares one
238
+ * Layer.value and can Y-interleave with sprite entities
239
+ * carrying the same value (e.g. a player sprite riding
240
+ * `SPRITE_LAYER_VALUE = layerOrder << 20`).
241
+ *
242
+ * Round-2 rename (D-V-3): the third arg was `ySort: boolean` before the
243
+ * sortScope union landed; it is now the closed `SortScope` union with
244
+ * default `'layer'`. The 0x200000 / chunked-bits semantics for the two
245
+ * arms are preserved exactly so existing pixel-parity baselines stay
246
+ * stable (only the AI-user surface widens to a self-documenting literal).
247
+ */
248
+ export function encodeTilemapLayerValue(
249
+ layerOrder: number,
250
+ chunkIndex: number,
251
+ sortScope: SortScope = 'layer',
252
+ ): number {
253
+ if (sortScope === 'per-cell') return (layerOrder << 20) | 0;
254
+ return (layerOrder << 20) | (chunkIndex & 0xfffff) | 0;
255
+ }
256
+
257
+ /**
258
+ * Resolve (regionIndex) into a per-tile sprite material handle by walking
259
+ * the 3-hop chain:
260
+ *
261
+ * tile.regionIndex -> regions[regionIndex] -> region.atlasIndex ?? 0
262
+ * -> atlases[atlasIndex]
263
+ *
264
+ * (plan-strategy §D-7 step 2 + requirements §AC-04 / §AC-11). Caches the
265
+ * resulting MaterialAsset handle so repeated lookups hit a single
266
+ * registered material per atlas-region pair; the cache key stays the
267
+ * binary tuple `(atlasHandle, regionIndex)` -- atlasHandle already
268
+ * encodes the atlasIndex pick so widening to a 3-tuple key would only
269
+ * inflate the SSOT without adding signal (charter P4 + plan-strategy
270
+ * §D-12). atlasIndex out-of-range is caught at register time by
271
+ * `validateTilesetPayload` (m1-t6); the runtime resolver falls through
272
+ * to handle 0 when an atlas slot is unexpectedly empty so the renderer
273
+ * skips the draw rather than silently sampling the wrong texture
274
+ * (charter P3 fail-safe).
275
+ *
276
+ * The shader is `forgeax::sprite`; values are filled with the atlas
277
+ * texture handle + a UV region rectangle covering the supplied
278
+ * TilesetRegion (half-texel inset added in m2-t4).
279
+ */
280
+ function resolveTilesetMaterial(
281
+ world: World,
282
+ tileset: TilesetAsset,
283
+ regionIndex: number,
284
+ lookup: (guid: string) => TilesetAtlasLookup,
285
+ ): Handle<'MaterialAsset', 'shared'> {
286
+ const region = tileset.regions[regionIndex];
287
+ if (region === undefined) return toShared<'MaterialAsset'>(0);
288
+ // 3-hop walk: regions[i].atlasIndex (default 0) -> atlases[atlasIndex].
289
+ // atlasIndex out-of-range is register-time fail-fast via
290
+ // `validateTilesetPayload` (m1-t6); the runtime resolver still bails
291
+ // when the slot is unexpectedly empty (charter P3 fail-safe).
292
+ const atlasIndex = region.atlasIndex ?? 0;
293
+ const atlasGuid = tileset.atlases[atlasIndex];
294
+ if (atlasGuid === undefined) return toShared<'MaterialAsset'>(0);
295
+ const atlas = resolveTilesetRuntime(world, atlasGuid, lookup);
296
+ if (!atlas.ok) return toShared<'MaterialAsset'>(0);
297
+ const atlasHandle = atlas.value.handle;
298
+ const atlasId = unwrapHandle(atlasHandle);
299
+ const cacheKey = `${atlasId}|${regionIndex}`;
300
+ const cache =
301
+ atlasMaterialCache.get(world) ?? new Map<string, Handle<'MaterialAsset', 'shared'>>();
302
+ atlasMaterialCache.set(world, cache);
303
+ const cached = cache.get(cacheKey);
304
+ if (cached !== undefined) return cached;
305
+
306
+ // Atlas-space rectangle -> normalised UV. M2 adds a half-texel inset
307
+ // on every edge (plan-strategy §D-7 step 3) so GPU bilinear filtering
308
+ // at the region boundary never samples the adjacent atlas tile. Atlas
309
+ // pixel extent comes from atlasSizes[atlasIndex] when present (exact
310
+ // per-atlas pixel dimensions); falls back to columns * tileWidth for
311
+ // single-atlas or legacy callers.
312
+ const atlasSize = tileset.atlasSizes?.[atlasIndex];
313
+ const atlasWidth = Math.max(
314
+ 1,
315
+ atlasSize !== undefined ? atlasSize.pixelWidth : tileset.columns * tileset.tileWidth,
316
+ );
317
+ const atlasHeight = Math.max(
318
+ 1,
319
+ atlasSize !== undefined ? atlasSize.pixelHeight : tileset.rows * tileset.tileHeight,
320
+ );
321
+ const halfTexelU = 0.5 / atlasWidth;
322
+ const halfTexelV = 0.5 / atlasHeight;
323
+ const u = region.x / atlasWidth + halfTexelU;
324
+ const v = region.y / atlasHeight + halfTexelV;
325
+ const w = region.width / atlasWidth - 2 * halfTexelU;
326
+ const h = region.height / atlasHeight - 2 * halfTexelV;
327
+
328
+ const matPayload: MaterialAsset = {
329
+ kind: 'material',
330
+ passes: [
331
+ {
332
+ name: 'Forward',
333
+ program: { module: 'forgeax::sprite' },
334
+ renderState: {
335
+ ...{ blend: SPRITE_PREMULTIPLIED_ALPHA_BLEND },
336
+ tags: { LightMode: 'Forward' },
337
+ queue: 3000,
338
+ },
339
+ },
340
+ ],
341
+ values: {
342
+ colorTint: [1.0, 1.0, 1.0, 1.0],
343
+ baseColorTexture: atlasHandle,
344
+ region: [u, v, w, h],
345
+ pivotAndSize: [0.5, 0.5, 1.0, 1.0],
346
+ flipY: 1.0,
347
+ },
348
+ };
349
+ const matHandle = world.allocSharedRef<'MaterialAsset', MaterialAsset>(
350
+ 'MaterialAsset',
351
+ matPayload,
352
+ );
353
+ cache.set(cacheKey, matHandle);
354
+ return matHandle;
355
+ }
356
+
357
+ /**
358
+ * Resolve an atlas-only sprite material (no baked-in UV region). One
359
+ * material per atlas — used by the SpriteInstances batched draw path
360
+ * (sortScope='layer'). The per-instance UV region rectangle lives in the
361
+ * `SpriteInstances.regions` buffer instead of in the material UBO.
362
+ *
363
+ * `values.region` is a `[0, 0, 1, 1]` placeholder. The sprite shader's
364
+ * `PER_INSTANCE_REGION=true` variant reads region from
365
+ * `instances[idx].region` and ignores the material slot; selecting that
366
+ * variant is the record stage's responsibility (see
367
+ * `render-system-record.ts` sprite pass pipeline selection).
368
+ *
369
+ * Returns the registered MaterialAsset slot id (unwrapped Handle u32) or
370
+ * 0 if the atlas slot is empty (charter P3 fail-safe; mirrors
371
+ * `resolveTilesetMaterial`).
372
+ */
373
+ function resolveAtlasOnlyMaterial(
374
+ world: World,
375
+ tileset: TilesetAsset,
376
+ atlasIndex: number,
377
+ lookup: (guid: string) => TilesetAtlasLookup,
378
+ ): Handle<'MaterialAsset', 'shared'> {
379
+ const atlasGuid = tileset.atlases[atlasIndex];
380
+ if (atlasGuid === undefined) return toShared<'MaterialAsset'>(0);
381
+ const atlas = resolveTilesetRuntime(world, atlasGuid, lookup);
382
+ if (!atlas.ok) return toShared<'MaterialAsset'>(0);
383
+ const atlasHandle = atlas.value.handle;
384
+ const atlasId = unwrapHandle(atlasHandle);
385
+ const cacheKey = String(atlasId);
386
+ const cache =
387
+ atlasOnlyMaterialCache.get(world) ?? new Map<string, Handle<'MaterialAsset', 'shared'>>();
388
+ atlasOnlyMaterialCache.set(world, cache);
389
+ const cached = cache.get(cacheKey);
390
+ if (cached !== undefined) return cached;
391
+
392
+ const matPayload: MaterialAsset = {
393
+ kind: 'material',
394
+ passes: [
395
+ {
396
+ name: 'Forward',
397
+ program: { module: 'forgeax::sprite' },
398
+ renderState: {
399
+ ...{ blend: SPRITE_PREMULTIPLIED_ALPHA_BLEND },
400
+ tags: { LightMode: 'Forward' },
401
+ queue: 3000,
402
+ },
403
+ },
404
+ ],
405
+ values: {
406
+ colorTint: [1.0, 1.0, 1.0, 1.0],
407
+ baseColorTexture: atlasHandle,
408
+ region: [0.0, 0.0, 1.0, 1.0],
409
+ pivotAndSize: [0.5, 0.5, 1.0, 1.0],
410
+ flipY: 1.0,
411
+ },
412
+ };
413
+ const matHandle = world.allocSharedRef<'MaterialAsset', MaterialAsset>(
414
+ 'MaterialAsset',
415
+ matPayload,
416
+ );
417
+ cache.set(cacheKey, matHandle);
418
+ return matHandle;
419
+ }
420
+
421
+ /**
422
+ * Compute the half-texel-inset normalised UV rectangle for a TilesetRegion
423
+ * (atlas-space pixels → atlas-normalised [u, v, w, h]). Mirrors the inset
424
+ * computation inside `resolveTilesetMaterial` so the per-instance region
425
+ * data fed into `SpriteInstances.regions` produces pixel-identical sampling
426
+ * to the per-region material path (charter P4 consistent abstraction).
427
+ */
428
+ function computeRegionUv(
429
+ tileset: TilesetAsset,
430
+ region: TilesetRegion,
431
+ atlasIndex: number,
432
+ ): [u: number, v: number, w: number, h: number] {
433
+ const atlasSize = tileset.atlasSizes?.[atlasIndex];
434
+ const atlasWidth = Math.max(
435
+ 1,
436
+ atlasSize !== undefined ? atlasSize.pixelWidth : tileset.columns * tileset.tileWidth,
437
+ );
438
+ const atlasHeight = Math.max(
439
+ 1,
440
+ atlasSize !== undefined ? atlasSize.pixelHeight : tileset.rows * tileset.tileHeight,
441
+ );
442
+ const halfTexelU = 0.5 / atlasWidth;
443
+ const halfTexelV = 0.5 / atlasHeight;
444
+ return [
445
+ region.x / atlasWidth + halfTexelU,
446
+ region.y / atlasHeight + halfTexelV,
447
+ region.width / atlasWidth - 2 * halfTexelU,
448
+ region.height / atlasHeight - 2 * halfTexelV,
449
+ ];
450
+ }
451
+
452
+ interface DerivedSpawnSpec {
453
+ readonly cellX: number;
454
+ readonly cellY: number;
455
+ readonly tileId: number;
456
+ readonly packedTile: number;
457
+ readonly materialHandle: Handle<'MaterialAsset', 'shared'>;
458
+ readonly chunkIndex: number;
459
+ // M2: multi-cell footprint + custom pivot land per-tile via TilesetTileEntry.
460
+ readonly widthCells: number;
461
+ readonly heightCells: number;
462
+ readonly pivotX: number;
463
+ readonly pivotY: number;
464
+ // Atlas + region indices retained so the SpriteInstances (sortScope=
465
+ // 'layer') path can group cells by atlas and look up per-instance UV
466
+ // without re-walking the 3-hop chain `tileId → regions → atlasIndex`.
467
+ readonly atlasIndex: number;
468
+ readonly regionIndex: number;
469
+ }
470
+
471
+ const SQRT1_2 = Math.SQRT1_2;
472
+
473
+ /**
474
+ * Compute the post-flip "effective pivot Y" for a tilemap tile entry.
475
+ *
476
+ * Mirrors the D-2 first-line composition used by `spawnDerivedRenderEntities`:
477
+ *
478
+ * basePivotForY = flipDiagonal ? pivotX : pivotY
479
+ * effectivePivotY = flipV ? (1 - basePivotForY) : basePivotForY
480
+ *
481
+ * Exported so the render-system-extract sprite-bucket sort key path can
482
+ * reproduce the *same* per-entity pivot value that landed on
483
+ * `Transform.posY` -- otherwise the foot-Y formula
484
+ * `posY - effectivePivotY * |scaleY|` would drift on flipped tiles
485
+ * (charter P4 single-source pivot SSOT; plan-strategy §D-1 + §D-2 +
486
+ * requirements §AC-12 / §AC-13).
487
+ */
488
+ export function effectivePivotYForTilemapFlip(
489
+ pivotY: number,
490
+ pivotX: number,
491
+ flipV: boolean,
492
+ flipDiagonal: boolean,
493
+ ): number {
494
+ const base = flipDiagonal ? pivotX : pivotY;
495
+ return flipV ? 1 - base : base;
496
+ }
497
+
498
+ /**
499
+ * Compute the per-cell derived spawn spec for one non-zero cell. Carries
500
+ * widthCells / heightCells / pivotX / pivotY from the TilesetTileEntry so
501
+ * `spawnDerivedRenderEntities` can apply the D-2 geometric correction
502
+ * without re-reading the asset registry.
503
+ */
504
+ function specFor(
505
+ layerCols: number,
506
+ chunkSize: number,
507
+ cellIndex: number,
508
+ packedTile: number,
509
+ materialHandle: Handle<'MaterialAsset', 'shared'>,
510
+ entry: TilesetTileEntry,
511
+ atlasIndex: number,
512
+ ): DerivedSpawnSpec {
513
+ const cellX = cellIndex % layerCols;
514
+ const cellY = Math.floor(cellIndex / layerCols);
515
+ const chunkX = Math.floor(cellX / chunkSize);
516
+ const chunkY = Math.floor(cellY / chunkSize);
517
+ const chunksPerRow = Math.max(1, Math.ceil(layerCols / chunkSize));
518
+ const chunkIndex = chunkY * chunksPerRow + chunkX;
519
+ const { tileId } = decodeTileBits(packedTile);
520
+ return {
521
+ cellX,
522
+ cellY,
523
+ tileId,
524
+ packedTile,
525
+ materialHandle,
526
+ chunkIndex,
527
+ widthCells: entry.widthCells ?? 1,
528
+ heightCells: entry.heightCells ?? 1,
529
+ pivotX: entry.pivotX ?? 0.5,
530
+ pivotY: entry.pivotY ?? 0.5,
531
+ atlasIndex,
532
+ regionIndex: entry.regionIndex,
533
+ };
534
+ }
535
+
536
+ /**
537
+ * Compute the post-flip TRS components for one tile cell. The plan-strategy
538
+ * §D-2 multi-cell + flip x pivot composite formula (first-line form):
539
+ *
540
+ * basePivotForX = D ? pivotY : pivotX
541
+ * basePivotForY = D ? pivotX : pivotY
542
+ * effectivePivotX = H ? (1 - basePivotForX) : basePivotForX
543
+ * effectivePivotY = V ? (1 - basePivotForY) : basePivotForY
544
+ * posX = (cellX + effectivePivotX + (0.5 - effectivePivotX) * widthCells)
545
+ * * tileSizeX
546
+ * posY = (cellY + effectivePivotY + (0.5 - effectivePivotY) * heightCells)
547
+ * * tileSizeY
548
+ * scaleX = (H ? -1 : 1) * widthCells * tileSizeX
549
+ * scaleY = (V ? -1 : 1) * heightCells * tileSizeY
550
+ * quatZ = D ? Math.SQRT1_2 : 0
551
+ * quatW = D ? Math.SQRT1_2 : 1
552
+ *
553
+ * Single SSOT consumed by both the per-cell entity spawn path and the
554
+ * SpriteInstances batched path so the two routes produce pixel-identical
555
+ * world transforms (charter P4 consistent abstraction).
556
+ */
557
+ function computeTileTrs(
558
+ tilemap: { tileSize: ArrayLike<number> },
559
+ spec: DerivedSpawnSpec,
560
+ packedTile: number,
561
+ ): {
562
+ posX: number;
563
+ posY: number;
564
+ scaleX: number;
565
+ scaleY: number;
566
+ quatZ: number;
567
+ quatW: number;
568
+ } {
569
+ const { flipH, flipV, flipDiagonal } = decodeTileBits(packedTile);
570
+ const basePivotForX = flipDiagonal ? spec.pivotY : spec.pivotX;
571
+ const effectivePivotX = flipH ? 1 - basePivotForX : basePivotForX;
572
+ const effectivePivotY = effectivePivotYForTilemapFlip(
573
+ spec.pivotY,
574
+ spec.pivotX,
575
+ flipV,
576
+ flipDiagonal,
577
+ );
578
+ const tileSizeX = tilemap.tileSize[0] ?? 1;
579
+ const tileSizeY = tilemap.tileSize[1] ?? 1;
580
+ return {
581
+ posX: (spec.cellX + effectivePivotX + (0.5 - effectivePivotX) * spec.widthCells) * tileSizeX,
582
+ posY: (spec.cellY + effectivePivotY + (0.5 - effectivePivotY) * spec.heightCells) * tileSizeY,
583
+ scaleX: (flipH ? -1 : 1) * spec.widthCells * tileSizeX,
584
+ scaleY: (flipV ? -1 : 1) * spec.heightCells * tileSizeY,
585
+ quatZ: flipDiagonal ? SQRT1_2 : 0,
586
+ quatW: flipDiagonal ? SQRT1_2 : 1,
587
+ };
588
+ }
589
+
590
+ /**
591
+ * Spawn a single derived per-cell render entity. Used by the per-cell
592
+ * sortScope path (`sortScope='per-cell'`, object layers) where each cell
593
+ * needs an independent Y-sort position to interleave with sprite entities
594
+ * (e.g. player) at arbitrary Y positions.
595
+ *
596
+ * `layerEntity` becomes the derived entity's `ChildOf.parent`, so
597
+ * `world.despawn(layerEntity)` cascade-despawns every derived cell entity
598
+ * via the engine's `linkedSpawn: true` default (feat-20260616). Combined
599
+ * with the Tilemap -> TileLayer ChildOf edge, a single
600
+ * `world.despawn(tilemapEntity)` unwinds the entire subtree without a
601
+ * bespoke tilemap-scoped cleanup pass (requirements AC-02 / AC-03).
602
+ */
603
+ function spawnDerivedRenderEntities(
604
+ world: World,
605
+ tilemap: { tileSize: ArrayLike<number> },
606
+ layerEntity: EntityHandle,
607
+ layerOrder: number,
608
+ spec: DerivedSpawnSpec,
609
+ packedTile: number,
610
+ sortScope: SortScope = 'layer',
611
+ ): EntityHandle {
612
+ const { posX, posY, scaleX, scaleY, quatZ, quatW } = computeTileTrs(tilemap, spec, packedTile);
613
+ const layerValue = encodeTilemapLayerValue(layerOrder, spec.chunkIndex, sortScope);
614
+ return world
615
+ .spawn(
616
+ {
617
+ component: Transform,
618
+ data: {
619
+ pos: [posX, posY, 0],
620
+ quat: [0, 0, quatZ, quatW],
621
+ scale: [scaleX, scaleY, 1],
622
+ },
623
+ },
624
+ { component: MeshFilter, data: { assetHandle: HANDLE_QUAD } },
625
+ {
626
+ component: MeshRenderer,
627
+ data: {
628
+ materials: [spec.materialHandle],
629
+ },
630
+ },
631
+ { component: Layer, data: { value: layerValue } },
632
+ { component: ChildOf, data: { parent: layerEntity } },
633
+ )
634
+ .unwrap();
635
+ }
636
+
637
+ /**
638
+ * Spawn one batched SpriteInstances entity per (TileLayer, chunk, atlas)
639
+ * group. Used by the `sortScope='layer'` path (terrain layers): every
640
+ * cell in the group becomes one instance in the same drawIndexed call.
641
+ *
642
+ * The chunk grid is the unit declared by `Tilemap.chunkSize` (default
643
+ * 16x16). One SpriteInstances entity per (chunk, atlas) pair keeps each
644
+ * GPU instance buffer small (256 cells x 80B = 20KB worst case) and lets
645
+ * downstream frustum culling / incremental rebuild operate at chunk
646
+ * granularity (industry standard: Godot quadrant_size, Bevy ChunkSize,
647
+ * Tiled TMX chunks).
648
+ *
649
+ * Layout:
650
+ * - Transform: identity (the per-instance mat4 carries world-space TRS).
651
+ * - MeshFilter: HANDLE_QUAD.
652
+ * - MeshRenderer: atlas-only sprite material (region placeholder; shader
653
+ * reads per-instance UV from SpriteInstances.regions under the
654
+ * PER_INSTANCE_REGION=true variant — selected at record stage).
655
+ * - SpriteInstances: { transforms: Float32Array(N*16),
656
+ * regions: Float32Array(N*4) }.
657
+ * - Layer: encodeTilemapLayerValue(layerOrder, chunkIndex, 'layer').
658
+ * chunkIndex distinguishes adjacent chunks of the same layer in the
659
+ * transparent-sort key so the back-to-front ordering between chunks
660
+ * remains stable (chunks within a layer don't visually overlap in
661
+ * normal tilemap scenes; the chunkIndex tiebreaker is byte-exact
662
+ * with the per-cell path's encoding).
663
+ *
664
+ * Returns the spawned EntityHandle. Empty groups (zero cells) short-circuit
665
+ * and return `undefined`.
666
+ */
667
+ function spawnSpriteInstancesGroup(
668
+ world: World,
669
+ tilemap: { cols: number; tileSize: ArrayLike<number>; chunkSize: number },
670
+ tileset: TilesetAsset,
671
+ layerEntity: EntityHandle,
672
+ layerOrder: number,
673
+ chunkIndex: number,
674
+ atlasIndex: number,
675
+ materialHandle: Handle<'MaterialAsset', 'shared'>,
676
+ cellSpecs: readonly DerivedSpawnSpec[],
677
+ ): EntityHandle | undefined {
678
+ if (cellSpecs.length === 0) return undefined;
679
+ const N = cellSpecs.length;
680
+ const transforms = new Float32Array(N * 16);
681
+ const regions = new Float32Array(N * 4);
682
+ const tmpMat = mat4.create();
683
+ const tmpT: [number, number, number] = [0, 0, 0];
684
+ const tmpR: [number, number, number, number] = [0, 0, 0, 1];
685
+ const tmpS: [number, number, number] = [1, 1, 1];
686
+
687
+ // Per-chunk frustum cull: entity Transform = chunk world center + chunk
688
+ // extents (scaleX/Y). The frustum culler reads the entity AABB (HANDLE_QUAD
689
+ // local [-0.5, 0.5]² expanded by entity scale/pos) → world AABB exactly
690
+ // covers the chunk footprint.
691
+ //
692
+ // Per-instance transforms become chunk-local: inv_chunk * world_tile.
693
+ // inv_chunk is a pure TRS-inverse (no rotation), so for each column c of
694
+ // the world mat W:
695
+ // local[c*4+0] = (W[c*4+0] - chunkCenterX * W[c*4+3]) * invSX
696
+ // local[c*4+1] = (W[c*4+1] - chunkCenterY * W[c*4+3]) * invSY
697
+ // local[c*4+2] = W[c*4+2]
698
+ // local[c*4+3] = W[c*4+3]
699
+ // The sprite shader then produces:
700
+ // world = chunk_TRS * local_tile * pos_local
701
+ // = chunk_TRS * (inv_chunk * world_tile) * pos_local
702
+ // = world_tile * pos_local ✓ (pixel-identical to identity path)
703
+ //
704
+ // Tiles whose widthCells/heightCells straddle a chunk boundary will have
705
+ // instance transforms that exceed the chunk AABB; the entity is still drawn
706
+ // (conservative cull only skips when the chunk AABB is fully outside).
707
+ const chunksPerRow = Math.max(1, Math.ceil(tilemap.cols / tilemap.chunkSize));
708
+ const chunkX = chunkIndex % chunksPerRow;
709
+ const chunkY = Math.floor(chunkIndex / chunksPerRow);
710
+ const chunkScaleX = tilemap.chunkSize * (tilemap.tileSize[0] ?? 1);
711
+ const chunkScaleY = tilemap.chunkSize * (tilemap.tileSize[1] ?? 1);
712
+ const chunkCenterX = (chunkX + 0.5) * chunkScaleX;
713
+ const chunkCenterY = (chunkY + 0.5) * chunkScaleY;
714
+ const invSX = chunkScaleX > 0 ? 1 / chunkScaleX : 1;
715
+ const invSY = chunkScaleY > 0 ? 1 / chunkScaleY : 1;
716
+
717
+ for (let i = 0; i < N; i++) {
718
+ const spec = cellSpecs[i] as DerivedSpawnSpec;
719
+ const { posX, posY, scaleX, scaleY, quatZ, quatW } = computeTileTrs(
720
+ tilemap,
721
+ spec,
722
+ spec.packedTile,
723
+ );
724
+ tmpT[0] = posX;
725
+ tmpT[1] = posY;
726
+ tmpT[2] = 0;
727
+ tmpR[0] = 0;
728
+ tmpR[1] = 0;
729
+ tmpR[2] = quatZ;
730
+ tmpR[3] = quatW;
731
+ tmpS[0] = scaleX;
732
+ tmpS[1] = scaleY;
733
+ tmpS[2] = 1;
734
+ mat4.compose(tmpMat, tmpT, tmpR, tmpS);
735
+
736
+ // Write chunk-local transform: inv_chunk * world_tile (see formula above).
737
+ const dst = i * 16;
738
+ for (let c = 0; c < 4; c++) {
739
+ const base = c * 4;
740
+ const w3 = tmpMat[base + 3] ?? 0;
741
+ transforms[dst + base + 0] = ((tmpMat[base + 0] ?? 0) - chunkCenterX * w3) * invSX;
742
+ transforms[dst + base + 1] = ((tmpMat[base + 1] ?? 0) - chunkCenterY * w3) * invSY;
743
+ transforms[dst + base + 2] = tmpMat[base + 2] ?? 0;
744
+ transforms[dst + base + 3] = w3;
745
+ }
746
+
747
+ const region = tileset.regions[spec.regionIndex];
748
+ if (region !== undefined) {
749
+ const [u, v, w, h] = computeRegionUv(tileset, region, atlasIndex);
750
+ regions[i * 4 + 0] = u;
751
+ regions[i * 4 + 1] = v;
752
+ regions[i * 4 + 2] = w;
753
+ regions[i * 4 + 3] = h;
754
+ }
755
+ }
756
+
757
+ const layerValue = encodeTilemapLayerValue(layerOrder, chunkIndex, 'layer');
758
+ // plan-strategy D-2: the SpriteInstances entity's Transform.posY is set
759
+ // to `chunkCenterY` (becomes `world[13]` in the entity's local-to-world
760
+ // matrix). This is the LAYER_Y fold sort key — terrain rows share a
761
+ // chunk Y, so per-chunk Y resolution is exactly the granularity the
762
+ // fold pass needs. Per-tile world[13] (instance-level) is intentionally
763
+ // _not_ used; instances live in the chunk-local space anchored at
764
+ // chunkCenterY (see line 601-647 above). This is intentional, not a bug.
765
+ return world
766
+ .spawn(
767
+ {
768
+ component: Transform,
769
+ data: {
770
+ pos: [chunkCenterX, chunkCenterY, 0],
771
+ quat: [0, 0, 0, 1],
772
+ scale: [chunkScaleX, chunkScaleY, 1],
773
+ },
774
+ },
775
+ { component: MeshFilter, data: { assetHandle: HANDLE_QUAD } },
776
+ {
777
+ component: MeshRenderer,
778
+ data: {
779
+ materials: [materialHandle],
780
+ },
781
+ },
782
+ { component: SpriteInstances, data: { transforms, regions } },
783
+ { component: Layer, data: { value: layerValue } },
784
+ { component: ChildOf, data: { parent: layerEntity } },
785
+ )
786
+ .unwrap();
787
+ }
788
+
789
+ /**
790
+ * Bucket a single layer's tiles into per-non-zero-cell spawn specs.
791
+ * Returns the parent Tilemap metadata + the resolved spawn specs.
792
+ */
793
+ function bucketTileLayer(
794
+ world: World,
795
+ layerEntity: EntityHandle,
796
+ parentEntity: EntityHandle,
797
+ lookup: (guid: string) => TilesetAtlasLookup,
798
+ ):
799
+ | {
800
+ readonly tilemap: {
801
+ cols: number;
802
+ rows: number;
803
+ tileSize: ArrayLike<number>;
804
+ chunkSize: number;
805
+ };
806
+ readonly layerOrder: number;
807
+ readonly sortScope: SortScope;
808
+ readonly tileset: TilesetAsset;
809
+ readonly specs: readonly DerivedSpawnSpec[];
810
+ }
811
+ | undefined {
812
+ const tilemapRes = world.get(parentEntity, Tilemap);
813
+ if (!tilemapRes.ok) return undefined;
814
+ const tilemap = tilemapRes.value;
815
+ const tilesetPayload = lookup(tilemap.tileset);
816
+ if (tilesetPayload === undefined || 'code' in tilesetPayload) return undefined;
817
+ if (tilesetPayload.kind !== 'tileset') return undefined;
818
+ const tileset = tilesetPayload;
819
+
820
+ const layerRes = world.get(layerEntity, TileLayer);
821
+ if (!layerRes.ok) return undefined;
822
+ const layer = layerRes.value;
823
+ const tiles = layer.tiles as Uint32Array;
824
+ const sortScope = decodeSortScope(layer.sortScope);
825
+
826
+ // sortScope='layer' (terrain) uses the SpriteInstances batched path which
827
+ // groups cells by atlas under an atlas-only material; sortScope='per-cell'
828
+ // (object) keeps the per-cell entity path with per-region materials so
829
+ // each cell can interleave with sprites by foot-Y. The material resolver
830
+ // selection here keeps the spec's `materialHandle` in the correct
831
+ // granularity for whichever spawn path consumes it downstream.
832
+ const useSpriteInstances = sortScope === 'layer';
833
+
834
+ const specs: DerivedSpawnSpec[] = [];
835
+ for (let i = 0; i < tiles.length; i++) {
836
+ const packed = tiles[i] ?? 0;
837
+ if (packed === 0) continue;
838
+ const { tileId } = decodeTileBits(packed);
839
+ if (tileId === 0) continue;
840
+ const entry = tileset.tiles[tileId - 1];
841
+ if (entry === undefined) continue;
842
+ const region = tileset.regions[entry.regionIndex];
843
+ if (region === undefined) continue;
844
+ const atlasIndex = region.atlasIndex ?? 0;
845
+ const materialHandle = useSpriteInstances
846
+ ? resolveAtlasOnlyMaterial(world, tileset, atlasIndex, lookup)
847
+ : resolveTilesetMaterial(world, tileset, entry.regionIndex, lookup);
848
+ const spec = specFor(
849
+ tilemap.cols,
850
+ tilemap.chunkSize,
851
+ i,
852
+ packed,
853
+ materialHandle,
854
+ entry,
855
+ atlasIndex,
856
+ );
857
+ specs.push(spec);
858
+ }
859
+ return {
860
+ tilemap,
861
+ layerOrder: layer.layerOrder,
862
+ sortScope,
863
+ tileset,
864
+ specs,
865
+ };
866
+ }
867
+
868
+ /**
869
+ * Compute the camera frustum planes for the first Camera + Transform entity
870
+ * found in the world. Returns null when no camera exists or the projection
871
+ * parameters are degenerate (callers treat null as always-visible).
872
+ *
873
+ * Mirrors the frustum-plane computation in render-system-extract so both
874
+ * cull paths use byte-identical planes (charter P4 consistent abstraction).
875
+ */
876
+ function buildCameraFrustumPlanes(world: World): frustum.Frustum | null {
877
+ const query = world.query({ with: [Camera, Transform] }).unwrap();
878
+ let result: frustum.Frustum | null = null;
879
+ for (const row of query) {
880
+ const camEntity = row.entity;
881
+
882
+ const camRes = world.get(camEntity, Camera);
883
+ const trRes = world.get(camEntity, Transform);
884
+ if (!camRes.ok || !trRes.ok) continue;
885
+
886
+ const cam = camRes.value;
887
+ const tr = trRes.value;
888
+
889
+ const { near, far } = cam;
890
+ if (near >= far) continue;
891
+
892
+ const proj = mat4.create();
893
+ if (cam.projection === CAMERA_PROJECTION_ORTHOGRAPHIC) {
894
+ mat4.orthographic(
895
+ proj as Parameters<typeof mat4.orthographic>[0],
896
+ cam.left,
897
+ cam.right,
898
+ cam.bottom,
899
+ cam.top,
900
+ near,
901
+ far,
902
+ );
903
+ } else {
904
+ mat4.perspective(
905
+ proj as Parameters<typeof mat4.perspective>[0],
906
+ cam.fov,
907
+ cam.aspect,
908
+ near,
909
+ far,
910
+ );
911
+ }
912
+ const view = mat4.create();
913
+ mat4.invert(
914
+ view as Parameters<typeof mat4.invert>[0],
915
+ tr.world as Parameters<typeof mat4.invert>[1],
916
+ );
917
+ const vp = mat4.create();
918
+ mat4.multiply(
919
+ vp as Parameters<typeof mat4.multiply>[0],
920
+ proj as Parameters<typeof mat4.multiply>[1],
921
+ view as Parameters<typeof mat4.multiply>[2],
922
+ );
923
+ const f = frustum.create();
924
+ frustum.fromViewProjection(f, vp as Parameters<typeof frustum.fromViewProjection>[1]);
925
+ result = f;
926
+ break;
927
+ }
928
+ return result;
929
+ }
930
+
931
+ /**
932
+ * bug-20260703-tilemap-chunk-stale-frustum-and-cull-overhang (D2): compute
933
+ * the world-space AABB that tightly encloses every tile inside a chunk
934
+ * after post-TRS placement (`computeTileTrs` applied per spec, SSOT shared
935
+ * with the per-cell spawn path and SpriteInstances batched path). This is
936
+ * the SSOT the streaming visibility test uses to decide whether a chunk's
937
+ * tiles could still be visible; using it instead of the tile-aligned grid
938
+ * box makes multi-cell / non-central-pivot overhang round-trip correctly,
939
+ * so an overhanging tile no longer disappears when its anchor chunk goes
940
+ * off-screen (plan-strategy §2 D-2).
941
+ *
942
+ * Empty spec list -> inverted `Number.MAX_VALUE` sentinel (plan-strategy
943
+ * §2 D-4). `bucketTileLayer` filters empty cells before the byChunk map
944
+ * is built, so this branch should never trigger in production; the
945
+ * sentinel is an explicit "no visible pixels" value that
946
+ * `frustum.intersectsBox` rejects immediately, avoiding a silent NaN
947
+ * propagation path (charter P3 explicit failure).
948
+ *
949
+ * Note: `frustum.intersectsBox` picks a p-vertex per plane and computes
950
+ * `dot(normal, p) + d`. When normal has a zero component, multiplying by
951
+ * `Infinity` yields `NaN` (`NaN < 0 === false` -> false positive
952
+ * intersection). The sentinel uses finite `Number.MAX_VALUE` on the axes
953
+ * we want inverted so `0 * MAX_VALUE === 0` and the non-zero-normal axis
954
+ * dominates, driving `dot` to `-Infinity` and correctly rejecting.
955
+ *
956
+ * Z is expanded to +/- 1 so flat sprite geometry (`pos_local.z === 0`) is
957
+ * never degenerate against the frustum's near/far planes.
958
+ *
959
+ * @internal exported for M2 overhang / empty-chunk sentinel unit tests
960
+ * (plan-strategy §5.3); not part of the AI user surface.
961
+ */
962
+ export function computeChunkStreamBounds(
963
+ tilemap: { tileSize: ArrayLike<number> },
964
+ specs: readonly DerivedSpawnSpec[],
965
+ ): box3.Box3Like {
966
+ let minX = Number.MAX_VALUE;
967
+ let minY = Number.MAX_VALUE;
968
+ let maxX = -Number.MAX_VALUE;
969
+ let maxY = -Number.MAX_VALUE;
970
+ for (const spec of specs) {
971
+ const { posX, posY, scaleX, scaleY } = computeTileTrs(tilemap, spec, spec.packedTile);
972
+ const halfW = Math.abs(scaleX) * 0.5;
973
+ const halfH = Math.abs(scaleY) * 0.5;
974
+ const x0 = posX - halfW;
975
+ const x1 = posX + halfW;
976
+ const y0 = posY - halfH;
977
+ const y1 = posY + halfH;
978
+ if (x0 < minX) minX = x0;
979
+ if (y0 < minY) minY = y0;
980
+ if (x1 > maxX) maxX = x1;
981
+ if (y1 > maxY) maxY = y1;
982
+ }
983
+ return [minX, minY, -1, maxX, maxY, 1];
984
+ }
985
+
986
+ /**
987
+ * Despawn every derived render entity currently attached to `layerEntity`.
988
+ *
989
+ * After tweak-20260714 M3 the terrain path locates its previously-spawned
990
+ * children via `Children.entities` on the TileLayer (mirror of ChildOf,
991
+ * engine-maintained via the relationship hook) rather than a module-level
992
+ * tracker Map — SSOT collapse per architecture-principles §2 (Derive,
993
+ * Don't Duplicate). The snapshot returned by
994
+ * `world.get(layerEntity, Children).entities` is a fresh read-only
995
+ * Uint32Array that is stable across the iteration even though each
996
+ * despawn prunes the mirror in place.
997
+ *
998
+ * Empty children (never-built layer, or already-purged) trivially
999
+ * short-circuits — this is the "empty array no-op" equivalence to the
1000
+ * old first-frame-heuristic guard (plan-strategy §2 D-3 edge case 3).
1001
+ */
1002
+ function purgeDerivedEntities(world: World, layerEntity: EntityHandle): void {
1003
+ const r = world.get(layerEntity, Children);
1004
+ if (!r.ok) return;
1005
+ const snap = r.value.entities;
1006
+ for (let i = 0; i < snap.length; i++) {
1007
+ const e = snap[i];
1008
+ if (e !== undefined) world.despawn(e as EntityHandle);
1009
+ }
1010
+ }
1011
+
1012
+ /**
1013
+ * Snapshot of a single TileLayer's per-frame work item — populated by the
1014
+ * main-loop query and consumed both by the per-cell diff-cleanup preamble
1015
+ * (`evictDeadPerCellStreamingCaches`) and the per-layer processing branches.
1016
+ */
1017
+ interface LayerWork {
1018
+ readonly layerEntity: EntityHandle;
1019
+ readonly parentEntity: EntityHandle;
1020
+ readonly dirty: number;
1021
+ readonly sortScopeRaw: number;
1022
+ }
1023
+
1024
+ /**
1025
+ * tweak-20260714 M4 diff-cleanup preamble (plan-strategy §2 D-4 +
1026
+ * requirements §5 AC-11 + §8 edge case #4).
1027
+ *
1028
+ * The per-cell streaming maps are owned by one weak World entry and outlive
1029
+ * individual TileLayer entities while that World is alive. When a TileLayer
1030
+ * is despawned (or cascade-collected via `world.despawn(tilemapEntity)`) the
1031
+ * ECS mirrors clean up entities and `Children.entities`, but these maps retain
1032
+ * the dead layer's entries until this diff. On slot reuse, the stale entries
1033
+ * would corrupt rebuild: `activeSet` still lists old
1034
+ * chunkIndexes, and the rebuild branch would attempt to despawn stale
1035
+ * entity IDs before repopulating.
1036
+ *
1037
+ * Fix: at the top of each frame, diff this World's cached layer keys against
1038
+ * the fresh query. The set
1039
+ * difference names layers that vanished since the previous call; evict
1040
+ * their entries from all three Maps. `activeSet` is the SSOT for "which
1041
+ * chunkIndexes have entries under this layer" (each insertion / removal
1042
+ * pairs a `chunkEntities` set/delete with an `activeSet` add/delete), so
1043
+ * cleanup iterates `activeSet` rather than scanning `chunkEntities.keys()` —
1044
+ * O(chunks-per-dead-layer) instead
1045
+ * of O(total-cache-keys).
1046
+ *
1047
+ * Cross-world isolation is structural: the WeakMap key is the World object.
1048
+ * No duplicated World id is retained in every layer/chunk key.
1049
+ *
1050
+ * Per-frame cost (OOS-3 invariant): the two `keys()` iterations scan
1051
+ * O(cache_size) ≤ O(all-ever-seen-layers-for-this-worldId). Typical
1052
+ * scenes have ≤ 10 layers so this is a handful of Map lookups; no
1053
+ * matrix arithmetic. Actual eviction work only runs on frames where a
1054
+ * layer vanished — steady-state frames pay only the diff scan.
1055
+ */
1056
+ function evictDeadPerCellStreamingCaches(work: readonly LayerWork[], world: World): void {
1057
+ const cache = streamingCache(world);
1058
+ const aliveLayerKeys = new Set<string>();
1059
+ for (const w of work) {
1060
+ aliveLayerKeys.add(String(w.layerEntity));
1061
+ }
1062
+ const deadLayerKeys = new Set<string>();
1063
+ for (const layerKey of cache.activeChunks.keys()) {
1064
+ if (!aliveLayerKeys.has(layerKey)) deadLayerKeys.add(layerKey);
1065
+ }
1066
+ for (const layerKey of cache.layers.keys()) {
1067
+ if (!aliveLayerKeys.has(layerKey)) deadLayerKeys.add(layerKey);
1068
+ }
1069
+ for (const deadKey of deadLayerKeys) {
1070
+ const activeSet = cache.activeChunks.get(deadKey);
1071
+ if (activeSet !== undefined) {
1072
+ for (const chunkIdx of activeSet) {
1073
+ cache.chunkEntities.delete(`${deadKey}:${chunkIdx}`);
1074
+ }
1075
+ cache.activeChunks.delete(deadKey);
1076
+ }
1077
+ cache.layers.delete(deadKey);
1078
+ }
1079
+ }
1080
+
1081
+ /**
1082
+ * Walk every TileLayer ChildOf-ing a Tilemap, extract its non-zero cells
1083
+ * into derived render entities. A Renderer attaches this system to FrameEnd;
1084
+ * World's final publication then resolves newly-created Transform.world values before the
1085
+ * read-only render walk begins.
1086
+ *
1087
+ * Two paths based on `TileLayer.sortScope`:
1088
+ *
1089
+ * `'layer'` (terrain, default):
1090
+ * Batched SpriteInstances path — spawned once, reused until dirty. Each
1091
+ * (chunk, atlas) pair becomes one SpriteInstances entity whose Transform
1092
+ * covers the chunk footprint, so the frustum culler rejects off-screen
1093
+ * chunks at entity granularity.
1094
+ *
1095
+ * `'per-cell'` (object layers):
1096
+ * Chunk-streaming path — specs are bucketed by chunkIndex once (rebuilt
1097
+ * on dirty) and the live entity set is updated every frame to match the
1098
+ * camera frustum. Only chunks whose world AABB intersects the frustum
1099
+ * have ECS entities alive, keeping `extractFrame` iteration proportional
1100
+ * to visible tile count rather than total map tile count.
1101
+ */
1102
+ export function tilemapChunkExtractSystem(
1103
+ world: World,
1104
+ lookup: (guid: string) => TilesetAtlasLookup = () => undefined,
1105
+ ): void {
1106
+ const streaming = streamingCache(world);
1107
+ const work: LayerWork[] = [];
1108
+ const tileLayerQuery = world.query({ read: [TileLayer, ChildOf] }).unwrap();
1109
+ for (const row of tileLayerQuery) {
1110
+ const layer = row.get(TileLayer);
1111
+ const parent = row.get(ChildOf).parent;
1112
+ if (parent === null) continue;
1113
+ work.push({
1114
+ layerEntity: row.entity,
1115
+ parentEntity: parent,
1116
+ dirty: layer.dirty,
1117
+ sortScopeRaw: layer.sortScope,
1118
+ });
1119
+ }
1120
+
1121
+ evictDeadPerCellStreamingCaches(work, world);
1122
+
1123
+ // Compute the camera frustum once per frame — only needed when at least
1124
+ // one streaming (per-cell) layer exists. Null = always-visible fallback.
1125
+ let frustumPlanes: frustum.Frustum | null | undefined;
1126
+
1127
+ for (const w of work) {
1128
+ // The outer WeakMap owns World identity; this generation-bearing packed
1129
+ // entity handle prevents aliasing when an ECS slot is reused.
1130
+ const layerKey = String(w.layerEntity);
1131
+ // AC-04: decode via the canonical SortScope union; avoid relying on the
1132
+ // raw encoding (0='layer', 1='per-cell') leaking through this call site.
1133
+ // `decodeSortScope` is the SSOT used throughout `bucketTileLayer` (see
1134
+ // line 730); this main-loop branch is the last remaining numeric check.
1135
+ const isStreaming = decodeSortScope(w.sortScopeRaw) === 'per-cell';
1136
+
1137
+ if (!isStreaming) {
1138
+ // ── Terrain batched path (sortScope='layer') ──────────────────────
1139
+ // Spawn once per dirty; entity AABB covers the chunk footprint for
1140
+ // entity-level frustum culling in render-system-extract.
1141
+ //
1142
+ // "already built" is derived from Children.entities (mirror of
1143
+ // ChildOf, engine-maintained). Empty children ⇒ never built (or
1144
+ // just purged) ⇒ don't skip. Populated children + dirty=0 ⇒
1145
+ // steady state ⇒ skip. Populated + dirty=1 ⇒ purge + rebuild.
1146
+ // architecture-principles §2 (Derive, Don't Duplicate): the
1147
+ // former module-level "ever-built" Set was a second copy of
1148
+ // information that Children.entities already carries.
1149
+ const childrenRes = world.get(w.layerEntity, Children);
1150
+ const childCount = childrenRes.ok ? childrenRes.value.entities.length : 0;
1151
+ if (childCount > 0 && w.dirty === 0) continue;
1152
+
1153
+ purgeDerivedEntities(world, w.layerEntity);
1154
+
1155
+ const bucket = bucketTileLayer(world, w.layerEntity, w.parentEntity, lookup);
1156
+ if (bucket === undefined) continue;
1157
+
1158
+ const byChunkAtlas = new Map<number, DerivedSpawnSpec[]>();
1159
+ for (const spec of bucket.specs) {
1160
+ const key = ((spec.chunkIndex & 0xfffff) << 16) | (spec.atlasIndex & 0xffff);
1161
+ const list = byChunkAtlas.get(key);
1162
+ if (list === undefined) {
1163
+ byChunkAtlas.set(key, [spec]);
1164
+ } else {
1165
+ list.push(spec);
1166
+ }
1167
+ }
1168
+ for (const groupSpecs of byChunkAtlas.values()) {
1169
+ const first = groupSpecs[0];
1170
+ if (first === undefined) continue;
1171
+ spawnSpriteInstancesGroup(
1172
+ world,
1173
+ bucket.tilemap,
1174
+ bucket.tileset,
1175
+ w.layerEntity,
1176
+ bucket.layerOrder,
1177
+ first.chunkIndex,
1178
+ first.atlasIndex,
1179
+ first.materialHandle,
1180
+ groupSpecs,
1181
+ );
1182
+ }
1183
+ if (w.dirty !== 0) {
1184
+ world.set(w.layerEntity, TileLayer, { dirty: 0 }).unwrap();
1185
+ }
1186
+ continue;
1187
+ }
1188
+
1189
+ // ── Object streaming path (sortScope='per-cell') ───────────────────
1190
+ // Step 1: rebuild specs cache when dirty or first time.
1191
+ if (w.dirty !== 0 || !streaming.layers.has(layerKey)) {
1192
+ // Despawn all currently-active chunks for this layer.
1193
+ const activeSet = streaming.activeChunks.get(layerKey);
1194
+ if (activeSet !== undefined) {
1195
+ for (const chunkIdx of activeSet) {
1196
+ const key = `${layerKey}:${chunkIdx}`;
1197
+ const entities = streaming.chunkEntities.get(key);
1198
+ if (entities !== undefined) {
1199
+ for (const e of entities) world.despawn(e as EntityHandle);
1200
+ streaming.chunkEntities.delete(key);
1201
+ }
1202
+ }
1203
+ activeSet.clear();
1204
+ }
1205
+ streaming.layers.delete(layerKey);
1206
+
1207
+ const bucket = bucketTileLayer(world, w.layerEntity, w.parentEntity, lookup);
1208
+ if (bucket !== undefined) {
1209
+ const byChunkSpecs = new Map<number, DerivedSpawnSpec[]>();
1210
+ for (const spec of bucket.specs) {
1211
+ const list = byChunkSpecs.get(spec.chunkIndex);
1212
+ if (list === undefined) {
1213
+ byChunkSpecs.set(spec.chunkIndex, [spec]);
1214
+ } else {
1215
+ list.push(spec);
1216
+ }
1217
+ }
1218
+ // bug-20260703 D2: promote each chunk's bucket to a ChunkStreamEntry
1219
+ // carrying the world-space bounds that enclose every tile's actual
1220
+ // footprint (not the tile-aligned grid box). The visibility test
1221
+ // then rejects the chunk only when its TRUE pixels are all
1222
+ // off-screen, so multi-cell / off-pivot tiles no longer flicker at
1223
+ // chunk-boundary crossings (plan-strategy §2 D-2 / D-3).
1224
+ const byChunk = new Map<number, ChunkStreamEntry>();
1225
+ for (const [chunkIdx, specs] of byChunkSpecs) {
1226
+ byChunk.set(chunkIdx, {
1227
+ specs,
1228
+ bounds: computeChunkStreamBounds(bucket.tilemap, specs),
1229
+ });
1230
+ }
1231
+ streaming.layers.set(layerKey, {
1232
+ byChunk,
1233
+ tilemap: bucket.tilemap,
1234
+ layerOrder: bucket.layerOrder,
1235
+ sortScope: bucket.sortScope,
1236
+ });
1237
+ }
1238
+ if (w.dirty !== 0) {
1239
+ world.set(w.layerEntity, TileLayer, { dirty: 0 }).unwrap();
1240
+ }
1241
+ }
1242
+
1243
+ const cache = streaming.layers.get(layerKey);
1244
+ if (cache === undefined) continue;
1245
+
1246
+ // Step 2: lazy-compute camera frustum on first streaming layer.
1247
+ if (frustumPlanes === undefined) {
1248
+ frustumPlanes = buildCameraFrustumPlanes(world);
1249
+ }
1250
+
1251
+ // Step 3: diff visible chunks against active set.
1252
+ const activeSet = streaming.activeChunks.get(layerKey) ?? new Set<number>();
1253
+ streaming.activeChunks.set(layerKey, activeSet);
1254
+ const { tilemap } = cache;
1255
+
1256
+ for (const [chunkIdx, entry] of cache.byChunk) {
1257
+ const { specs, bounds } = entry;
1258
+ const visible = frustumPlanes === null || frustum.intersectsBox(frustumPlanes, bounds);
1259
+ const wasActive = activeSet.has(chunkIdx);
1260
+
1261
+ if (visible && !wasActive) {
1262
+ // Spawn per-cell entities for this newly-visible chunk.
1263
+ const spawned: EntityHandle[] = [];
1264
+ for (const spec of specs) {
1265
+ const e = spawnDerivedRenderEntities(
1266
+ world,
1267
+ tilemap,
1268
+ w.layerEntity,
1269
+ cache.layerOrder,
1270
+ spec,
1271
+ spec.packedTile,
1272
+ cache.sortScope,
1273
+ );
1274
+ spawned.push(e);
1275
+ }
1276
+ streaming.chunkEntities.set(`${layerKey}:${chunkIdx}`, spawned);
1277
+ activeSet.add(chunkIdx);
1278
+ } else if (!visible && wasActive) {
1279
+ // Despawn per-cell entities for this newly-invisible chunk.
1280
+ const key = `${layerKey}:${chunkIdx}`;
1281
+ const entities = streaming.chunkEntities.get(key);
1282
+ if (entities !== undefined) {
1283
+ for (const e of entities) world.despawn(e);
1284
+ streaming.chunkEntities.delete(key);
1285
+ }
1286
+ activeSet.delete(chunkIdx);
1287
+ }
1288
+ }
1289
+ }
1290
+ }