@forgeax/engine-render 0.1.33 → 0.1.35

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 (624) hide show
  1. package/README.md +1062 -127
  2. package/dist/assembly/backend-contract.d.ts +2 -0
  3. package/dist/assembly/backend-contract.d.ts.map +1 -1
  4. package/dist/assembly/barrel-distortion-inspection.d.ts +3 -0
  5. package/dist/assembly/barrel-distortion-inspection.d.ts.map +1 -0
  6. package/dist/assembly/canvas-draw-guard.d.ts +3 -0
  7. package/dist/assembly/canvas-draw-guard.d.ts.map +1 -0
  8. package/dist/assembly/cloud-shadow-entry.d.ts +4 -0
  9. package/dist/assembly/cloud-shadow-entry.d.ts.map +1 -0
  10. package/dist/assembly/device-feature-admission.d.ts +2 -2
  11. package/dist/assembly/dynamic-geometry-host.d.ts +4 -1
  12. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  13. package/dist/assembly/dynamic-geometry-runtime.d.ts +1 -1
  14. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -1
  15. package/dist/assembly/factory.d.ts +1 -1
  16. package/dist/assembly/factory.d.ts.map +1 -1
  17. package/dist/assembly/host-contract.d.ts +6 -5
  18. package/dist/assembly/host-contract.d.ts.map +1 -1
  19. package/dist/assembly/material/assembly.d.ts.map +1 -1
  20. package/dist/assembly/material/pipeline-helpers.d.ts +5 -0
  21. package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -1
  22. package/dist/assembly/material-shader-policy.d.ts +16 -2
  23. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  24. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -1
  25. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
  26. package/dist/assembly/recovery/renderer-recover.d.ts +2 -0
  27. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  28. package/dist/assembly/render-feature-post-processes.d.ts +6 -0
  29. package/dist/assembly/render-feature-post-processes.d.ts.map +1 -0
  30. package/dist/assembly/render-target-host.d.ts +5 -0
  31. package/dist/assembly/render-target-host.d.ts.map +1 -1
  32. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  33. package/dist/assembly/renderer-feature-inspection.d.ts +1 -1
  34. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -1
  35. package/dist/assembly/renderer-frame-transaction.d.ts +12 -0
  36. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  37. package/dist/assembly/renderer-inspection.d.ts +11 -0
  38. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  39. package/dist/assembly/webgpu-pbr-ready.d.ts +1 -0
  40. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -1
  41. package/dist/assembly/webgpu-ready-per-pass.d.ts +39 -0
  42. package/dist/assembly/webgpu-ready-per-pass.d.ts.map +1 -0
  43. package/dist/assembly/webgpu-ready.d.ts +1 -1
  44. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  45. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -1
  46. package/dist/assembly/webgpu-renderer-observation-owner.d.ts +35 -0
  47. package/dist/assembly/webgpu-renderer-observation-owner.d.ts.map +1 -0
  48. package/dist/assembly/webgpu-renderer.d.ts +2 -1
  49. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  50. package/dist/authoring.mjs +2 -4
  51. package/dist/authoring.mjs.map +1 -1
  52. package/dist/barrel-distortion.d.ts +47 -0
  53. package/dist/barrel-distortion.d.ts.map +1 -0
  54. package/dist/chunk-3XUBWRY2.mjs +1842 -0
  55. package/dist/chunk-3XUBWRY2.mjs.map +1 -0
  56. package/dist/{chunk-OO3DRYK7.mjs → chunk-G6HDNGY7.mjs} +2400 -1767
  57. package/dist/chunk-G6HDNGY7.mjs.map +1 -0
  58. package/dist/{chunk-ZDQWGQW7.mjs → chunk-LM5IE7GF.mjs} +4 -4
  59. package/dist/{chunk-ZDQWGQW7.mjs.map → chunk-LM5IE7GF.mjs.map} +1 -1
  60. package/dist/{chunk-VOZWDM3L.mjs → chunk-LNIUIKQ7.mjs} +1514 -498
  61. package/dist/chunk-LNIUIKQ7.mjs.map +1 -0
  62. package/dist/chunk-MXTWKZO4.mjs +3268 -0
  63. package/dist/chunk-MXTWKZO4.mjs.map +1 -0
  64. package/dist/chunk-MZ4C7EHF.mjs +6942 -0
  65. package/dist/chunk-MZ4C7EHF.mjs.map +1 -0
  66. package/dist/chunk-RIPAEGI6.mjs +48 -0
  67. package/dist/chunk-RIPAEGI6.mjs.map +1 -0
  68. package/dist/{chunk-LEAXOMYJ.mjs → chunk-SGPUJW3F.mjs} +117 -5
  69. package/dist/chunk-SGPUJW3F.mjs.map +1 -0
  70. package/dist/{chunk-PA5DRJNI.mjs → chunk-X2KA6WHM.mjs} +29 -4
  71. package/dist/chunk-X2KA6WHM.mjs.map +1 -0
  72. package/dist/{chunk-BLXGONR2.mjs → chunk-XSFB7XXQ.mjs} +51 -4
  73. package/dist/chunk-XSFB7XXQ.mjs.map +1 -0
  74. package/dist/cloud/density.d.ts +55 -0
  75. package/dist/cloud/density.d.ts.map +1 -0
  76. package/dist/cloud/extract.d.ts +25 -0
  77. package/dist/cloud/extract.d.ts.map +1 -0
  78. package/dist/cloud/feature.d.ts +57 -0
  79. package/dist/cloud/feature.d.ts.map +1 -0
  80. package/dist/cloud/frame-context.d.ts +8 -0
  81. package/dist/cloud/frame-context.d.ts.map +1 -0
  82. package/dist/cloud/inspection.d.ts +59 -0
  83. package/dist/cloud/inspection.d.ts.map +1 -0
  84. package/dist/cloud/optics.d.ts +56 -0
  85. package/dist/cloud/optics.d.ts.map +1 -0
  86. package/dist/cloud/parameters.d.ts +73 -0
  87. package/dist/cloud/parameters.d.ts.map +1 -0
  88. package/dist/cloud/resources.d.ts +60 -0
  89. package/dist/cloud/resources.d.ts.map +1 -0
  90. package/dist/cloud/shadow.d.ts +42 -0
  91. package/dist/cloud/shadow.d.ts.map +1 -0
  92. package/dist/cloud/temporal.d.ts +74 -0
  93. package/dist/cloud/temporal.d.ts.map +1 -0
  94. package/dist/components/barrel-distortion.d.ts +13 -0
  95. package/dist/components/barrel-distortion.d.ts.map +1 -0
  96. package/dist/components/cloud-layer.d.ts +29 -0
  97. package/dist/components/cloud-layer.d.ts.map +1 -0
  98. package/dist/components/index.d.ts +2 -0
  99. package/dist/components/index.d.ts.map +1 -1
  100. package/dist/components/instances.d.ts +20 -6
  101. package/dist/components/instances.d.ts.map +1 -1
  102. package/dist/construct-renderer.d.ts +1 -1
  103. package/dist/construct-renderer.d.ts.map +1 -1
  104. package/dist/construct-renderer.mjs +8406 -5785
  105. package/dist/construct-renderer.mjs.map +1 -1
  106. package/dist/device/gpu-residency.d.ts +19 -19
  107. package/dist/device/gpu-residency.d.ts.map +1 -1
  108. package/dist/dynamic-geometry.d.ts +5 -1
  109. package/dist/dynamic-geometry.d.ts.map +1 -1
  110. package/dist/errors/cloud.d.ts +59 -0
  111. package/dist/errors/cloud.d.ts.map +1 -0
  112. package/dist/errors/render.d.ts +20 -5
  113. package/dist/errors/render.d.ts.map +1 -1
  114. package/dist/examples/single-layer-medium-surface.d.ts +47 -0
  115. package/dist/examples/single-layer-medium-surface.d.ts.map +1 -0
  116. package/dist/extract/camera.d.ts.map +1 -1
  117. package/dist/extract/contracts.d.ts +5 -1
  118. package/dist/extract/contracts.d.ts.map +1 -1
  119. package/dist/extract/material-context.d.ts +7 -0
  120. package/dist/extract/material-context.d.ts.map +1 -0
  121. package/dist/features/barrel-distortion.d.ts +29 -0
  122. package/dist/features/barrel-distortion.d.ts.map +1 -0
  123. package/dist/features/error-reporting.d.ts +6 -2
  124. package/dist/features/error-reporting.d.ts.map +1 -1
  125. package/dist/features/host.d.ts +15 -0
  126. package/dist/features/host.d.ts.map +1 -1
  127. package/dist/features/plan.d.ts +10 -0
  128. package/dist/features/plan.d.ts.map +1 -1
  129. package/dist/features/render-graph-contribution.d.ts +5 -0
  130. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  131. package/dist/features/render-graph-raster.d.ts +1 -0
  132. package/dist/features/render-graph-raster.d.ts.map +1 -1
  133. package/dist/features/targets.d.ts +2 -0
  134. package/dist/features/targets.d.ts.map +1 -1
  135. package/dist/features/types.d.ts +48 -0
  136. package/dist/features/types.d.ts.map +1 -1
  137. package/dist/fullscreen-post-process-pass.d.ts +15 -7
  138. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  139. package/dist/glyph-text-layout-system.d.ts +1 -1
  140. package/dist/glyph-text-layout-system.d.ts.map +1 -1
  141. package/dist/gpu-dirty-ranges.d.ts +11 -0
  142. package/dist/gpu-dirty-ranges.d.ts.map +1 -0
  143. package/dist/gpu-driven/batch-topology.d.ts +17 -0
  144. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  145. package/dist/gpu-driven/material-artifacts.d.ts +6 -0
  146. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
  147. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  148. package/dist/gpu-driven/production-raster-material.d.ts +178 -0
  149. package/dist/gpu-driven/production-raster-material.d.ts.map +1 -0
  150. package/dist/gpu-driven/production-raster.d.ts +47 -3
  151. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  152. package/dist/gpu-driven/resource-allocation.d.ts +31 -0
  153. package/dist/gpu-driven/resource-allocation.d.ts.map +1 -0
  154. package/dist/gpu-driven/shadow-ownership.d.ts +14 -0
  155. package/dist/gpu-driven/shadow-ownership.d.ts.map +1 -0
  156. package/dist/gpu-driven/view-gpu.d.ts +26 -2
  157. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  158. package/dist/gpu-scene.d.ts +5 -0
  159. package/dist/gpu-scene.d.ts.map +1 -1
  160. package/dist/hdrp-buffers.d.ts +2 -2
  161. package/dist/hdrp-buffers.d.ts.map +1 -1
  162. package/dist/ibl/frame-resources.d.ts.map +1 -1
  163. package/dist/ibl/skylight-bind-group.d.ts +14 -5
  164. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  165. package/dist/index.d.ts +30 -7
  166. package/dist/index.d.ts.map +1 -1
  167. package/dist/index.mjs +780 -9
  168. package/dist/index.mjs.map +1 -1
  169. package/dist/inspection-types.d.ts +135 -1
  170. package/dist/inspection-types.d.ts.map +1 -1
  171. package/dist/instance-buffer-cache.d.ts +4 -4
  172. package/dist/instance-buffer-cache.d.ts.map +1 -1
  173. package/dist/instances.d.ts +47 -100
  174. package/dist/instances.d.ts.map +1 -1
  175. package/dist/internal.mjs +7 -8
  176. package/dist/internal.mjs.map +1 -1
  177. package/dist/pbr-pipeline.d.ts +22 -4
  178. package/dist/pbr-pipeline.d.ts.map +1 -1
  179. package/dist/pipeline/single-layer-medium-passes.d.ts +73 -0
  180. package/dist/pipeline/single-layer-medium-passes.d.ts.map +1 -0
  181. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  182. package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
  183. package/dist/pipeline/standard-output/lut-gpu.d.ts +2 -2
  184. package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -1
  185. package/dist/pipeline/standard-output/resources.d.ts +2 -2
  186. package/dist/pipeline/standard-output/resources.d.ts.map +1 -1
  187. package/dist/pipeline/standard-output/types.d.ts +4 -2
  188. package/dist/pipeline/standard-output/types.d.ts.map +1 -1
  189. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  190. package/dist/pipeline/standard-post.d.ts.map +1 -1
  191. package/dist/pipeline-builder.d.ts +1 -1
  192. package/dist/pipeline-builder.d.ts.map +1 -1
  193. package/dist/plugin.d.ts.map +1 -1
  194. package/dist/prepare/prepared-graphics-resolver.d.ts +2 -0
  195. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  196. package/dist/publication/contract.d.ts +77 -0
  197. package/dist/publication/contract.d.ts.map +1 -0
  198. package/dist/publication/dependencies.d.ts +7 -0
  199. package/dist/publication/dependencies.d.ts.map +1 -0
  200. package/dist/publication/prepare-geometry.d.ts +9 -0
  201. package/dist/publication/prepare-geometry.d.ts.map +1 -0
  202. package/dist/publication/programs.d.ts +5 -0
  203. package/dist/publication/programs.d.ts.map +1 -0
  204. package/dist/publication/publisher.d.ts +33 -0
  205. package/dist/publication/publisher.d.ts.map +1 -0
  206. package/dist/publication/receiver.d.ts +29 -0
  207. package/dist/publication/receiver.d.ts.map +1 -0
  208. package/dist/publication/resource-scope.d.ts +18 -0
  209. package/dist/publication/resource-scope.d.ts.map +1 -0
  210. package/dist/publication/targets.d.ts +33 -0
  211. package/dist/publication/targets.d.ts.map +1 -0
  212. package/dist/publication/video.d.ts +5 -0
  213. package/dist/publication/video.d.ts.map +1 -0
  214. package/dist/record/barrel-distortion-frame.d.ts +15 -0
  215. package/dist/record/barrel-distortion-frame.d.ts.map +1 -0
  216. package/dist/record/dynamic-geometry-consumption.d.ts +10 -2
  217. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -1
  218. package/dist/record/fold-buckets.d.ts +95 -0
  219. package/dist/record/fold-buckets.d.ts.map +1 -0
  220. package/dist/record/fold-instance-buffer.d.ts +6 -0
  221. package/dist/record/fold-instance-buffer.d.ts.map +1 -0
  222. package/dist/record/frame-lighting.d.ts +1 -0
  223. package/dist/record/frame-lighting.d.ts.map +1 -1
  224. package/dist/record/frame-snapshot.d.ts +43 -9
  225. package/dist/record/frame-snapshot.d.ts.map +1 -1
  226. package/dist/record/frame.d.ts +15 -65
  227. package/dist/record/frame.d.ts.map +1 -1
  228. package/dist/record/gpu-draw-receipts.d.ts +11 -0
  229. package/dist/record/gpu-draw-receipts.d.ts.map +1 -0
  230. package/dist/record/gpu-timing.d.ts +8 -0
  231. package/dist/record/gpu-timing.d.ts.map +1 -1
  232. package/dist/record/helpers.d.ts +2 -2
  233. package/dist/record/helpers.d.ts.map +1 -1
  234. package/dist/record/main-pass-geometry.d.ts +22 -6
  235. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  236. package/dist/record/main-pass-material.d.ts +14 -10
  237. package/dist/record/main-pass-material.d.ts.map +1 -1
  238. package/dist/record/main-pass-sprite-draws.d.ts +3 -16
  239. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  240. package/dist/record/main-pass.d.ts +13 -2
  241. package/dist/record/main-pass.d.ts.map +1 -1
  242. package/dist/record/motion-blur-receipt.d.ts +11 -0
  243. package/dist/record/motion-blur-receipt.d.ts.map +1 -0
  244. package/dist/record/prepared-material-bindings.d.ts +3 -3
  245. package/dist/record/prepared-material-bindings.d.ts.map +1 -1
  246. package/dist/record/probe-blend-buffer.d.ts +16 -0
  247. package/dist/record/probe-blend-buffer.d.ts.map +1 -0
  248. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  249. package/dist/record/render-context.d.ts +24 -5
  250. package/dist/record/render-context.d.ts.map +1 -1
  251. package/dist/record/shadow-pass.d.ts +2 -0
  252. package/dist/record/shadow-pass.d.ts.map +1 -1
  253. package/dist/record/skybox-post-pass.d.ts +1 -1
  254. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  255. package/dist/record/sprite-instance-buffer.d.ts +31 -0
  256. package/dist/record/sprite-instance-buffer.d.ts.map +1 -0
  257. package/dist/record/target-capture-graph.d.ts +19 -0
  258. package/dist/record/target-capture-graph.d.ts.map +1 -0
  259. package/dist/record/typed-frame-graph.d.ts +24 -15
  260. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  261. package/dist/record/view-ubo.d.ts +11 -3
  262. package/dist/record/view-ubo.d.ts.map +1 -1
  263. package/dist/record/volume-params.d.ts +9 -0
  264. package/dist/record/volume-params.d.ts.map +1 -0
  265. package/dist/recovery/render-system-candidate.d.ts +14 -4
  266. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  267. package/dist/reflection/record-owner.d.ts.map +1 -1
  268. package/dist/render-contract.d.ts +46 -5
  269. package/dist/render-contract.d.ts.map +1 -1
  270. package/dist/render-graph-primitives.d.ts.map +1 -1
  271. package/dist/render-pipeline.d.ts +103 -2
  272. package/dist/render-pipeline.d.ts.map +1 -1
  273. package/dist/render-system-extract-tail.d.ts +3 -2
  274. package/dist/render-system-extract-tail.d.ts.map +1 -1
  275. package/dist/render-system-extract.d.ts +18 -9
  276. package/dist/render-system-extract.d.ts.map +1 -1
  277. package/dist/render-system-presentation.d.ts +23 -0
  278. package/dist/render-system-presentation.d.ts.map +1 -0
  279. package/dist/render-system-producer-roots.d.ts +10 -0
  280. package/dist/render-system-producer-roots.d.ts.map +1 -0
  281. package/dist/render-system-projections.d.ts +3 -2
  282. package/dist/render-system-projections.d.ts.map +1 -1
  283. package/dist/render-system-residency.d.ts +12 -0
  284. package/dist/render-system-residency.d.ts.map +1 -0
  285. package/dist/render-system.d.ts +22 -21
  286. package/dist/render-system.d.ts.map +1 -1
  287. package/dist/scene/probe-blend.d.ts +27 -0
  288. package/dist/scene/probe-blend.d.ts.map +1 -1
  289. package/dist/scene/render-scene.d.ts +18 -7
  290. package/dist/scene/render-scene.d.ts.map +1 -1
  291. package/dist/scene/render-source.d.ts +42 -0
  292. package/dist/scene/render-source.d.ts.map +1 -0
  293. package/dist/scene/source-systems.d.ts +6 -0
  294. package/dist/scene/source-systems.d.ts.map +1 -0
  295. package/dist/ssao-buffers.d.ts +9 -0
  296. package/dist/ssao-buffers.d.ts.map +1 -1
  297. package/dist/surface/dynamic-input.d.ts +133 -0
  298. package/dist/surface/dynamic-input.d.ts.map +1 -0
  299. package/dist/surface/gpu-driven.d.ts +41 -0
  300. package/dist/surface/gpu-driven.d.ts.map +1 -0
  301. package/dist/surface/submission-observation.d.ts +136 -0
  302. package/dist/surface/submission-observation.d.ts.map +1 -0
  303. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  304. package/dist/systems/skin-palette-types.d.ts +10 -0
  305. package/dist/systems/skin-palette-types.d.ts.map +1 -1
  306. package/dist/systems/transparent-dispatch.d.ts +2 -2
  307. package/dist/systems/transparent-dispatch.d.ts.map +1 -1
  308. package/dist/systems/transparent-sort-config.d.ts +2 -1
  309. package/dist/systems/transparent-sort-config.d.ts.map +1 -1
  310. package/dist/temporal/gpu.d.ts +13 -1
  311. package/dist/temporal/gpu.d.ts.map +1 -1
  312. package/dist/temporal/index.mjs +6 -7
  313. package/dist/temporal/inspection.d.ts.map +1 -1
  314. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  315. package/dist/temporal/target-coverage-attachment.d.ts.map +1 -1
  316. package/dist/transmission/single-layer-medium.d.ts +89 -0
  317. package/dist/transmission/single-layer-medium.d.ts.map +1 -0
  318. package/dist/typed-render-graph-primitives.d.ts +14 -2
  319. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  320. package/dist/volume/component.d.ts +9 -4
  321. package/dist/volume/component.d.ts.map +1 -1
  322. package/dist/volume/extract.d.ts +1 -1
  323. package/dist/volume/extract.d.ts.map +1 -1
  324. package/dist/volume/inspection.d.ts +2 -0
  325. package/dist/volume/inspection.d.ts.map +1 -1
  326. package/dist/volume/passes.d.ts +4 -1
  327. package/dist/volume/passes.d.ts.map +1 -1
  328. package/dist/volume/resources.d.ts.map +1 -1
  329. package/package.json +22 -21
  330. package/src/__tests__/atmosphere-daylight.dawn.test.ts +72 -0
  331. package/src/__tests__/auto-exposure-transaction.integration.test.ts +4 -9
  332. package/src/__tests__/barrel-distortion-lkg.integration.test.ts +353 -0
  333. package/src/__tests__/barrel-distortion.dawn.test.ts +41 -0
  334. package/src/__tests__/barrel-distortion.unit.test.ts +364 -0
  335. package/src/__tests__/construct-renderer-identity.unit.test.ts +49 -0
  336. package/src/__tests__/detached-recovery-graph.unit.test.ts +277 -0
  337. package/src/__tests__/device-feature-admission.unit.test.ts +3 -2
  338. package/src/__tests__/device-loss-fanout.unit.test.ts +36 -1
  339. package/src/__tests__/docs-gate-green.unit.test.ts +112 -1
  340. package/src/__tests__/factory-contract.integration.test.ts +295 -45
  341. package/src/__tests__/feature-depth-input.dawn.test.ts +129 -2
  342. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +14 -3
  343. package/src/__tests__/gpu-dirty-ranges.unit.test.ts +22 -0
  344. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +16 -3
  345. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +12 -0
  346. package/src/__tests__/gpu-driven-material-artifacts.unit.test.ts +143 -0
  347. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +5 -6
  348. package/src/__tests__/gpu-driven-production.integration.test.ts +482 -3
  349. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +6 -5
  350. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +94 -6
  351. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +18 -2
  352. package/src/__tests__/gpu-driven-view.browser.test.ts +5 -8
  353. package/src/__tests__/gpu-driven-view.dawn.test.ts +6 -2
  354. package/src/__tests__/gpu-driven-view.unit.test.ts +1 -1
  355. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +1 -0
  356. package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -4
  357. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +1 -0
  358. package/src/__tests__/gpu-scene.unit.test.ts +119 -9
  359. package/src/__tests__/instance-allocation-failure.unit.test.ts +57 -0
  360. package/src/__tests__/instances-culling.integration.test.ts +10 -36
  361. package/src/__tests__/instances-store.unit.test.ts +97 -305
  362. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  363. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +4 -1
  364. package/src/__tests__/main-pass-material-medium-selection.unit.test.ts +145 -0
  365. package/src/__tests__/material-shader-prewarm.unit.test.ts +59 -0
  366. package/src/__tests__/material-snapshot-mutation.integration.test.ts +39 -1
  367. package/src/__tests__/materials-standard-contract.unit.test.ts +0 -1
  368. package/src/__tests__/morph-culling-reentry.dawn.test.ts +5 -5
  369. package/src/__tests__/morph-visual-evidence.integration.test.ts +8 -7
  370. package/src/__tests__/motion-blur-compute.dawn.test.ts +13 -7
  371. package/src/__tests__/motion-blur.pixel.integration.test.ts +2 -2
  372. package/src/__tests__/nineslice-handle-surface.unit.test.ts +4 -4
  373. package/src/__tests__/pbr-view-layout.unit.test.ts +9 -0
  374. package/src/__tests__/physical-clearcoat.integration.test.ts +66 -8
  375. package/src/__tests__/physical-material-budget.unit.test.ts +40 -0
  376. package/src/__tests__/prepared-graphics-capability.unit.test.ts +66 -2
  377. package/src/__tests__/probe-bindings.unit.test.ts +582 -1
  378. package/src/__tests__/probe-scene-dirty.integration.test.ts +28 -1
  379. package/src/__tests__/receipt-observation-retirement.unit.test.ts +65 -0
  380. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +69 -0
  381. package/src/__tests__/render-error-code-owner.test-d.ts +8 -0
  382. package/src/__tests__/render-feature-diagnostics.unit.test.ts +15 -2
  383. package/src/__tests__/render-feature-error-reporting.unit.test.ts +21 -1
  384. package/src/__tests__/render-feature-prepared-diagnostics.unit.test.ts +10 -1
  385. package/src/__tests__/render-feature-retry.unit.test.ts +12 -1
  386. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +145 -19
  387. package/src/__tests__/render-scene-projection.unit.test.ts +36 -11
  388. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +20 -20
  389. package/src/__tests__/render-target-probe-soak.perf.test.ts +2 -2
  390. package/src/__tests__/render-temporal-environment-consumer.browser.test.ts +3 -3
  391. package/src/__tests__/render-temporal-environment-consumer.dawn.test.ts +5 -2
  392. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +42 -8
  393. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +16 -0
  394. package/src/__tests__/renderer-frame-transaction.integration.test.ts +26 -10
  395. package/src/__tests__/resource-allocation.unit.test.ts +123 -0
  396. package/src/__tests__/single-layer-medium-passes.unit.test.ts +275 -0
  397. package/src/__tests__/skin-motion-regression.unit.test.ts +70 -1
  398. package/src/__tests__/skin-palette-recycling.dawn.test.ts +97 -0
  399. package/src/__tests__/skinned-shadow-caster.test.ts +15 -0
  400. package/src/__tests__/ssr-carrier-public-contract.unit.test.ts +21 -0
  401. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +4 -4
  402. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +2 -2
  403. package/src/__tests__/ssr-gpu-dispatch.ts +1 -1
  404. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +6 -2
  405. package/src/__tests__/standard-output-chain-contract.unit.test.ts +28 -0
  406. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +46 -1
  407. package/src/__tests__/standard-pipeline.integration.test.ts +182 -1
  408. package/src/__tests__/surface-dynamic-buffer-lifecycle.unit.test.ts +506 -0
  409. package/src/__tests__/surface-submission-observation.unit.test.ts +812 -0
  410. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +32 -0
  411. package/src/__tests__/temporal-performance.integration.test.ts +3 -3
  412. package/src/__tests__/visibility-instances.integration.test.ts +7 -19
  413. package/src/__tests__/visibility-producer-matrix.integration.test.ts +9 -20
  414. package/src/__tests__/volume-optics-oracle.unit.test.ts +4 -2
  415. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +30 -20
  416. package/src/__tests__/volumetric-fog-errors.unit.test.ts +4 -3
  417. package/src/__tests__/volumetric-fog-multiple.unit.test.ts +55 -0
  418. package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +1 -1
  419. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +113 -9
  420. package/src/__tests__/volumetric-fog-topology.unit.test.ts +5 -3
  421. package/src/assembly/backend-contract.ts +30 -0
  422. package/src/assembly/barrel-distortion-inspection.ts +22 -0
  423. package/src/assembly/canvas-draw-guard.ts +11 -0
  424. package/src/assembly/cloud-shadow-entry.ts +22 -0
  425. package/src/assembly/device-feature-admission.ts +2 -2
  426. package/src/assembly/dynamic-geometry-host.ts +65 -37
  427. package/src/assembly/dynamic-geometry-runtime.ts +7 -1
  428. package/src/assembly/factory.ts +2 -30
  429. package/src/assembly/host-contract.ts +8 -5
  430. package/src/assembly/material/assembly.ts +25 -13
  431. package/src/assembly/material/pipeline-helpers.ts +53 -12
  432. package/src/assembly/material-shader-policy.ts +55 -0
  433. package/src/assembly/material-shader-prewarm.ts +21 -10
  434. package/src/assembly/recovery/device-loss-fanout.ts +13 -0
  435. package/src/assembly/recovery/renderer-recover.ts +50 -18
  436. package/src/assembly/render-feature-post-processes.ts +54 -0
  437. package/src/assembly/render-target-host.ts +57 -47
  438. package/src/assembly/renderer-facade.ts +7 -2
  439. package/src/assembly/renderer-feature-inspection.ts +8 -2
  440. package/src/assembly/renderer-frame-transaction.ts +23 -0
  441. package/src/assembly/renderer-inspection.ts +28 -0
  442. package/src/assembly/skin-palette-owner.ts +1 -1
  443. package/src/assembly/temporal-post-process-prewarm.ts +3 -3
  444. package/src/assembly/webgpu-pbr-ready.ts +9 -1
  445. package/src/assembly/webgpu-ready-per-pass.ts +104 -0
  446. package/src/assembly/webgpu-ready.ts +116 -62
  447. package/src/assembly/webgpu-renderer-bootstrap.ts +0 -12
  448. package/src/assembly/webgpu-renderer-observation-owner.ts +222 -0
  449. package/src/assembly/webgpu-renderer.ts +303 -313
  450. package/src/assembly/webgpu-vertex-layouts.ts +1 -1
  451. package/src/barrel-distortion.ts +247 -0
  452. package/src/cloud/__tests__/cloud-density-parity.dawn.test.ts +173 -0
  453. package/src/cloud/__tests__/cloud-layer-framebuffer-y.dawn.test.ts +265 -0
  454. package/src/cloud/__tests__/cloud-layer.browser.test.ts +432 -0
  455. package/src/cloud/__tests__/cloud-layer.unit.test.ts +634 -0
  456. package/src/cloud/density.ts +600 -0
  457. package/src/cloud/extract.ts +110 -0
  458. package/src/cloud/feature.ts +1810 -0
  459. package/src/cloud/frame-context.ts +42 -0
  460. package/src/cloud/inspection.ts +130 -0
  461. package/src/cloud/optics.ts +285 -0
  462. package/src/cloud/parameters.ts +255 -0
  463. package/src/cloud/resources.ts +122 -0
  464. package/src/cloud/shadow.ts +167 -0
  465. package/src/cloud/temporal.ts +226 -0
  466. package/src/components/barrel-distortion.ts +46 -0
  467. package/src/components/cloud-layer.ts +47 -0
  468. package/src/components/index.ts +2 -0
  469. package/src/components/instances.ts +18 -45
  470. package/src/components/sprite-instances.ts +1 -1
  471. package/src/construct-renderer.ts +1 -1
  472. package/src/device/gpu-residency.ts +26 -25
  473. package/src/dynamic-geometry.ts +12 -1
  474. package/src/errors/cloud.ts +103 -0
  475. package/src/errors/render.ts +47 -8
  476. package/src/examples/single-layer-medium-surface.ts +59 -0
  477. package/src/extract/camera.ts +15 -1
  478. package/src/extract/contracts.ts +5 -0
  479. package/src/extract/material-context.ts +21 -0
  480. package/src/features/barrel-distortion.ts +172 -0
  481. package/src/features/error-reporting.ts +26 -6
  482. package/src/features/host.ts +128 -19
  483. package/src/features/plan.ts +10 -0
  484. package/src/features/render-graph-contribution.ts +9 -0
  485. package/src/features/render-graph-raster.ts +45 -3
  486. package/src/features/targets.ts +66 -0
  487. package/src/features/types.ts +51 -0
  488. package/src/fullscreen-post-process-pass.ts +41 -9
  489. package/src/glyph-text-layout-system.ts +2 -2
  490. package/src/gpu-dirty-ranges.ts +48 -0
  491. package/src/gpu-driven/batch-topology.ts +53 -0
  492. package/src/gpu-driven/material-artifacts.ts +33 -25
  493. package/src/gpu-driven/prepared-draw.ts +12 -4
  494. package/src/gpu-driven/production-raster-material.ts +1894 -0
  495. package/src/gpu-driven/production-raster.ts +825 -784
  496. package/src/gpu-driven/resource-allocation.ts +142 -0
  497. package/src/gpu-driven/shadow-ownership.ts +71 -0
  498. package/src/gpu-driven/view-gpu.ts +164 -10
  499. package/src/gpu-scene.ts +75 -84
  500. package/src/hdrp-buffers.ts +4 -3
  501. package/src/ibl/frame-resources.ts +0 -1
  502. package/src/ibl/skylight-bind-group.ts +62 -32
  503. package/src/index.ts +205 -9
  504. package/src/inspection-types.ts +162 -0
  505. package/src/instance-buffer-cache.ts +4 -4
  506. package/src/instances.ts +152 -569
  507. package/src/pbr-pipeline.ts +230 -30
  508. package/src/pipeline/single-layer-medium-passes.ts +267 -0
  509. package/src/pipeline/standard-forward-lane.ts +313 -57
  510. package/src/pipeline/standard-output/graph.ts +8 -2
  511. package/src/pipeline/standard-output/lut-gpu.ts +3 -3
  512. package/src/pipeline/standard-output/resources.ts +2 -2
  513. package/src/pipeline/standard-output/types.ts +12 -1
  514. package/src/pipeline/standard-pipeline.ts +291 -18
  515. package/src/pipeline/standard-post.ts +90 -24
  516. package/src/pipeline-builder.ts +1 -1
  517. package/src/plugin.ts +4 -0
  518. package/src/points-lines/__tests__/points-lines.bench.ts +17 -8
  519. package/src/prepare/prepared-graphics-resolver.ts +2 -0
  520. package/src/publication/__tests__/derived-geometry.integration.test.ts +177 -0
  521. package/src/publication/__tests__/features.integration.test.ts +122 -0
  522. package/src/publication/__tests__/programs.unit.test.ts +25 -0
  523. package/src/publication/__tests__/publication.integration.test.ts +370 -0
  524. package/src/publication/__tests__/targets.integration.test.ts +120 -0
  525. package/src/publication/__tests__/video.browser.test.ts +50 -0
  526. package/src/publication/contract.ts +104 -0
  527. package/src/publication/dependencies.ts +54 -0
  528. package/src/publication/prepare-geometry.ts +94 -0
  529. package/src/publication/programs.ts +45 -0
  530. package/src/publication/publisher.ts +618 -0
  531. package/src/publication/receiver.ts +297 -0
  532. package/src/publication/resource-scope.ts +31 -0
  533. package/src/publication/targets.ts +182 -0
  534. package/src/publication/video.ts +33 -0
  535. package/src/record/__tests__/main-pass-geometry.unit.test.ts +30 -0
  536. package/src/record/__tests__/material-bg-cache.unit.test.ts +15 -1
  537. package/src/record/__tests__/sprite-material-abi.unit.test.ts +9 -11
  538. package/src/record/__tests__/temporal-payload.unit.test.ts +1 -1
  539. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +11 -0
  540. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -0
  541. package/src/record/__tests__/video-texture-view.unit.test.ts +9 -9
  542. package/src/record/__tests__/view-ubo-layout.unit.test.ts +4 -3
  543. package/src/record/barrel-distortion-frame.ts +110 -0
  544. package/src/record/dynamic-geometry-consumption.ts +39 -9
  545. package/src/record/fold-buckets.ts +855 -0
  546. package/src/record/fold-instance-buffer.ts +51 -0
  547. package/src/record/frame-lighting.ts +36 -2
  548. package/src/record/frame-snapshot.ts +45 -9
  549. package/src/record/frame.ts +258 -1105
  550. package/src/record/gpu-draw-receipts.ts +38 -0
  551. package/src/record/gpu-timing.ts +48 -5
  552. package/src/record/helpers.ts +2 -2
  553. package/src/record/main-pass-geometry.ts +272 -84
  554. package/src/record/main-pass-material.ts +102 -36
  555. package/src/record/main-pass-sprite-draws.ts +13 -217
  556. package/src/record/main-pass.ts +164 -27
  557. package/src/record/motion-blur-receipt.ts +91 -0
  558. package/src/record/prepared-material-bindings.ts +11 -6
  559. package/src/record/probe-blend-buffer.ts +180 -0
  560. package/src/record/recovery-pipeline.ts +35 -3
  561. package/src/record/render-context.ts +41 -5
  562. package/src/record/shadow-pass.ts +30 -4
  563. package/src/record/skybox-post-pass.ts +28 -4
  564. package/src/record/sprite-instance-buffer.ts +155 -0
  565. package/src/record/target-capture-graph.ts +258 -0
  566. package/src/record/typed-frame-graph.ts +776 -340
  567. package/src/record/view-ubo.ts +27 -4
  568. package/src/record/volume-params.ts +225 -0
  569. package/src/recovery/render-system-candidate.ts +243 -50
  570. package/src/reflection/record-owner.ts +7 -3
  571. package/src/render-contract.ts +54 -5
  572. package/src/render-graph-primitives.ts +7 -1
  573. package/src/render-pipeline.ts +130 -0
  574. package/src/render-system-extract-tail.ts +90 -78
  575. package/src/render-system-extract.ts +76 -76
  576. package/src/render-system-presentation.ts +56 -0
  577. package/src/render-system-producer-roots.ts +96 -0
  578. package/src/render-system-projections.ts +3 -2
  579. package/src/render-system-residency.ts +159 -0
  580. package/src/render-system.ts +421 -408
  581. package/src/scene/probe-blend.ts +96 -4
  582. package/src/scene/render-scene.ts +416 -337
  583. package/src/scene/render-source.ts +90 -0
  584. package/src/scene/source-systems.ts +33 -0
  585. package/src/ssao-buffers.ts +95 -2
  586. package/src/surface/__tests__/dynamic-input.unit.test.ts +374 -0
  587. package/src/surface/__tests__/gpu-driven.unit.test.ts +87 -0
  588. package/src/surface/dynamic-input.ts +658 -0
  589. package/src/surface/gpu-driven.ts +147 -0
  590. package/src/surface/submission-observation.ts +517 -0
  591. package/src/systems/skin-palette-allocator.ts +5 -1
  592. package/src/systems/skin-palette-types.ts +11 -0
  593. package/src/systems/transparent-dispatch.ts +2 -2
  594. package/src/systems/transparent-sort-config.ts +3 -1
  595. package/src/temporal/gpu.ts +101 -17
  596. package/src/temporal/inspection.ts +7 -1
  597. package/src/temporal/standard-scene-data.ts +5 -0
  598. package/src/temporal/target-coverage-attachment.ts +4 -0
  599. package/src/transmission/__tests__/single-layer-medium.unit.test.ts +149 -0
  600. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +12 -11
  601. package/src/transmission/single-layer-medium.ts +322 -0
  602. package/src/typed-render-graph-primitives.ts +70 -2
  603. package/src/volume/component.ts +10 -9
  604. package/src/volume/extract.ts +11 -5
  605. package/src/volume/inspection.ts +3 -0
  606. package/src/volume/passes.ts +87 -4
  607. package/src/volume/resources.ts +2 -1
  608. package/dist/chunk-3RTCJTH4.mjs +0 -1610
  609. package/dist/chunk-3RTCJTH4.mjs.map +0 -1
  610. package/dist/chunk-BLXGONR2.mjs.map +0 -1
  611. package/dist/chunk-ILQ5MEZF.mjs +0 -32
  612. package/dist/chunk-ILQ5MEZF.mjs.map +0 -1
  613. package/dist/chunk-LEAXOMYJ.mjs.map +0 -1
  614. package/dist/chunk-NB2S7ZRX.mjs +0 -1783
  615. package/dist/chunk-NB2S7ZRX.mjs.map +0 -1
  616. package/dist/chunk-NNQL7CB3.mjs +0 -1713
  617. package/dist/chunk-NNQL7CB3.mjs.map +0 -1
  618. package/dist/chunk-OO3DRYK7.mjs.map +0 -1
  619. package/dist/chunk-PA5DRJNI.mjs.map +0 -1
  620. package/dist/chunk-PLAXOOX5.mjs +0 -1907
  621. package/dist/chunk-PLAXOOX5.mjs.map +0 -1
  622. package/dist/chunk-RXZ3RGOU.mjs +0 -53
  623. package/dist/chunk-RXZ3RGOU.mjs.map +0 -1
  624. package/dist/chunk-VOZWDM3L.mjs.map +0 -1
@@ -0,0 +1,855 @@
1
+ import { HANDLE_NINESLICE_QUAD, resolveAssetHandle } from '@forgeax/engine-assets-runtime';
2
+ import { deriveVertexCount } from '@forgeax/engine-geometry';
3
+ import { RhiError } from '@forgeax/engine-rhi';
4
+ import type { MaterialRenderState, MeshAsset } from '@forgeax/engine-types';
5
+ import { handleSlot, toShared } from '@forgeax/engine-types';
6
+ import type { MeshGpuHandles } from '../device/gpu-residency';
7
+ import { materialBindingKey } from '../gpu-driven/material-bindings';
8
+ import { GpuBuffer } from '../gpu-resource';
9
+ import { GPU_BUFFER_USAGE_COPY_DST, GPU_BUFFER_USAGE_VERTEX } from '../gpu-usage';
10
+ import { disposeTransientInstanceBuffers } from '../instance-buffer-cache';
11
+ import type { RenderResourceScope } from '../publication/resource-scope';
12
+ import { renderAssetByGuid } from '../publication/resource-scope';
13
+ import type { DispatchEntry, MaterialSnapshot, RenderableSnapshot } from '../render-system-extract';
14
+ import {
15
+ getTransparentSortConfig,
16
+ TRANSPARENT_SORT_MODE_LAYER_Y,
17
+ TRANSPARENT_SORT_MODE_LAYER_Z,
18
+ } from '../systems/transparent-sort-config';
19
+ import {
20
+ instanceCollectionCacheKey,
21
+ type RenderFrameState,
22
+ type ValidatedRenderable,
23
+ worldEntityKey,
24
+ } from './frame-snapshot';
25
+ import { variantSetFromDefines } from './helpers';
26
+ import {
27
+ buildFoldDispatchPlan,
28
+ cleanPerEntityCache,
29
+ ensureMeshSsboCapacity,
30
+ evaluateFoldBucketUniformCap,
31
+ type FoldBucket,
32
+ type FoldDispatchPlan,
33
+ foldDispatchBuckets,
34
+ incrementFoldedDrawsMetric,
35
+ } from './mesh-ssbo';
36
+ import type { PipelineState, RenderSystemInternals } from './render-context';
37
+
38
+ /**
39
+ * A frame-local material table. Repeated snapshot objects share one UBO slot;
40
+ * each renderable keeps only the slot index used by each authored material.
41
+ */
42
+ export interface MaterialSlotPlan<T extends object> {
43
+ readonly slotIndices: readonly (readonly number[])[];
44
+ readonly slots: readonly T[];
45
+ /** Index of the first renderable that referenced each slot. */
46
+ readonly slotOwners: readonly number[];
47
+ }
48
+
49
+ /**
50
+ * Intern identical material snapshots into one frame-local slot table.
51
+ * Snapshot identity is the extract layer's invalidation token, so this keeps
52
+ * the same correctness boundary while avoiding duplicate payload assembly and
53
+ * upload for repeated scene instances.
54
+ */
55
+ export function buildMaterialSlotPlan<T extends object>(
56
+ materialGroups: readonly (readonly T[])[],
57
+ bindingClass?: (material: T, owner: number) => string,
58
+ ): MaterialSlotPlan<T> {
59
+ const slotByMaterial = new Map<T, Map<string, number>>();
60
+ const slots: T[] = [];
61
+ const slotOwners: number[] = [];
62
+ const slotIndices = materialGroups.map((materials, ownerIndex) =>
63
+ materials.map((material) => {
64
+ const resourceClass = bindingClass?.(material, ownerIndex) ?? '';
65
+ let materialClasses = slotByMaterial.get(material);
66
+ if (materialClasses === undefined) {
67
+ materialClasses = new Map();
68
+ slotByMaterial.set(material, materialClasses);
69
+ }
70
+ const cached = materialClasses.get(resourceClass);
71
+ if (cached !== undefined) return cached;
72
+ const slot = slots.length;
73
+ slots.push(material);
74
+ slotOwners.push(ownerIndex);
75
+ materialClasses.set(resourceClass, slot);
76
+ return slot;
77
+ }),
78
+ );
79
+ return { slotIndices, slots, slotOwners };
80
+ }
81
+
82
+ /**
83
+ * Return the largest complete renderable prefix supported by a shared
84
+ * mesh/material capacity. Mesh rows consume one slot per renderable; material
85
+ * rows use the deduplicated slot indices assigned in first-seen order.
86
+ */
87
+ export function findRenderablePrefixForSlotCapacity(
88
+ materialSlotIndices: readonly (readonly number[])[],
89
+ slotCapacity: number,
90
+ ): number {
91
+ const capacity = Math.max(0, Math.floor(slotCapacity));
92
+ const maxRenderableCount = Math.min(materialSlotIndices.length, capacity);
93
+ let renderableCount = 0;
94
+ while (renderableCount < maxRenderableCount) {
95
+ const slots = materialSlotIndices[renderableCount] ?? [];
96
+ if (slots.some((slot) => slot >= capacity)) break;
97
+ renderableCount += 1;
98
+ }
99
+ return renderableCount;
100
+ }
101
+
102
+ /** Count material slots reachable from a prefix of a first-seen slot plan. */
103
+ export function materialSlotCountForPrefix(
104
+ materialSlotIndices: readonly (readonly number[])[],
105
+ renderableCount: number,
106
+ ): number {
107
+ let maxSlot = -1;
108
+ for (let index = 0; index < renderableCount; index += 1) {
109
+ for (const slot of materialSlotIndices[index] ?? []) maxSlot = Math.max(maxSlot, slot);
110
+ }
111
+ return maxSlot + 1;
112
+ }
113
+
114
+ type MeshSsboCapacityResult = ReturnType<typeof ensureMeshSsboCapacity>;
115
+
116
+ /**
117
+ * Resolve the actual mesh-SSBO ceiling after the shared grow contract ran.
118
+ * A missing grow controller is the legacy/test surface with no renderer-owned
119
+ * ceiling; a failed grow explicitly exposes the pre-grow capacity and must
120
+ * constrain every row written by this frame, including retained shadow rows.
121
+ */
122
+ function meshSsboSlotCapacityAfterEnsure(
123
+ internals: Pick<RenderSystemInternals, 'growMeshSsbo' | 'meshSsboState'>,
124
+ neededSlots: number,
125
+ result: MeshSsboCapacityResult,
126
+ ): number {
127
+ if (internals.growMeshSsbo === undefined) return Number.POSITIVE_INFINITY;
128
+ if (!result.ok) return Math.max(0, result.degradedToSlotCount);
129
+ return internals.meshSsboState?.slotCount ?? neededSlots;
130
+ }
131
+
132
+ /**
133
+ * Keep a row sequence inside the capacity published by the mesh-SSBO owner.
134
+ * The caller has already reserved earlier rows (the primary view), so this
135
+ * helper only accepts the remaining slots for the retained shadow sequence.
136
+ */
137
+ function limitRowsToMeshSsboCapacity<T>(rows: readonly T[], availableSlots: number): readonly T[] {
138
+ if (availableSlots === Number.POSITIVE_INFINITY) return rows;
139
+ return rows.slice(0, Math.max(0, Math.floor(availableSlots)));
140
+ }
141
+
142
+ /**
143
+ * feat-20260704 M3/w18: validate renderable handles + collect the render plan,
144
+ * extracted verbatim from `recordFrame`. Empty `renderables` input or all-
145
+ * unregistered handles both yield an empty result (the Case E clear-pass-only
146
+ * path). Builds per-entry renderState / stencilReference overlays from the
147
+ * transparent-dispatch entries, resolves each MeshFilter.assetHandle through
148
+ * the AssetRegistry + GPU store (with the sprite 9-slice mesh swap), and fires
149
+ * structured `asset-not-registered` errors for handles that fail to resolve.
150
+ *
151
+ * @internal
152
+ */
153
+ export function validateRenderables(
154
+ internals: RenderSystemInternals,
155
+ world: RenderResourceScope,
156
+ // feat-20260709-editor-world-partition ENGINE-fix-round2 (defect 2): the full
157
+ // worlds[] list. Each renderable's mesh handle is a user-tier slot in its OWN
158
+ // world's sharedRefs; resolving against a foreign world either misses
159
+ // (asset-not-registered) or resolves the wrong slot payload. `world` (the
160
+ // resource-owner) is retained as the fallback for renderables whose worldId
161
+ // is out of range (defensive; extractFrames always stamps a valid index).
162
+ worlds: readonly RenderResourceScope[],
163
+ pipelineState: PipelineState,
164
+ frameState: RenderFrameState,
165
+ renderables: readonly RenderableSnapshot[],
166
+ transparentDispatch: readonly DispatchEntry[],
167
+ excludedEntityKeys: ReadonlySet<number> = new Set<number>(),
168
+ worldKeys?: readonly number[],
169
+ ): ValidatedRenderable[] {
170
+ // bug-20260527-renderstate-pipeline-dispatch-gap D-4:
171
+ // build a renderableIndex -> renderState map from dispatch entries
172
+ // so each ValidatedRenderable carries its per-material renderState
173
+ // override without an O(n^2) back-scan in the draw loop.
174
+ const renderStateByRenderableIdx = new Map<number, MaterialRenderState | undefined>();
175
+ // w10: also build a renderableIndex -> stencilReference map from
176
+ // dispatch entries for per-draw setStencilReference calls.
177
+ const stencilRefByRenderableIdx = new Map<number, number | undefined>();
178
+ const variantSetByRenderableIdx = new Map<number, string | undefined>();
179
+ // Keep these three independent maps because each overlay has its own
180
+ // consumer and the last dispatch entry remains authoritative. Populate
181
+ // them in one pass: the old code walked the same dispatch list three times
182
+ // and repeated the same renderableIndex branch / hash writes.
183
+ for (const de of transparentDispatch) {
184
+ const renderableIndex = de.renderableIndex;
185
+ if (renderableIndex === undefined) continue;
186
+ renderStateByRenderableIdx.set(renderableIndex, de.renderState);
187
+ stencilRefByRenderableIdx.set(renderableIndex, de.stencilReference);
188
+ variantSetByRenderableIdx.set(renderableIndex, variantSetFromDefines(de.defines));
189
+ }
190
+ const validated: ValidatedRenderable[] = [];
191
+ for (let rIdx = 0; rIdx < renderables.length; rIdx++) {
192
+ const r = renderables[rIdx];
193
+ if (r === undefined) continue;
194
+ if (excludedEntityKeys.has(worldEntityKey(worldKeys?.[r.worldId] ?? r.worldId, r.entityKey)))
195
+ continue;
196
+ // feat-20260709-editor-world-partition ENGINE-fix-round2 (defect 2): resolve
197
+ // this renderable's mesh against the world it was EXTRACTED from
198
+ // (worlds[r.worldId]) — NOT the single resource-owner `world`. Builtin mesh
199
+ // slots (< BUILTIN_BASE) resolve process-statically regardless of world, so
200
+ // the per-world pick only matters for user-tier handles, but selecting it
201
+ // unconditionally keeps a single code path. Falls back to the resource-owner
202
+ // world if worldId is out of range (defensive; extractFrames always stamps
203
+ // a valid index into worlds[]).
204
+ const renderableWorld = worlds[r.worldId] ?? world;
205
+ const assetRes = resolveAssetHandle<MeshAsset>(
206
+ renderableWorld,
207
+ toShared<'MeshAsset'>(r.assetHandle),
208
+ );
209
+ if (!assetRes.ok) {
210
+ internals.errorRegistry.fire(
211
+ new RhiError({
212
+ code: 'asset-not-registered',
213
+ expected: 'MeshFilter.assetHandle in AssetRegistry',
214
+ hint: 'use HANDLE_CUBE / HANDLE_TRIANGLE imports; custom mesh register path: feat-future-asset-system',
215
+ detail: { assetHandle: r.assetHandle },
216
+ }),
217
+ );
218
+ continue;
219
+ }
220
+ // feat-20260601-device/gpu-residency-extraction M1 (D-1): builtin meshes
221
+ // (slots through HANDLE_NINESLICE_QUAD) keep the createRenderer step-3
222
+ // direct-upload + `pipelineState.meshes` path -- they are NOT routed
223
+ // through `ensureResident`. User-registered meshes pull through the store
224
+ // on first access (the register->upload push was severed in this M1);
225
+ // the POD fetched above (assetRes.value) is passed in, store holds no
226
+ // registry ref (D-2). A first-access miss builds the GPU buffers; later
227
+ // frames hit the O(1) cache.
228
+ const meshAssetHandle = toShared<'MeshAsset'>(r.assetHandle);
229
+ let meshHandles = internals.gpuStore.getMeshGpuHandles(meshAssetHandle, renderableWorld);
230
+ const lodMeshes = r.lods?.map((lod) =>
231
+ renderAssetByGuid<MeshAsset>(renderableWorld, internals.assets, lod.mesh),
232
+ );
233
+ const resolvedLodMeshes =
234
+ lodMeshes?.every((lod): lod is MeshAsset => lod?.kind === 'mesh') === true
235
+ ? lodMeshes
236
+ : undefined;
237
+ if (meshHandles === undefined && r.assetHandle > handleSlot(HANDLE_NINESLICE_QUAD)) {
238
+ const residentRes = internals.gpuStore.ensureResident(
239
+ meshAssetHandle,
240
+ assetRes.value,
241
+ renderableWorld,
242
+ resolvedLodMeshes,
243
+ );
244
+ if (residentRes.ok) {
245
+ meshHandles = residentRes.value;
246
+ } else if (residentRes.error instanceof RhiError) {
247
+ internals.errorRegistry.fire(residentRes.error);
248
+ }
249
+ }
250
+ meshHandles = meshHandles ?? pipelineState.meshes.get(r.assetHandle);
251
+ if (meshHandles === undefined) {
252
+ internals.errorRegistry.fire(
253
+ new RhiError({
254
+ code: 'asset-not-registered',
255
+ expected: 'GPU mesh buffers uploaded for assetHandle',
256
+ hint: 'await renderer.initialization before draw([world], { cameraOwner: 0, resourceOwner: 0 }); ensure AssetRegistry.configureGpuDevice ran so user meshes are uploaded',
257
+ detail: { assetHandle: r.assetHandle },
258
+ }),
259
+ );
260
+ continue;
261
+ }
262
+ // feat-20260527-sprite-nineslice M2 / w11 (plan-strategy section D-2):
263
+ // sprite branch with non-zero `slicesAndMode` (post-w12 paramSnapshot
264
+ // entry name) overrides the user-supplied mesh handle (typically
265
+ // HANDLE_QUAD = 3) with the 16-vertex / 54-index HANDLE_NINESLICE_QUAD
266
+ // (id=5) topology so the vertex shader sees the 4x4 grid required for
267
+ // 9-region anchor mapping. Default slicesAndMode ([0, 0, 0, 0]) keeps
268
+ // the legacy HANDLE_QUAD path; a flip from zero to non-zero on the
269
+ // same entity routes here per-frame so AI users can toggle 9-slice
270
+ // on the fly without re-spawning the entity (charter F1 minimum
271
+ // surface). The HANDLE_NINESLICE_QUAD GPU buffers are seeded by
272
+ // createRenderer step-3.
273
+ //
274
+ // feat-20260625-refactor-sprite-as-transparent-mesh M3 / w13: judgement
275
+ // key migrated from `shadingModel === 'sprite'` to
276
+ // `materialShaderId === 'forgeax::sprite'` (plan-strategy D-10); slices
277
+ // sourced from `paramSnapshot.slicesAndMode` (post-w12 UBO-aligned
278
+ // overlay path).
279
+ //
280
+ // feat-20260624-sprite-lit-shading-model-pure-2d-lighting M1' / t7:
281
+ // sprite-lit shares the sprite paramSchema (5 fields, t4 mirror) so
282
+ // the 9-slices mesh swap applies identically.
283
+ let effectiveMeshHandles = meshHandles;
284
+ if (
285
+ r.material.materialShaderId === 'forgeax::sprite' ||
286
+ r.material.materialShaderId === 'forgeax::sprite-lit'
287
+ ) {
288
+ const slicesArr = r.material.paramSnapshot?.slicesAndMode as readonly number[] | undefined;
289
+ if (
290
+ slicesArr !== undefined &&
291
+ slicesArr.length >= 4 &&
292
+ (slicesArr[0] !== 0 || slicesArr[1] !== 0 || slicesArr[2] !== 0 || slicesArr[3] !== 0)
293
+ ) {
294
+ const nineSliceHandles = pipelineState.meshes.get(handleSlot(HANDLE_NINESLICE_QUAD));
295
+ if (nineSliceHandles !== undefined) {
296
+ effectiveMeshHandles = nineSliceHandles;
297
+ }
298
+ }
299
+ }
300
+ if (r.morph !== undefined) {
301
+ const morphed = prepareMorphMesh(
302
+ internals,
303
+ frameState,
304
+ worldEntityKey(r.worldId, r.entityKey),
305
+ effectiveMeshHandles,
306
+ assetRes.value,
307
+ r.morph,
308
+ );
309
+ if (morphed !== undefined) effectiveMeshHandles = morphed;
310
+ }
311
+ validated.push({
312
+ source: r,
313
+ world: renderableWorld,
314
+ mesh: effectiveMeshHandles,
315
+ renderableIndex: rIdx,
316
+ renderState: renderStateByRenderableIdx.get(rIdx),
317
+ variantSet: variantSetByRenderableIdx.get(rIdx),
318
+ stencilReference: stencilRefByRenderableIdx.get(rIdx),
319
+ });
320
+ }
321
+ return validated;
322
+ }
323
+
324
+ // Keep the primary record-stage name explicit at its call site while sharing
325
+ // the same owner implementation for the complete retained ShadowCaster view.
326
+
327
+ function prepareMorphMesh(
328
+ internals: RenderSystemInternals,
329
+ frameState: RenderFrameState,
330
+ cacheKey: number,
331
+ base: MeshGpuHandles,
332
+ mesh: MeshAsset,
333
+ morph: RenderableSnapshot['morph'],
334
+ ): MeshGpuHandles | undefined {
335
+ if (morph === undefined || frameState.morphBuffers === undefined) return undefined;
336
+ const targets = mesh.morphTargets;
337
+ if (targets === undefined || targets.length !== morph.targetCount) return undefined;
338
+ const position = base.layoutProjection.attributes.find(
339
+ (attribute) => attribute.key === 'position',
340
+ );
341
+ if (
342
+ position === undefined ||
343
+ position.offset !== 0 ||
344
+ position.shaderLocation !== 0 ||
345
+ position.format !== 'float32x3' ||
346
+ position.byteLength !== 12
347
+ ) {
348
+ return undefined;
349
+ }
350
+ const vertexCount = deriveVertexCount(mesh.vertices, base.layoutProjection);
351
+ if (vertexCount === undefined || vertexCount !== base.vertexCount || vertexCount <= 0) {
352
+ return undefined;
353
+ }
354
+ const stride = base.layoutProjection.arrayStride / Float32Array.BYTES_PER_ELEMENT;
355
+
356
+ let active = false;
357
+ for (const weight of morph.weights) {
358
+ if (weight !== 0) {
359
+ active = true;
360
+ break;
361
+ }
362
+ }
363
+ if (!active) {
364
+ frameState.morphBuffers.get(cacheKey)?.buffer.destroy();
365
+ frameState.morphBuffers.delete(cacheKey);
366
+ return undefined;
367
+ }
368
+
369
+ const vertices = new Float32Array(mesh.vertices);
370
+ for (let targetIndex = 0; targetIndex < targets.length; targetIndex += 1) {
371
+ const weight = morph.weights[targetIndex] ?? 0;
372
+ const positions = targets[targetIndex]?.position;
373
+ if (positions === undefined || positions.length !== base.vertexCount * 3) return undefined;
374
+ for (let vertex = 0; vertex < base.vertexCount; vertex += 1) {
375
+ const vertexBase = vertex * stride;
376
+ const positionBase = vertex * 3;
377
+ vertices[vertexBase] = (vertices[vertexBase] ?? 0) + (positions[positionBase] ?? 0) * weight;
378
+ vertices[vertexBase + 1] =
379
+ (vertices[vertexBase + 1] ?? 0) + (positions[positionBase + 1] ?? 0) * weight;
380
+ vertices[vertexBase + 2] =
381
+ (vertices[vertexBase + 2] ?? 0) + (positions[positionBase + 2] ?? 0) * weight;
382
+ }
383
+ }
384
+
385
+ let cached = frameState.morphBuffers.get(cacheKey);
386
+ if (cached === undefined || cached.byteLength !== vertices.byteLength) {
387
+ if (cached !== undefined && !cached.buffer.isDestroyed) {
388
+ const destroyed = cached.buffer.destroy();
389
+ if (!destroyed.ok) internals.errorRegistry.fire(destroyed.error);
390
+ }
391
+ const created = internals.device.createBuffer({
392
+ label: `morph-${cacheKey}-vbo`,
393
+ size: vertices.byteLength,
394
+ usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
395
+ mappedAtCreation: false,
396
+ });
397
+ if (!created.ok) {
398
+ internals.errorRegistry.fire(created.error);
399
+ frameState.morphBuffers.delete(cacheKey);
400
+ return undefined;
401
+ }
402
+ const buffer = new GpuBuffer(internals.device, created.value);
403
+ internals.deviceScope._adopt('buffer', buffer, (owned) => {
404
+ if (!owned.isDestroyed) owned.destroy();
405
+ });
406
+ cached = {
407
+ buffer,
408
+ byteLength: vertices.byteLength,
409
+ };
410
+ frameState.morphBuffers.set(cacheKey, cached);
411
+ }
412
+ const written = internals.device.queue.writeBuffer(cached.buffer.handle, 0, vertices);
413
+ if (!written.ok) {
414
+ internals.errorRegistry.fire(written.error);
415
+ return undefined;
416
+ }
417
+ return { ...base, vertexBuffer: cached.buffer };
418
+ }
419
+
420
+ /**
421
+ * feat-20260704 M3/w18: build the dispatch-ordered render plan, extracted
422
+ * verbatim from `recordFrame`. (1) M3/w26 dispatch-ordered reorder: reorder
423
+ * `validated` to follow the transparent-dispatch order (extract-order fallback
424
+ * for unmatched entries). (2) feat-20260608 mesh-SSBO capacity gate: size the
425
+ * mesh-SSBO + material-UBO pair to the larger of entity vs cumulative
426
+ * material-slot count, truncating on ceiling (graceful degradation). (3)
427
+ * feat-20260622 fold dispatch plan: build + apply the WebGL2 uniform-cap
428
+ * fallback + bump the folded-draws metric.
429
+ *
430
+ * @internal
431
+ */
432
+ export function buildDispatchPlan(
433
+ internals: RenderSystemInternals,
434
+ validated: readonly ValidatedRenderable[],
435
+ transparentDispatch: readonly DispatchEntry[],
436
+ foldBuckets: readonly FoldBucket[],
437
+ shadowValidated: readonly ValidatedRenderable[] = [],
438
+ materialBindingClasses?: ReadonlyMap<string, string>,
439
+ ): {
440
+ validatedOrdered: readonly ValidatedRenderable[];
441
+ shadowValidatedOrdered: readonly ValidatedRenderable[];
442
+ foldDispatchPlan: FoldDispatchPlan | null;
443
+ materialSlotIndices: readonly (readonly number[])[];
444
+ materialSlots: readonly MaterialSnapshot[];
445
+ materialSlotOwners: readonly ValidatedRenderable[];
446
+ materialSlotCount: number;
447
+ } {
448
+ // A production GPU-owned frame has no CPU validated rows by design. The
449
+ // dispatch list can still contain one entry per authored entity, but no
450
+ // CPU plan, material table, or fold bucket is consumed in that lane. Return
451
+ // the empty plan without walking that list so a steady 100k scene remains
452
+ // bounded by the persistent GPU batches rather than per-entity record work.
453
+ if (validated.length === 0 && shadowValidated.length === 0) {
454
+ const meshSsboCapResult = ensureMeshSsboCapacity(internals, shadowValidated.length);
455
+ const meshSsboSlotCapacity = meshSsboSlotCapacityAfterEnsure(
456
+ internals,
457
+ shadowValidated.length,
458
+ meshSsboCapResult,
459
+ );
460
+ return {
461
+ validatedOrdered: [],
462
+ shadowValidatedOrdered: limitRowsToMeshSsboCapacity(shadowValidated, meshSsboSlotCapacity),
463
+ foldDispatchPlan: null,
464
+ materialSlotIndices: [],
465
+ materialSlots: [],
466
+ materialSlotOwners: [],
467
+ materialSlotCount: 0,
468
+ };
469
+ }
470
+ // M3 / w26: dispatch-ordered render. The dispatch list is pre-sorted
471
+ // by queue (ascending, stable) by the extract stage per plan-strategy D-3.
472
+ // Reorder validated renderables to follow the dispatch order, falling
473
+ // back to extract order for renderables with no matching dispatch entry.
474
+ let dispatchOrderMatchesValidated = transparentDispatch.length === validated.length;
475
+ if (dispatchOrderMatchesValidated) {
476
+ for (let i = 0; i < validated.length; i++) {
477
+ if (transparentDispatch[i]?.renderableIndex !== validated[i]?.renderableIndex) {
478
+ dispatchOrderMatchesValidated = false;
479
+ break;
480
+ }
481
+ }
482
+ }
483
+ let validatedOrdered: readonly ValidatedRenderable[] = dispatchOrderMatchesValidated
484
+ ? validated
485
+ : [...validated];
486
+ if (transparentDispatch.length > 0 && !dispatchOrderMatchesValidated) {
487
+ const validatedByRenderableIdx = new Map<number, ValidatedRenderable>();
488
+ const seen = new Set<number>();
489
+ for (const v of validated) {
490
+ validatedByRenderableIdx.set(v.renderableIndex, v);
491
+ }
492
+ const ordered: ValidatedRenderable[] = [];
493
+ for (const de of transparentDispatch) {
494
+ if (de.renderableIndex === undefined) continue;
495
+ const v = validatedByRenderableIdx.get(de.renderableIndex);
496
+ if (v !== undefined && !seen.has(de.renderableIndex)) {
497
+ seen.add(de.renderableIndex);
498
+ ordered.push(v);
499
+ }
500
+ }
501
+ // Append renderables not in the dispatch list (e.g. default-material entities)
502
+ for (const v of validated) {
503
+ if (!seen.has(v.renderableIndex)) {
504
+ ordered.push(v);
505
+ }
506
+ }
507
+ validatedOrdered = ordered;
508
+ }
509
+
510
+ // Shadow-only/off-camera CPU residuals consume the same material UBO as
511
+ // visible draws. Their preparation must not depend on main-view visibility.
512
+ const materialRows = [...validatedOrdered, ...shadowValidated];
513
+ const materialGroups = materialRows.map((entry): readonly MaterialSnapshot[] => {
514
+ const shaderId = entry.source.material.materialShaderId;
515
+ if (shaderId === 'forgeax::sprite' || shaderId === 'forgeax::sprite-lit') {
516
+ return [entry.source.material];
517
+ }
518
+ return entry.source.materials.length > 0 ? entry.source.materials : [entry.source.material];
519
+ });
520
+ const materialSlotPlan = buildMaterialSlotPlan(materialGroups, (material, owner) => {
521
+ const row = materialRows[owner];
522
+ return row === undefined
523
+ ? ''
524
+ : (materialBindingClasses?.get(
525
+ materialBindingKey(row.source, material.materialHandle ?? -1),
526
+ ) ?? '');
527
+ });
528
+ let materialSlotIndices = materialSlotPlan.slotIndices;
529
+ let materialSlots = materialSlotPlan.slots;
530
+ let materialSlotOwners = materialSlotPlan.slotOwners.map((index) => {
531
+ const owner = materialRows[index];
532
+ if (owner === undefined) throw new Error('material slot has no validated owner');
533
+ return owner;
534
+ });
535
+ let materialSlotCount = materialSlots.length;
536
+ let materialRenderableCount = materialRows.length;
537
+
538
+ // feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M3 / T-M3-04:
539
+ // ensure the mesh-SSBO + material-UBO buffer pair is large enough to hold
540
+ // the render plan BEFORE the first per-entity writeBuffer.
541
+ // On `ok:false` the controller has already fired a structured RuntimeError
542
+ // (`mesh-ssbo-ceiling-reached` / `mesh-ssbo-capacity-exceeded`); we truncate
543
+ // the draw list to the largest complete entity prefix whose cumulative
544
+ // material slots fit `degradedToSlotCount` (graceful degradation per
545
+ // plan-strategy D-2): render the subset that fits, discard overflow, no
546
+ // black frame. The helper is idempotent across same-frame re-calls (AC-09)
547
+ // and short-circuits on length=0 / length<=slotCount (boundary table).
548
+ //
549
+ // bug-20260609: feat-20260608 M5 amend made the material UBO indexed by
550
+ // cumulative *material-slot* count (one slot per submesh material),
551
+ // which is >= entity count once an entity carries `materials.length>1`.
552
+ // The mesh + material buffer pair share `slotCount` (single allocator),
553
+ // so we size against the larger of the two requirements: entity count
554
+ // (mesh-SSBO consumer) vs cumulative material-slot count (material-UBO
555
+ // consumer). Sprite entities collapse to 1 slot in the material table,
556
+ // mirroring the same rule (sprite per-submesh OOS-1;
557
+ // post-w13 judgement key migrated to materialShaderId).
558
+ //
559
+ // feat-20260624 M1' / t7: sprite-lit treated identically to sprite
560
+ // for material-slot accounting (paramSchema mirror, t4).
561
+ const neededSlots = Math.max(validatedOrdered.length + shadowValidated.length, materialSlotCount);
562
+ const meshSsboCapResult = ensureMeshSsboCapacity(internals, neededSlots);
563
+ const meshSsboSlotCapacity = meshSsboSlotCapacityAfterEnsure(
564
+ internals,
565
+ neededSlots,
566
+ meshSsboCapResult,
567
+ );
568
+ if (!meshSsboCapResult.ok) {
569
+ // Graceful degradation: the controller reports slots, but this stage
570
+ // consumes entities. Find a complete prefix instead of slicing at the
571
+ // numeric slot count; a multi-material entity can consume several slots.
572
+ const degradedRenderableCount = findRenderablePrefixForSlotCapacity(
573
+ materialSlotIndices,
574
+ meshSsboCapResult.degradedToSlotCount,
575
+ );
576
+ materialRenderableCount = degradedRenderableCount;
577
+ materialSlotCount = materialSlotCountForPrefix(materialSlotIndices, degradedRenderableCount);
578
+ materialSlotIndices = materialSlotIndices.slice(0, degradedRenderableCount);
579
+ materialSlots = materialSlots.slice(0, materialSlotCount);
580
+ materialSlotOwners = materialSlotOwners.slice(0, materialSlotCount);
581
+ validatedOrdered = validatedOrdered.slice(0, degradedRenderableCount);
582
+ }
583
+ const shadowValidatedOrdered = limitRowsToMeshSsboCapacity(
584
+ shadowValidated,
585
+ Math.min(meshSsboSlotCapacity, materialRenderableCount) - validatedOrdered.length,
586
+ );
587
+
588
+ // feat-20260622-chunk-gpu-instancing-sprite-tilemap M1 / w4-record-swap
589
+ // (D-1): build the fold dispatch plan once `validatedOrdered` is final
590
+ // (post truncation by mesh-SSBO capacity gate). The plan re-keys each
591
+ // non-singleton bucket from `renderableIndex` to the validated-ordered
592
+ // index `i` consumed by the dispatch loops; the loops use it to skip
593
+ // non-head bucket members and emit one instanced drawIndexed per
594
+ // bucket head. Empty plan (no fold-eligible buckets) is a byte-
595
+ // identical no-op for the dispatch loops below (charter P3: silent
596
+ // pass-through, no error path).
597
+ let renderableToValidatedIdx: Map<number, number> | null = null;
598
+ let foldDispatchPlan: FoldDispatchPlan | null = null;
599
+ if (foldBuckets.length > 0) {
600
+ renderableToValidatedIdx = new Map<number, number>();
601
+ for (let i = 0; i < validatedOrdered.length; i++) {
602
+ const e = validatedOrdered[i];
603
+ if (e === undefined) continue;
604
+ renderableToValidatedIdx.set(e.renderableIndex, i);
605
+ }
606
+ foldDispatchPlan = buildFoldDispatchPlan(foldBuckets, renderableToValidatedIdx);
607
+
608
+ // feat-20260622 M2 / w11 (D-2 + D-9 + AC-05): WebGL2 uniform-fallback
609
+ // per-bucket instance-count cap. When caps.storageBuffer===false AND
610
+ // a fold bucket carries more than FOLD_UNIFORM_INSTANCE_CAP (128)
611
+ // instances, fire RhiError({code:'instancing-exceeds-uniform-cap'})
612
+ // AND remove the bucket from the dispatch plan so its members fall
613
+ // through to the per-entity drawIndexed exit (the same exit the
614
+ // mode-gate bypass uses — D-9 "shared fallback exit"). The frame
615
+ // stays visually correct (charter proposition 9 graceful
616
+ // degradation: no identity-collapse / black screen) while the cap
617
+ // event surfaces structurally for AI users (proposition 4 explicit
618
+ // failure on .code).
619
+ //
620
+ // Scope discrimination: tilemap-chunk-extract-system encodes
621
+ // Layer.value = (layerOrder<<20) | (chunkIndex & 0xfffff), so a
622
+ // bucket whose head entry carries non-zero low-20-bits is
623
+ // definitively a tilemap-chunk dispatch site. Plain sprite buckets
624
+ // use SPRITE_LAYER_VALUE = layerOrder<<20 (low-20 zero) by the
625
+ // documented convention (apps/hello/asi-world main.ts pattern).
626
+ // The chunkIndex===0 edge case maps to 'sprite' (the helper's
627
+ // default branch) — a one-bucket ambiguity per layerOrder that is
628
+ // acceptable for the AI-user affordance level (the error semantics
629
+ // — "this bucket exceeded the cap" — is the actionable signal;
630
+ // scope=sprite vs tilemap-chunk only refines the recovery hint).
631
+ if (foldDispatchPlan.headBuckets.size > 0 && !internals.device.caps.storageBuffer) {
632
+ const filteredHeads = new Map<number, FoldBucket>(foldDispatchPlan.headBuckets);
633
+ const filteredSkips = new Set<number>(foldDispatchPlan.skipIndices);
634
+ let filteredCount = foldDispatchPlan.foldedBucketCount;
635
+ for (const [headIdx, bucket] of foldDispatchPlan.headBuckets) {
636
+ const scope: 'sprite' | 'tilemap-chunk' =
637
+ (bucket.layer & 0xfffff) !== 0 ? 'tilemap-chunk' : 'sprite';
638
+ const decision = evaluateFoldBucketUniformCap(bucket, internals.device.caps, scope);
639
+ if (decision.fallback && decision.error !== undefined) {
640
+ internals.errorRegistry.fire(decision.error);
641
+ filteredHeads.delete(headIdx);
642
+ filteredCount -= 1;
643
+ for (let j = 1; j < bucket.entries.length; j++) {
644
+ const memberEntry = bucket.entries[j];
645
+ if (memberEntry === undefined) continue;
646
+ const memberValidatedIdx = renderableToValidatedIdx.get(memberEntry.renderableIndex);
647
+ if (memberValidatedIdx !== undefined) {
648
+ filteredSkips.delete(memberValidatedIdx);
649
+ }
650
+ }
651
+ }
652
+ }
653
+ if (filteredCount !== foldDispatchPlan.foldedBucketCount) {
654
+ foldDispatchPlan = {
655
+ headBuckets: filteredHeads,
656
+ skipIndices: filteredSkips,
657
+ foldedBucketCount: filteredCount,
658
+ };
659
+ }
660
+ }
661
+
662
+ // feat-20260622-chunk-gpu-instancing-sprite-tilemap M3 / w13 (D-3 +
663
+ // AC-06): increment `render.instancing.foldedDraws` once per fold-
664
+ // eligible head bucket retained after the cap-fallback filter above.
665
+ // The metric tracks instanced drawIndexed call count for this frame
666
+ // — cap-overrun buckets routed through the per-entity fallback exit
667
+ // are removed from `foldDispatchPlan` and therefore not counted, by
668
+ // construction (M2 / w11 cap-fallback + plan-strategy D-3 semantics).
669
+ // Singleton buckets (mode-bypass under D-5, or non-foldable under
670
+ // mode 0) carry `bucketSize === 1` and never enter `headBuckets`, so
671
+ // the per-entity drawIndexed path correctly does not count.
672
+ // SSOT helper lives in this record owner — engine code never
673
+ // hardcodes the metric key string.
674
+ incrementFoldedDrawsMetric(foldDispatchPlan, internals.metrics);
675
+ }
676
+
677
+ return {
678
+ validatedOrdered,
679
+ shadowValidatedOrdered,
680
+ foldDispatchPlan,
681
+ materialSlotIndices,
682
+ materialSlots,
683
+ materialSlotOwners,
684
+ materialSlotCount,
685
+ };
686
+ }
687
+
688
+ /**
689
+ * feat-20260704 M3/w18: per-frame cache clean-up (despawn eviction), extracted
690
+ * verbatim from `recordFrame`.
691
+ *
692
+ * feat-20260531-per-frame-bind-group-cache M4 / w14 (D-5): drop per-entity BG
693
+ * cache entries (materialBgPerEntity / instancesBgPerEntity) + instance buffers
694
+ * whose outer-Map entityKey (packed Entity u32) is absent from the current
695
+ * validated set, preventing unbounded growth after entity despawn. view + mesh
696
+ * caches are frame-shared (keyed by GPU resource handle objects, WeakMap chains
697
+ * naturally bounded) and are not touched. feat-20260619 M4 / F11: destroy the
698
+ * GPU instance buffer before Map.delete (D-6 symmetric release); failure fires
699
+ * errorRegistry + continues the sweep.
700
+ *
701
+ * @internal
702
+ */
703
+ export function cleanPerFrameCaches(
704
+ internals: RenderSystemInternals,
705
+ frameState: RenderFrameState,
706
+ validated: readonly ValidatedRenderable[],
707
+ retainedEntityKeys: ReadonlySet<number> = new Set<number>(),
708
+ ): void {
709
+ if (frameState.transientInstanceBuffers !== undefined) {
710
+ disposeTransientInstanceBuffers(frameState.transientInstanceBuffers, internals.errorRegistry);
711
+ }
712
+ // Build a Set<number> of worldEntityKey composites from the validated
713
+ // renderables. D-1a #4: validatedEntityKeys are worldEntityKey(worldId, entityKey)
714
+ // composites matching the write-side keys of #1-#3 — cross-world false eviction
715
+ // is prevented because worldEntityKey(0, k) !== worldEntityKey(1, k).
716
+ const validatedEntityKeys = new Set<number>(retainedEntityKeys);
717
+ const validatedInstanceKeys = new Set<number>(retainedEntityKeys);
718
+ for (const v of validated) {
719
+ const entityKey = worldEntityKey(v.source.worldId, v.source.entityKey);
720
+ validatedEntityKeys.add(entityKey);
721
+ validatedInstanceKeys.add(entityKey);
722
+ if (v.source.instances !== undefined) {
723
+ validatedInstanceKeys.add(instanceCollectionCacheKey(v.source.worldId, v.source.instances));
724
+ }
725
+ }
726
+
727
+ // Clean per-entity material BG cache: drop outer-Map entries whose
728
+ // entityKey is absent from the current validated set. The shared and
729
+ // singleton material caches have no entityKey and are not touched here.
730
+ cleanPerEntityCache(frameState.materialBgPerEntity, validatedEntityKeys);
731
+
732
+ // Clean per-entity instances BG cache.
733
+ cleanPerEntityCache(frameState.instancesBgPerEntity, validatedEntityKeys);
734
+
735
+ // D-5 retrofit: instanceBuffers clean-up. The instanceBuffers Map is
736
+ // keyed by cacheKey (packed Entity u32, same as entityKey on
737
+ // RenderableSnapshot). Drop entries whose key is no longer in the
738
+ // validated set (OQ-3 / R-4). feat-20260619 M4 / F11: destroy the GPU
739
+ // buffer before Map.delete so despawned entities release their
740
+ // instance-buffer backing memory symmetrically (D-6).
741
+ //
742
+ // D-1a #1: instanceBuffers keys on the positive half (>= 0) are
743
+ // worldEntityKey composites matching the write side. Negative half
744
+ // fold-bucket keys (< 0) are NOT worldEntityKey; they are
745
+ // material-handle-based and cross-world collision is semantically
746
+ // correct (same material renders in same fold bucket).
747
+ for (const [key, entry] of frameState.instanceBuffers.entries()) {
748
+ if (!validatedInstanceKeys.has(key)) {
749
+ if (!entry.buffer.isDestroyed) {
750
+ const r = entry.buffer.destroy();
751
+ if (!r.ok) internals.errorRegistry.fire(r.error);
752
+ }
753
+ frameState.instanceBuffers.delete(key);
754
+ }
755
+ }
756
+
757
+ // Chunk keys are `${worldEntityKey}:${chunkStart}`. Keep the key opaque to
758
+ // the authoring API, but retain the numeric owner prefix for eviction.
759
+ if (frameState.instanceBufferChunks !== undefined) {
760
+ for (const [key, entry] of frameState.instanceBufferChunks.entries()) {
761
+ const separator = key.indexOf(':');
762
+ const ownerKey = separator < 0 ? Number.NaN : Number(key.slice(0, separator));
763
+ if (validatedInstanceKeys.has(ownerKey)) continue;
764
+ if (!entry.buffer.isDestroyed) {
765
+ const r = entry.buffer.destroy();
766
+ if (!r.ok) internals.errorRegistry.fire(r.error);
767
+ }
768
+ frameState.instanceBufferChunks.delete(key);
769
+ }
770
+ }
771
+
772
+ if (frameState.morphBuffers !== undefined) {
773
+ for (const [key, entry] of frameState.morphBuffers.entries()) {
774
+ if (validatedEntityKeys.has(key)) continue;
775
+ if (!entry.buffer.isDestroyed) {
776
+ const result = entry.buffer.destroy();
777
+ if (!result.ok) internals.errorRegistry.fire(result.error);
778
+ }
779
+ frameState.morphBuffers.delete(key);
780
+ }
781
+ }
782
+ }
783
+
784
+ /**
785
+ * feat-20260704 M3/w18: record-stage fold operator linear scan, extracted
786
+ * verbatim from `recordFrame`.
787
+ *
788
+ * feat-20260622-chunk-gpu-instancing-sprite-tilemap M1 / w4 + w5 (D-1, D-5):
789
+ * groups transparent-sort-ordered dispatch entries with equal (Layer.value,
790
+ * sortKey, materialHandle) into fold buckets. Mode-gate (D-5 extended): modes 0
791
+ * (LAYER_Z) and 1 (LAYER_Y) fold using the pos z/y lanes; modes 2/3 bypass per-entity
792
+ * (each entry a singleton bucket). Records `frameState.lastFoldBucketCount` =
793
+ * fold-eligible buckets (bucketSize > 1) for the AC-06 metric. Empty dispatch
794
+ * short-circuits (test fixtures pass null world).
795
+ *
796
+ * @internal
797
+ */
798
+ export function computeFoldBuckets(
799
+ world: RenderResourceScope,
800
+ frameState: RenderFrameState,
801
+ transparentDispatch: readonly DispatchEntry[],
802
+ renderables: readonly RenderableSnapshot[],
803
+ ): readonly FoldBucket[] {
804
+ if (transparentDispatch.length === 0) {
805
+ frameState.lastFoldBucketCount = 0;
806
+ return [];
807
+ }
808
+ const transparentSortCfg = getTransparentSortConfig(world);
809
+
810
+ // Only layer-Z / layer-Y modes can produce non-singleton fold buckets.
811
+ // The other modes make one inert singleton per entry, which is discarded by
812
+ // buildFoldDispatchPlan. Returning the same empty plan here avoids allocating
813
+ // a transform matrix for every renderable when folding is disabled by mode.
814
+ if (
815
+ transparentSortCfg.mode !== TRANSPARENT_SORT_MODE_LAYER_Z &&
816
+ transparentSortCfg.mode !== TRANSPARENT_SORT_MODE_LAYER_Y
817
+ ) {
818
+ frameState.lastFoldBucketCount = 0;
819
+ return [];
820
+ }
821
+
822
+ // `transparentDispatch` is the legacy name for the complete sorted dispatch
823
+ // list. Opaque entries can never participate in the transparent-only fold
824
+ // plan, yet foldDispatchBuckets must preserve its general helper contract and
825
+ // therefore materializes an inert singleton (including a Float32Array(16))
826
+ // for each one. Filter only at this private production call site so the fold
827
+ // helper keeps its testable semantics while record avoids work whose result
828
+ // is provably discarded.
829
+ let transparentCount = 0;
830
+ for (const entry of transparentDispatch) {
831
+ if (renderables[entry.renderableIndex]?.material.transparent === true) {
832
+ transparentCount += 1;
833
+ }
834
+ }
835
+ if (transparentCount === 0) {
836
+ frameState.lastFoldBucketCount = 0;
837
+ return [];
838
+ }
839
+ const foldCandidates =
840
+ transparentCount === transparentDispatch.length
841
+ ? transparentDispatch
842
+ : transparentDispatch.filter(
843
+ (entry) => renderables[entry.renderableIndex]?.material.transparent === true,
844
+ );
845
+ const foldBuckets = foldDispatchBuckets(foldCandidates, transparentSortCfg.mode, renderables);
846
+ // Count only fold-eligible buckets (bucketSize > 1) so the metric
847
+ // surfaces fold actually reducing draws — singleton buckets under
848
+ // mode bypass do not change draw count, so they do not contribute.
849
+ let foldEligibleCount = 0;
850
+ for (const b of foldBuckets) {
851
+ if (b.bucketSize > 1) foldEligibleCount += 1;
852
+ }
853
+ frameState.lastFoldBucketCount = foldEligibleCount;
854
+ return foldBuckets;
855
+ }