@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,1822 @@
1
+ // @forgeax/engine-runtime - GpuResidencyCache
2
+ // (feat-20260601-device/gpu-residency-extraction M1; M2 added the deriveRenderData
3
+ // projection seam — see render-data.ts).
4
+ //
5
+ // Owns the GPU residency layer extracted from AssetRegistry: per-handle GPU
6
+ // texture / cubemap / mesh buffer caches plus the upload primitives that
7
+ // build them. The store is engine-agnostic by default (no @webgpu/types
8
+ // imports) and holds ZERO reference to AssetRegistry (D-2): every upload
9
+ // primitive receives its source POD from the caller, never reaches back into
10
+ // a registry. The cubemap path mints its EquirectAsset POD handle through
11
+ // a wire-injected `registerCube` callback (D-3) so CPU cataloguing stays the
12
+ // registry's job while the store keeps the single-call upload contract. The
13
+ // cubemap projection (`_uploadCubemapFromEquirect`) is internal (@internal,
14
+ // feat-20260630): AI users declare `Skylight{equirect}` and the render-system
15
+ // record arm drives the projection; the method is never reached from user code.
16
+ //
17
+ // Residency model (D-2 pull): consumers call `ensureResident(handle, pod)`
18
+ // at first draw-time access; the store builds the GPU resource on a miss and
19
+ // returns the cached handles on subsequent O(1) hits. There is no global
20
+ // replay queue -- the pull model is purely lazy (user ruling). Builtin
21
+ // meshes are NOT routed through `ensureResident`; createRenderer seeds them
22
+ // via its step-3 direct upload + pipelineState.meshes (D-1).
23
+ //
24
+ // Texture residency is SYNCHRONOUS (D-9): `uploadTexture`'s only async source
25
+ // was the one-time per-device mipmap shader-module build, which is hoisted to
26
+ // `prewarmMipmapPipeline` (called from `createRenderer.initialization`, already async).
27
+ // After prewarm the per-texture mipmap blit is pure synchronous encoder work,
28
+ // so the sync `draw(world)` frame contract is preserved. A format that was
29
+ // not prewarmed surfaces a structured RhiError on the sync path -- it is never
30
+ // lazily awaited (that would break the sync draw contract).
31
+
32
+ import {
33
+ blitMipmapsSync,
34
+ getOrCreateMipmapPipeline,
35
+ type MipmapShaderModuleFactory,
36
+ } from '@forgeax/engine-assets-runtime';
37
+ import type { World } from '@forgeax/engine-ecs';
38
+ import {
39
+ PROCEDURAL_FLOATS_PER_VERTEX,
40
+ type VertexLayoutProjection,
41
+ } from '@forgeax/engine-geometry';
42
+ import { halfFloat } from '@forgeax/engine-math';
43
+ import {
44
+ type Buffer,
45
+ type BufferDescriptor,
46
+ err,
47
+ ok,
48
+ type Result,
49
+ type RhiCaps,
50
+ type RhiDevice,
51
+ RhiError,
52
+ type RhiQueue,
53
+ type Sampler,
54
+ type Texture,
55
+ type TextureFormat,
56
+ type TextureView,
57
+ } from '@forgeax/engine-rhi';
58
+ import {
59
+ ASSET_ERROR_HINTS,
60
+ type AssetError,
61
+ countExtraUvSets,
62
+ type DecodedImage,
63
+ type EquirectAsset,
64
+ type Handle,
65
+ handleSlot,
66
+ IMAGE_ERROR_HINTS,
67
+ type ImageError,
68
+ type ImageErrorCode,
69
+ type ImageErrorDetailFor,
70
+ type ImageErrorFor,
71
+ type PrimitiveTopology,
72
+ type SamplerAsset,
73
+ type Submesh,
74
+ type TextureAsset,
75
+ type MeshAsset as TypesMeshAsset,
76
+ toShared,
77
+ } from '@forgeax/engine-types';
78
+ import { GpuBuffer, GpuTexture } from '../gpu-resource';
79
+ import {
80
+ GPU_TEXTURE_USAGE_COPY_DST,
81
+ GPU_TEXTURE_USAGE_TEXTURE_BINDING,
82
+ } from '../gpu-texture-usage';
83
+ import {
84
+ GPU_BUFFER_USAGE_COPY_DST,
85
+ GPU_BUFFER_USAGE_INDEX,
86
+ GPU_BUFFER_USAGE_UNIFORM,
87
+ GPU_BUFFER_USAGE_VERTEX,
88
+ } from '../gpu-usage';
89
+ import {
90
+ CAPTURE_VIEW_PROJS,
91
+ CUBEMAP_FACE_VERTICES,
92
+ createIblPipelines,
93
+ type IblShaderModuleFactory,
94
+ PREFILTER_MIP_LEVELS,
95
+ PREFILTER_SIZE,
96
+ runIblPrecompute,
97
+ } from '../ibl/IblPipelineCache';
98
+ import type { RhiErrorListenerRegistry } from '../lifecycle';
99
+ import {
100
+ type CubeRenderData,
101
+ deriveMipUploadLayout,
102
+ deriveRenderDataCubemap,
103
+ deriveRenderDataMesh,
104
+ deriveRenderDataTexture,
105
+ type MeshRenderData,
106
+ type TextureRenderData,
107
+ } from '../render-data';
108
+ import type { DeviceScope } from './device-scope';
109
+
110
+ const DYNAMIC_OFFSET_STRIDE = 256;
111
+ const FACE_COUNT = 6;
112
+ const PREFILTER_SUBPASS_COUNT = PREFILTER_MIP_LEVELS * FACE_COUNT;
113
+
114
+ export type FaceUniformsDevice = Pick<RhiDevice, 'createBuffer'> & {
115
+ readonly queue: Pick<RhiQueue, 'writeBuffer'>;
116
+ };
117
+
118
+ function invalidUniformInput(expected: string, hint: string): Result<never, RhiError> {
119
+ return err(new RhiError({ code: 'webgpu-runtime-error', expected, hint }));
120
+ }
121
+
122
+ export function createFaceUniformsBuffer(device: FaceUniformsDevice): Result<Buffer, RhiError> {
123
+ const descriptor: BufferDescriptor = {
124
+ label: 'ibl-face-uniforms',
125
+ size: FACE_COUNT * DYNAMIC_OFFSET_STRIDE,
126
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
127
+ mappedAtCreation: false,
128
+ };
129
+ return device.createBuffer(descriptor);
130
+ }
131
+
132
+ export function createPrefilterUniformsBuffer(
133
+ device: FaceUniformsDevice,
134
+ ): Result<Buffer, RhiError> {
135
+ const descriptor: BufferDescriptor = {
136
+ label: 'ibl-prefilter-uniforms',
137
+ size: PREFILTER_SUBPASS_COUNT * DYNAMIC_OFFSET_STRIDE,
138
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
139
+ mappedAtCreation: false,
140
+ };
141
+ return device.createBuffer(descriptor);
142
+ }
143
+
144
+ export function writeFaceUniforms(
145
+ device: FaceUniformsDevice,
146
+ buffer: Buffer,
147
+ faceIdx: number,
148
+ viewProj: Float32Array,
149
+ ): Result<void, RhiError> {
150
+ if (faceIdx < 0 || faceIdx >= FACE_COUNT) {
151
+ return invalidUniformInput(
152
+ 'faceIdx is an integer in the inclusive range [0, 5]',
153
+ `writeFaceUniforms received faceIdx=${faceIdx}`,
154
+ );
155
+ }
156
+ if (viewProj.length !== 16) {
157
+ return invalidUniformInput(
158
+ 'viewProj contains exactly 16 floats',
159
+ `writeFaceUniforms received ${viewProj.length} floats`,
160
+ );
161
+ }
162
+ return device.queue.writeBuffer(buffer, faceIdx * DYNAMIC_OFFSET_STRIDE, viewProj);
163
+ }
164
+
165
+ export function writeAllFaceUniforms(
166
+ device: FaceUniformsDevice,
167
+ buffer: Buffer,
168
+ ): Result<void, RhiError> {
169
+ for (let face = 0; face < FACE_COUNT; face++) {
170
+ const viewProj = CAPTURE_VIEW_PROJS[face];
171
+ if (viewProj === undefined) continue;
172
+ const result = writeFaceUniforms(device, buffer, face, viewProj);
173
+ if (!result.ok) return result;
174
+ }
175
+ return ok(undefined);
176
+ }
177
+
178
+ export function writePrefilterUniforms(
179
+ device: FaceUniformsDevice,
180
+ buffer: Buffer,
181
+ subPassIdx: number,
182
+ roughness: number,
183
+ mipFaceSize: number,
184
+ ): Result<void, RhiError> {
185
+ if (subPassIdx < 0 || subPassIdx >= PREFILTER_SUBPASS_COUNT) {
186
+ return invalidUniformInput(
187
+ `subPassIdx is an integer in the inclusive range [0, ${PREFILTER_SUBPASS_COUNT - 1}]`,
188
+ `writePrefilterUniforms received subPassIdx=${subPassIdx}`,
189
+ );
190
+ }
191
+ const payload = new Float32Array([roughness, mipFaceSize, 0, 0]);
192
+ return device.queue.writeBuffer(buffer, subPassIdx * DYNAMIC_OFFSET_STRIDE, payload);
193
+ }
194
+
195
+ export function writeAllPrefilterUniforms(
196
+ device: FaceUniformsDevice,
197
+ buffer: Buffer,
198
+ ): Result<void, RhiError> {
199
+ for (let mip = 0; mip < PREFILTER_MIP_LEVELS; mip++) {
200
+ const roughness = mip / (PREFILTER_MIP_LEVELS - 1);
201
+ const mipFaceSize = PREFILTER_SIZE / 2 ** mip;
202
+ for (let face = 0; face < FACE_COUNT; face++) {
203
+ const result = writePrefilterUniforms(
204
+ device,
205
+ buffer,
206
+ mip * FACE_COUNT + face,
207
+ roughness,
208
+ mipFaceSize,
209
+ );
210
+ if (!result.ok) return result;
211
+ }
212
+ }
213
+ return ok(undefined);
214
+ }
215
+
216
+ // AssetError is constructed without importing the AssetRegistry's class; the
217
+ // store builds the 4-field surface (.code / .expected / .hint / .detail)
218
+ // directly against the @forgeax/engine-types SSOT (charter P5 producer /
219
+ // consumer split; mirrors AssetRegistry's local RuntimeImageError).
220
+ class RuntimeAssetError extends Error implements AssetError {
221
+ readonly code: AssetError['code'];
222
+ readonly expected: string;
223
+ readonly hint: string;
224
+ constructor(fields: { code: AssetError['code']; expected: string; hint: string }) {
225
+ super(`[AssetError ${fields.code}] expected: ${fields.expected}; hint: ${fields.hint}`);
226
+ this.name = 'AssetError';
227
+ this.code = fields.code;
228
+ this.expected = fields.expected;
229
+ this.hint = fields.hint;
230
+ }
231
+ }
232
+
233
+ function makeAssetError(fields: {
234
+ code: AssetError['code'];
235
+ expected: string;
236
+ hint: string;
237
+ }): AssetError {
238
+ return new RuntimeAssetError(fields);
239
+ }
240
+
241
+ const IMAGE_ERROR_EXPECTED_LOCAL: Readonly<Record<string, string>> = {
242
+ 'image-decode-failed': 'PNG / JPG byte stream decodes successfully',
243
+ 'image-format-unsupported': "format ends in '-srgb' iff colorSpace is 'srgb' (linear otherwise)",
244
+ 'image-dimension-out-of-bounds':
245
+ 'width and height fall under device caps maxTextureDimension2D (or 16384 hard cap)',
246
+ };
247
+
248
+ class RuntimeImageError<C extends ImageErrorCode> extends Error implements ImageErrorFor<C> {
249
+ readonly code: C;
250
+ readonly expected: string;
251
+ readonly hint: string;
252
+ readonly detail: ImageErrorDetailFor<C>;
253
+ constructor(detail: ImageErrorDetailFor<C>) {
254
+ const code = detail.code;
255
+ const expected = IMAGE_ERROR_EXPECTED_LOCAL[code] ?? '';
256
+ const hint = IMAGE_ERROR_HINTS[code];
257
+ super(`[ImageError ${code}] expected: ${expected}; hint: ${hint}`);
258
+ this.name = 'ImageError';
259
+ this.code = code;
260
+ this.expected = expected;
261
+ this.hint = hint;
262
+ this.detail = detail;
263
+ }
264
+ }
265
+
266
+ function makeImageError<C extends ImageErrorCode>(
267
+ detail: ImageErrorDetailFor<C>,
268
+ ): ImageErrorFor<C> {
269
+ return new RuntimeImageError<C>(detail);
270
+ }
271
+
272
+ /** Cube-POD register-relay injected by the wire layer (D-3). */
273
+ type RegisterCube = (
274
+ world: World,
275
+ pod: EquirectAsset,
276
+ ) => Result<Handle<'EquirectAsset', 'shared'>, AssetError>;
277
+
278
+ // feat-20260612-rhi-destroy-renderer-dispose-gpu-lifecycle / M-3 / w11:
279
+ // the three handle map value types now hold GpuTexture / GpuBuffer wrappers
280
+ // (plan-strategy D-9). Views (TextureView) stay raw RHI handles -- they are
281
+ // not destroyable on their own; their lifetime is bound to the parent
282
+ // GpuTexture, which the wrapper owns. destroyAll() walks the GpuResource
283
+ // fields and forwards .destroy() to the RHI shim.
284
+ interface TextureGpuEntry {
285
+ readonly texture: GpuTexture;
286
+ /** Opaque RHI view handle; lifetime is bound to the parent texture. */
287
+ readonly view: TextureView;
288
+ }
289
+
290
+ // feat-20260630-equirect-kind-internalized-ibl-declarative-skyligh M2 / w11
291
+ // (D-3): the projection-status truth lives here (the store's cubemap map is the
292
+ // single authority — the SkylightSnapshot carries no status, D-3). `status`:
293
+ // - 'pending' — projection launched (fire-and-forget) but not complete; the
294
+ // record arm binds the white-cube fallback for this frame.
295
+ // - 'ready' — texture + cube view + 6 face views are live; bind the real
296
+ // IBL cube.
297
+ // - 'failed' — projection errored; recorded EXPLICITLY so the record arm
298
+ // stops and does NOT retry every frame (R-2 / AC-09). A failed entry has
299
+ // no GPU resources (texture/view are placeholders never bound).
300
+ // `texture`/`view`/`faceViews` are non-readonly so a 'pending' entry can be
301
+ // promoted to 'ready' in place (rebuilds the same slot). A 'failed' entry holds
302
+ // no GPU resources (`texture:null` / `view:undefined`): it exists only to mark
303
+ // the source as terminally failed so the record arm stops retrying (R-2).
304
+ interface CubemapGpuEntry {
305
+ status: 'pending' | 'ready' | 'failed';
306
+ texture: GpuTexture | null;
307
+ view: TextureView | undefined;
308
+ faceViews: readonly TextureView[];
309
+ }
310
+
311
+ export interface MeshGpuHandles {
312
+ readonly vertexBuffer: GpuBuffer;
313
+ /**
314
+ * Index buffer wrapper, or `null` for a vertex-only mesh
315
+ * (no `MeshAsset.indices`). When `null` the record stage takes the
316
+ * non-indexed `pass.draw(vertexCount)` path and never calls
317
+ * `setIndexBuffer`. Gated on `indexed` below.
318
+ */
319
+ readonly indexBuffer: GpuBuffer | null;
320
+ /**
321
+ * Allocation byte sizes for the vbo / ibo (mirrors GPUBuffer.size). Used
322
+ * by `updateMeshById` to decide reuse vs reallocation; tracked here so
323
+ * the runtime never reads `.size` off the opaque RHI handle (which is
324
+ * not on the spec-aligned RHI Buffer interface; charter §RHI form rules).
325
+ */
326
+ readonly vboBytes: number;
327
+ /** Allocation byte size for the ibo, or 0 when indexBuffer is null. */
328
+ readonly iboBytes: number;
329
+ readonly indexCount: number;
330
+ readonly indexFormat: 'uint16' | 'uint32';
331
+ /**
332
+ * Vertex stride discriminator (feat-20260611). `'12F'` = 48 B
333
+ * (position+normal+uv+tangent); `'18F'` = 72 B (12F + skinIndex(uint16x4) +
334
+ * skinWeight(float32x4)). Mirrors `MeshRenderData.layout` in render-data.ts
335
+ * and `MeshRenderData.layout` -- the residency and render-data layout
336
+ * fields are the same union and move together.
337
+ */
338
+ readonly layout: '12F' | '18F';
339
+ /** Geometry-owned vertex layout projection used by every GPU consumer. */
340
+ readonly layoutProjection: VertexLayoutProjection;
341
+ /**
342
+ * Number of UV sets the interleaved buffer carries (1 = single `uv`, +1 per
343
+ * `uv1..uv7`). feat-20260629-multi-uv-set-support: threaded to the record
344
+ * stage so the forward PSO's vertex layout stride matches the buffer for
345
+ * meshes with a real extra UV set. Mirrors MeshRenderData.uvSetCount /
346
+ * MeshRenderData.uvSetCount.
347
+ */
348
+ readonly uvSetCount: number;
349
+ /** Vertex count uses 18F for skin or the geometry-owned canonical base stride. */
350
+ readonly vertexCount: number;
351
+ /** True when `MeshAsset.indices` is present (indexed draw path). */
352
+ readonly indexed: boolean;
353
+ /** Primitive topology (default 'triangle-list'). */
354
+ readonly topology: PrimitiveTopology;
355
+ /**
356
+ * Submeshes from MeshAsset.submeshes, carried through to the record stage
357
+ * so per-submesh drawIndexed can iterate independently (feat-20260608 M4 / w16).
358
+ * For single-submesh meshes this is a 1-element array (byte-identical to pre-M4).
359
+ */
360
+ readonly submeshes: readonly import('@forgeax/engine-types').Submesh[];
361
+ }
362
+
363
+ /**
364
+ * GPU residency store. Owns the per-handle GPU caches and the upload
365
+ * primitives that build them. Constructed once per renderer; wired with the
366
+ * GPU device via `configureGpuDevice` after `Renderer.initialization` resolves.
367
+ */
368
+ export class GpuResidencyCache {
369
+ /** Monotonic token for mesh GPU residency or contents changes. */
370
+ meshResidencyEpoch = 0;
371
+ /** Monotonic token for texture/sampler identities used by material bind groups. */
372
+ materialResourceEpoch = 0;
373
+ private gpuDevice: RhiDevice | undefined = undefined;
374
+ /** RHI-only owner for IBL projection; never populated with a raw GPUDevice. */
375
+ private iblDevice: RhiDevice | undefined = undefined;
376
+ /** RHI Result-returning shader factory paired with `iblDevice`. */
377
+ private iblShaderModuleFactory: IblShaderModuleFactory | undefined = undefined;
378
+ private deviceScope: DeviceScope | undefined;
379
+ // Shader-module factory injected at `configureGpuDevice`; threaded into the
380
+ // mipmap-pipeline prewarm + the IBL precompute path. `undefined` until wired.
381
+ private asyncCreateShaderModule: MipmapShaderModuleFactory | undefined = undefined;
382
+ // Cube-POD register relay injected at `configureGpuDevice` (D-3); the store
383
+ // never imports AssetRegistry, so CPU cataloguing flows through this fn.
384
+ private registerCube: RegisterCube | undefined = undefined;
385
+ // Error registry injected by createRenderer; evict / releaseUnreferenced
386
+ // fire structured errors through this channel (feat-20260619 D-1/D-6).
387
+ private errorRegistry: RhiErrorListenerRegistry | undefined = undefined;
388
+ // Hardware-probe caps injected at `configureGpuDevice`; guards the HDR cubemap
389
+ // path (_uploadCubemapFromEquirect) when `rgba16floatRenderable` is false.
390
+ private caps: RhiCaps | undefined = undefined;
391
+
392
+ private readonly textureGpuHandles: Map<string | number, TextureGpuEntry> = new Map();
393
+ private readonly samplerGpuHandles: Map<string | number, Sampler> = new Map();
394
+ private readonly cubemapGpuHandles: Map<number, CubemapGpuEntry> = new Map();
395
+ // Maps source EquirectAsset handle id -> minted cubemap handle so the same
396
+ // equirect source always resolves to the same cubemap (idempotent, A2).
397
+ private readonly cubemapIdempotentMap: Map<number, Handle<'EquirectAsset', 'shared'>> = new Map();
398
+ private readonly meshGpuHandles: Map<string | number, MeshGpuHandles> = new Map();
399
+
400
+ /**
401
+ * Compose a cache key from the raw handle slot and its owning World. The
402
+ * draw-time world index is only a position in one frame's `worlds[]`; it is
403
+ * not an asset identity and can be reused by a later World. World objects
404
+ * therefore receive a renderer-local namespace, while numeric callers keep
405
+ * the legacy namespace for low-level tests and built-in upload paths.
406
+ */
407
+ private readonly worldNamespaces = new WeakMap<World, number>();
408
+ private nextWorldNamespace = 1;
409
+
410
+ private worldKey(slot: number, world: World | number): string | number {
411
+ if (typeof world === 'number') {
412
+ return world === 0 ? slot : world * 0x1000000 + slot;
413
+ }
414
+ let namespace = this.worldNamespaces.get(world);
415
+ if (namespace === undefined) {
416
+ namespace = this.nextWorldNamespace;
417
+ this.nextWorldNamespace += 1;
418
+ this.worldNamespaces.set(world, namespace);
419
+ }
420
+ return `world:${namespace}:${slot}`;
421
+ }
422
+
423
+ /**
424
+ * Wire the GPU device, shader-module factory, and cube-POD register relay.
425
+ * Called once by createRenderer after `Renderer.initialization` resolves. Unlike the
426
+ * pre-extraction AssetRegistry.configureGpuDevice, this performs NO replay:
427
+ * the pull model builds resources lazily at first `ensureResident` access.
428
+ */
429
+ configureGpuDevice(
430
+ device: RhiDevice,
431
+ asyncCreateShaderModule: MipmapShaderModuleFactory | undefined,
432
+ registerCube: RegisterCube,
433
+ caps: RhiCaps,
434
+ ): void {
435
+ this.meshResidencyEpoch += 1;
436
+ this.gpuDevice = device;
437
+ this.asyncCreateShaderModule = asyncCreateShaderModule;
438
+ this.registerCube = registerCube;
439
+ this.caps = caps;
440
+ }
441
+
442
+ /**
443
+ * Bind the IBL projection to the same opaque RhiDevice used by assembly.
444
+ * This is deliberately separate from the legacy-compatible mipmap fixture
445
+ * seam: unit fixtures that only exercise ordinary residency do not need an
446
+ * IBL device, while a real IBL projection never accepts a raw GPUDevice.
447
+ */
448
+ configureIblDevice(
449
+ device: RhiDevice,
450
+ shaderModuleFactory: IblShaderModuleFactory | undefined,
451
+ ): void {
452
+ this.iblDevice = device;
453
+ this.iblShaderModuleFactory = shaderModuleFactory;
454
+ }
455
+
456
+ /** Bind residency ownership to the renderer's single DeviceScope tree. */
457
+ bindDeviceScope(scope: DeviceScope): void {
458
+ this.deviceScope = scope;
459
+ }
460
+
461
+ /**
462
+ * Wrap a raw RHI Buffer handle into a GpuBuffer. The wrapper holds a
463
+ * reference to the device so `.destroy()` forwards to
464
+ * `device.destroyBuffer(handle)` (M-3 / w11; plan-strategy D-2).
465
+ *
466
+ * Pre: `configureGpuDevice` has been called. Callers that build a buffer
467
+ * via `device.createBuffer` already gate on a wired device, so this private
468
+ * helper trusts the caller; an unwired call is a programmer error and
469
+ * surfaces as a non-null assertion rather than a structured error.
470
+ */
471
+ private wrapBuf(rawHandle: Buffer): GpuBuffer {
472
+ const device = this.gpuDevice;
473
+ if (device === undefined) {
474
+ throw new Error('GpuResidencyCache.wrapBuf called before configureGpuDevice');
475
+ }
476
+ const resource = new GpuBuffer(device, rawHandle);
477
+ this.deviceScope?._adopt('buffer', resource, (value: GpuBuffer) => {
478
+ if (!value.isDestroyed) value.destroy();
479
+ });
480
+ return resource;
481
+ }
482
+
483
+ /** Wrap a raw RHI Texture handle into a GpuTexture (mirror of wrapBuf). */
484
+ private wrapTex(rawHandle: Texture): GpuTexture {
485
+ const device = this.gpuDevice;
486
+ if (device === undefined) {
487
+ throw new Error('GpuResidencyCache.wrapTex called before configureGpuDevice');
488
+ }
489
+ const resource = new GpuTexture(device, rawHandle);
490
+ this.deviceScope?._adopt('texture', resource, (value: GpuTexture) => {
491
+ if (!value.isDestroyed) value.destroy();
492
+ });
493
+ return resource;
494
+ }
495
+
496
+ /**
497
+ * Walk the three handle maps and destroy every GpuResource, then clear
498
+ * the maps. Called from `Renderer.dispose()` (M-5) as the first step of
499
+ * the dispose chain (plan-strategy D-2: dispose chain walks
500
+ * `gpuStore.destroyAll()` → `graph.drain()` → `instanceBuffers.clear()`
501
+ * → ...).
502
+ *
503
+ * Idempotent: a second call after the maps were cleared is a no-op
504
+ * (architecture-principles §6 idempotency). Each `.destroy()` call
505
+ * routes through the RHI shim's per-handle bookkeeping, so a stray
506
+ * second-destroy on a handle the runtime did not flip to destroyed
507
+ * surfaces as the structured `'destroy-after-destroy'` error from the
508
+ * RHI shim; that error is *not* re-thrown here -- destroyAll is a
509
+ * sweep that tolerates per-handle failures so the dispose chain can
510
+ * make progress (plan-strategy D-3 / D-8).
511
+ */
512
+ destroyAll(): void {
513
+ if (this.meshGpuHandles.size > 0) this.meshResidencyEpoch += 1;
514
+ const scopeOwnsResources = this.deviceScope !== undefined;
515
+ // The cubemap path registers two entries (sourceId + cubeId) sharing one
516
+ // GpuTexture wrapper, so destroyAll must dedupe on the wrapper identity
517
+ // before forwarding `.destroy()` -- otherwise the second call surfaces
518
+ // `'destroy-after-destroy'` from the RHI shim. The `isDestroyed` getter
519
+ // on the wrapper is the dedupe gate (architecture-principles §6).
520
+ const destroyTex = (gpuTex: GpuTexture): void => {
521
+ if (!scopeOwnsResources && !gpuTex.isDestroyed) gpuTex.destroy();
522
+ };
523
+ const destroyBuf = (gpuBuf: GpuBuffer): void => {
524
+ if (!scopeOwnsResources && !gpuBuf.isDestroyed) gpuBuf.destroy();
525
+ };
526
+
527
+ for (const entry of this.textureGpuHandles.values()) {
528
+ destroyTex(entry.texture);
529
+ }
530
+ if (this.textureGpuHandles.size > 0 || this.samplerGpuHandles.size > 0) {
531
+ this.materialResourceEpoch += 1;
532
+ }
533
+ this.textureGpuHandles.clear();
534
+ this.samplerGpuHandles.clear();
535
+
536
+ for (const entry of this.cubemapGpuHandles.values()) {
537
+ if (entry.texture !== null) destroyTex(entry.texture);
538
+ }
539
+ this.cubemapGpuHandles.clear();
540
+ this.cubemapIdempotentMap.clear();
541
+
542
+ for (const entry of this.meshGpuHandles.values()) {
543
+ destroyBuf(entry.vertexBuffer);
544
+ if (entry.indexBuffer !== null) destroyBuf(entry.indexBuffer);
545
+ }
546
+ this.meshGpuHandles.clear();
547
+ }
548
+
549
+ /**
550
+ * Set the error registry channel (wired by createRenderer). evict /
551
+ * releaseUnreferenced fire structured errors here instead of throwing -
552
+ * sweeps continue past individual failures (feat-20260619 D-1/D-6).
553
+ */
554
+ setErrorRegistry(registry: RhiErrorListenerRegistry): void {
555
+ this.errorRegistry = registry;
556
+ }
557
+
558
+ /**
559
+ * evictTexture / evictMesh / evictCubemap — per-handle evict primitives.
560
+ *
561
+ * Reuses destroyAll's isDestroyed dedup logic + cubemap wrapper
562
+ * shared-dedup (feat-20260619 D-1). Returns {freed, errors} so callers
563
+ * can consume the aggregate result (D-3). Key not present -> no-op
564
+ * returning {freed:0, errors:[]}.
565
+ */
566
+ evictTexture(
567
+ handle: Handle<'TextureAsset', 'shared'>,
568
+ worldId: World | number = 0,
569
+ ): { freed: number; errors: RhiError[] } {
570
+ const id = this.worldKey(handleSlot(handle), worldId);
571
+ const entry = this.textureGpuHandles.get(id);
572
+ if (entry === undefined) return { freed: 0, errors: [] };
573
+
574
+ let freed = 0;
575
+ const errors: RhiError[] = [];
576
+
577
+ if (!entry.texture.isDestroyed) {
578
+ const r = entry.texture.destroy();
579
+ if (r.ok) {
580
+ freed = 1;
581
+ } else {
582
+ errors.push(r.error);
583
+ if (this.errorRegistry) this.errorRegistry.fire(r.error);
584
+ }
585
+ }
586
+
587
+ this.textureGpuHandles.delete(id);
588
+ this.materialResourceEpoch += 1;
589
+ return { freed, errors };
590
+ }
591
+
592
+ evictMesh(
593
+ handle: Handle<'MeshAsset', 'shared'>,
594
+ worldId: World | number = 0,
595
+ ): { freed: number; errors: RhiError[] } {
596
+ const id = this.worldKey(handleSlot(handle), worldId);
597
+ const entry = this.meshGpuHandles.get(id);
598
+ if (entry === undefined) return { freed: 0, errors: [] };
599
+
600
+ let freed = 0;
601
+ const errors: RhiError[] = [];
602
+
603
+ const destroyBuf = (gpuBuf: GpuBuffer): void => {
604
+ if (!gpuBuf.isDestroyed) {
605
+ const r = gpuBuf.destroy();
606
+ if (r.ok) {
607
+ freed += 1;
608
+ } else {
609
+ errors.push(r.error);
610
+ if (this.errorRegistry) this.errorRegistry.fire(r.error);
611
+ }
612
+ }
613
+ };
614
+
615
+ destroyBuf(entry.vertexBuffer);
616
+ if (entry.indexBuffer !== null) destroyBuf(entry.indexBuffer);
617
+
618
+ this.meshGpuHandles.delete(id);
619
+ this.meshResidencyEpoch += 1;
620
+ return { freed: freed > 0 ? 1 : 0, errors };
621
+ }
622
+
623
+ evictCubemap(id: number): { freed: number; errors: RhiError[] } {
624
+ const entry = this.cubemapGpuHandles.get(id);
625
+ if (entry === undefined) return { freed: 0, errors: [] };
626
+
627
+ let freed = 0;
628
+ const errors: RhiError[] = [];
629
+
630
+ // cubemap wrapper shared-dedup (D-1): sourceId and cubeId may share one
631
+ // GpuTexture wrapper. The isDestroyed gate ensures the underlying RHI
632
+ // texture is destroyed at most once, even when both entries are evicted.
633
+ // A 'failed' entry holds no GPU texture (texture:null) — nothing to free.
634
+ if (entry.texture !== null && !entry.texture.isDestroyed) {
635
+ const r = entry.texture.destroy();
636
+ if (r.ok) {
637
+ freed = 1;
638
+ } else {
639
+ errors.push(r.error);
640
+ if (this.errorRegistry) this.errorRegistry.fire(r.error);
641
+ }
642
+ }
643
+
644
+ this.cubemapGpuHandles.delete(id);
645
+ return { freed, errors };
646
+ }
647
+
648
+ /**
649
+ * releaseUnreferenced — iterate the three handle maps and evict entries
650
+ * whose key is NOT in `liveSet`.
651
+ *
652
+ * Iterates Map keys (not liveSet — store has no reverse index, D-8).
653
+ * IDs in liveSet that don't exist in the store are naturally ignored.
654
+ * Empty liveSet -> full release; second call -> no-op (maps empty,
655
+ * evict primitives are key-not-present no-op).
656
+ */
657
+ releaseUnreferenced(liveSet: Set<number>): { freed: number; errors: RhiError[] } {
658
+ let freed = 0;
659
+ const errors: RhiError[] = [];
660
+ let meshResidencyChanged = false;
661
+ let materialResidencyChanged = false;
662
+ const liveSlots = new Set([...liveSet].map((slot) => String(slot)));
663
+ const isLiveKey = (key: string | number): boolean => {
664
+ const slot =
665
+ typeof key === 'number' ? String(key % 0x1000000) : key.slice(key.lastIndexOf(':') + 1);
666
+ return liveSlots.has(slot);
667
+ };
668
+
669
+ for (const key of this.textureGpuHandles.keys()) {
670
+ if (!isLiveKey(key)) {
671
+ const entry = this.textureGpuHandles.get(key);
672
+ if (entry !== undefined) {
673
+ if (!entry.texture.isDestroyed) {
674
+ const r = entry.texture.destroy();
675
+ if (r.ok) {
676
+ freed += 1;
677
+ } else {
678
+ errors.push(r.error);
679
+ if (this.errorRegistry) this.errorRegistry.fire(r.error);
680
+ }
681
+ }
682
+ this.textureGpuHandles.delete(key);
683
+ materialResidencyChanged = true;
684
+ }
685
+ }
686
+ }
687
+
688
+ for (const key of this.samplerGpuHandles.keys()) {
689
+ if (!isLiveKey(key)) {
690
+ this.samplerGpuHandles.delete(key);
691
+ materialResidencyChanged = true;
692
+ }
693
+ }
694
+
695
+ for (const key of this.cubemapGpuHandles.keys()) {
696
+ if (!liveSlots.has(String(key))) {
697
+ const entry = this.cubemapGpuHandles.get(key);
698
+ if (entry !== undefined) {
699
+ // A 'failed' entry holds no GPU texture (texture:null) — nothing to free.
700
+ if (entry.texture !== null && !entry.texture.isDestroyed) {
701
+ const r = entry.texture.destroy();
702
+ if (r.ok) {
703
+ freed += 1;
704
+ } else {
705
+ errors.push(r.error);
706
+ if (this.errorRegistry) this.errorRegistry.fire(r.error);
707
+ }
708
+ }
709
+ this.cubemapGpuHandles.delete(key);
710
+ }
711
+ }
712
+ }
713
+
714
+ for (const key of this.meshGpuHandles.keys()) {
715
+ if (!isLiveKey(key)) {
716
+ const entry = this.meshGpuHandles.get(key);
717
+ if (entry !== undefined) {
718
+ if (!entry.vertexBuffer.isDestroyed) {
719
+ const r = entry.vertexBuffer.destroy();
720
+ if (r.ok) {
721
+ freed += 1;
722
+ } else {
723
+ errors.push(r.error);
724
+ if (this.errorRegistry) this.errorRegistry.fire(r.error);
725
+ }
726
+ }
727
+ if (entry.indexBuffer !== null && !entry.indexBuffer.isDestroyed) {
728
+ const r = entry.indexBuffer.destroy();
729
+ if (r.ok) {
730
+ freed += 1;
731
+ } else {
732
+ errors.push(r.error);
733
+ if (this.errorRegistry) this.errorRegistry.fire(r.error);
734
+ }
735
+ }
736
+ this.meshGpuHandles.delete(key);
737
+ meshResidencyChanged = true;
738
+ }
739
+ }
740
+ }
741
+
742
+ if (meshResidencyChanged) this.meshResidencyEpoch += 1;
743
+ if (materialResidencyChanged) this.materialResourceEpoch += 1;
744
+
745
+ return { freed, errors };
746
+ }
747
+
748
+ /**
749
+ * Prewarm the mipmap pipeline cache for the given texture formats (D-9).
750
+ * Called from `createRenderer.initialization` (already async): builds the one-time
751
+ * mipmap shader module + per-format render pipeline into the mipmap-generator
752
+ * deviceCache so the per-texture mipmap blit at record-stage `ensureResident`
753
+ * is pure synchronous encoder work. A format absent from this list will make
754
+ * the sync `ensureResident` texture arm return a structured RhiError rather
755
+ * than lazily await a build (which would break the sync draw contract).
756
+ */
757
+ async prewarmMipmapPipeline(
758
+ device: RhiDevice,
759
+ formats: readonly TextureFormat[],
760
+ ): Promise<Result<void, RhiError>> {
761
+ const factory = this.asyncCreateShaderModule;
762
+ if (factory === undefined) {
763
+ return err(
764
+ new RhiError({
765
+ code: 'rhi-not-available',
766
+ expected: 'asyncCreateShaderModule wired by configureGpuDevice before prewarm',
767
+ hint: 'call gpuStore.configureGpuDevice(device, packShaderFactory, registerCube) before prewarmMipmapPipeline',
768
+ }),
769
+ );
770
+ }
771
+ for (const format of formats) {
772
+ const res = await getOrCreateMipmapPipeline(device, format, factory);
773
+ if (!res.ok) {
774
+ return err(
775
+ new RhiError({
776
+ code: 'rhi-not-available',
777
+ expected: `mipmap pipeline for format ${format} builds during prewarm`,
778
+ hint: `prewarmMipmapPipeline failed building the mipmap pipeline for ${format}; the format may be unsupported by the device`,
779
+ }),
780
+ );
781
+ }
782
+ }
783
+ return ok(undefined);
784
+ }
785
+
786
+ /**
787
+ * Return the GpuTexture wrapper for a `Handle<TextureAsset>` if it has been
788
+ * made resident, else `undefined`.
789
+ *
790
+ * @internal — test-only seam for cow-survivor / AC-02 integration tests; not
791
+ * part of the engine's public API surface.
792
+ */
793
+ _getTextureGpuTexture(
794
+ handle: Handle<'TextureAsset', 'shared'>,
795
+ worldId: World | number = 0,
796
+ ): GpuTexture | undefined {
797
+ return this.textureGpuHandles.get(this.worldKey(handleSlot(handle), worldId))?.texture;
798
+ }
799
+
800
+ /**
801
+ * Return the GPU texture-view for a `Handle<TextureAsset>` if it has been
802
+ * made resident, else `undefined`.
803
+ */
804
+ getTextureGpuView(
805
+ handle: Handle<'TextureAsset', 'shared'>,
806
+ worldId: World | number = 0,
807
+ ): TextureView | undefined {
808
+ return this.textureGpuHandles.get(this.worldKey(handleSlot(handle), worldId))?.view;
809
+ }
810
+
811
+ /**
812
+ * Lazily materialise and cache a sampler descriptor for a material texture.
813
+ * Samplers are opaque, non-destroyable RHI handles, so disposal only drops
814
+ * this store's cache and lets the device own their lifetime.
815
+ */
816
+ ensureSamplerResident(
817
+ handle: Handle<'SamplerAsset', 'shared'>,
818
+ pod: SamplerAsset,
819
+ worldId: World | number = 0,
820
+ ): Result<Sampler, RhiError> {
821
+ const key = this.worldKey(handleSlot(handle), worldId);
822
+ const existing = this.samplerGpuHandles.get(key);
823
+ if (existing !== undefined) return ok(existing);
824
+ const device = this.gpuDevice;
825
+ if (device === undefined) {
826
+ return err(
827
+ new RhiError({
828
+ code: 'rhi-not-available',
829
+ expected: 'GPU device configured before SamplerAsset residency',
830
+ hint: 'call gpuStore.configureGpuDevice before drawing a material with a sampler',
831
+ }),
832
+ );
833
+ }
834
+ const { kind: _kind, ...descriptor } = pod;
835
+ const created = device.createSampler(descriptor);
836
+ if (!created.ok) return created;
837
+ this.samplerGpuHandles.set(key, created.value);
838
+ this.materialResourceEpoch += 1;
839
+ return created;
840
+ }
841
+
842
+ /**
843
+ * Return the GpuTexture wrapper for the cubemap, or `undefined` if not
844
+ * uploaded yet. Consumers that need the raw RHI Texture handle (e.g.
845
+ * `device.createTextureView` arguments) read `.handle` on the wrapper;
846
+ * `.destroy()` routes through the destroy chain (M-3 / w11).
847
+ */
848
+ getCubemapGpuTexture(handle: Handle<'EquirectAsset', 'shared'>): GpuTexture | undefined {
849
+ // A 'failed' entry has texture:null; normalise to undefined (not resident).
850
+ return this.cubemapGpuHandles.get(handleSlot(handle))?.texture ?? undefined;
851
+ }
852
+
853
+ /** Return the full-cube texture view, or `undefined` if not uploaded yet. */
854
+ getCubemapGpuView(handle: Handle<'EquirectAsset', 'shared'>): TextureView | undefined {
855
+ return this.cubemapGpuHandles.get(handleSlot(handle))?.view;
856
+ }
857
+
858
+ /** Return per-face 2D views (6 faces), or `undefined` if not uploaded yet. */
859
+ getCubemapFaceViews(
860
+ handle: Handle<'EquirectAsset', 'shared'>,
861
+ ): readonly TextureView[] | undefined {
862
+ return this.cubemapGpuHandles.get(handleSlot(handle))?.faceViews;
863
+ }
864
+
865
+ /**
866
+ * Query the projection status for an equirect source handle (D-3 SSOT: the
867
+ * store's CubemapGpuEntry is the single authority). Returns `undefined` when
868
+ * no projection has been launched for this source yet (the record arm reads
869
+ * this to decide whether to fire the lazy projection; feat-20260630 M3 / w18):
870
+ * - undefined -> no entry: caller may launch projection (fire-and-forget)
871
+ * - 'pending' -> projection launched, not complete: bind white-cube fallback
872
+ * - 'ready' -> projected cube + IBL views live: bind real IBL
873
+ * - 'failed' -> projection errored: bind white fallback, do NOT retry (R-2)
874
+ */
875
+ getCubemapStatus(
876
+ handle: Handle<'EquirectAsset', 'shared'>,
877
+ ): 'pending' | 'ready' | 'failed' | undefined {
878
+ return this.cubemapGpuHandles.get(handleSlot(handle))?.status;
879
+ }
880
+
881
+ /**
882
+ * Return the GPU vertex / index buffer handles + index count for a
883
+ * `Handle<MeshAsset>` if resident, else `undefined`. Consumers treat this
884
+ * as the canonical "mesh asset has GPU residency" probe.
885
+ */
886
+ getMeshGpuHandles(
887
+ handle: Handle<'MeshAsset', 'shared'>,
888
+ worldId: World | number = 0,
889
+ ): MeshGpuHandles | undefined {
890
+ return this.meshGpuHandles.get(this.worldKey(handleSlot(handle), worldId));
891
+ }
892
+
893
+ /**
894
+ * Drop one mutable shared mesh's GPU copy after ECS publishes `markChanged`.
895
+ * The next record pass resolves the same POD and rebuilds residency through
896
+ * the normal pull path, so ECS remains the payload authority.
897
+ */
898
+ invalidateMesh(handle: number, worldId: World | number = 0): void {
899
+ // SharedRefMutation publishes the encoded handle value, while the cache
900
+ // key is derived from the decoded slot. Keep the invalidation path aligned
901
+ // with getMeshGpuHandles/ensureResident so an in-place ECS payload change
902
+ // cannot leave the old VBO resident.
903
+ const key = this.worldKey(handleSlot(toShared<'MeshAsset'>(handle)), worldId);
904
+ const entry = this.meshGpuHandles.get(key);
905
+ if (entry === undefined) return;
906
+ entry.vertexBuffer.destroy();
907
+ if (entry.indexBuffer !== null) entry.indexBuffer.destroy();
908
+ this.meshGpuHandles.delete(key);
909
+ this.meshResidencyEpoch += 1;
910
+ }
911
+
912
+ /**
913
+ * Pull-model residency: build the GPU resource for `handle` from the
914
+ * caller-provided POD on a miss, return the cached handles on a hit (D-2).
915
+ *
916
+ * - mesh POD -> synchronous vertex / index buffer upload (mirrors the
917
+ * pre-extraction `uploadMeshById`).
918
+ * - texture POD -> synchronous texture upload + mipmap blit (D-9). The
919
+ * mipmap pipeline MUST have been prewarmed via `prewarmMipmapPipeline`;
920
+ * an un-prewarmed format returns a structured RhiError (never awaits).
921
+ *
922
+ * Cubemap is NOT routed here -- it is an eager user call
923
+ * (`_uploadCubemapFromEquirect`). Builtin meshes are NOT routed here either
924
+ * (createRenderer step-3 owns them; D-1).
925
+ */
926
+ ensureResident(
927
+ handle: Handle<'MeshAsset', 'shared'>,
928
+ pod: TypesMeshAsset,
929
+ worldId?: World | number,
930
+ ): Result<MeshGpuHandles, RhiError | AssetError | ImageError>;
931
+ ensureResident(
932
+ handle: Handle<'TextureAsset', 'shared'>,
933
+ pod: TextureAsset,
934
+ worldId?: World | number,
935
+ ): Result<TextureGpuEntry, RhiError | AssetError | ImageError>;
936
+ ensureResident(
937
+ handle: Handle<'MeshAsset', 'shared'> | Handle<'TextureAsset', 'shared'>,
938
+ pod: TypesMeshAsset | TextureAsset,
939
+ worldId: World | number = 0,
940
+ ): Result<TextureGpuEntry | MeshGpuHandles, RhiError | AssetError | ImageError> {
941
+ const id = handleSlot(handle);
942
+ // Hit: O(1) cache lookup, never re-projects (AC-09 -- deriveRenderData*
943
+ // runs only on a miss). The miss arms below dispatch on `pod.kind` with NO
944
+ // default: only the two GPU-resource kinds reachable here (mesh / texture)
945
+ // have arms, so a third reachable kind would surface as a `tsc -b`
946
+ // exhaustiveness error at the switch rather than a silent fallthrough
947
+ // (AC-06; the cube-texture kind is the eager `_uploadCubemapFromEquirect`
948
+ // path, not routed here).
949
+ switch (pod.kind) {
950
+ case 'mesh': {
951
+ const key = this.worldKey(id, worldId);
952
+ const existing = this.meshGpuHandles.get(key);
953
+ if (existing !== undefined) return ok(existing);
954
+ const projected = deriveRenderDataMesh(pod);
955
+ if (!projected.ok) return projected;
956
+ return this.uploadMeshById(key, pod, projected.value);
957
+ }
958
+ case 'texture': {
959
+ const texKey = this.worldKey(id, worldId);
960
+ const existing = this.textureGpuHandles.get(texKey);
961
+ if (existing !== undefined) return ok(existing);
962
+ const projected = deriveRenderDataTexture(pod);
963
+ if (!projected.ok) return projected;
964
+ const decoded: DecodedImage = decodedFromTexture(pod);
965
+ return this.uploadTextureSync(
966
+ handle as Handle<'TextureAsset', 'shared'>,
967
+ pod,
968
+ decoded,
969
+ projected.value,
970
+ worldId,
971
+ );
972
+ }
973
+ }
974
+ }
975
+
976
+ /**
977
+ * Eager public surface: upload decoded image bytes into a GPU texture and
978
+ * cache (texture + view) under the handle. The TextureAsset POD supplies the
979
+ * GPU format (D-2: caller provides POD, store never reaches a registry); the
980
+ * DecodedImage supplies the pixel bytes + colorSpace. Retains the async
981
+ * signature so callers that have not prewarmed can drive a mipmap build here
982
+ * (the one async source).
983
+ */
984
+ async uploadTexture(
985
+ handle: Handle<'TextureAsset', 'shared'>,
986
+ pod: TextureAsset,
987
+ decoded: DecodedImage,
988
+ worldId: World | number = 0,
989
+ ): Promise<Result<void, AssetError | ImageError | RhiError>> {
990
+ const device = this.gpuDevice;
991
+ const projected = deriveRenderDataTexture(pod);
992
+ if (!projected.ok) return projected;
993
+ const prepared = this.prepareTextureUpload(handle, pod, decoded, projected.value);
994
+ if (!prepared.ok) return prepared;
995
+ if (device === undefined) return ok(undefined);
996
+ const { tex, levels, gpuTexture } = prepared.value;
997
+ const cacheKey = this.worldKey(handleSlot(handle), worldId);
998
+ if (gpuTexture === undefined) return ok(undefined);
999
+
1000
+ if (!projected.value.compressed && decoded.mipmap === true && levels > 1) {
1001
+ const factory = this.asyncCreateShaderModule;
1002
+ if (factory === undefined) {
1003
+ return err(
1004
+ new RhiError({
1005
+ code: 'rhi-not-available',
1006
+ expected: 'asyncCreateShaderModule wired by configureGpuDevice',
1007
+ hint: 'GpuResidencyCache.uploadTexture mipmap branch needs a shader-module factory; rhi-webgpu / rhi-wgpu shims expose it via pack.createShaderModule. Explicit-rhi instances must surface the factory on the RhiInstance.',
1008
+ }),
1009
+ );
1010
+ }
1011
+ const pipelineRes = await getOrCreateMipmapPipeline(device, tex.format, factory);
1012
+ if (!pipelineRes.ok) {
1013
+ return err(
1014
+ new RhiError({
1015
+ code: 'rhi-not-available',
1016
+ expected: `mipmap pipeline for format ${tex.format} builds`,
1017
+ hint: 'GpuResidencyCache.uploadTexture could not build the mipmap pipeline',
1018
+ }),
1019
+ );
1020
+ }
1021
+ const blitRes = blitMipmapsSync(device, gpuTexture, {
1022
+ format: tex.format,
1023
+ width: tex.width,
1024
+ height: tex.height,
1025
+ levels,
1026
+ });
1027
+ if (!blitRes.ok) return blitRes as Result<void, RhiError>;
1028
+ }
1029
+
1030
+ const viewRes = device.createTextureView(gpuTexture, {
1031
+ label: `texture-view-${cacheKey}`,
1032
+ dimension: '2d',
1033
+ });
1034
+ if (!viewRes.ok) return viewRes;
1035
+ const previous = this.textureGpuHandles.get(cacheKey);
1036
+ const next = {
1037
+ texture: this.wrapTex(gpuTexture),
1038
+ view: viewRes.value,
1039
+ };
1040
+ this.textureGpuHandles.set(cacheKey, next);
1041
+ this.materialResourceEpoch += 1;
1042
+ if (
1043
+ previous !== undefined &&
1044
+ previous.texture !== next.texture &&
1045
+ !previous.texture.isDestroyed
1046
+ ) {
1047
+ previous.texture.destroy();
1048
+ }
1049
+ return ok(undefined);
1050
+ }
1051
+
1052
+ /**
1053
+ * Synchronous texture upload used by `ensureResident` (D-9). Identical to
1054
+ * `uploadTexture` except the mipmap pipeline is read from the prewarmed
1055
+ * cache via the synchronous `blitMipmapsSync` -- an un-prewarmed format
1056
+ * returns a structured RhiError instead of awaiting a build.
1057
+ */
1058
+ private uploadTextureSync(
1059
+ handle: Handle<'TextureAsset', 'shared'>,
1060
+ pod: TextureAsset,
1061
+ decoded: DecodedImage,
1062
+ renderData: TextureRenderData,
1063
+ worldId: World | number = 0,
1064
+ ): Result<TextureGpuEntry, AssetError | ImageError | RhiError> {
1065
+ const device = this.gpuDevice;
1066
+ const prepared = this.prepareTextureUpload(handle, pod, decoded, renderData);
1067
+ if (!prepared.ok) return prepared;
1068
+ if (device === undefined || prepared.value.gpuTexture === undefined) {
1069
+ return err(
1070
+ new RhiError({
1071
+ code: 'rhi-not-available',
1072
+ expected: 'GpuResidencyCache.configureGpuDevice wired before ensureResident',
1073
+ hint: 'a texture ensureResident ran before the GPU device was wired; call gpuStore.configureGpuDevice in createRenderer',
1074
+ }),
1075
+ );
1076
+ }
1077
+ const { tex, levels, gpuTexture } = prepared.value;
1078
+ const cacheKey = this.worldKey(handleSlot(handle), worldId);
1079
+
1080
+ if (!renderData.compressed && decoded.mipmap === true && levels > 1) {
1081
+ const blitRes = blitMipmapsSync(device, gpuTexture, {
1082
+ format: tex.format,
1083
+ width: tex.width,
1084
+ height: tex.height,
1085
+ levels,
1086
+ });
1087
+ if (!blitRes.ok) return blitRes as Result<TextureGpuEntry, RhiError>;
1088
+ }
1089
+
1090
+ const viewRes = device.createTextureView(gpuTexture, {
1091
+ label: `texture-view-${cacheKey}`,
1092
+ dimension: '2d',
1093
+ });
1094
+ if (!viewRes.ok) return viewRes;
1095
+ const entry: TextureGpuEntry = { texture: this.wrapTex(gpuTexture), view: viewRes.value };
1096
+ const previous = this.textureGpuHandles.get(cacheKey);
1097
+ this.textureGpuHandles.set(cacheKey, entry);
1098
+ this.materialResourceEpoch += 1;
1099
+ if (
1100
+ previous !== undefined &&
1101
+ previous.texture !== entry.texture &&
1102
+ !previous.texture.isDestroyed
1103
+ ) {
1104
+ previous.texture.destroy();
1105
+ }
1106
+ return ok(entry);
1107
+ }
1108
+
1109
+ /**
1110
+ * Shared texture-upload prelude: format / colorSpace consistency assertion,
1111
+ * GPU texture allocation + writeTexture. Returns `gpuTexture: undefined`
1112
+ * (with ok) when no device is wired (deferred path).
1113
+ */
1114
+ private prepareTextureUpload(
1115
+ handle: Handle<'TextureAsset', 'shared'>,
1116
+ pod: TextureAsset,
1117
+ decoded: DecodedImage,
1118
+ renderData: TextureRenderData,
1119
+ ): Result<
1120
+ { id: number; tex: TextureAsset; levels: number; gpuTexture: Texture | undefined },
1121
+ AssetError | ImageError | RhiError
1122
+ > {
1123
+ const id = handleSlot(handle);
1124
+ // The store does not hold the registry (D-2); the caller-provided POD is
1125
+ // the GPU-format SSOT and `renderData` is its projection (format / usage /
1126
+ // mipLevelCount). `decoded` supplies the pixel bytes + colorSpace; the
1127
+ // assertion below guards the actual upload-source colorSpace against the
1128
+ // projected format (the projection already checked the POD's own colorSpace).
1129
+ const tex = pod;
1130
+ const levels = renderData.mipLevelCount;
1131
+
1132
+ const formatExpected: 'srgb' | 'linear' = renderData.format.endsWith('-srgb')
1133
+ ? 'srgb'
1134
+ : 'linear';
1135
+ if (decoded.colorSpace !== formatExpected) {
1136
+ const detail: ImageErrorDetailFor<'image-format-unsupported'> = {
1137
+ code: 'image-format-unsupported',
1138
+ actualMime: decoded.mime,
1139
+ formatColorSpaceConflict: {
1140
+ format: renderData.format,
1141
+ colorSpace: decoded.colorSpace,
1142
+ expected: formatExpected,
1143
+ },
1144
+ };
1145
+ return err(makeImageError(detail));
1146
+ }
1147
+
1148
+ const device = this.gpuDevice;
1149
+ if (device === undefined) {
1150
+ return ok({ id, tex, levels, gpuTexture: undefined });
1151
+ }
1152
+
1153
+ // WebGL2/wgpu exposes texture limits that are lower than many source
1154
+ // images (for example the 2085px LearnOpenGL Mars texture). Reject the
1155
+ // upload before allocating a handle; binding a texture that the backend
1156
+ // accepted syntactically but cannot use later surfaces only as a generic
1157
+ // invalid-bind-group submit error.
1158
+ const maxDimension = device.limits.maxTextureDimension2D;
1159
+ if (tex.width > maxDimension || tex.height > maxDimension) {
1160
+ return err(
1161
+ makeImageError({
1162
+ code: 'image-dimension-out-of-bounds',
1163
+ requested: { width: tex.width, height: tex.height },
1164
+ limit: maxDimension,
1165
+ }),
1166
+ );
1167
+ }
1168
+
1169
+ const createTexRes = device.createTexture({
1170
+ label: `texture-${id}`,
1171
+ size: {
1172
+ width: renderData.physicalExtent.width,
1173
+ height: renderData.physicalExtent.height,
1174
+ depthOrArrayLayers: 1,
1175
+ },
1176
+ mipLevelCount: levels,
1177
+ sampleCount: 1,
1178
+ dimension: '2d',
1179
+ format: renderData.format,
1180
+ usage: renderData.usage,
1181
+ viewFormats: [],
1182
+ textureBindingViewDimension: undefined,
1183
+ });
1184
+ if (!createTexRes.ok) return createTexRes;
1185
+ const gpuTexture = createTexRes.value;
1186
+ const queue = device.queue;
1187
+
1188
+ if (renderData.compressed) {
1189
+ // feat-20260707 M5 / w36 (AC-08): block-compressed textures carry their
1190
+ // mip chain in `data` (offline-baked -- the GPU cannot generate compressed
1191
+ // mips). Upload each level with its own block-padded bytesPerRow /
1192
+ // rowsPerImage and mip-major byte offset (deriveMipUploadLayout, SSOT
1193
+ // block math). Full-subresource copies retain their logical size, including
1194
+ // non-block-aligned dimensions. queue.writeTexture avoids the 256 B
1195
+ // copyBufferToTexture alignment trap.
1196
+ const layout = deriveMipUploadLayout(renderData.format, tex.width, tex.height, levels);
1197
+ for (const lvl of layout) {
1198
+ const slice = decoded.bytes.subarray(lvl.byteOffset, lvl.byteOffset + lvl.byteLength);
1199
+ const writeRes = queue.writeTexture(
1200
+ { texture: gpuTexture, mipLevel: lvl.level, origin: { x: 0, y: 0, z: 0 } },
1201
+ slice,
1202
+ { offset: 0, bytesPerRow: lvl.bytesPerRow, rowsPerImage: lvl.rowsPerImage },
1203
+ { width: lvl.copyWidth, height: lvl.copyHeight, depthOrArrayLayers: 1 },
1204
+ );
1205
+ if (!writeRes.ok) return writeRes;
1206
+ }
1207
+ return ok({ id, tex, levels, gpuTexture });
1208
+ }
1209
+
1210
+ // Uncompressed base-mip write; higher mips (when mipmap) are GPU-generated by
1211
+ // the caller's blit pass. rowsPerImage is the pixel height for linear RGBA.
1212
+ const bytesPerRow = renderData.bytesPerRow;
1213
+ const writeTextureRes = queue.writeTexture(
1214
+ { texture: gpuTexture, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
1215
+ decoded.bytes,
1216
+ { offset: 0, bytesPerRow, rowsPerImage: tex.height },
1217
+ { width: tex.width, height: tex.height, depthOrArrayLayers: 1 },
1218
+ );
1219
+ if (!writeTextureRes.ok) return writeTextureRes;
1220
+
1221
+ return ok({ id, tex, levels, gpuTexture });
1222
+ }
1223
+
1224
+ /**
1225
+ * Project an equirectangular HDR `EquirectAsset` into a GPU cubemap + IBL
1226
+ * precompute. NOT part of the `ensureResident` pull path.
1227
+ *
1228
+ * @internal — feat-20260630 D-3 / F-9: the cubemap projection is engine
1229
+ * internals, not a user surface. AI users declare `Skylight{equirect}` and
1230
+ * the render-system record arm (same package) drives this method per-frame;
1231
+ * the `_` prefix + `@internal` mark it package-internal (lint:internal gate)
1232
+ * so it never appears as a user-facing call. (A cross-file `private` is not
1233
+ * reachable by the record arm; package-internal is the correct visibility.)
1234
+ *
1235
+ * Idempotent: a second call with the same source handle returns the cached
1236
+ * cubemap handle (no second GPU texture). The minted cubemap handle is an
1237
+ * `EquirectAsset` shared ref catalogued via the injected `registerCube` relay
1238
+ * (D-3); the store never imports AssetRegistry.
1239
+ *
1240
+ * Status (D-3): every entry written carries `status`. A failed projection
1241
+ * records `status:'failed'` EXPLICITLY (R-2 / AC-09) so the caller never
1242
+ * retries by inferring "no entry => try again".
1243
+ */
1244
+ async _uploadCubemapFromEquirect(
1245
+ world: World,
1246
+ sourceHandle: Handle<'EquirectAsset', 'shared'>,
1247
+ sourcePod: EquirectAsset,
1248
+ ): Promise<Result<Handle<'EquirectAsset', 'shared'>, AssetError | RhiError>> {
1249
+ const sourceId = handleSlot(sourceHandle);
1250
+
1251
+ const existing = this.cubemapIdempotentMap.get(sourceId);
1252
+ if (existing !== undefined) {
1253
+ return ok(existing);
1254
+ }
1255
+
1256
+ // R-2 / AC-09: a previously-failed projection is recorded EXPLICITLY as a
1257
+ // 'failed' entry. Short-circuit here so the record arm never retries the
1258
+ // upload every frame (the missing-key inference would loop forever).
1259
+ if (this.cubemapGpuHandles.get(sourceId)?.status === 'failed') {
1260
+ return err(
1261
+ new RhiError({
1262
+ code: 'feature-not-enabled',
1263
+ expected: 'a prior cubemap projection for this equirect source did not fail',
1264
+ hint: 'this equirect source already failed projection; the record arm must not retry (R-2). Inspect the original failure on the error channel',
1265
+ }),
1266
+ );
1267
+ }
1268
+
1269
+ // feat-20260630 M3 / w18: a projection already in flight is marked
1270
+ // 'pending' synchronously below (before the first await). A re-entry while
1271
+ // pending short-circuits so the fire-and-forget record arm launches the
1272
+ // async projection exactly once per source (D-4 idempotent; the record arm
1273
+ // calls this every frame until status flips to ready/failed). The pending
1274
+ // entry holds no live GPU resources yet; the record arm binds the white
1275
+ // fallback while pending.
1276
+ if (this.cubemapGpuHandles.get(sourceId)?.status === 'pending') {
1277
+ return ok(sourceHandle);
1278
+ }
1279
+
1280
+ // Record a failed projection explicitly so a re-query short-circuits and the
1281
+ // record arm never retries every frame (R-2 / AC-09). The placeholder
1282
+ // texture wrapper is never bound (a 'failed' entry has no live GPU view).
1283
+ const recordFailed = <E>(error: E): Result<never, E> => {
1284
+ this.cubemapGpuHandles.set(sourceId, {
1285
+ status: 'failed',
1286
+ texture: null,
1287
+ view: undefined,
1288
+ faceViews: [],
1289
+ });
1290
+ return err(error);
1291
+ };
1292
+
1293
+ const registerCube = this.registerCube;
1294
+ if (registerCube === undefined) {
1295
+ return recordFailed(
1296
+ makeAssetError({
1297
+ code: 'asset-not-found',
1298
+ expected: 'GpuResidencyCache.configureGpuDevice wired with registerCube',
1299
+ hint: 'call gpuStore.configureGpuDevice(device, factory, registerCube) before _uploadCubemapFromEquirect',
1300
+ }),
1301
+ );
1302
+ }
1303
+
1304
+ if (sourcePod.kind !== 'equirect') {
1305
+ return recordFailed(
1306
+ makeAssetError({
1307
+ code: 'asset-not-found',
1308
+ expected: `source POD for handle id ${sourceId} is an EquirectAsset`,
1309
+ hint: ASSET_ERROR_HINTS['asset-not-found'],
1310
+ }),
1311
+ );
1312
+ }
1313
+
1314
+ // feat-20260630 M3 / w18: mark the source 'pending' SYNCHRONOUSLY (this runs
1315
+ // before the first `await` below, so the fire-and-forget record arm that
1316
+ // never awaits still observes the pending entry on the next frame's
1317
+ // getCubemapStatus). All sync fail-fast gates (idempotent / failed / pending
1318
+ // / registerCube / caps / kind) have passed, so the projection is committed;
1319
+ // a re-entry while async work is in flight short-circuits on the 'pending'
1320
+ // guard above. The entry holds no live GPU resources yet (record binds the
1321
+ // white fallback while pending); the texture/views are filled in below when
1322
+ // the projection completes (status flips to 'ready'), or replaced by a
1323
+ // 'failed' entry via recordFailed if a later step errors.
1324
+ this.cubemapGpuHandles.set(sourceId, {
1325
+ status: 'pending',
1326
+ texture: null,
1327
+ view: undefined,
1328
+ faceViews: [],
1329
+ });
1330
+
1331
+ // The cubemap projection + IBL precompute consume a 2D-image view of the
1332
+ // source. EquirectAsset mirrors the TextureAsset 2D surface (width / height
1333
+ // / format / data / colorSpace) but has no CPU mip chain, so the derived
1334
+ // texture view declares `mipmap:false` (the IBL prefilter mip chain is a
1335
+ // GPU-side pass, not a CPU-authored level set).
1336
+ const sourceTex: TextureAsset = {
1337
+ kind: 'texture',
1338
+ width: sourcePod.width,
1339
+ height: sourcePod.height,
1340
+ format: sourcePod.format,
1341
+ data: sourcePod.data,
1342
+ colorSpace: sourcePod.colorSpace,
1343
+ mipmap: false,
1344
+ };
1345
+
1346
+ // Project the equirect source POD into the cubemap descriptor (D-5): the
1347
+ // asset-semantic decisions (format / colorSpace validation, square cube
1348
+ // face size, rgba32f->rgba16f narrowing) live in the pure projection; the
1349
+ // resource build (texture / views / IBL precompute) + the byte conversion
1350
+ // stay here in the store.
1351
+ const projected: CubeRenderData | undefined = (() => {
1352
+ const p = deriveRenderDataCubemap(sourceTex);
1353
+ return p.ok ? p.value : undefined;
1354
+ })();
1355
+ if (projected === undefined) {
1356
+ return recordFailed(
1357
+ makeAssetError({
1358
+ code: 'invalid-source-format',
1359
+ expected: "format 'rgba16float' or 'rgba32float' with colorSpace 'linear'",
1360
+ hint: ASSET_ERROR_HINTS['invalid-source-format'],
1361
+ }),
1362
+ );
1363
+ }
1364
+
1365
+ const tex: TextureAsset = projected.needsHalfConversion
1366
+ ? {
1367
+ kind: 'texture',
1368
+ width: sourceTex.width,
1369
+ height: sourceTex.height,
1370
+ format: projected.outputFormat,
1371
+ data: halfFloat.f32ToF16Bytes(
1372
+ sourceTex.data instanceof Uint8ClampedArray
1373
+ ? new Uint8Array(
1374
+ sourceTex.data.buffer,
1375
+ sourceTex.data.byteOffset,
1376
+ sourceTex.data.byteLength,
1377
+ )
1378
+ : sourceTex.data,
1379
+ ),
1380
+ colorSpace: 'linear',
1381
+ mipmap: false,
1382
+ }
1383
+ : sourceTex;
1384
+
1385
+ const cubeFaceSize = projected.cubeFaceSize;
1386
+ // The source equirect stays float for filtering, but WebGL2 cannot render
1387
+ // rgba16float attachments. Downlevel only the precompute outputs to the
1388
+ // universally renderable rgba8 path; the same format is threaded through
1389
+ // the cubemap and all three IBL side textures by IblPipelineCache.
1390
+ const outputFormat = this.caps?.rgba16floatRenderable === false ? 'rgba8unorm' : tex.format;
1391
+
1392
+ // Lazy projection runs from the record arm, where the RHI device is always
1393
+ // wired (research R-5). The IBL path is deliberately independent from the
1394
+ // ordinary mipmap fixture seam: it accepts only the opaque RhiDevice and
1395
+ // its Result-returning shader factory, never a raw GPUDevice or unwrap
1396
+ // fallback.
1397
+ const device = this.iblDevice;
1398
+ const shaderModuleFactory = this.iblShaderModuleFactory;
1399
+ const scope = this.deviceScope;
1400
+ if (device === undefined || shaderModuleFactory === undefined || scope === undefined) {
1401
+ return recordFailed(
1402
+ new RhiError({
1403
+ code: 'rhi-not-available',
1404
+ expected:
1405
+ 'GpuResidencyCache is configured with an RhiDevice, shader factory, and DeviceScope',
1406
+ hint: 'bind the renderer DeviceScope before a cubemap projection; raw GPU fallback is not supported',
1407
+ }),
1408
+ );
1409
+ }
1410
+
1411
+ // Usage flags for the small equirect helper texture (sampled source for the
1412
+ // IBL precompute pass); the cube texture's usage comes from the projection.
1413
+ const gpuTextureResult = device.createTexture({
1414
+ label: `cubemap-${sourceId}`,
1415
+ size: { width: cubeFaceSize, height: cubeFaceSize, depthOrArrayLayers: 6 },
1416
+ mipLevelCount: 1,
1417
+ sampleCount: 1,
1418
+ dimension: '2d',
1419
+ format: outputFormat,
1420
+ usage: projected.cubeUsage,
1421
+ viewFormats: [],
1422
+ textureBindingViewDimension: undefined,
1423
+ });
1424
+ if (!gpuTextureResult.ok) return recordFailed(gpuTextureResult.error);
1425
+ const gpuTexture = gpuTextureResult.value;
1426
+
1427
+ const cubeViewResult = device.createTextureView(gpuTexture, {
1428
+ label: `cubemap-view-${sourceId}`,
1429
+ dimension: 'cube',
1430
+ arrayLayerCount: 6,
1431
+ });
1432
+ if (!cubeViewResult.ok) return recordFailed(cubeViewResult.error);
1433
+ const cubeView = cubeViewResult.value;
1434
+
1435
+ const faceViews: TextureView[] = [];
1436
+ for (let face = 0; face < 6; face++) {
1437
+ const faceViewResult = device.createTextureView(gpuTexture, {
1438
+ label: `cubemap-face-${sourceId}-${face}`,
1439
+ dimension: '2d',
1440
+ baseArrayLayer: face,
1441
+ arrayLayerCount: 1,
1442
+ });
1443
+ if (!faceViewResult.ok) return recordFailed(faceViewResult.error);
1444
+ faceViews.push(faceViewResult.value);
1445
+ }
1446
+
1447
+ // M-3 / w11: a single GpuTexture wrapper is shared between sourceId
1448
+ // and cubeId entries (the underlying RHI handle is the same physical
1449
+ // texture). destroyAll() walks each entry, so attempting to destroy
1450
+ // twice via two wrappers would surface 'destroy-after-destroy' on
1451
+ // the second pass; sharing one wrapper keeps the destroy chain
1452
+ // surfacing exactly one destroy call per physical resource.
1453
+ const gpuTextureWrapper = this.wrapTex(gpuTexture);
1454
+
1455
+ this.cubemapGpuHandles.set(sourceId, {
1456
+ status: 'ready',
1457
+ texture: gpuTextureWrapper,
1458
+ view: cubeView,
1459
+ faceViews,
1460
+ });
1461
+
1462
+ // The minted cubemap handle is a synthetic shared ref (identity token for
1463
+ // the GPU cube residency); its POD is an EquirectAsset placeholder matching
1464
+ // the projected dimensions/format (D-3: the retired cube-texture asset kind
1465
+ // is gone; the relay mints an EquirectAsset shared ref instead).
1466
+ const cubeAsset: EquirectAsset = {
1467
+ kind: 'equirect',
1468
+ width: cubeFaceSize,
1469
+ height: cubeFaceSize,
1470
+ format: tex.format,
1471
+ data: tex.data,
1472
+ colorSpace: 'linear',
1473
+ };
1474
+ const regResult = registerCube(world, cubeAsset);
1475
+ if (!regResult.ok) return recordFailed(regResult.error);
1476
+ const cubeHandle = regResult.value;
1477
+ const cubeId = handleSlot(cubeHandle);
1478
+
1479
+ this.cubemapGpuHandles.set(cubeId, {
1480
+ status: 'ready',
1481
+ texture: gpuTextureWrapper,
1482
+ view: cubeView,
1483
+ faceViews,
1484
+ });
1485
+
1486
+ this.cubemapIdempotentMap.set(sourceId, cubeHandle);
1487
+
1488
+ const pipelinesResult = await createIblPipelines(
1489
+ scope,
1490
+ device,
1491
+ shaderModuleFactory,
1492
+ outputFormat,
1493
+ );
1494
+ if (!pipelinesResult.ok) {
1495
+ return recordFailed(
1496
+ makeAssetError({
1497
+ code: 'ibl-precompute-not-dispatched',
1498
+ expected: pipelinesResult.error.expected,
1499
+ hint: pipelinesResult.error.hint,
1500
+ }),
1501
+ );
1502
+ }
1503
+
1504
+ const equirectTextureResult = device.createTexture({
1505
+ label: `equirect-${sourceId}`,
1506
+ size: { width: tex.width, height: tex.height, depthOrArrayLayers: 1 },
1507
+ mipLevelCount: 1,
1508
+ sampleCount: 1,
1509
+ dimension: '2d',
1510
+ format: tex.format,
1511
+ usage: GPU_TEXTURE_USAGE_TEXTURE_BINDING | GPU_TEXTURE_USAGE_COPY_DST,
1512
+ viewFormats: [],
1513
+ textureBindingViewDimension: undefined,
1514
+ });
1515
+ if (!equirectTextureResult.ok) return recordFailed(equirectTextureResult.error);
1516
+ const equirectGpuTex = equirectTextureResult.value;
1517
+
1518
+ const equirectViewResult = device.createTextureView(equirectGpuTex, {
1519
+ label: `equirect-view-${sourceId}`,
1520
+ dimension: '2d',
1521
+ });
1522
+ if (!equirectViewResult.ok) return recordFailed(equirectViewResult.error);
1523
+ const equirectView = equirectViewResult.value;
1524
+
1525
+ const bytesPerPixel = tex.format === 'rgba32float' ? 16 : tex.format === 'rgba16float' ? 8 : 4;
1526
+ const equirectWriteResult = device.queue.writeTexture(
1527
+ { texture: equirectGpuTex },
1528
+ tex.data,
1529
+ { bytesPerRow: tex.width * bytesPerPixel, rowsPerImage: tex.height },
1530
+ { width: tex.width, height: tex.height, depthOrArrayLayers: 1 },
1531
+ );
1532
+ if (!equirectWriteResult.ok) return recordFailed(equirectWriteResult.error);
1533
+
1534
+ const faceBufferResult = createFaceUniformsBuffer(device);
1535
+ if (!faceBufferResult.ok) return recordFailed(faceBufferResult.error);
1536
+ const faceBuf = faceBufferResult.value;
1537
+ const prefilterBufferResult = createPrefilterUniformsBuffer(device);
1538
+ if (!prefilterBufferResult.ok) return recordFailed(prefilterBufferResult.error);
1539
+ const prefBuf = prefilterBufferResult.value;
1540
+
1541
+ const cubeVertexResult = device.createBuffer({
1542
+ label: 'ibl-cube-verts',
1543
+ size: CUBEMAP_FACE_VERTICES.byteLength,
1544
+ usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
1545
+ mappedAtCreation: false,
1546
+ });
1547
+ if (!cubeVertexResult.ok) return recordFailed(cubeVertexResult.error);
1548
+ const cubeVertex = cubeVertexResult.value;
1549
+
1550
+ const cubeVertexWriteResult = device.queue.writeBuffer(cubeVertex, 0, CUBEMAP_FACE_VERTICES);
1551
+ if (!cubeVertexWriteResult.ok) return recordFailed(cubeVertexWriteResult.error);
1552
+ const faceUniformsResult = writeAllFaceUniforms(device, faceBuf);
1553
+ if (!faceUniformsResult.ok) return recordFailed(faceUniformsResult.error);
1554
+ const prefilterUniformsResult = writeAllPrefilterUniforms(device, prefBuf);
1555
+ if (!prefilterUniformsResult.ok) return recordFailed(prefilterUniformsResult.error);
1556
+
1557
+ const runResult = runIblPrecompute({
1558
+ scope,
1559
+ device,
1560
+ equirectGpuTex,
1561
+ equirectView,
1562
+ cubeGpuTex: gpuTexture,
1563
+ cubeView,
1564
+ cubeFaceViews: faceViews,
1565
+ faceUniformsBuffer: faceBuf,
1566
+ prefilterUniformsBuffer: prefBuf,
1567
+ cubeVertexBuffer: cubeVertex,
1568
+ });
1569
+ if (!runResult.ok) {
1570
+ return recordFailed(
1571
+ makeAssetError({
1572
+ code: 'ibl-precompute-not-dispatched',
1573
+ expected: runResult.error.expected,
1574
+ hint: runResult.error.hint,
1575
+ }),
1576
+ );
1577
+ }
1578
+
1579
+ return ok(cubeHandle);
1580
+ }
1581
+
1582
+ /**
1583
+ * Upload `MeshAsset.vertices` + `MeshAsset.indices` to GPU buffers and cache
1584
+ * the handles under the asset id. Synchronous; mirrors the pre-extraction
1585
+ * `uploadMeshById` byte-for-byte. Returns the cached entry on success.
1586
+ */
1587
+ private uploadMeshById(
1588
+ id: string | number,
1589
+ mesh: TypesMeshAsset,
1590
+ renderData: MeshRenderData,
1591
+ ): Result<MeshGpuHandles, RhiError> {
1592
+ const device = this.gpuDevice;
1593
+ if (device === undefined) {
1594
+ return err(
1595
+ new RhiError({
1596
+ code: 'rhi-not-available',
1597
+ expected: 'GpuResidencyCache.configureGpuDevice wired before mesh ensureResident',
1598
+ hint: 'a mesh ensureResident ran before the GPU device was wired; call gpuStore.configureGpuDevice in createRenderer',
1599
+ }),
1600
+ );
1601
+ }
1602
+
1603
+ const indices = mesh.indices;
1604
+ const indexBytesUnpadded = indices === undefined ? 0 : indices.byteLength;
1605
+ const indexBytes = renderData.indexByteLength;
1606
+
1607
+ const vboResult = device.createBuffer({
1608
+ label: `mesh-${id}-vbo`,
1609
+ size: renderData.vertexByteLength,
1610
+ usage: renderData.vertexUsage,
1611
+ mappedAtCreation: false,
1612
+ });
1613
+ if (!vboResult.ok) return vboResult;
1614
+ const vbo = vboResult.value;
1615
+ // Vertex-only mesh: skip the index buffer entirely. The indexed path below
1616
+ // is unchanged byte-for-byte when `indices` is present.
1617
+ let ibo: Buffer | null = null;
1618
+ if (indices !== undefined) {
1619
+ const iboResult = device.createBuffer({
1620
+ label: `mesh-${id}-ibo`,
1621
+ size: indexBytes,
1622
+ usage: renderData.indexUsage,
1623
+ mappedAtCreation: false,
1624
+ });
1625
+ if (!iboResult.ok) return iboResult;
1626
+ ibo = iboResult.value;
1627
+ }
1628
+
1629
+ const vboWriteResult = device.queue.writeBuffer(vbo, 0, mesh.vertices);
1630
+ if (!vboWriteResult.ok) return err(vboWriteResult.error);
1631
+
1632
+ if (indices !== undefined) {
1633
+ const indexSrc = new Uint8Array(indexBytes);
1634
+ indexSrc.set(new Uint8Array(indices.buffer, indices.byteOffset, indexBytesUnpadded));
1635
+ if (ibo === null)
1636
+ return err(
1637
+ new RhiError({
1638
+ code: 'webgpu-runtime-error',
1639
+ expected: `mesh-${id} index buffer to be allocated before its upload`,
1640
+ hint: 'the indexed mesh upload lost its RHI index-buffer owner',
1641
+ }),
1642
+ );
1643
+ const iboWriteResult = device.queue.writeBuffer(ibo, 0, indexSrc);
1644
+ if (!iboWriteResult.ok) return err(iboWriteResult.error);
1645
+ }
1646
+
1647
+ // The geometry projection already contains every packed attribute,
1648
+ // including skin and additional UV sets. Dividing by a second
1649
+ // attribute-derived count would double-count those fields and produce a
1650
+ // fractional vertex count for otherwise valid meshes.
1651
+ const extraUvSets = countExtraUvSets(mesh.attributes);
1652
+ const entry: MeshGpuHandles = {
1653
+ vertexBuffer: this.wrapBuf(vbo),
1654
+ indexBuffer: ibo === null ? null : this.wrapBuf(ibo),
1655
+ vboBytes: renderData.vertexByteLength,
1656
+ iboBytes: indices === undefined ? 0 : indexBytes,
1657
+ indexCount: renderData.indexCount,
1658
+ indexFormat: renderData.indexFormat,
1659
+ layout: renderData.layoutProjection.attributes.some(
1660
+ (attribute) => attribute.key === 'skinIndex' || attribute.key === 'skinWeight',
1661
+ )
1662
+ ? '18F'
1663
+ : '12F',
1664
+ layoutProjection: renderData.layoutProjection,
1665
+ uvSetCount: 1 + extraUvSets,
1666
+ vertexCount: mesh.vertices.byteLength / renderData.layoutProjection.arrayStride,
1667
+ indexed: indices !== undefined,
1668
+ topology: renderData.submeshes[0]?.topology ?? 'triangle-list',
1669
+ submeshes: renderData.submeshes,
1670
+ };
1671
+ this.meshGpuHandles.set(id, entry);
1672
+ this.meshResidencyEpoch += 1;
1673
+ return ok(entry);
1674
+ }
1675
+
1676
+ /**
1677
+ * Update an existing mesh's GPU buffer data in-place (or expand). The handle
1678
+ * must already be resident. Mirrors the pre-extraction `updateMeshById`.
1679
+ */
1680
+ private updateMeshById(
1681
+ id: string | number,
1682
+ newVertices: Float32Array,
1683
+ newIndices: Uint16Array,
1684
+ submeshes?: readonly Submesh[],
1685
+ ): void {
1686
+ const device = this.gpuDevice;
1687
+ if (device === undefined) return;
1688
+ const entry = this.meshGpuHandles.get(id);
1689
+ if (entry === undefined) return;
1690
+
1691
+ const newVertexBytes = newVertices.byteLength;
1692
+ const indexBytesUnpadded = newIndices.byteLength;
1693
+ const newIndexBytes = ((indexBytesUnpadded + 3) >> 2) << 2;
1694
+
1695
+ // M-3 / w11: read allocation bytes from the entry's vboBytes / iboBytes
1696
+ // (tracked at upload time) instead of reaching into the opaque RHI Buffer
1697
+ // for a `.size` property -- the RHI Buffer interface is spec-aligned and
1698
+ // does NOT expose `.size`; allocation bytes remain derived cache facts.
1699
+ const existingIbo = entry.indexBuffer;
1700
+ const nextSubmeshes = submeshes ?? entry.submeshes;
1701
+ if (
1702
+ existingIbo !== null &&
1703
+ newVertexBytes <= entry.vboBytes &&
1704
+ newIndexBytes <= entry.iboBytes
1705
+ ) {
1706
+ const vboWriteRet = device.queue.writeBuffer(entry.vertexBuffer.handle, 0, newVertices);
1707
+ if (!vboWriteRet.ok) return;
1708
+
1709
+ const indexSrc = new Uint8Array(newIndexBytes);
1710
+ indexSrc.set(new Uint8Array(newIndices.buffer, newIndices.byteOffset, indexBytesUnpadded));
1711
+ const iboWriteRet = device.queue.writeBuffer(existingIbo.handle, 0, indexSrc);
1712
+ if (!iboWriteRet.ok) return;
1713
+
1714
+ this.meshGpuHandles.set(id, {
1715
+ vertexBuffer: entry.vertexBuffer,
1716
+ indexBuffer: existingIbo,
1717
+ vboBytes: entry.vboBytes,
1718
+ iboBytes: entry.iboBytes,
1719
+ indexCount: newIndices.length,
1720
+ indexFormat: 'uint16',
1721
+ layout: '12F',
1722
+ layoutProjection: entry.layoutProjection,
1723
+ uvSetCount: entry.uvSetCount,
1724
+ vertexCount: newVertices.length / PROCEDURAL_FLOATS_PER_VERTEX,
1725
+ indexed: true,
1726
+ topology: entry.topology,
1727
+ submeshes: nextSubmeshes,
1728
+ });
1729
+ this.meshResidencyEpoch += 1;
1730
+ return;
1731
+ }
1732
+
1733
+ const vboResult = device.createBuffer({
1734
+ label: `mesh-${id}-vbo`,
1735
+ size: newVertexBytes,
1736
+ usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
1737
+ mappedAtCreation: false,
1738
+ });
1739
+ if (!vboResult.ok) return;
1740
+ const vbo = vboResult.value;
1741
+ const iboResult = device.createBuffer({
1742
+ label: `mesh-${id}-ibo`,
1743
+ size: newIndexBytes,
1744
+ usage: GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST,
1745
+ mappedAtCreation: false,
1746
+ });
1747
+ if (!iboResult.ok) return;
1748
+ const ibo = iboResult.value;
1749
+
1750
+ const vboWriteResult = device.queue.writeBuffer(vbo, 0, newVertices);
1751
+ if (!vboWriteResult.ok) return;
1752
+
1753
+ const indexSrc = new Uint8Array(newIndexBytes);
1754
+ indexSrc.set(new Uint8Array(newIndices.buffer, newIndices.byteOffset, indexBytesUnpadded));
1755
+ const iboWriteResult = device.queue.writeBuffer(ibo, 0, indexSrc);
1756
+ if (!iboWriteResult.ok) return;
1757
+
1758
+ this.meshGpuHandles.set(id, {
1759
+ vertexBuffer: this.wrapBuf(vbo),
1760
+ indexBuffer: this.wrapBuf(ibo),
1761
+ vboBytes: newVertexBytes,
1762
+ iboBytes: newIndexBytes,
1763
+ indexCount: newIndices.length,
1764
+ indexFormat: 'uint16',
1765
+ layout: '12F',
1766
+ layoutProjection: entry.layoutProjection,
1767
+ uvSetCount: entry.uvSetCount,
1768
+ vertexCount: newVertices.length / PROCEDURAL_FLOATS_PER_VERTEX,
1769
+ indexed: true,
1770
+ topology: entry.topology,
1771
+ submeshes: nextSubmeshes,
1772
+ });
1773
+ this.meshResidencyEpoch += 1;
1774
+
1775
+ // M-3 / w11: release the replaced buffers through their structured
1776
+ // GpuBuffer owner rather than reaching into opaque handles with a
1777
+ // backend-specific property.
1778
+ // `GpuBuffer.destroy()` routes through
1779
+ // `device.destroyBuffer(handle)` (RHI shim is the lifecycle SSOT;
1780
+ // architecture-principles §1 / charter §F1). Errors are swallowed here:
1781
+ // updateMeshById is the in-place reallocation path; the structured fail
1782
+ // surfaces on subsequent buffer use rather than blocking the resize.
1783
+ entry.vertexBuffer.destroy();
1784
+ if (existingIbo !== null) existingIbo.destroy();
1785
+ }
1786
+
1787
+ /**
1788
+ * Public surface for updating an existing unmanaged mesh handle's GPU buffer
1789
+ * data in-place. The handle must have been made resident.
1790
+ */
1791
+ updateMesh(
1792
+ handle: Handle<'MeshAsset', 'shared'>,
1793
+ newVertices: Float32Array,
1794
+ newIndices: Uint16Array,
1795
+ worldId: World | number = 0,
1796
+ submeshes?: readonly Submesh[],
1797
+ ): void {
1798
+ const id = this.worldKey(handleSlot(handle), worldId);
1799
+ this.updateMeshById(id, newVertices, newIndices, submeshes);
1800
+ }
1801
+ }
1802
+
1803
+ /**
1804
+ * Synthesize the `DecodedImage` view the texture-upload prelude consumes from
1805
+ * a `TextureAsset` POD (the record-stage pull path holds only the POD). The
1806
+ * POD's `data` is the pixel bytes; `colorSpace` + `mipmap` drive the
1807
+ * consistency assertion + mip count.
1808
+ */
1809
+ function decodedFromTexture(tex: TextureAsset): DecodedImage {
1810
+ const bytes =
1811
+ tex.data instanceof Uint8ClampedArray
1812
+ ? new Uint8Array(tex.data.buffer, tex.data.byteOffset, tex.data.byteLength)
1813
+ : tex.data;
1814
+ return {
1815
+ bytes,
1816
+ width: tex.width,
1817
+ height: tex.height,
1818
+ mime: 'image/png',
1819
+ colorSpace: tex.colorSpace,
1820
+ mipmap: tex.mipmap,
1821
+ };
1822
+ }