@forgeax/engine-render 0.1.28 → 0.1.30

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 (768) hide show
  1. package/README.md +856 -126
  2. package/dist/assembly/atmosphere-shader-prewarm.d.ts +6 -0
  3. package/dist/assembly/atmosphere-shader-prewarm.d.ts.map +1 -0
  4. package/dist/assembly/device-feature-admission.d.ts +4 -3
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -1
  6. package/dist/assembly/dynamic-geometry-host.d.ts +27 -0
  7. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  8. package/dist/assembly/dynamic-geometry-runtime.d.ts +20 -0
  9. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -0
  10. package/dist/assembly/factory.d.ts +3 -1
  11. package/dist/assembly/factory.d.ts.map +1 -1
  12. package/dist/assembly/gpu-driven-owner.d.ts +10 -0
  13. package/dist/assembly/gpu-driven-owner.d.ts.map +1 -0
  14. package/dist/assembly/host-contract.d.ts +14 -0
  15. package/dist/assembly/host-contract.d.ts.map +1 -1
  16. package/dist/assembly/material/assembly.d.ts +31 -1
  17. package/dist/assembly/material/assembly.d.ts.map +1 -1
  18. package/dist/assembly/material/pipeline-helpers.d.ts +24 -0
  19. package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -0
  20. package/dist/assembly/material-shader-policy.d.ts +3 -1
  21. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  22. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
  23. package/dist/assembly/recovery/failure-location.d.ts +12 -0
  24. package/dist/assembly/recovery/failure-location.d.ts.map +1 -0
  25. package/dist/assembly/recovery/recovery-attempt.d.ts +1 -6
  26. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -1
  27. package/dist/assembly/recovery/renderer-recover.d.ts +7 -4
  28. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  29. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  30. package/dist/assembly/renderer-feature-inspection.d.ts +5 -0
  31. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -0
  32. package/dist/assembly/renderer-frame-transaction.d.ts +19 -0
  33. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  34. package/dist/assembly/renderer-helpers.d.ts +3 -0
  35. package/dist/assembly/renderer-helpers.d.ts.map +1 -1
  36. package/dist/assembly/renderer-inspection.d.ts +4 -0
  37. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  38. package/dist/assembly/shader-prewarm-policy.d.ts +7 -1
  39. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -1
  40. package/dist/assembly/skin-palette-owner.d.ts +8 -0
  41. package/dist/assembly/skin-palette-owner.d.ts.map +1 -0
  42. package/dist/assembly/ssr-shader-prewarm.d.ts +6 -0
  43. package/dist/assembly/ssr-shader-prewarm.d.ts.map +1 -0
  44. package/dist/assembly/webgpu-device-loss.d.ts +15 -0
  45. package/dist/assembly/webgpu-device-loss.d.ts.map +1 -0
  46. package/dist/assembly/webgpu-pbr-ready.d.ts +38 -0
  47. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -0
  48. package/dist/assembly/webgpu-ready-mipmap.d.ts +11 -0
  49. package/dist/assembly/webgpu-ready-mipmap.d.ts.map +1 -0
  50. package/dist/assembly/webgpu-ready.d.ts +7 -3
  51. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  52. package/dist/assembly/webgpu-renderer-bootstrap.d.ts +10 -0
  53. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -0
  54. package/dist/assembly/webgpu-renderer-contract.d.ts +62 -0
  55. package/dist/assembly/webgpu-renderer-contract.d.ts.map +1 -0
  56. package/dist/assembly/webgpu-renderer-guards.d.ts +9 -0
  57. package/dist/assembly/webgpu-renderer-guards.d.ts.map +1 -0
  58. package/dist/assembly/webgpu-renderer-observation.d.ts +6 -0
  59. package/dist/assembly/webgpu-renderer-observation.d.ts.map +1 -0
  60. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts +12 -0
  61. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts.map +1 -0
  62. package/dist/assembly/webgpu-renderer.d.ts +7 -141
  63. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  64. package/dist/assembly/webgpu-vertex-layouts.d.ts +28 -84
  65. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -1
  66. package/dist/authoring.mjs +2 -2
  67. package/dist/capture/scene-projection.d.ts +41 -0
  68. package/dist/capture/scene-projection.d.ts.map +1 -0
  69. package/dist/{chunk-J52NMQKR.mjs → chunk-7MQ3XBSP.mjs} +1098 -844
  70. package/dist/chunk-7MQ3XBSP.mjs.map +1 -0
  71. package/dist/{chunk-GF523LHF.mjs → chunk-BLXGONR2.mjs} +30 -12
  72. package/dist/chunk-BLXGONR2.mjs.map +1 -0
  73. package/dist/{chunk-VSL23LZE.mjs → chunk-C3DDSXYK.mjs} +1188 -463
  74. package/dist/chunk-C3DDSXYK.mjs.map +1 -0
  75. package/dist/{chunk-IQMKJLNU.mjs → chunk-FUUO4UND.mjs} +443 -30
  76. package/dist/chunk-FUUO4UND.mjs.map +1 -0
  77. package/dist/{chunk-J6KACNOH.mjs → chunk-JRBDO2KB.mjs} +617 -7
  78. package/dist/chunk-JRBDO2KB.mjs.map +1 -0
  79. package/dist/{chunk-GE3SDD72.mjs → chunk-MDQJPY3T.mjs} +20 -40
  80. package/dist/chunk-MDQJPY3T.mjs.map +1 -0
  81. package/dist/{chunk-MYJ5RPJO.mjs → chunk-NO4WZCAH.mjs} +3 -3
  82. package/dist/chunk-NO4WZCAH.mjs.map +1 -0
  83. package/dist/{chunk-RBDBZ5R7.mjs → chunk-PA5DRJNI.mjs} +2 -2
  84. package/dist/chunk-PA5DRJNI.mjs.map +1 -0
  85. package/dist/{chunk-MBU4ZVE7.mjs → chunk-PSEQ6QPH.mjs} +4655 -892
  86. package/dist/chunk-PSEQ6QPH.mjs.map +1 -0
  87. package/dist/{chunk-JAASCMXA.mjs → chunk-S5UE2T5P.mjs} +289 -41
  88. package/dist/chunk-S5UE2T5P.mjs.map +1 -0
  89. package/dist/components/camera.d.ts +62 -2
  90. package/dist/components/camera.d.ts.map +1 -1
  91. package/dist/components/directional-light.d.ts +2 -2
  92. package/dist/components/dynamic-resolution.d.ts +15 -0
  93. package/dist/components/dynamic-resolution.d.ts.map +1 -0
  94. package/dist/components/index.d.ts +2 -0
  95. package/dist/components/index.d.ts.map +1 -1
  96. package/dist/components/instances.d.ts +6 -20
  97. package/dist/components/instances.d.ts.map +1 -1
  98. package/dist/components/light-helpers.d.ts.map +1 -1
  99. package/dist/components/reflection-probe.d.ts +1 -0
  100. package/dist/components/reflection-probe.d.ts.map +1 -1
  101. package/dist/components/scene-instance.d.ts +2 -1
  102. package/dist/components/scene-instance.d.ts.map +1 -1
  103. package/dist/components/screen-space-reflection.d.ts +14 -0
  104. package/dist/components/screen-space-reflection.d.ts.map +1 -0
  105. package/dist/components/skylight.d.ts +4 -1
  106. package/dist/components/skylight.d.ts.map +1 -1
  107. package/dist/construct-renderer.mjs +14809 -7570
  108. package/dist/construct-renderer.mjs.map +1 -1
  109. package/dist/device/device-scope.d.ts +2 -0
  110. package/dist/device/device-scope.d.ts.map +1 -1
  111. package/dist/device/gpu-residency.d.ts +3 -14
  112. package/dist/device/gpu-residency.d.ts.map +1 -1
  113. package/dist/dynamic-geometry.d.ts.map +1 -1
  114. package/dist/environment/background.d.ts +7 -0
  115. package/dist/environment/background.d.ts.map +1 -0
  116. package/dist/environment/ibl.d.ts +11 -0
  117. package/dist/environment/ibl.d.ts.map +1 -0
  118. package/dist/environment/storage.d.ts +14 -0
  119. package/dist/environment/storage.d.ts.map +1 -0
  120. package/dist/errors/gpu-driven.d.ts +6 -3
  121. package/dist/errors/gpu-driven.d.ts.map +1 -1
  122. package/dist/errors/render.d.ts +41 -4
  123. package/dist/errors/render.d.ts.map +1 -1
  124. package/dist/extract/camera.d.ts.map +1 -1
  125. package/dist/extract/contracts.d.ts +7 -0
  126. package/dist/extract/contracts.d.ts.map +1 -1
  127. package/dist/extract/gpu-driven.d.ts +50 -3
  128. package/dist/extract/gpu-driven.d.ts.map +1 -1
  129. package/dist/features/host.d.ts +23 -1
  130. package/dist/features/host.d.ts.map +1 -1
  131. package/dist/features/noise-texture.d.ts +17 -0
  132. package/dist/features/noise-texture.d.ts.map +1 -0
  133. package/dist/features/plan.d.ts +83 -2
  134. package/dist/features/plan.d.ts.map +1 -1
  135. package/dist/features/prepared-gpu-work.d.ts +37 -4
  136. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  137. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  138. package/dist/features/prepared-graphics.d.ts +5 -0
  139. package/dist/features/prepared-graphics.d.ts.map +1 -1
  140. package/dist/features/render-graph-compute.d.ts +5 -3
  141. package/dist/features/render-graph-compute.d.ts.map +1 -1
  142. package/dist/features/render-graph-contribution.d.ts +11 -3
  143. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  144. package/dist/features/render-graph-raster.d.ts +19 -3
  145. package/dist/features/render-graph-raster.d.ts.map +1 -1
  146. package/dist/features/types.d.ts +11 -0
  147. package/dist/features/types.d.ts.map +1 -1
  148. package/dist/fullscreen-post-process-pass.d.ts +2 -0
  149. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  150. package/dist/gpu-driven/batch-topology.d.ts +34 -1
  151. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  152. package/dist/gpu-driven/material-artifacts.d.ts +24 -0
  153. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -0
  154. package/dist/gpu-driven/material-bindings.d.ts +7 -0
  155. package/dist/gpu-driven/material-bindings.d.ts.map +1 -0
  156. package/dist/gpu-driven/pbr-program.d.ts +10 -0
  157. package/dist/gpu-driven/pbr-program.d.ts.map +1 -0
  158. package/dist/gpu-driven/prepared-draw.d.ts +2 -0
  159. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  160. package/dist/gpu-driven/production-raster.d.ts +169 -27
  161. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  162. package/dist/gpu-driven/shadow-views.d.ts +126 -0
  163. package/dist/gpu-driven/shadow-views.d.ts.map +1 -0
  164. package/dist/gpu-driven/view-gpu.d.ts +18 -2
  165. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  166. package/dist/gpu-resource.d.ts +5 -2
  167. package/dist/gpu-resource.d.ts.map +1 -1
  168. package/dist/gpu-scene-schema.d.ts +2 -8
  169. package/dist/gpu-scene-schema.d.ts.map +1 -1
  170. package/dist/gpu-scene.d.ts +14 -0
  171. package/dist/gpu-scene.d.ts.map +1 -1
  172. package/dist/gpu-texture-usage.d.ts +1 -0
  173. package/dist/gpu-texture-usage.d.ts.map +1 -1
  174. package/dist/hdrp-buffers.d.ts +9 -0
  175. package/dist/hdrp-buffers.d.ts.map +1 -1
  176. package/dist/ibl/IblPipelineCache.d.ts +2 -0
  177. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  178. package/dist/ibl/frame-resources.d.ts +8 -0
  179. package/dist/ibl/frame-resources.d.ts.map +1 -0
  180. package/dist/ibl/skylight-bind-group.d.ts +3 -1
  181. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  182. package/dist/index.d.ts +87 -75
  183. package/dist/index.d.ts.map +1 -1
  184. package/dist/index.mjs +10 -8
  185. package/dist/index.mjs.map +1 -1
  186. package/dist/inspection-types.d.ts +180 -1
  187. package/dist/inspection-types.d.ts.map +1 -1
  188. package/dist/instances-derived-bounds.d.ts +11 -0
  189. package/dist/instances-derived-bounds.d.ts.map +1 -1
  190. package/dist/instances.d.ts +89 -44
  191. package/dist/instances.d.ts.map +1 -1
  192. package/dist/internal.d.ts +3 -0
  193. package/dist/internal.d.ts.map +1 -1
  194. package/dist/internal.mjs +117 -9
  195. package/dist/internal.mjs.map +1 -1
  196. package/dist/lifecycle.d.ts +2 -1
  197. package/dist/lifecycle.d.ts.map +1 -1
  198. package/dist/material-row.d.ts +15 -0
  199. package/dist/material-row.d.ts.map +1 -0
  200. package/dist/materials.d.ts +15 -0
  201. package/dist/materials.d.ts.map +1 -1
  202. package/dist/pbr-pipeline.d.ts +23 -5
  203. package/dist/pbr-pipeline.d.ts.map +1 -1
  204. package/dist/pipeline/render-extent.d.ts +51 -0
  205. package/dist/pipeline/render-extent.d.ts.map +1 -0
  206. package/dist/pipeline/standard-build-context.d.ts +7 -0
  207. package/dist/pipeline/standard-build-context.d.ts.map +1 -0
  208. package/dist/pipeline/standard-forward-lane.d.ts +3 -2
  209. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  210. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts +12 -0
  211. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts.map +1 -0
  212. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts +70 -0
  213. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts.map +1 -0
  214. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts +75 -0
  215. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts.map +1 -0
  216. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts +111 -0
  217. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts.map +1 -0
  218. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts +16 -0
  219. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts.map +1 -0
  220. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts +14 -0
  221. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts.map +1 -0
  222. package/dist/pipeline/standard-output/auto-exposure/state.d.ts +46 -0
  223. package/dist/pipeline/standard-output/auto-exposure/state.d.ts.map +1 -0
  224. package/dist/pipeline/standard-output/color-transform.d.ts +27 -0
  225. package/dist/pipeline/standard-output/color-transform.d.ts.map +1 -0
  226. package/dist/pipeline/standard-output/graph.d.ts +15 -0
  227. package/dist/pipeline/standard-output/graph.d.ts.map +1 -0
  228. package/dist/pipeline/standard-output/lut-admission.d.ts +29 -0
  229. package/dist/pipeline/standard-output/lut-admission.d.ts.map +1 -0
  230. package/dist/pipeline/standard-output/lut-gpu.d.ts +48 -0
  231. package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -0
  232. package/dist/pipeline/standard-output/lut-state.d.ts +68 -0
  233. package/dist/pipeline/standard-output/lut-state.d.ts.map +1 -0
  234. package/dist/pipeline/standard-output/resources.d.ts +10 -0
  235. package/dist/pipeline/standard-output/resources.d.ts.map +1 -0
  236. package/dist/pipeline/standard-output/types.d.ts +39 -0
  237. package/dist/pipeline/standard-output/types.d.ts.map +1 -0
  238. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  239. package/dist/pipeline/standard-post.d.ts +2 -2
  240. package/dist/pipeline/standard-post.d.ts.map +1 -1
  241. package/dist/pipeline/standard-profile.d.ts +2 -1
  242. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  243. package/dist/pipeline-builder.d.ts +5 -2
  244. package/dist/pipeline-builder.d.ts.map +1 -1
  245. package/dist/pipeline-spec-types.d.ts +4 -0
  246. package/dist/pipeline-spec-types.d.ts.map +1 -1
  247. package/dist/pipeline-spec.d.ts +1 -9
  248. package/dist/pipeline-spec.d.ts.map +1 -1
  249. package/dist/plugin.d.ts.map +1 -1
  250. package/dist/points-lines/standard-owner.d.ts +1 -1
  251. package/dist/points-lines/standard-owner.d.ts.map +1 -1
  252. package/dist/post-process-errors.d.ts +9 -2
  253. package/dist/post-process-errors.d.ts.map +1 -1
  254. package/dist/prepare/extended-lighting/resources.d.ts +3 -3
  255. package/dist/prepare/prepared-graphics-resolver.d.ts +6 -1
  256. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  257. package/dist/record/dynamic-geometry-consumption.d.ts +15 -0
  258. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -0
  259. package/dist/record/frame-lighting.d.ts +3 -3
  260. package/dist/record/frame-lighting.d.ts.map +1 -1
  261. package/dist/record/frame-snapshot.d.ts +109 -13
  262. package/dist/record/frame-snapshot.d.ts.map +1 -1
  263. package/dist/record/frame-targets.d.ts.map +1 -1
  264. package/dist/record/frame.d.ts +41 -5
  265. package/dist/record/frame.d.ts.map +1 -1
  266. package/dist/record/main-pass-geometry.d.ts +13 -9
  267. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  268. package/dist/record/main-pass-material.d.ts +43 -3
  269. package/dist/record/main-pass-material.d.ts.map +1 -1
  270. package/dist/record/main-pass-sprite-draws.d.ts +2 -10
  271. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  272. package/dist/record/main-pass.d.ts +14 -2
  273. package/dist/record/main-pass.d.ts.map +1 -1
  274. package/dist/record/mesh-ssbo.d.ts +15 -6
  275. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  276. package/dist/record/prepared-material-bindings.d.ts +19 -0
  277. package/dist/record/prepared-material-bindings.d.ts.map +1 -0
  278. package/dist/record/recovery-pipeline.d.ts +1 -0
  279. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  280. package/dist/record/render-context.d.ts +101 -22
  281. package/dist/record/render-context.d.ts.map +1 -1
  282. package/dist/record/shadow-pass.d.ts +62 -7
  283. package/dist/record/shadow-pass.d.ts.map +1 -1
  284. package/dist/record/skybox-post-pass.d.ts +1 -1
  285. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  286. package/dist/record/typed-frame-graph.d.ts +39 -8
  287. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  288. package/dist/record/view-ubo.d.ts +1 -0
  289. package/dist/record/view-ubo.d.ts.map +1 -1
  290. package/dist/recovery/render-system-candidate.d.ts +10 -2
  291. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  292. package/dist/recovery/render-system-roots.d.ts +11 -0
  293. package/dist/recovery/render-system-roots.d.ts.map +1 -0
  294. package/dist/recovery/types.d.ts +76 -0
  295. package/dist/recovery/types.d.ts.map +1 -0
  296. package/dist/reflection/background.d.ts +14 -0
  297. package/dist/reflection/background.d.ts.map +1 -0
  298. package/dist/reflection/filter.d.ts.map +1 -1
  299. package/dist/reflection/projection.d.ts +1 -0
  300. package/dist/reflection/projection.d.ts.map +1 -1
  301. package/dist/reflection/record-owner.d.ts +4 -4
  302. package/dist/reflection/record-owner.d.ts.map +1 -1
  303. package/dist/render-contract.d.ts +211 -53
  304. package/dist/render-contract.d.ts.map +1 -1
  305. package/dist/render-graph-primitives.d.ts +1 -0
  306. package/dist/render-graph-primitives.d.ts.map +1 -1
  307. package/dist/render-pipeline.d.ts +81 -6
  308. package/dist/render-pipeline.d.ts.map +1 -1
  309. package/dist/render-system-extract-tail.d.ts +15 -6
  310. package/dist/render-system-extract-tail.d.ts.map +1 -1
  311. package/dist/render-system-extract.d.ts +128 -41
  312. package/dist/render-system-extract.d.ts.map +1 -1
  313. package/dist/render-system-projections.d.ts +22 -0
  314. package/dist/render-system-projections.d.ts.map +1 -0
  315. package/dist/render-system.d.ts +27 -77
  316. package/dist/render-system.d.ts.map +1 -1
  317. package/dist/scene/render-scene-types.d.ts +14 -7
  318. package/dist/scene/render-scene-types.d.ts.map +1 -1
  319. package/dist/scene/render-scene.d.ts +31 -11
  320. package/dist/scene/render-scene.d.ts.map +1 -1
  321. package/dist/scene/shadow-visibility.d.ts +5 -0
  322. package/dist/scene/shadow-visibility.d.ts.map +1 -0
  323. package/dist/ssao-config.d.ts +8 -6
  324. package/dist/ssao-config.d.ts.map +1 -1
  325. package/dist/ssr/admission.d.ts +70 -1
  326. package/dist/ssr/admission.d.ts.map +1 -1
  327. package/dist/ssr/compose.d.ts +13 -0
  328. package/dist/ssr/compose.d.ts.map +1 -0
  329. package/dist/ssr/errors.d.ts +35 -0
  330. package/dist/ssr/errors.d.ts.map +1 -1
  331. package/dist/ssr/graph.d.ts +60 -0
  332. package/dist/ssr/graph.d.ts.map +1 -0
  333. package/dist/ssr/history.d.ts +97 -0
  334. package/dist/ssr/history.d.ts.map +1 -0
  335. package/dist/ssr/hiz.d.ts +37 -0
  336. package/dist/ssr/hiz.d.ts.map +1 -0
  337. package/dist/ssr/inspection.d.ts +42 -0
  338. package/dist/ssr/inspection.d.ts.map +1 -0
  339. package/dist/ssr/resources.d.ts +58 -0
  340. package/dist/ssr/resources.d.ts.map +1 -0
  341. package/dist/ssr/temporal.d.ts +38 -0
  342. package/dist/ssr/temporal.d.ts.map +1 -0
  343. package/dist/systems/skin-palette-allocator.d.ts +32 -10
  344. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  345. package/dist/systems/skin-palette-types.d.ts +15 -0
  346. package/dist/systems/skin-palette-types.d.ts.map +1 -1
  347. package/dist/systems/transparent-dispatch.d.ts +9 -0
  348. package/dist/systems/transparent-dispatch.d.ts.map +1 -0
  349. package/dist/targets/material-source.d.ts +7 -0
  350. package/dist/targets/material-source.d.ts.map +1 -1
  351. package/dist/temporal/frame-coordinator.d.ts +7 -0
  352. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  353. package/dist/temporal/gpu.d.ts +8 -0
  354. package/dist/temporal/gpu.d.ts.map +1 -1
  355. package/dist/temporal/history.d.ts.map +1 -1
  356. package/dist/temporal/index.mjs +6 -6
  357. package/dist/temporal/inspection.d.ts.map +1 -1
  358. package/dist/temporal/standard-scene-data.d.ts +4 -2
  359. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  360. package/dist/temporal/target-coverage-attachment.d.ts +15 -0
  361. package/dist/temporal/target-coverage-attachment.d.ts.map +1 -0
  362. package/dist/temporal/temporal-view.d.ts +6 -0
  363. package/dist/temporal/temporal-view.d.ts.map +1 -1
  364. package/dist/temporal/view.d.ts +6 -0
  365. package/dist/temporal/view.d.ts.map +1 -1
  366. package/dist/typed-render-graph-primitives.d.ts +41 -1
  367. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  368. package/dist/typed-shadow-passes.d.ts +3 -1
  369. package/dist/typed-shadow-passes.d.ts.map +1 -1
  370. package/dist/volume/component.d.ts +1 -0
  371. package/dist/volume/component.d.ts.map +1 -1
  372. package/dist/volume/temporal.d.ts +6 -0
  373. package/dist/volume/temporal.d.ts.map +1 -1
  374. package/package.json +21 -20
  375. package/src/__tests__/auto-exposure-gpu-evidence.ts +291 -0
  376. package/src/__tests__/auto-exposure-inspection.unit.test.ts +80 -0
  377. package/src/__tests__/auto-exposure-transaction.integration.test.ts +99 -0
  378. package/src/__tests__/auto-exposure.browser.test.ts +33 -0
  379. package/src/__tests__/auto-exposure.dawn.test.ts +33 -0
  380. package/src/__tests__/capture-scene-projection.unit.test.ts +24 -0
  381. package/src/__tests__/detached-inspection.integration.test.ts +5 -0
  382. package/src/__tests__/device-feature-admission.unit.test.ts +15 -24
  383. package/src/__tests__/device-loss-fanout.unit.test.ts +43 -0
  384. package/src/__tests__/device-scope-lifecycle.unit.test.ts +18 -0
  385. package/src/__tests__/device-scope-stale-matrix.unit.test.ts +18 -0
  386. package/src/__tests__/docs-gate-green.unit.test.ts +3 -3
  387. package/src/__tests__/dynamic-geometry-batch.unit.test.ts +157 -0
  388. package/src/__tests__/dynamic-geometry.unit.test.ts +36 -0
  389. package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +81 -0
  390. package/src/__tests__/factory-contract.integration.test.ts +231 -9
  391. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +19 -0
  392. package/src/__tests__/feature-depth-input.dawn.test.ts +121 -0
  393. package/src/__tests__/feature-noise.unit.test.ts +38 -0
  394. package/src/__tests__/fixtures/taa-resolve-pinned-baseline.wgsl +301 -0
  395. package/src/__tests__/frame-camera-selection.unit.test.ts +131 -0
  396. package/src/__tests__/frame-observation.contract.test.ts +21 -0
  397. package/src/__tests__/frame-plan-contract.unit.test.ts +24 -0
  398. package/src/__tests__/frame-targets.unit.test.ts +24 -0
  399. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +3 -0
  400. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +79 -0
  401. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +307 -7
  402. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +122 -142
  403. package/src/__tests__/gpu-driven-indirect-raster.browser.test.ts +10 -1
  404. package/src/__tests__/gpu-driven-lod-cache.unit.test.ts +261 -0
  405. package/src/__tests__/gpu-driven-mesh-world-identity.unit.test.ts +346 -0
  406. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +1422 -0
  407. package/src/__tests__/gpu-driven-pbr.integration.test.ts +21 -0
  408. package/src/__tests__/gpu-driven-production-fixture.ts +96 -0
  409. package/src/__tests__/gpu-driven-production.integration.test.ts +1076 -106
  410. package/src/__tests__/gpu-driven-scaling.unit.test.ts +1 -3
  411. package/src/__tests__/gpu-driven-shadow-views.browser.test.ts +63 -0
  412. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +294 -0
  413. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +97 -0
  414. package/src/__tests__/gpu-driven-shadow-views.unit.test.ts +168 -0
  415. package/src/__tests__/gpu-driven-skin-contract.test-d.ts +11 -0
  416. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +72 -20
  417. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +30 -9
  418. package/src/__tests__/gpu-driven-view.unit.test.ts +160 -52
  419. package/src/__tests__/gpu-pass-timing-browser-runner.ts +15 -0
  420. package/src/__tests__/gpu-pass-timing.browser.test.ts +20 -2
  421. package/src/__tests__/gpu-resource-scope.unit.test.ts +71 -0
  422. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +42 -0
  423. package/src/__tests__/gpu-scene-render-graph-gpu.ts +19 -5
  424. package/src/__tests__/gpu-scene.dawn.test.ts +40 -13
  425. package/src/__tests__/gpu-scene.unit.test.ts +613 -33
  426. package/src/__tests__/instances-culling.integration.test.ts +36 -10
  427. package/src/__tests__/instances-derived-bounds.unit.test.ts +54 -0
  428. package/src/__tests__/instances-store.unit.test.ts +242 -39
  429. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +6 -0
  430. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +13 -16
  431. package/src/__tests__/lut-lkg-transaction.integration.test.ts +122 -0
  432. package/src/__tests__/material-contract-inventory.unit.test.ts +5 -6
  433. package/src/__tests__/material-dispatch-shader-id.unit.test.ts +51 -0
  434. package/src/__tests__/material-lane-selection.unit.test.ts +60 -0
  435. package/src/__tests__/material-numeric-upload.unit.test.ts +53 -0
  436. package/src/__tests__/material-texture-handles.unit.test.ts +18 -0
  437. package/src/__tests__/material-texture-source.unit.test.ts +87 -0
  438. package/src/__tests__/materials-authoring-validation.unit.test.ts +41 -0
  439. package/src/__tests__/materials-standard-contract.unit.test.ts +57 -3
  440. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +4 -0
  441. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +8 -0
  442. package/src/__tests__/morph-neutral-admission.unit.test.ts +19 -0
  443. package/src/__tests__/normal-fallback.dawn.test.ts +122 -0
  444. package/src/__tests__/particle-material-input-layout.unit.test.ts +104 -0
  445. package/src/__tests__/physical-clearcoat.integration.test.ts +42 -3
  446. package/src/__tests__/pipeline-texture-specialization.unit.test.ts +138 -0
  447. package/src/__tests__/point-shadow-inspection.unit.test.ts +2 -0
  448. package/src/__tests__/post-process-error-policy-owner.unit.test.ts +6 -5
  449. package/src/__tests__/prepared-gpu-driven-pbr-contract.test-d.ts +31 -0
  450. package/src/__tests__/prepared-gpu-driven-pbr-contract.unit.test.ts +39 -0
  451. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +251 -9
  452. package/src/__tests__/prepared-graphics-capability.unit.test.ts +21 -0
  453. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +6 -2
  454. package/src/__tests__/profiler-phase-catalog.test.ts +5 -0
  455. package/src/__tests__/public-surface.test-d.ts +3 -0
  456. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +30 -12
  457. package/src/__tests__/reflection-fallback-async-publication.unit.test.ts +171 -0
  458. package/src/__tests__/reflection-probe-authoring.unit.test.ts +12 -10
  459. package/src/__tests__/reflection-probe-box.dawn.test.ts +71 -0
  460. package/src/__tests__/reflection-probe-filter.unit.test.ts +9 -0
  461. package/src/__tests__/reflection-probe-orientation.dawn.test.ts +294 -0
  462. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +5 -14
  463. package/src/__tests__/reflection-probe-updates.unit.test.ts +129 -0
  464. package/src/__tests__/render-error-code-owner.test-d.ts +7 -0
  465. package/src/__tests__/render-error-detail-ssot.test-d.ts +27 -1
  466. package/src/__tests__/render-error-exhaustive.test-d.ts +15 -0
  467. package/src/__tests__/render-extent.unit.test.ts +63 -0
  468. package/src/__tests__/render-feature-gpu-work.integration.test.ts +285 -2
  469. package/src/__tests__/render-feature-plan.unit.test.ts +245 -0
  470. package/src/__tests__/render-feature-shadow-order.unit.test.ts +228 -0
  471. package/src/__tests__/render-feature-zero-work.unit.test.ts +35 -1
  472. package/src/__tests__/render-graph-fallback.integration.test.ts +2 -2
  473. package/src/__tests__/render-owner-cohesion.unit.test.ts +55 -0
  474. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +426 -0
  475. package/src/__tests__/render-scene-projection.unit.test.ts +915 -23
  476. package/src/__tests__/render-scene-resource-updates.integration.test.ts +214 -0
  477. package/src/__tests__/render-scene-skin-lifecycle.integration.test.ts +173 -0
  478. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +133 -0
  479. package/src/__tests__/render-target-material-source.test-d.ts +27 -3
  480. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +233 -9
  481. package/src/__tests__/renderer-frame-transaction.integration.test.ts +923 -5
  482. package/src/__tests__/shader-manifest-fixture.ts +45 -0
  483. package/src/__tests__/skin-motion-regression.unit.test.ts +0 -33
  484. package/src/__tests__/skin-palette-persistent.integration.test.ts +28 -0
  485. package/src/__tests__/skin-palette-persistent.unit.test.ts +247 -0
  486. package/src/__tests__/skinned-shadow-caster.test.ts +125 -14
  487. package/src/__tests__/specular-environment-scale.dawn.test.ts +158 -0
  488. package/src/__tests__/ssao-config.unit.test.ts +7 -0
  489. package/src/__tests__/ssao-surface.dawn.test.ts +252 -0
  490. package/src/__tests__/ssr-admission-matrix.integration.test.ts +7 -1
  491. package/src/__tests__/ssr-confidence-compose.unit.test.ts +84 -0
  492. package/src/__tests__/ssr-coordinate-oracle.unit.test.ts +28 -0
  493. package/src/__tests__/ssr-cube-silhouette.dawn.test.ts +270 -0
  494. package/src/__tests__/ssr-format-probe-gating.integration.test.ts +82 -0
  495. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +99 -0
  496. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +116 -0
  497. package/src/__tests__/ssr-gpu-dispatch.ts +951 -0
  498. package/src/__tests__/ssr-graph-topology.unit.test.ts +104 -0
  499. package/src/__tests__/ssr-history-filter.dawn.test.ts +198 -0
  500. package/src/__tests__/ssr-hit-color.dawn.test.ts +273 -0
  501. package/src/__tests__/ssr-hiz-odd-reduction.dawn.test.ts +569 -0
  502. package/src/__tests__/ssr-hiz-rescue.dawn.test.ts +279 -0
  503. package/src/__tests__/ssr-hiz.browser.test.ts +150 -0
  504. package/src/__tests__/ssr-hiz.dawn.test.ts +150 -0
  505. package/src/__tests__/ssr-hiz.unit.test.ts +56 -0
  506. package/src/__tests__/ssr-inspection.unit.test.ts +105 -0
  507. package/src/__tests__/ssr-memory-budget.unit.test.ts +45 -0
  508. package/src/__tests__/ssr-performance-sequence-aggregation.unit.test.ts +15 -0
  509. package/src/__tests__/ssr-public-surface.test-d.ts +37 -0
  510. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +387 -0
  511. package/src/__tests__/ssr-reflection-mip-reducer.dawn.test.ts +309 -0
  512. package/src/__tests__/ssr-reprojection-probe.ts +90 -0
  513. package/src/__tests__/ssr-roughness-mip.dawn.test.ts +163 -0
  514. package/src/__tests__/ssr-shader-prewarm.unit.test.ts +59 -0
  515. package/src/__tests__/ssr-spatial.integration.test.ts +141 -0
  516. package/src/__tests__/ssr-stable-lattice.dawn.test.ts +475 -0
  517. package/src/__tests__/ssr-temporal-history.unit.test.ts +202 -0
  518. package/src/__tests__/ssr-texel-crossing.dawn.test.ts +448 -0
  519. package/src/__tests__/ssr-zero-work.integration.test.ts +87 -0
  520. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +84 -12
  521. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +29 -0
  522. package/src/__tests__/standard-output-chain-contract.unit.test.ts +99 -0
  523. package/src/__tests__/standard-output-chain.integration.test.ts +73 -0
  524. package/src/__tests__/standard-output-gpu-evidence.ts +205 -0
  525. package/src/__tests__/standard-output.browser.test.ts +15 -0
  526. package/src/__tests__/standard-output.dawn.test.ts +15 -0
  527. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +275 -0
  528. package/src/__tests__/standard-pipeline.integration.test.ts +320 -3
  529. package/src/__tests__/standard-profile-oracle.unit.test.ts +49 -0
  530. package/src/__tests__/standard-surface-lighting.unit.test.ts +41 -0
  531. package/src/__tests__/standard-texture-specialization.dawn.test.ts +141 -0
  532. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +6 -3
  533. package/src/__tests__/taa-coverage-edge.dawn.test.ts +199 -0
  534. package/src/__tests__/taa-history-clipping.dawn.test.ts +328 -0
  535. package/src/__tests__/taa-neighborhood-equivalence.dawn.test.ts +782 -0
  536. package/src/__tests__/taa-stability-feedback.browser.test.ts +23 -0
  537. package/src/__tests__/taa-stability-feedback.dawn.test.ts +68 -0
  538. package/src/__tests__/taa-stability-feedback.ts +342 -0
  539. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +49 -7
  540. package/src/__tests__/temporal-reset-matrix.unit.test.ts +9 -0
  541. package/src/__tests__/temporal-transaction.integration.test.ts +28 -0
  542. package/src/__tests__/temporal-view.unit.test.ts +17 -0
  543. package/src/__tests__/texture-usage-surface.unit.test.ts +4 -2
  544. package/src/__tests__/transmission-prewarm.unit.test.ts +57 -0
  545. package/src/__tests__/typed-pipeline-topology.unit.test.ts +109 -4
  546. package/src/__tests__/typed-scene-pass-material-cache.integration.test.ts +126 -0
  547. package/src/__tests__/vertex-layouts-ssot.unit.test.ts +47 -0
  548. package/src/__tests__/visibility-extract.perf.test.ts +20 -8
  549. package/src/__tests__/visibility-instances.integration.test.ts +19 -7
  550. package/src/__tests__/visibility-producer-matrix.integration.test.ts +20 -9
  551. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +62 -13
  552. package/src/__tests__/volumetric-fog-topology.unit.test.ts +7 -7
  553. package/src/assembly/atmosphere-shader-prewarm.ts +39 -0
  554. package/src/assembly/device-feature-admission.ts +9 -4
  555. package/src/assembly/dynamic-geometry-host.ts +284 -163
  556. package/src/assembly/dynamic-geometry-runtime.ts +71 -0
  557. package/src/assembly/factory.ts +4 -0
  558. package/src/assembly/gpu-driven-owner.ts +29 -0
  559. package/src/assembly/host-contract.ts +19 -0
  560. package/src/assembly/material/assembly.ts +383 -1
  561. package/src/assembly/material/pipeline-helpers.ts +100 -0
  562. package/src/assembly/material-shader-policy.ts +110 -46
  563. package/src/assembly/recovery/device-loss-fanout.ts +5 -0
  564. package/src/assembly/recovery/failure-location.ts +62 -0
  565. package/src/assembly/recovery/recovery-attempt.ts +2 -29
  566. package/src/assembly/recovery/renderer-recover.ts +16 -17
  567. package/src/assembly/renderer-facade.ts +34 -1
  568. package/src/assembly/renderer-feature-inspection.ts +13 -0
  569. package/src/assembly/renderer-frame-transaction.ts +63 -0
  570. package/src/assembly/renderer-helpers.ts +26 -0
  571. package/src/assembly/renderer-inspection.ts +6 -0
  572. package/src/assembly/shader-prewarm-policy.ts +46 -1
  573. package/src/assembly/skin-palette-owner.ts +21 -0
  574. package/src/assembly/ssr-shader-prewarm.ts +41 -0
  575. package/src/assembly/webgpu-device-loss.ts +75 -0
  576. package/src/assembly/webgpu-pbr-ready.ts +232 -0
  577. package/src/assembly/webgpu-ready-mipmap.ts +26 -0
  578. package/src/assembly/webgpu-ready.ts +161 -131
  579. package/src/assembly/webgpu-renderer-bootstrap.ts +85 -0
  580. package/src/assembly/webgpu-renderer-contract.ts +69 -0
  581. package/src/assembly/webgpu-renderer-guards.ts +32 -0
  582. package/src/assembly/webgpu-renderer-observation.ts +43 -0
  583. package/src/assembly/webgpu-renderer-recovery-failure.ts +62 -0
  584. package/src/assembly/webgpu-renderer.ts +744 -730
  585. package/src/assembly/webgpu-vertex-layouts.ts +233 -87
  586. package/src/capture/scene-projection.ts +37 -0
  587. package/src/components/__tests__/camera-exposure.contract.unit.test.ts +153 -0
  588. package/src/components/__tests__/dynamic-resolution-schema.unit.test.ts +65 -0
  589. package/src/components/__tests__/light-radius.integration.test.ts +59 -0
  590. package/src/components/camera.ts +287 -7
  591. package/src/components/directional-light.ts +3 -3
  592. package/src/components/dynamic-resolution.ts +67 -0
  593. package/src/components/index.ts +2 -0
  594. package/src/components/instances.ts +45 -18
  595. package/src/components/light-helpers.ts +6 -1
  596. package/src/components/reflection-probe.ts +1 -0
  597. package/src/components/scene-instance.ts +2 -1
  598. package/src/components/screen-space-reflection.ts +21 -0
  599. package/src/components/skylight.ts +4 -1
  600. package/src/components/sprite-animation.ts +2 -2
  601. package/src/components/sprite-instances.ts +1 -1
  602. package/src/device/device-scope.ts +13 -7
  603. package/src/device/gpu-residency.ts +45 -42
  604. package/src/dynamic-geometry.ts +13 -8
  605. package/src/environment/background.ts +281 -0
  606. package/src/environment/ibl.ts +211 -0
  607. package/src/environment/storage.ts +63 -0
  608. package/src/errors/gpu-driven.ts +49 -8
  609. package/src/errors/render.ts +91 -3
  610. package/src/extract/camera.ts +37 -1
  611. package/src/extract/contracts.ts +10 -0
  612. package/src/extract/gpu-driven.ts +240 -10
  613. package/src/features/host.ts +237 -19
  614. package/src/features/noise-texture.ts +91 -0
  615. package/src/features/plan.ts +553 -24
  616. package/src/features/prepared-gpu-work.ts +631 -49
  617. package/src/features/prepared-graphics-store.ts +31 -0
  618. package/src/features/prepared-graphics.ts +5 -0
  619. package/src/features/render-graph-compute.ts +41 -8
  620. package/src/features/render-graph-contribution.ts +141 -3
  621. package/src/features/render-graph-raster.ts +143 -30
  622. package/src/features/types.ts +11 -0
  623. package/src/fullscreen-post-process-pass.ts +3 -0
  624. package/src/gpu-driven/batch-topology.ts +452 -108
  625. package/src/gpu-driven/material-artifacts.ts +179 -0
  626. package/src/gpu-driven/material-bindings.ts +41 -0
  627. package/src/gpu-driven/pbr-program.ts +31 -0
  628. package/src/gpu-driven/prepared-draw.ts +42 -6
  629. package/src/gpu-driven/production-raster.ts +2806 -716
  630. package/src/gpu-driven/shadow-views.ts +539 -0
  631. package/src/gpu-driven/view-gpu.ts +188 -41
  632. package/src/gpu-resource.ts +21 -2
  633. package/src/gpu-scene-schema.ts +61 -11
  634. package/src/gpu-scene.ts +197 -99
  635. package/src/gpu-texture-usage.ts +1 -0
  636. package/src/hdrp-buffers.ts +141 -36
  637. package/src/ibl/IblPipelineCache.ts +5 -0
  638. package/src/ibl/frame-resources.ts +38 -0
  639. package/src/ibl/skylight-bind-group.ts +32 -21
  640. package/src/index.ts +194 -75
  641. package/src/inspection-types.ts +207 -1
  642. package/src/instances-derived-bounds.ts +258 -34
  643. package/src/instances.ts +459 -150
  644. package/src/internal.ts +22 -0
  645. package/src/lifecycle.ts +2 -0
  646. package/src/material-row.ts +194 -0
  647. package/src/materials.ts +44 -8
  648. package/src/pbr-pipeline.ts +172 -19
  649. package/src/pipeline/render-extent.ts +111 -0
  650. package/src/pipeline/standard-build-context.ts +7 -0
  651. package/src/pipeline/standard-forward-lane.ts +74 -29
  652. package/src/pipeline/standard-output/__tests__/lut-admission.unit.test.ts +103 -0
  653. package/src/pipeline/standard-output/auto-exposure/__tests__/capability.unit.test.ts +40 -0
  654. package/src/pipeline/standard-output/auto-exposure/__tests__/gpu.contract.unit.test.ts +125 -0
  655. package/src/pipeline/standard-output/auto-exposure/__tests__/graph.contract.unit.test.ts +94 -0
  656. package/src/pipeline/standard-output/auto-exposure/__tests__/oracle.test.ts +108 -0
  657. package/src/pipeline/standard-output/auto-exposure/__tests__/preset.test.ts +26 -0
  658. package/src/pipeline/standard-output/auto-exposure/capability.ts +38 -0
  659. package/src/pipeline/standard-output/auto-exposure/gpu.ts +567 -0
  660. package/src/pipeline/standard-output/auto-exposure/graph.ts +221 -0
  661. package/src/pipeline/standard-output/auto-exposure/inspection.ts +231 -0
  662. package/src/pipeline/standard-output/auto-exposure/oracle.ts +291 -0
  663. package/src/pipeline/standard-output/auto-exposure/preset.ts +28 -0
  664. package/src/pipeline/standard-output/auto-exposure/state.ts +156 -0
  665. package/src/pipeline/standard-output/color-transform.ts +124 -0
  666. package/src/pipeline/standard-output/graph.ts +139 -0
  667. package/src/pipeline/standard-output/lut-admission.ts +121 -0
  668. package/src/pipeline/standard-output/lut-gpu.ts +399 -0
  669. package/src/pipeline/standard-output/lut-state.ts +176 -0
  670. package/src/pipeline/standard-output/resources.ts +121 -0
  671. package/src/pipeline/standard-output/types.ts +58 -0
  672. package/src/pipeline/standard-pipeline.ts +232 -70
  673. package/src/pipeline/standard-post.ts +190 -27
  674. package/src/pipeline/standard-profile.ts +2 -1
  675. package/src/pipeline-builder.ts +28 -9
  676. package/src/pipeline-spec-types.ts +4 -0
  677. package/src/pipeline-spec.ts +37 -29
  678. package/src/plugin.ts +4 -0
  679. package/src/points-lines/standard-owner.ts +6 -14
  680. package/src/post-process-errors.ts +26 -18
  681. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +2 -2
  682. package/src/prepare/extended-lighting/resources.ts +3 -3
  683. package/src/prepare/prepared-graphics-resolver.ts +9 -2
  684. package/src/record/__tests__/main-pass-geometry.unit.test.ts +68 -0
  685. package/src/record/__tests__/material-bg-cache.unit.test.ts +45 -0
  686. package/src/record/__tests__/material-slot-plan.unit.test.ts +57 -0
  687. package/src/record/__tests__/mesh-ssbo.unit.test.ts +40 -1
  688. package/src/record/__tests__/recovery-pipeline.unit.test.ts +20 -1
  689. package/src/record/__tests__/sprite-material-abi.unit.test.ts +41 -6
  690. package/src/record/__tests__/temporal-payload.unit.test.ts +2 -0
  691. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +45 -0
  692. package/src/record/__tests__/view-ubo-layout.unit.test.ts +5 -0
  693. package/src/record/dynamic-geometry-consumption.ts +59 -0
  694. package/src/record/frame-lighting.ts +15 -12
  695. package/src/record/frame-snapshot.ts +146 -34
  696. package/src/record/frame-targets.ts +9 -0
  697. package/src/record/frame.ts +838 -170
  698. package/src/record/main-pass-geometry.ts +209 -103
  699. package/src/record/main-pass-material.ts +157 -40
  700. package/src/record/main-pass-sprite-draws.ts +16 -107
  701. package/src/record/main-pass.ts +141 -12
  702. package/src/record/material-uniforms.ts +1 -1
  703. package/src/record/mesh-ssbo.ts +59 -15
  704. package/src/record/prepared-material-bindings.ts +238 -0
  705. package/src/record/recovery-pipeline.ts +69 -90
  706. package/src/record/render-context.ts +123 -24
  707. package/src/record/shadow-pass.ts +1389 -230
  708. package/src/record/skybox-post-pass.ts +14 -3
  709. package/src/record/typed-frame-graph.ts +1064 -47
  710. package/src/record/view-ubo.ts +12 -1
  711. package/src/recovery/render-system-candidate.ts +135 -68
  712. package/src/recovery/render-system-roots.ts +113 -0
  713. package/src/recovery/types.ts +77 -0
  714. package/src/reflection/background.ts +151 -0
  715. package/src/reflection/filter.ts +3 -0
  716. package/src/reflection/projection.ts +16 -6
  717. package/src/reflection/record-owner.ts +308 -140
  718. package/src/render-contract.ts +237 -49
  719. package/src/render-graph-primitives.ts +34 -5
  720. package/src/render-pipeline.ts +90 -2
  721. package/src/render-system-extract-tail.ts +431 -162
  722. package/src/render-system-extract.ts +478 -119
  723. package/src/render-system-projections.ts +241 -0
  724. package/src/render-system.ts +715 -828
  725. package/src/scene/render-scene-types.ts +13 -4
  726. package/src/scene/render-scene.ts +577 -355
  727. package/src/scene/shadow-visibility.ts +21 -0
  728. package/src/ssao-config.ts +25 -8
  729. package/src/ssr/admission.ts +380 -14
  730. package/src/ssr/compose.ts +144 -0
  731. package/src/ssr/errors.ts +48 -0
  732. package/src/ssr/graph.ts +797 -0
  733. package/src/ssr/history.ts +436 -0
  734. package/src/ssr/hiz.ts +124 -0
  735. package/src/ssr/inspection.ts +124 -0
  736. package/src/ssr/resources.ts +202 -0
  737. package/src/ssr/temporal.ts +164 -0
  738. package/src/systems/skin-palette-allocator.ts +495 -148
  739. package/src/systems/skin-palette-types.ts +17 -0
  740. package/src/systems/transparent-dispatch.ts +90 -0
  741. package/src/targets/material-source.ts +34 -0
  742. package/src/temporal/coverage.ts +1 -1
  743. package/src/temporal/frame-coordinator.ts +23 -2
  744. package/src/temporal/gpu.ts +79 -12
  745. package/src/temporal/history.ts +3 -1
  746. package/src/temporal/inspection.ts +2 -1
  747. package/src/temporal/standard-scene-data.ts +10 -2
  748. package/src/temporal/target-coverage-attachment.ts +72 -0
  749. package/src/temporal/temporal-view.ts +11 -2
  750. package/src/temporal/view.ts +12 -2
  751. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +219 -139
  752. package/src/typed-render-graph-primitives.ts +304 -74
  753. package/src/typed-shadow-passes.ts +122 -12
  754. package/src/volume/component.ts +1 -0
  755. package/src/volume/passes.ts +3 -3
  756. package/src/volume/temporal.ts +10 -0
  757. package/dist/chunk-GE3SDD72.mjs.map +0 -1
  758. package/dist/chunk-GF523LHF.mjs.map +0 -1
  759. package/dist/chunk-IQMKJLNU.mjs.map +0 -1
  760. package/dist/chunk-J52NMQKR.mjs.map +0 -1
  761. package/dist/chunk-J6KACNOH.mjs.map +0 -1
  762. package/dist/chunk-JAASCMXA.mjs.map +0 -1
  763. package/dist/chunk-MBU4ZVE7.mjs.map +0 -1
  764. package/dist/chunk-MYJ5RPJO.mjs.map +0 -1
  765. package/dist/chunk-RBDBZ5R7.mjs.map +0 -1
  766. package/dist/chunk-VSL23LZE.mjs.map +0 -1
  767. package/src/__tests__/instances-world-ownership.integration.test.ts +0 -62
  768. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +0 -36
@@ -1,3 +1,5 @@
1
+ import { collectGpuDrivenMaterialArtifacts } from '../gpu-driven/material-artifacts';
2
+ import { materialBindingKey, projectMaterialBindingClasses } from '../gpu-driven/material-bindings';
1
3
  import { uploadMaterialUniforms } from './material-uniforms';
2
4
  // @forgeax/engine-runtime - RenderSystem record stage: frame.
3
5
  // Extracted from render-system-record.ts (feat-20260704 M3/w17, pure move).
@@ -5,7 +7,8 @@ import { uploadMaterialUniforms } from './material-uniforms';
5
7
  import { HANDLE_NINESLICE_QUAD, resolveAssetHandle } from '@forgeax/engine-assets-runtime';
6
8
  import type { World } from '@forgeax/engine-ecs';
7
9
  import type { RenderReadLease } from '@forgeax/engine-ecs/projection';
8
- import { mat4, vec3 } from '@forgeax/engine-math';
10
+ import { deriveVertexCount } from '@forgeax/engine-geometry';
11
+ import { frustum, mat4, vec3 } from '@forgeax/engine-math';
9
12
  import {
10
13
  type CurrentFrameObservationLease,
11
14
  createCurrentFrameObservationLease,
@@ -21,7 +24,7 @@ import {
21
24
  type TextureView,
22
25
  } from '@forgeax/engine-rhi';
23
26
  import type { MaterialRenderState, MeshAsset } from '@forgeax/engine-types';
24
- import { handleSlot, toShared } from '@forgeax/engine-types';
27
+ import { BUILTIN_BASE, handleSlot, toShared } from '@forgeax/engine-types';
25
28
  import { CUBE_CAMERA_FACE_ORDER } from '../components/cube-camera';
26
29
  import type { DirectionalShadowQuality } from '../components/directional-shadow-filter';
27
30
  import type { MeshGpuHandles } from '../device/gpu-residency';
@@ -30,7 +33,9 @@ import {
30
33
  type ObservationUnavailableReason,
31
34
  RendererOperationError,
32
35
  } from '../errors/render';
36
+ import { gpuDrivenDrawKey, gpuDrivenSourceDrawItemIndex } from '../extract/gpu-driven';
33
37
  import type { GpuDrivenProduction, PreparedGpuDrivenFrame } from '../gpu-driven/production-raster';
38
+ import type { ShadowViewIdentity } from '../gpu-driven/shadow-views';
34
39
  import { GpuBuffer } from '../gpu-resource';
35
40
  import { GPU_TEXTURE_USAGE_COPY_SRC } from '../gpu-texture-usage';
36
41
  import {
@@ -40,6 +45,7 @@ import {
40
45
  GPU_BUFFER_USAGE_VERTEX,
41
46
  } from '../gpu-usage';
42
47
  import { disposeTransientInstanceBuffers } from '../instance-buffer-cache';
48
+ import { deriveRenderExtent } from '../pipeline/render-extent';
43
49
  import { inspectStandardLighting } from '../pipeline/standard-lighting/inspection';
44
50
  import { standardLightingTopologySignature } from '../pipeline/standard-lighting/topology';
45
51
  import { inspectPointShadow } from '../point-shadow-inspection';
@@ -56,12 +62,22 @@ import type {
56
62
  ExtractedVolumetricFog,
57
63
  MaterialSnapshot,
58
64
  RenderableSnapshot,
65
+ ShadowCasterMembership,
59
66
  SkyboxSnapshot,
60
67
  SkylightSnapshot,
61
68
  } from '../render-system-extract';
62
- import type { PersistentGpuDrivenState } from '../scene/render-scene';
69
+ import type {
70
+ PersistentGpuDrivenState,
71
+ PersistentShadowCasterProjection,
72
+ } from '../scene/render-scene';
63
73
  import type { OcclusionFrameProjection } from '../scene/visibility/occlusion-runtime';
64
74
  import { SHADOW_ATLAS_DEFAULT_LAYERS } from '../shadow-atlas';
75
+ import {
76
+ admitSsrSpatial,
77
+ type SsrDependenciesInspection,
78
+ type SsrSpatialAdmission,
79
+ } from '../ssr/admission';
80
+ import { createSsrHistoryOwner } from '../ssr/history';
65
81
  import {
66
82
  getTransparentSortConfig,
67
83
  TRANSPARENT_SORT_MODE_LAYER_Y,
@@ -123,6 +139,66 @@ export type FrameObservationReadback = (
123
139
  lease: CurrentFrameObservationLease,
124
140
  ) => Promise<Result<Uint8Array, Error>>;
125
141
 
142
+ /**
143
+ * Fold World identity into the renderer-owned validation token. The GPU
144
+ * production owner receives only this opaque key; ECS identity and ordering
145
+ * stay in the record owner where the World list is authoritative.
146
+ */
147
+ function residencyValidationCacheKey(world: World, worlds: readonly World[]): string {
148
+ return [world.identity, ...worlds.map((candidate) => candidate.identity)].join('\u001f');
149
+ }
150
+
151
+ function renderableIdentity(snapshot: Pick<RenderableSnapshot, 'worldId' | 'entityKey'>): string {
152
+ return `${snapshot.worldId}:${snapshot.entityKey}`;
153
+ }
154
+
155
+ /**
156
+ * A GPU-owned indirect draw has no CPU submit callback. Keep the same
157
+ * record-stage consumption proof for downstream lifecycle owners by checking
158
+ * the concrete draw-key projection that was actually encoded for this frame.
159
+ */
160
+ function gpuDrivenRenderableConsumed(
161
+ entry: ValidatedRenderable,
162
+ drawKeys: ReadonlySet<string>,
163
+ ): boolean {
164
+ const draws = entry.source.gpuDrivenDraws;
165
+ if (draws === undefined || draws.length === 0) return false;
166
+ const worldEntity = worldEntityKey(entry.source.worldId, entry.source.entityKey);
167
+ return draws.some((draw, compactIndex) => {
168
+ const material = entry.source.materials[draw.materialSlot] ?? entry.source.material;
169
+ return drawKeys.has(
170
+ gpuDrivenDrawKey(
171
+ worldEntity,
172
+ material.materialHandle ?? -1,
173
+ gpuDrivenSourceDrawItemIndex(draw, compactIndex),
174
+ ),
175
+ );
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Project one shared validation result into a cull-specific renderable index
181
+ * domain. The retained ShadowCaster projection is the superset; the primary
182
+ * camera submission reuses its resolved mesh rows without a second residency
183
+ * walk or a second validation authority.
184
+ */
185
+ function projectValidatedRows(
186
+ validated: readonly ValidatedRenderable[],
187
+ target: readonly RenderableSnapshot[],
188
+ ): ValidatedRenderable[] {
189
+ const byIdentity = new Map<string, ValidatedRenderable>();
190
+ for (const row of validated) byIdentity.set(renderableIdentity(row.source), row);
191
+ const projected: ValidatedRenderable[] = [];
192
+ for (let index = 0; index < target.length; index += 1) {
193
+ const snapshot = target[index];
194
+ if (snapshot === undefined) continue;
195
+ const row = byIdentity.get(renderableIdentity(snapshot));
196
+ if (row === undefined) continue;
197
+ projected.push({ ...row, source: snapshot, renderableIndex: index });
198
+ }
199
+ return projected;
200
+ }
201
+
126
202
  export interface FrameObservationOptions {
127
203
  readonly semantic: 'linear-hdr';
128
204
  readonly readback: FrameObservationReadback;
@@ -219,7 +295,8 @@ export async function observeCurrentFrame(
219
295
  });
220
296
  }
221
297
 
222
- import { createTemporalView, resolveTemporalReset } from '../temporal/view';
298
+ import { acknowledgeRenderFeatureSubmission } from '../features/host';
299
+ import { createTemporalView, resolveTemporalReset, type TemporalView } from '../temporal/view';
223
300
  import { inspectVolumetricFog } from '../volume/inspection';
224
301
  import {
225
302
  acquireSwapChainTarget,
@@ -244,6 +321,7 @@ import {
244
321
  type CubeCaptureGraphState,
245
322
  ensureCompiledFrameGraph,
246
323
  executeCompiledFrameGraph,
324
+ getRenderFeatureGraphState,
247
325
  type RenderFeatureGraphCandidate,
248
326
  settleVolumetricFogGraphCandidate,
249
327
  } from './typed-frame-graph';
@@ -256,35 +334,34 @@ import {
256
334
 
257
335
  /**
258
336
  * Resolve the GPU mesh view used by the optional production raster before its
259
- * filtering pass runs. User meshes are pull-resident during record; relying
260
- * only on the boot-time builtin map would make the first frame classify every
261
- * imported mesh as a CPU fallback and would hide its LOD ranges from the GPU
262
- * cull. Keep the map frame-local so the pipeline state remains the builtin
263
- * compatibility owner while the residency cache remains the user-mesh SSOT.
337
+ * filtering pass runs. The result is keyed by the retained RenderScene slot,
338
+ * which preserves the World namespace already used by gpuStore. User meshes
339
+ * are pull-resident during record; relying only on the boot-time builtin map
340
+ * would make the first frame classify every imported mesh as a CPU fallback
341
+ * and would hide its LOD ranges from the GPU cull. Builtins are the only
342
+ * process-static entries and may be read from PipelineState.meshes.
264
343
  */
265
- function gpuDrivenMeshesForFrame(
344
+ export function gpuDrivenMeshesForFrame(
266
345
  internals: RenderSystemInternals,
267
346
  pipelineState: PipelineState,
268
347
  worlds: readonly World[],
269
348
  scene: PersistentGpuDrivenState | undefined,
270
349
  ): ReadonlyMap<number, MeshGpuHandles> {
271
- const meshes = new Map(pipelineState.meshes);
350
+ const meshes = new Map<number, MeshGpuHandles>();
272
351
  if (scene === undefined) return meshes;
273
- const cached = gpuDrivenMeshCache.get(scene.scene);
274
- if (
275
- cached !== undefined &&
276
- cached.meshResidencyEpoch === internals.gpuStore.meshResidencyEpoch &&
277
- cached.pipelineMeshes === pipelineState.meshes
278
- ) {
279
- return cached.value;
280
- }
281
352
  for (const slot of scene.slots) {
282
- const world = worlds[slot.snapshot.worldId];
283
- if (world === undefined) continue;
284
353
  const assetHandle = slot.snapshot.assetHandle;
285
354
  const sharedHandle = toShared<'MeshAsset'>(assetHandle);
355
+ const assetSlot = handleSlot(sharedHandle);
356
+ if (assetSlot < BUILTIN_BASE) {
357
+ const gpu = pipelineState.meshes.get(assetSlot);
358
+ if (gpu !== undefined) meshes.set(slot.slot, gpu);
359
+ continue;
360
+ }
361
+ const world = worlds[slot.snapshot.worldId];
362
+ if (world === undefined) continue;
286
363
  let gpu = internals.gpuStore.getMeshGpuHandles(sharedHandle, world);
287
- if (gpu === undefined && !meshes.has(assetHandle)) {
364
+ if (gpu === undefined) {
288
365
  const asset = resolveAssetHandle<MeshAsset>(world, sharedHandle);
289
366
  if (asset.ok) {
290
367
  const lodMeshes = slot.snapshot.lods?.map((lod) =>
@@ -303,26 +380,11 @@ function gpuDrivenMeshesForFrame(
303
380
  if (resident.ok) gpu = resident.value;
304
381
  }
305
382
  }
306
- if (gpu !== undefined) meshes.set(assetHandle, gpu);
383
+ if (gpu !== undefined) meshes.set(slot.slot, gpu);
307
384
  }
308
- const value = meshes;
309
- gpuDrivenMeshCache.set(scene.scene, {
310
- meshResidencyEpoch: internals.gpuStore.meshResidencyEpoch,
311
- pipelineMeshes: pipelineState.meshes,
312
- value,
313
- });
314
- return value;
385
+ return meshes;
315
386
  }
316
387
 
317
- const gpuDrivenMeshCache = new WeakMap<
318
- object,
319
- {
320
- readonly meshResidencyEpoch: number;
321
- readonly pipelineMeshes: ReadonlyMap<number, MeshGpuHandles>;
322
- readonly value: ReadonlyMap<number, MeshGpuHandles>;
323
- }
324
- >();
325
-
326
388
  /**
327
389
  * A frame-local material table. Repeated snapshot objects share one UBO slot;
328
390
  * each renderable keeps only the slot index used by each authored material.
@@ -342,18 +404,25 @@ export interface MaterialSlotPlan<T extends object> {
342
404
  */
343
405
  export function buildMaterialSlotPlan<T extends object>(
344
406
  materialGroups: readonly (readonly T[])[],
407
+ bindingClass?: (material: T, owner: number) => string,
345
408
  ): MaterialSlotPlan<T> {
346
- const slotByMaterial = new Map<T, number>();
409
+ const slotByMaterial = new Map<T, Map<string, number>>();
347
410
  const slots: T[] = [];
348
411
  const slotOwners: number[] = [];
349
412
  const slotIndices = materialGroups.map((materials, ownerIndex) =>
350
413
  materials.map((material) => {
351
- const cached = slotByMaterial.get(material);
414
+ const resourceClass = bindingClass?.(material, ownerIndex) ?? '';
415
+ let materialClasses = slotByMaterial.get(material);
416
+ if (materialClasses === undefined) {
417
+ materialClasses = new Map();
418
+ slotByMaterial.set(material, materialClasses);
419
+ }
420
+ const cached = materialClasses.get(resourceClass);
352
421
  if (cached !== undefined) return cached;
353
422
  const slot = slots.length;
354
423
  slots.push(material);
355
424
  slotOwners.push(ownerIndex);
356
- slotByMaterial.set(material, slot);
425
+ materialClasses.set(resourceClass, slot);
357
426
  return slot;
358
427
  }),
359
428
  );
@@ -392,9 +461,38 @@ export function materialSlotCountForPrefix(
392
461
  return maxSlot + 1;
393
462
  }
394
463
 
464
+ type MeshSsboCapacityResult = ReturnType<typeof ensureMeshSsboCapacity>;
465
+
466
+ /**
467
+ * Resolve the actual mesh-SSBO ceiling after the shared grow contract ran.
468
+ * A missing grow controller is the legacy/test surface with no renderer-owned
469
+ * ceiling; a failed grow explicitly exposes the pre-grow capacity and must
470
+ * constrain every row written by this frame, including retained shadow rows.
471
+ */
472
+ function meshSsboSlotCapacityAfterEnsure(
473
+ internals: Pick<RenderSystemInternals, 'growMeshSsbo' | 'meshSsboState'>,
474
+ neededSlots: number,
475
+ result: MeshSsboCapacityResult,
476
+ ): number {
477
+ if (internals.growMeshSsbo === undefined) return Number.POSITIVE_INFINITY;
478
+ if (!result.ok) return Math.max(0, result.degradedToSlotCount);
479
+ return internals.meshSsboState?.slotCount ?? neededSlots;
480
+ }
481
+
482
+ /**
483
+ * Keep a row sequence inside the capacity published by the mesh-SSBO owner.
484
+ * The caller has already reserved earlier rows (the primary view), so this
485
+ * helper only accepts the remaining slots for the retained shadow sequence.
486
+ */
487
+ function limitRowsToMeshSsboCapacity<T>(rows: readonly T[], availableSlots: number): readonly T[] {
488
+ if (availableSlots === Number.POSITIVE_INFINITY) return rows;
489
+ return rows.slice(0, Math.max(0, Math.floor(availableSlots)));
490
+ }
491
+
395
492
  export type RecordProfileRunner = <T>(phase: RenderRecordPhase, action: () => T) => T;
396
493
 
397
494
  export interface CubeCaptureFrameInput {
495
+ readonly captureDispatch?: readonly DispatchEntry[];
398
496
  readonly snapshots: readonly CubeCameraSnapshot[];
399
497
  readonly scheduler: import('../capture/scheduler').CubeCaptureScheduler;
400
498
  readonly state: CubeCaptureGraphState;
@@ -456,6 +554,18 @@ function prepareCubeCapture(
456
554
  }
457
555
 
458
556
  const EMPTY_ENTITY_KEYS: ReadonlySet<number> = new Set<number>();
557
+ const EMPTY_DRAW_KEYS: ReadonlySet<string> = new Set<string>();
558
+
559
+ function cacheEntityKeysFromDrawKeys(drawKeys: ReadonlySet<string>): ReadonlySet<number> {
560
+ const entityKeys = new Set<number>();
561
+ for (const key of drawKeys) {
562
+ const separator = key.indexOf(':');
563
+ if (separator <= 0) continue;
564
+ const entityKey = Number(key.slice(0, separator));
565
+ if (Number.isSafeInteger(entityKey)) entityKeys.add(entityKey);
566
+ }
567
+ return entityKeys;
568
+ }
459
569
 
460
570
  function runRecordProfilePhase<T>(
461
571
  runner: RecordProfileRunner | undefined,
@@ -465,6 +575,14 @@ function runRecordProfilePhase<T>(
465
575
  return runner === undefined ? action() : runner(phase, action);
466
576
  }
467
577
 
578
+ function shadowPlanes(matrix: Float32Array): Float32Array {
579
+ return frustum.fromViewProjection(frustum.create(), matrix as never);
580
+ }
581
+
582
+ function shadowViewToken(matrix: Float32Array): string {
583
+ return Array.from(matrix).join(',');
584
+ }
585
+
468
586
  function halfFloatToNumber(bits: number): number {
469
587
  const sign = (bits & 0x8000) === 0 ? 1 : -1;
470
588
  const exponent = (bits >>> 10) & 0x1f;
@@ -552,15 +670,19 @@ async function mapReflectionFallbackReadback(
552
670
  for (let y = 0; y < request.height; y += 1) {
553
671
  for (let x = 0; x < request.width; x += 1) {
554
672
  const offset = y * request.bytesPerRow + x * 8;
555
- const pixel = [0, 1, 2, 3].map((channel) =>
556
- halfFloatToNumber(view.getUint16(offset + channel * 2, true)),
557
- ) as [number, number, number, number];
558
- // The clear attachment carries alpha=1, so alpha alone is not
559
- // evidence that the Standard BRDF wrote a fallback lobe. Decode the
560
- // RGB channels from the mapped MRT and admit the first finite
561
- // non-zero color sample only.
562
- if (pixel.slice(0, 3).some((value) => Number.isFinite(value) && value !== 0)) {
563
- linearHdr = pixel;
673
+ // Validation above already rejects non-finite half floats. Scan the
674
+ // raw RGB magnitudes (both signed zero encodings are empty) without
675
+ // allocating/decoding millions of background pixels each frame.
676
+ const red = view.getUint16(offset, true);
677
+ const green = view.getUint16(offset + 2, true);
678
+ const blue = view.getUint16(offset + 4, true);
679
+ if ((red & 0x7fff) !== 0 || (green & 0x7fff) !== 0 || (blue & 0x7fff) !== 0) {
680
+ linearHdr = [
681
+ halfFloatToNumber(red),
682
+ halfFloatToNumber(green),
683
+ halfFloatToNumber(blue),
684
+ halfFloatToNumber(view.getUint16(offset + 6, true)),
685
+ ];
564
686
  y = request.height;
565
687
  break;
566
688
  }
@@ -1048,6 +1170,157 @@ function stageAcceptedVolumetricFogFrame(
1048
1170
  return { ...accepted, paramsBuffer };
1049
1171
  }
1050
1172
 
1173
+ function ssrRuntimeError(
1174
+ cause: unknown,
1175
+ ): RhiError | { code: string; message: string; name?: string } {
1176
+ if (cause instanceof RhiError) return cause;
1177
+ if (cause instanceof Error)
1178
+ return { code: 'ssr-history-failure', message: cause.message, name: cause.name };
1179
+ return { code: 'ssr-history-failure', message: String(cause) };
1180
+ }
1181
+
1182
+ function reportSsrHistoryFailure(
1183
+ internals: RenderSystemInternals,
1184
+ stage: 'build' | 'encode' | 'finish' | 'submit',
1185
+ cause: unknown,
1186
+ ): void {
1187
+ internals.errorRegistry.fire(
1188
+ new RhiError({
1189
+ code: 'webgpu-runtime-error',
1190
+ expected: `SSR history ${stage} succeeds before queue submission`,
1191
+ hint: 'inspect the SSR history owner failure and retry the next frame',
1192
+ detail: { error: ssrRuntimeError(cause) },
1193
+ }),
1194
+ );
1195
+ }
1196
+
1197
+ function mapSsrHistoryResetReason(
1198
+ reason: TemporalView['resetReason'],
1199
+ ): import('../ssr/history').SsrHistoryResetReason | undefined {
1200
+ switch (reason) {
1201
+ case 'first-frame':
1202
+ return 'first-enable';
1203
+ case 'resize':
1204
+ return 'resize';
1205
+ case 'camera-cut':
1206
+ case 'view-switch':
1207
+ return 'camera-cut';
1208
+ case 'history-version':
1209
+ return 'history-version';
1210
+ case 'environment-change':
1211
+ case 'fog-change':
1212
+ return 'reflection-generation';
1213
+ case 'device-recover':
1214
+ return 'device-recovery';
1215
+ case 'submit-failure':
1216
+ case undefined:
1217
+ return undefined;
1218
+ }
1219
+ }
1220
+
1221
+ function stageSsrHistoryCandidate(
1222
+ internals: RenderSystemInternals,
1223
+ frameState: RenderFrameState,
1224
+ camera: CameraSnapshot,
1225
+ width: number,
1226
+ height: number,
1227
+ admission: SsrSpatialAdmission,
1228
+ temporalView: TemporalView,
1229
+ ): boolean {
1230
+ const previousAdmission = frameState.ssrSpatialAdmission;
1231
+ frameState.ssrSpatialAdmission = admission;
1232
+ if (admission.status !== 'admitted') {
1233
+ if (frameState.ssrHistoryOwner !== undefined && previousAdmission?.status === 'admitted') {
1234
+ const owner = frameState.ssrHistoryOwner;
1235
+ owner.reset('disable');
1236
+ owner.retireAfterFence(internals.device.queue, (cause) => {
1237
+ reportSsrHistoryFailure(internals, 'submit', cause);
1238
+ });
1239
+ frameState.ssrHistoryOwner = undefined;
1240
+ }
1241
+ frameState.ssrHistoryCandidate = undefined;
1242
+ frameState.ssrLastCameraEntity = camera.entityKey;
1243
+ frameState.ssrLastHistoryVersion = camera.historyVersion;
1244
+ return true;
1245
+ }
1246
+
1247
+ let owner = frameState.ssrHistoryOwner;
1248
+ if (owner === undefined) {
1249
+ const created = createSsrHistoryOwner({
1250
+ device: internals.device,
1251
+ scope: internals.deviceScope,
1252
+ width,
1253
+ height,
1254
+ });
1255
+ if (!created.ok) {
1256
+ reportSsrHistoryFailure(internals, 'build', created.error);
1257
+ return false;
1258
+ }
1259
+ owner = created.value;
1260
+ frameState.ssrHistoryOwner = owner;
1261
+ } else if (owner.resources.width !== width || owner.resources.height !== height) {
1262
+ const resized = owner.resize(width, height);
1263
+ if (!resized.ok) {
1264
+ reportSsrHistoryFailure(internals, 'build', resized.error);
1265
+ return false;
1266
+ }
1267
+ owner.retireAfterFence(internals.device.queue, (cause) => {
1268
+ reportSsrHistoryFailure(internals, 'submit', cause);
1269
+ });
1270
+ }
1271
+
1272
+ let resetReason: import('../ssr/history').SsrHistoryResetReason | undefined;
1273
+ if (
1274
+ frameState.ssrLastCameraEntity !== undefined &&
1275
+ frameState.ssrLastCameraEntity !== camera.entityKey
1276
+ ) {
1277
+ resetReason = 'camera-cut';
1278
+ } else if (
1279
+ frameState.ssrLastHistoryVersion !== undefined &&
1280
+ frameState.ssrLastHistoryVersion !== camera.historyVersion
1281
+ ) {
1282
+ resetReason = 'history-version';
1283
+ } else {
1284
+ resetReason = mapSsrHistoryResetReason(temporalView.resetReason);
1285
+ }
1286
+ if (resetReason !== undefined) owner.reset(resetReason);
1287
+
1288
+ const started = owner.beginFrame();
1289
+ if (!started.ok) {
1290
+ reportSsrHistoryFailure(internals, 'build', started.error);
1291
+ return false;
1292
+ }
1293
+ const candidate = started.value;
1294
+ const payload =
1295
+ frameState.ssrTemporalParamsPayload?.byteLength === 32
1296
+ ? frameState.ssrTemporalParamsPayload
1297
+ : new Uint8Array(32);
1298
+ payload.fill(0);
1299
+ const values = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
1300
+ values.setUint32(0, candidate.historyValid ? 1 : 0, true);
1301
+ values.setFloat32(4, 0.9, true);
1302
+ values.setFloat32(8, admission.config.thickness, true);
1303
+ values.setFloat32(12, 0.5, true);
1304
+ // Reuse the shared successful-submit snapshot; SSR owns no jitter sequence
1305
+ // or independently advancing previous-camera state.
1306
+ const previousJitter = frameState.lastSuccessfulTemporalView?.currentJitterUv;
1307
+ values.setFloat32(16, temporalView.currentJitterUv?.[0] ?? 0, true);
1308
+ values.setFloat32(20, temporalView.currentJitterUv?.[1] ?? 0, true);
1309
+ values.setFloat32(24, previousJitter?.[0] ?? 0, true);
1310
+ values.setFloat32(28, previousJitter?.[1] ?? 0, true);
1311
+ const written = internals.device.queue.writeBuffer(owner.resources.paramsBuffer, 0, payload);
1312
+ if (!written.ok) {
1313
+ owner.abortFrame(candidate, 'encode');
1314
+ reportSsrHistoryFailure(internals, 'encode', written.error);
1315
+ return false;
1316
+ }
1317
+ frameState.ssrTemporalParamsPayload = payload;
1318
+ frameState.ssrHistoryCandidate = candidate;
1319
+ frameState.ssrLastCameraEntity = camera.entityKey;
1320
+ frameState.ssrLastHistoryVersion = camera.historyVersion;
1321
+ return true;
1322
+ }
1323
+
1051
1324
  function updateVolumetricFogInspection(
1052
1325
  frameState: RenderFrameState,
1053
1326
  fog: ExtractedVolumetricFog | undefined,
@@ -1303,7 +1576,7 @@ export function recordFrame(
1303
1576
  world: World,
1304
1577
  cameras: CameraSnapshot[],
1305
1578
  lights: ExtractedLights,
1306
- renderables: RenderableSnapshot[],
1579
+ renderables: readonly RenderableSnapshot[],
1307
1580
  transparentDispatch: readonly DispatchEntry[],
1308
1581
  frameState: RenderFrameState,
1309
1582
  dispatchCounts: DispatchCounts,
@@ -1339,10 +1612,18 @@ export function recordFrame(
1339
1612
  fogSignature = '',
1340
1613
  environmentReady = true,
1341
1614
  transmissionDemand?: TransmissionDemand,
1615
+ shadowCasterEntityKeys: ReadonlySet<number> = EMPTY_ENTITY_KEYS,
1616
+ shadowCasterDrawKeys: ReadonlySet<string> | undefined = undefined,
1342
1617
  volumetricFog?: ExtractedVolumetricFog,
1343
1618
  timingCapture?: GpuTimingCapture,
1619
+ shadowCasterMembership?: readonly ShadowCasterMembership[],
1620
+ shadowCasterProjection?: PersistentShadowCasterProjection,
1621
+ shadowCasterWorldKeys?: readonly number[],
1622
+ preparedLighting?: ReturnType<typeof prepareFrameLighting>,
1623
+ ssrDependencies?: SsrDependenciesInspection,
1344
1624
  onRenderableDraw?: (entry: ValidatedRenderable) => void,
1345
1625
  ): boolean {
1626
+ void shadowCasterEntityKeys;
1346
1627
  frameState.reflectionFallbackObservationSource = undefined;
1347
1628
  frameState.reflectionFallbackCompletion = undefined;
1348
1629
  // The `try / finally` wrapper advances `frameState.frameNumber` exactly
@@ -1392,6 +1673,7 @@ export function recordFrame(
1392
1673
  }
1393
1674
  const camera = activeCameras[0];
1394
1675
  if (!camera) return false;
1676
+ frameState.ssrRequested = camera.screenSpaceReflection !== undefined;
1395
1677
  const pipelineState = internals.getPipelineState();
1396
1678
  if (pipelineState === null) return false;
1397
1679
  if (pipelineState.device !== internals.device) {
@@ -1433,8 +1715,12 @@ export function recordFrame(
1433
1715
  // ensure + ExtractedLights three-arm destructure (directional fallback,
1434
1716
  // point/spot/Rect arrays, totalLightCount). Extracted to prepareFrameLighting
1435
1717
  // (M3/w18) so recordFrame stays a skeleton.
1436
- const lighting = runRecordProfilePhase(profilePhase, 'record/scene-state/lighting-prep', () =>
1437
- prepareFrameLighting(internals, frameState, lights, camera, pipelineState),
1718
+ const lighting = runRecordProfilePhase(
1719
+ profilePhase,
1720
+ 'record/scene-state/lighting-prep',
1721
+ () =>
1722
+ preparedLighting ??
1723
+ prepareFrameLighting(internals, frameState, lights, camera, pipelineState),
1438
1724
  );
1439
1725
  if (!lighting.ok) return lighting;
1440
1726
  const { light, totalLightCount, standard } = lighting.value;
@@ -1519,6 +1805,10 @@ export function recordFrame(
1519
1805
  const effectiveShadowMapSize = resolveShadowMapSize(internals, lights);
1520
1806
  const effectiveSpotShadowMapSize = resolveSpotShadowMapSize(internals, lights);
1521
1807
  const graphShadowMapSize = effectiveShadowMapSize ?? effectiveSpotShadowMapSize;
1808
+ const hasShadowViews =
1809
+ (effectiveShadowMapSize ?? 0) > 0 ||
1810
+ lights.pointShadow.length > 0 ||
1811
+ lights.spot.some((spot) => spot.castShadow);
1522
1812
  const volumeCapability = hasVolumetricFogCapability(
1523
1813
  internals.device.caps,
1524
1814
  internals.volumetricFogShaders,
@@ -1579,89 +1869,287 @@ export function recordFrame(
1579
1869
  (volumetricFog?.status === 'degraded' && acceptedFallback !== undefined);
1580
1870
 
1581
1871
  let preparedGpuDriven: PreparedGpuDrivenFrame | undefined;
1582
- if (gpuDriven !== undefined) {
1583
- const standardPbrArtifact = internals.getMaterialShaderArtifact?.(
1584
- 'forgeax::default-standard-pbr',
1872
+ let materialBindingClasses: ReadonlyMap<string, string> | undefined;
1873
+ const abortPreparedGpuDriven = (): void => {
1874
+ preparedGpuDriven?._abortResourceReplacement?.();
1875
+ };
1876
+ const cubeCaptureWork = prepareCubeCapture(internals, cubeCapture, camera);
1877
+ const reflectionFallbackCandidate = cubeCapture?.reflectionProbes?.fallbackDemand === true;
1878
+ // Capture views have independent draw ownership; display-camera GPU work
1879
+ // remains active while their faces and SSR fallback attachments are recorded.
1880
+ // ProbeBlendRecord is an object-level fragment ABI. The scene-index GPU
1881
+ // lane has no binding for that record yet, so claiming a probe-bearing
1882
+ // row would silently fall back to Skylight and make local diffuse probes
1883
+ // visually ineffective. Keep the whole frame on the CPU producer until
1884
+ // the scene-index lane carries the same record contract.
1885
+ const probeBlendFrame = renderables.some((entry) => entry.probeBlendRecord !== undefined);
1886
+ const frameGpuDriven = probeBlendFrame ? undefined : gpuDriven;
1887
+
1888
+ // Validate first so user meshes are resident in the same frame. The GPU
1889
+ // owner consumes this producer-owned map together with the builtin mesh
1890
+ // entries; it never resolves an asset or creates a fallback residency.
1891
+ // Stable persistent-scene frames reuse this exact producer result. The
1892
+ // cache is fenced by source identities plus mesh/device/pipeline/catalog
1893
+ // generations, so recovery or any scene/resource replacement performs a
1894
+ // fresh owner validation before a capable submission.
1895
+ const shadowValidationActive = hasShadowViews && shadowCasterProjection !== undefined;
1896
+ const validationRenderables = shadowValidationActive
1897
+ ? shadowCasterProjection.renderables
1898
+ : renderables;
1899
+ const validationDispatch = shadowValidationActive
1900
+ ? shadowCasterProjection.dispatch
1901
+ : transparentDispatch;
1902
+ const validationInput = {
1903
+ cacheKey: residencyValidationCacheKey(world, worlds),
1904
+ renderables: validationRenderables,
1905
+ transparentDispatch: validationDispatch,
1906
+ pipelineState,
1907
+ pipelineHandle: frameState.installedPipelineHandle,
1908
+ meshResidencyEpoch: internals.gpuStore.meshResidencyEpoch,
1909
+ deviceGeneration: internals.deviceScope.generation,
1910
+ assetCatalogEpoch: internals.assets.catalogEpoch,
1911
+ } as const;
1912
+ const cachedValidation = frameGpuDriven?.owner.reuseResidencyValidation(validationInput);
1913
+ const validatedProjection =
1914
+ cachedValidation ??
1915
+ runRecordProfilePhase(profilePhase, 'record/validation', () =>
1916
+ validateRenderables(
1917
+ internals,
1918
+ world,
1919
+ worlds,
1920
+ pipelineState,
1921
+ frameState,
1922
+ validationRenderables,
1923
+ validationDispatch,
1924
+ EMPTY_ENTITY_KEYS,
1925
+ ),
1585
1926
  );
1586
- const prepared = gpuDriven.owner.prepare({
1587
- scene: gpuDriven.scene,
1927
+ if (cachedValidation === undefined) {
1928
+ frameGpuDriven?.owner.rememberResidencyValidation(validationInput, validatedProjection);
1929
+ }
1930
+ // Camera culling only changes the primary index domain. When shadows are
1931
+ // active, the retained projection is the validation superset; project the
1932
+ // same resolved rows into the camera-visible subset without another walk.
1933
+ const validatedShadowForResidency = shadowValidationActive ? validatedProjection : undefined;
1934
+ const validatedForResidency = shadowValidationActive
1935
+ ? projectValidatedRows(validatedProjection, renderables)
1936
+ : validatedProjection;
1937
+
1938
+ // S_fallback is the second output of the built-in Standard PBR BRDF. A
1939
+ // graph may still carry the detached owner tickets for inspection, but
1940
+ // the MRT is admitted only when every CPU-rendered primary row in this
1941
+ // frame uses that exact output contract. Unsupported compositions remain
1942
+ // fail-closed for SSR instead of submitting a device-invalid command.
1943
+ const sceneInputs =
1944
+ validatedForResidency.length > 0 &&
1945
+ validatedForResidency.every(
1946
+ (entry) => entry.source.material.materialShaderId === 'forgeax::default-standard-pbr',
1947
+ );
1948
+ const ssrAdmission =
1949
+ ssrDependencies === undefined
1950
+ ? undefined
1951
+ : admitSsrSpatial({
1952
+ camera,
1953
+ environment: {
1954
+ lane: standard.prepared.renderPath === 'deferred' ? 'deferred' : 'forward',
1955
+ m0: ssrDependencies.admission,
1956
+ sceneInputs,
1957
+ // Standard geometry produces current motion/coverage in this
1958
+ // graph, including its first frame. Prior history is optional.
1959
+ temporal: sceneInputs,
1960
+ reflectionFallback: ssrDependencies.reflectionFallback !== undefined,
1961
+ capabilities: {
1962
+ compute: internals.device.caps.compute,
1963
+ storageTexture: internals.device.caps.storageTexture,
1964
+ rgba16floatRenderable: internals.device.caps.rgba16floatRenderable,
1965
+ r32floatSampledStorage: ssrDependencies.format?.verdict === 'admitted',
1966
+ },
1967
+ recovery: true,
1968
+ },
1969
+ });
1970
+ frameState.reflectionFallbackDemand =
1971
+ (reflectionFallbackCandidate || ssrAdmission?.status === 'admitted') && sceneInputs;
1972
+
1973
+ if (frameGpuDriven !== undefined) {
1974
+ const gpuDrivenMeshes = new Map(
1975
+ gpuDrivenMeshesForFrame(internals, pipelineState, worlds, frameGpuDriven.scene),
1976
+ );
1977
+ const meshSlotByIdentity = new Map(
1978
+ (frameGpuDriven.scene?.slots ?? []).map((slot) => [
1979
+ worldEntityKey(slot.snapshot.worldId, slot.snapshot.entityKey),
1980
+ slot.slot,
1981
+ ]),
1982
+ );
1983
+ for (const row of [...validatedForResidency, ...(validatedShadowForResidency ?? [])]) {
1984
+ const slot = meshSlotByIdentity.get(
1985
+ worldEntityKey(row.source.worldId, row.source.entityKey),
1986
+ );
1987
+ if (slot !== undefined) gpuDrivenMeshes.set(slot, row.mesh);
1988
+ }
1989
+ // Resolve one producer artifact for every prepared draw. COLOR_0 is a
1990
+ // per-geometry ABI axis, so a residency-wide `.some(color)` would pair
1991
+ // a no-color draw with the colored receipt (or vice versa) and collapse
1992
+ // two distinct pipeline/projection identities before indirect record.
1993
+ materialBindingClasses = projectMaterialBindingClasses(
1994
+ validatedForResidency,
1995
+ transparentDispatch,
1996
+ cubeCapture?.reflectionProbes,
1997
+ );
1998
+ const { materialArtifacts, shadowMaterialArtifacts, firstArtifact, firstSkinArtifact } =
1999
+ collectGpuDrivenMaterialArtifacts({
2000
+ rows: validatedForResidency,
2001
+ shadowRows: validatedShadowForResidency ?? [],
2002
+ dispatch: transparentDispatch,
2003
+ shadowDispatch: shadowCasterProjection?.dispatch ?? [],
2004
+ resolve: internals.getMaterialShaderArtifact,
2005
+ clustered:
2006
+ standard.kind === 'clustered' &&
2007
+ (standard.prepared.local.length > 0 ||
2008
+ frameState.installedPipelineConfig?.ssao?.enabled === true),
2009
+ reflectionFallback: frameState.reflectionFallbackDemand === true,
2010
+ });
2011
+ const paletteUploadBytes = validatedForResidency.reduce(
2012
+ (total, row) => total + (row.source.skin?.uploadBytes ?? 0),
2013
+ 0,
2014
+ );
2015
+ const prepared = frameGpuDriven.owner.prepare({
2016
+ scene: frameGpuDriven.scene,
1588
2017
  camera,
1589
- meshes: gpuDrivenMeshesForFrame(internals, pipelineState, worlds, gpuDriven.scene),
2018
+ meshBySlot: gpuDrivenMeshes,
1590
2019
  viewBindGroupLayout: pipelineState.viewBindGroupLayout,
1591
2020
  meshResidencyEpoch: internals.gpuStore.meshResidencyEpoch,
1592
- // The built-in Standard graph (including its clustered transport) is
1593
- // eligible for the GPU-driven LOD lane. The old HDRP boolean no
1594
- // longer exists in the unified pipeline state, so it must not be
1595
- // inferred from `standard.kind`.
1596
- hdrp: false,
1597
- ...(gpuDriven.activeEntityKeys === undefined
2021
+ materialBindingClasses,
2022
+ ...(frameGpuDriven.activeEntityKeys === undefined
1598
2023
  ? {}
1599
- : { activeEntityKeys: gpuDriven.activeEntityKeys }),
1600
- ...(gpuDriven.activeEntityRevision === undefined
2024
+ : { activeEntityKeys: frameGpuDriven.activeEntityKeys }),
2025
+ ...(frameGpuDriven.activeEntityRevision === undefined
1601
2026
  ? {}
1602
- : { activeEntityRevision: gpuDriven.activeEntityRevision }),
1603
- ...(gpuDriven.telemetryCandidateCount === undefined
2027
+ : { activeEntityRevision: frameGpuDriven.activeEntityRevision }),
2028
+ ...(frameGpuDriven.telemetryCandidateCount === undefined
1604
2029
  ? {}
1605
- : { telemetryCandidateCount: gpuDriven.telemetryCandidateCount }),
1606
- ...(gpuDriven.occlusion === undefined ? {} : { occlusion: gpuDriven.occlusion }),
1607
- ...(standardPbrArtifact === undefined ? {} : { standardPbrArtifact }),
1608
- standardPbrPipelineState: pipelineState,
2030
+ : { telemetryCandidateCount: frameGpuDriven.telemetryCandidateCount }),
2031
+ ...(frameGpuDriven.occlusion === undefined ? {} : { occlusion: frameGpuDriven.occlusion }),
2032
+ ...(frameGpuDriven.scene?.structureMetrics === undefined
2033
+ ? {}
2034
+ : {
2035
+ structureMetrics: {
2036
+ ...frameGpuDriven.scene.structureMetrics,
2037
+ paletteUploadBytes,
2038
+ },
2039
+ }),
2040
+ ...(firstArtifact === undefined ? {} : { materialArtifact: firstArtifact }),
2041
+ ...(firstSkinArtifact === undefined ? {} : { materialSkinArtifact: firstSkinArtifact }),
2042
+ ...(materialArtifacts.size === 0 ? {} : { materialArtifacts }),
2043
+ ...(shadowMaterialArtifacts.size === 0 ? {} : { shadowMaterialArtifacts }),
2044
+ materialPipelineState: pipelineState,
2045
+ ...(internals.getMaterialShaderPipelineEntry === undefined
2046
+ ? {}
2047
+ : { materialPipelineFactory: internals.getMaterialShaderPipelineEntry }),
2048
+ ...(shadowCasterDrawKeys === undefined ? {} : { shadowCasterDrawKeys }),
2049
+ ...(shadowCasterMembership === undefined ? {} : { shadowCasterMembership }),
1609
2050
  });
1610
- if (prepared.ok) preparedGpuDriven = prepared.value;
1611
- else if (prepared.error.code !== 'rhi-not-available')
2051
+ if (!prepared.ok) {
2052
+ // A capable GPU-owned candidate with missing producer resources is a
2053
+ // structured stop, not permission to enumerate the same draw through
2054
+ // the CPU semantic path. Preserve the last-known-good graph and retry
2055
+ // after the producer publishes a validated artifact.
1612
2056
  internals.errorRegistry.fire(prepared.error);
2057
+ abortPreparedGpuDriven();
2058
+ return false;
2059
+ } else if (prepared.value !== undefined) {
2060
+ preparedGpuDriven = prepared.value;
2061
+ const shadowViews: Array<{
2062
+ readonly identity: ShadowViewIdentity;
2063
+ readonly planes: Float32Array;
2064
+ readonly cacheToken: string;
2065
+ }> = [];
2066
+ for (const [index, matrix] of (lights.lightViewProj ?? []).entries()) {
2067
+ shadowViews.push({
2068
+ identity: { kind: 'directional', index },
2069
+ planes: shadowPlanes(matrix),
2070
+ cacheToken: `${shadowViewToken(matrix)}|target:${graphShadowMapSize}:${frameState.graphGeneration}`,
2071
+ });
2072
+ }
2073
+ for (const [index, point] of lights.pointShadow.entries()) {
2074
+ for (let face = 0; face < 6; face += 1) {
2075
+ const matrix = point.shadowMatrices.subarray(face * 16, face * 16 + 16);
2076
+ shadowViews.push({
2077
+ identity: { kind: 'point', index, face },
2078
+ planes: shadowPlanes(matrix),
2079
+ cacheToken: `${shadowViewToken(matrix)}|target:${graphShadowMapSize}:${frameState.graphGeneration}`,
2080
+ });
2081
+ }
2082
+ }
2083
+ let shadowSpotIndex = 0;
2084
+ for (const spot of lights.spot) {
2085
+ if (spot.lightViewProj === undefined) continue;
2086
+ shadowViews.push({
2087
+ identity: { kind: 'spot', index: shadowSpotIndex },
2088
+ planes: shadowPlanes(spot.lightViewProj),
2089
+ cacheToken: `${shadowViewToken(spot.lightViewProj)}|target:${graphShadowMapSize}:${frameState.graphGeneration}`,
2090
+ });
2091
+ shadowSpotIndex += 1;
2092
+ }
2093
+ if (prepared.value.updateShadowViews !== undefined) {
2094
+ const shadowUpdate = prepared.value.updateShadowViews(shadowViews);
2095
+ if (!shadowUpdate.ok) {
2096
+ internals.errorRegistry.fire(shadowUpdate.error);
2097
+ abortPreparedGpuDriven();
2098
+ return false;
2099
+ }
2100
+ }
2101
+ }
1613
2102
  }
1614
2103
 
2104
+ const gpuOwnedDrawKeys = preparedGpuDriven?.drawKeys ?? EMPTY_DRAW_KEYS;
1615
2105
  // The GPU lane owns only the primary opaque raster today. Directional,
1616
- // point, and spot shadow views still consume the CPU-validated draw plan.
1617
- const cpuShadowCastersActive =
1618
- (effectiveShadowMapSize ?? 0) > 0 ||
1619
- lights.pointShadow.length > 0 ||
1620
- lights.spot.some((spot) => spot.castShadow);
1621
- const gpuOwnsAllPrimaryRaster = preparedGpuDriven?.ownsAllRenderables === true;
1622
- gpuDriven?.onSceneDemand(gpuDriven.owner.requiresSceneRows());
1623
-
1624
- const cubeCaptureWork = prepareCubeCapture(internals, cubeCapture, camera);
1625
- const reflectionFallbackCandidate = cubeCapture?.reflectionProbes?.fallbackDemand === true;
1626
-
1627
- // Validate renderables before graph admission. Besides producing the draw
1628
- // plan, this is the owner of per-frame cache eviction; running it before a
1629
- // graph candidate can fail guarantees despawned instance buffers are
1630
- // released even when the frame cannot be submitted.
1631
- const gpuOwnedEntityKeys = preparedGpuDriven?.entityKeys ?? EMPTY_ENTITY_KEYS;
1632
- const gpuDrivenWorldKeys = gpuDriven?.scene?.worldKeys;
1633
- const validated = runRecordProfilePhase(profilePhase, 'record/validation', () =>
1634
- gpuOwnsAllPrimaryRaster && !cpuShadowCastersActive && cubeCaptureWork.length === 0
1635
- ? []
1636
- : validateRenderables(
1637
- internals,
1638
- world,
1639
- worlds,
1640
- pipelineState,
1641
- frameState,
1642
- renderables,
1643
- transparentDispatch,
1644
- cpuShadowCastersActive || cubeCaptureWork.length > 0
1645
- ? EMPTY_ENTITY_KEYS
1646
- : gpuOwnedEntityKeys,
1647
- gpuDrivenWorldKeys,
1648
- ),
2106
+ // point, and spot shadow views consume the same CPU-validated rows; they do
2107
+ // not require a second residency walk because the first validation already
2108
+ // resolved every renderable used by both lanes.
2109
+ // The GPU lane owns the actual geometry, but the main-pass material
2110
+ // producer still needs the validated snapshot rows to build the
2111
+ // frame-global bind-group table consumed by indirect draws. The record
2112
+ // loop skips claimed submeshes through `gpuDrivenDrawKeys`, so retaining
2113
+ // these rows does not issue duplicate CPU geometry. Reusing this one
2114
+ // validation result also keeps active shadow views from repeating the
2115
+ // residency walk.
2116
+ const validated = validatedForResidency;
2117
+ frameGpuDriven?.owner.recordCpuValidation(
2118
+ validated,
2119
+ gpuOwnedDrawKeys,
2120
+ validationInput.cacheKey,
2121
+ );
2122
+ const cacheValidated =
2123
+ validatedShadowForResidency === undefined
2124
+ ? validated
2125
+ : [...validated, ...validatedShadowForResidency];
2126
+ cleanPerFrameCaches(
2127
+ internals,
2128
+ frameState,
2129
+ cacheValidated,
2130
+ cacheEntityKeysFromDrawKeys(gpuOwnedDrawKeys),
1649
2131
  );
1650
- gpuDriven?.owner.recordCpuValidation(validated, gpuOwnedEntityKeys, gpuDrivenWorldKeys);
1651
- cleanPerFrameCaches(internals, frameState, validated, gpuOwnedEntityKeys);
1652
-
1653
- // S_fallback is the second output of the built-in Standard PBR BRDF. A
1654
- // graph may still carry the detached owner tickets for inspection, but
1655
- // the MRT is admitted only when every CPU-rendered primary row in this
1656
- // frame uses that exact output contract. Unsupported compositions remain
1657
- // fail-closed for SSR instead of submitting a device-invalid command.
1658
- frameState.reflectionFallbackDemand =
1659
- reflectionFallbackCandidate &&
1660
- validated.length > 0 &&
1661
- validated.every(
1662
- (entry) => entry.source.material.materialShaderId === 'forgeax::default-standard-pbr',
1663
- );
1664
2132
 
2133
+ // Compile the single frame graph only after GPU preparation has projected
2134
+ // the current frame. This keeps the graph topology and its execution
2135
+ // closure on the same prepared GPU-owned draw set.
2136
+
2137
+ // RenderExtent is an authored DynamicResolution path. The native TAA path
2138
+ // intentionally has no extent object, so every Standard consumer keeps the
2139
+ // exact surface domain even when a surface is not 8-pixel aligned. An
2140
+ // adaptive range remains unavailable until a renderer-owned timing/
2141
+ // controller chain exists; do not turn maxScale into a fake performance
2142
+ // decision. Fixed minScale=maxScale is the only extent currently admitted.
2143
+ const renderExtent =
2144
+ camera.dynamicResolution === undefined ||
2145
+ camera.dynamicResolution.minScale !== camera.dynamicResolution.maxScale
2146
+ ? undefined
2147
+ : deriveRenderExtent({
2148
+ outputWidth: Math.max(1, internals.canvas.width),
2149
+ outputHeight: Math.max(1, internals.canvas.height),
2150
+ requestedScale: camera.dynamicResolution.minScale,
2151
+ generation: internals.deviceScope?.generation ?? 0,
2152
+ });
1665
2153
  const preflightGraph = runRecordProfilePhase(profilePhase, 'record/render-graph', () =>
1666
2154
  ensureCompiledFrameGraph(
1667
2155
  internals,
@@ -1677,13 +2165,19 @@ export function recordFrame(
1677
2165
  cubeCapture?.state,
1678
2166
  cameras.length === 0,
1679
2167
  transmissionDemand,
1680
- gpuDriven?.occlusion,
2168
+ frameGpuDriven?.occlusion,
1681
2169
  graphVolumetricFog,
1682
2170
  volumeTopologyCandidate,
1683
2171
  standard,
2172
+ undefined,
2173
+ ssrAdmission,
2174
+ renderExtent,
1684
2175
  ),
1685
2176
  );
1686
- if (preflightGraph === null) return false;
2177
+ if (preflightGraph === null) {
2178
+ abortPreparedGpuDriven();
2179
+ return false;
2180
+ }
1687
2181
 
1688
2182
  // The compiled graph is the acceptance boundary for the prepared
1689
2183
  // Standard lighting declaration. Upload the same declaration only after
@@ -1704,6 +2198,7 @@ export function recordFrame(
1704
2198
  },
1705
2199
  }),
1706
2200
  );
2201
+ abortPreparedGpuDriven();
1707
2202
  return false;
1708
2203
  }
1709
2204
  if (standard.kind === 'clustered') {
@@ -1732,6 +2227,7 @@ export function recordFrame(
1732
2227
  );
1733
2228
  if (!clusterWrite.ok) {
1734
2229
  internals.errorRegistry.fire(clusterWrite.error);
2230
+ abortPreparedGpuDriven();
1735
2231
  return false;
1736
2232
  }
1737
2233
  }
@@ -1744,7 +2240,10 @@ export function recordFrame(
1744
2240
  const swapTarget = runRecordProfilePhase(profilePhase, 'record/swapchain', () =>
1745
2241
  acquireSwapChainTarget(internals, pipelineState),
1746
2242
  );
1747
- if (swapTarget === null) return false;
2243
+ if (swapTarget === null) {
2244
+ abortPreparedGpuDriven();
2245
+ return false;
2246
+ }
1748
2247
  const currentTexture = swapTarget.currentTexture;
1749
2248
  const view = swapTarget.view;
1750
2249
  const targetW = swapTarget.targetW;
@@ -1770,6 +2269,12 @@ export function recordFrame(
1770
2269
  environmentSignature,
1771
2270
  fogSignature,
1772
2271
  deviceGeneration: internals.deviceScope?.generation ?? 0,
2272
+ ...(renderExtent === undefined
2273
+ ? {}
2274
+ : {
2275
+ internalWidth: renderExtent.internalWidth,
2276
+ internalHeight: renderExtent.internalHeight,
2277
+ }),
1773
2278
  ...(camera.historyVersion === undefined ? {} : { historyVersion: camera.historyVersion }),
1774
2279
  historyValid: false,
1775
2280
  currentUnjitteredViewProjection,
@@ -1802,6 +2307,23 @@ export function recordFrame(
1802
2307
  : { kind: 'off', view: temporalView };
1803
2308
  const recordCamera: CameraSnapshot =
1804
2309
  temporalView.mode === 'taa' ? { ...camera, temporal: temporalView } : camera;
2310
+ if (ssrAdmission !== undefined) {
2311
+ const historyWidth = Math.max(1, internals.canvas.width);
2312
+ const historyHeight = Math.max(1, internals.canvas.height);
2313
+ if (
2314
+ !stageSsrHistoryCandidate(
2315
+ internals,
2316
+ frameState,
2317
+ camera,
2318
+ historyWidth,
2319
+ historyHeight,
2320
+ ssrAdmission,
2321
+ temporalView,
2322
+ )
2323
+ ) {
2324
+ return false;
2325
+ }
2326
+ }
1805
2327
  let activeVolumetricFogContext: VolumetricFogFrameContext | undefined;
1806
2328
  let activeVolumetricFogSignature: VolumeTemporalSignature | undefined;
1807
2329
  if (graphVolumetricFog?.status === 'available' && baseVolumetricFogContext !== undefined) {
@@ -1828,6 +2350,7 @@ export function recordFrame(
1828
2350
  );
1829
2351
  if (!historyFlagWritten.ok) {
1830
2352
  internals.errorRegistry.fire(historyFlagWritten.error);
2353
+ abortPreparedGpuDriven();
1831
2354
  return false;
1832
2355
  }
1833
2356
  }
@@ -1903,9 +2426,19 @@ export function recordFrame(
1903
2426
  // order, run the mesh-SSBO capacity gate (graceful truncation), and build
1904
2427
  // the fold dispatch plan. Extracted to buildDispatchPlan (M3/w18).
1905
2428
  const dispatchPlan = runRecordProfilePhase(profilePhase, 'record/dispatch-plan', () =>
1906
- buildDispatchPlan(internals, validated, transparentDispatch, foldBuckets),
2429
+ buildDispatchPlan(
2430
+ internals,
2431
+ validated,
2432
+ transparentDispatch,
2433
+ foldBuckets,
2434
+ validatedShadowForResidency ?? [],
2435
+ materialBindingClasses,
2436
+ ),
1907
2437
  );
1908
2438
  const validatedOrdered = dispatchPlan.validatedOrdered;
2439
+ const shadowValidatedOrdered = dispatchPlan.shadowValidatedOrdered;
2440
+ const shadowMeshSsboBase = validatedOrdered.length;
2441
+ const shadowDispatch = shadowCasterProjection?.dispatch;
1909
2442
  const foldDispatchPlan = dispatchPlan.foldDispatchPlan;
1910
2443
  const materialSlotIndices = dispatchPlan.materialSlotIndices;
1911
2444
  const materialSlots = dispatchPlan.materialSlots;
@@ -1937,12 +2470,13 @@ export function recordFrame(
1937
2470
  const unormViewRes = internals.device.createTextureView(currentTexture, {});
1938
2471
  if (!unormViewRes.ok) {
1939
2472
  internals.errorRegistry.fire(unormViewRes.error);
2473
+ abortPreparedGpuDriven();
1940
2474
  return false;
1941
2475
  }
1942
2476
  ldrSpritePassView = unormViewRes.value;
1943
2477
  }
1944
2478
 
1945
- // View / mesh uniform uploads are only needed when geometry will be drawn,
2479
+ // View / mesh uniform uploads are needed when geometry or a feature consumes View,
1946
2480
  // OR when a skybox is active (skybox pass reads inverseViewProj from
1947
2481
  // the View UBO). Skip the writeBuffer round-trips on the Case E
1948
2482
  // (clear-pass-only) path only when neither condition is met.
@@ -1957,7 +2491,10 @@ export function recordFrame(
1957
2491
  );
1958
2492
  if (
1959
2493
  validatedOrdered.length > 0 ||
2494
+ shadowValidatedOrdered.length > 0 ||
2495
+ getRenderFeatureGraphState(internals).plans.some(({ plan }) => plan.passes.length > 0) ||
1960
2496
  skyboxActive ||
2497
+ frameState.environmentFrame?.source.kind === 'atmosphere' ||
1961
2498
  activeVolumetricFogContext !== undefined ||
1962
2499
  preparedVolumetricFog !== undefined
1963
2500
  ) {
@@ -2000,6 +2537,15 @@ export function recordFrame(
2000
2537
  validatedOrdered,
2001
2538
  foldDispatchPlan,
2002
2539
  );
2540
+ if (shadowValidatedOrdered.length > 0) {
2541
+ uploadMeshSsboBatch(
2542
+ internals.device.queue,
2543
+ pipelineState.meshStorageBuffer,
2544
+ shadowValidatedOrdered,
2545
+ null,
2546
+ shadowMeshSsboBase,
2547
+ );
2548
+ }
2003
2549
  });
2004
2550
  }
2005
2551
 
@@ -2022,6 +2568,7 @@ export function recordFrame(
2022
2568
  const encoderResult = internals.device.createCommandEncoder({ label: 'render-system-frame' });
2023
2569
  if (!encoderResult.ok) {
2024
2570
  internals.errorRegistry.fire(encoderResult.error);
2571
+ abortPreparedGpuDriven();
2025
2572
  return false;
2026
2573
  }
2027
2574
  const encoder: RhiCommandEncoder = encoderResult.value;
@@ -2100,8 +2647,6 @@ export function recordFrame(
2100
2647
  const passCtx: _InternalRenderPipelineContext = {
2101
2648
  assets: internals.assets,
2102
2649
  world,
2103
- gpuDrivenEntityKeys: gpuOwnedEntityKeys,
2104
- ...(gpuDrivenWorldKeys === undefined ? {} : { gpuDrivenWorldKeys }),
2105
2650
  store: internals.gpuStore,
2106
2651
  pipelineState,
2107
2652
  bloomResources: pipelineState.perPassResources.getBloomResources?.() ?? null,
@@ -2111,6 +2656,7 @@ export function recordFrame(
2111
2656
  clear,
2112
2657
  targetW,
2113
2658
  targetH,
2659
+ ...(renderExtent === undefined ? {} : { extent: renderExtent }),
2114
2660
  currentTexture,
2115
2661
  camera: recordCamera,
2116
2662
  tonemapActive,
@@ -2137,6 +2683,10 @@ export function recordFrame(
2137
2683
  ...(internals.volumetricFogShaders === undefined
2138
2684
  ? {}
2139
2685
  : { volumetricFogShaders: internals.volumetricFogShaders }),
2686
+ ...(internals.ssrShaders === undefined ? {} : { ssrShaders: internals.ssrShaders }),
2687
+ ...(internals.atmosphereShaders === undefined
2688
+ ? {}
2689
+ : { atmosphereShaders: internals.atmosphereShaders }),
2140
2690
  ...(activeVolumetricFogContext === undefined
2141
2691
  ? {}
2142
2692
  : { volumetricFog: activeVolumetricFogContext }),
@@ -2146,24 +2696,49 @@ export function recordFrame(
2146
2696
  dispatch: transparentDispatch,
2147
2697
  hdrpClusterBindGroup,
2148
2698
  hdrpClusterMembershipBindGroup,
2149
- ...(gpuDriven?.occlusion === undefined ? {} : { occlusion: gpuDriven.occlusion }),
2699
+ ...(frameGpuDriven?.occlusion === undefined ? {} : { occlusion: frameGpuDriven.occlusion }),
2150
2700
  standardLighting: standard,
2151
2701
  foldDispatchPlan,
2152
2702
  materialSlotIndices,
2153
2703
  materialSlots,
2154
2704
  materialSlotOwners,
2155
2705
  materialSlotCount,
2706
+ ...(preparedGpuDriven === undefined
2707
+ ? {}
2708
+ : {
2709
+ gpuDrivenStandardPbrFrameResources: preparedGpuDriven.standardPbrFrameResources,
2710
+ gpuDrivenDrawKeys: preparedGpuDriven.drawKeys,
2711
+ gpuDrivenShadowViews: preparedGpuDriven.shadowViewPool,
2712
+ gpuDrivenShadowBatchProjections: preparedGpuDriven.shadowBatchProjections,
2713
+ gpuDrivenShadowDrawKeys: preparedGpuDriven.shadowDrawKeys,
2714
+ gpuDrivenShadowDrawKeysByView: preparedGpuDriven.shadowDrawKeysByView,
2715
+ }),
2716
+ ...(frameGpuDriven?.scene === undefined
2717
+ ? {}
2718
+ : { gpuDrivenSceneMaterialBuffer: frameGpuDriven.scene.scene.materialBuffer }),
2156
2719
  ...(cubeCapture?.reflectionProbes === undefined
2157
2720
  ? {}
2158
2721
  : { reflectionProbes: cubeCapture.reflectionProbes }),
2722
+ ...(cubeCapture?.captureDispatch === undefined
2723
+ ? {}
2724
+ : { captureDispatch: cubeCapture.captureDispatch }),
2159
2725
  pointsLines: pointsLinesOwner,
2160
2726
  materialBgAssemblyCache: frameState.materialBgAssemblyCache,
2161
2727
  ...(onRenderableDraw === undefined ? {} : { onRenderableDraw }),
2162
2728
  directionalShadowCacheReuse: directionalShadowCache.reuse,
2729
+ ...(shadowCasterMembership === undefined ? {} : { shadowCasterMembership }),
2730
+ ...(shadowValidatedOrdered.length === 0
2731
+ ? {}
2732
+ : { shadowValidatedOrdered, shadowMeshSsboBase }),
2733
+ ...(shadowDispatch === undefined ? {} : { shadowDispatch }),
2734
+ ...(shadowCasterWorldKeys === undefined ? {} : { shadowCasterWorldKeys }),
2163
2735
  ...(profilePhase !== undefined ? { profilePhase } : {}),
2164
2736
  };
2165
2737
  uploadMaterialUniforms(passCtx);
2166
- const fallbackReadbackRequest = createReflectionFallbackReadbackRequest(internals, frameState);
2738
+ const fallbackReadbackRequest =
2739
+ internals.captureReflectionFallbackReadback === true
2740
+ ? createReflectionFallbackReadbackRequest(internals, frameState)
2741
+ : undefined;
2167
2742
  if (fallbackReadbackRequest !== undefined) {
2168
2743
  frameState.reflectionFallbackReadback = fallbackReadbackRequest;
2169
2744
  }
@@ -2190,6 +2765,37 @@ export function recordFrame(
2190
2765
  frameState.temporalFrameInput = stagedTemporalFrame;
2191
2766
  frameState.temporalFrameTransaction.stage(stagedTemporalFrame);
2192
2767
  volumeSubmissionAttempted = volumeTopologyCandidate || activeVolumetricFogContext !== undefined;
2768
+ const ssrOwner = frameState.ssrHistoryOwner;
2769
+ const ssrCandidate = frameState.ssrHistoryCandidate;
2770
+ const frameHooks =
2771
+ preparedGpuDriven === undefined &&
2772
+ frameGpuDriven?.occlusion === undefined &&
2773
+ ssrCandidate === undefined
2774
+ ? undefined
2775
+ : {
2776
+ afterGraphExecute: () =>
2777
+ frameGpuDriven?.occlusion?.recordResolve(encoder) ?? ok(undefined),
2778
+ onSubmitted: () => {
2779
+ if (ssrOwner !== undefined && ssrCandidate !== undefined) {
2780
+ const committed = ssrOwner.commitFrame(ssrCandidate);
2781
+ if (!committed.ok) {
2782
+ ssrOwner.abortFrame(ssrCandidate, 'submit');
2783
+ reportSsrHistoryFailure(internals, 'submit', committed.error);
2784
+ }
2785
+ frameState.ssrHistoryCandidate = undefined;
2786
+ }
2787
+ preparedGpuDriven?._commitResourceReplacement();
2788
+ frameGpuDriven?.occlusion?.commit(true, frameState.frameNumber, profilePhase);
2789
+ },
2790
+ onAborted: () => {
2791
+ if (ssrOwner !== undefined && ssrCandidate !== undefined) {
2792
+ ssrOwner.abortFrame(ssrCandidate, 'submit');
2793
+ frameState.ssrHistoryCandidate = undefined;
2794
+ }
2795
+ preparedGpuDriven?._abortResourceReplacement?.();
2796
+ frameGpuDriven?.occlusion?.commit(false, frameState.frameNumber);
2797
+ },
2798
+ };
2193
2799
  submitted = runRecordProfilePhase(profilePhase, 'record/graph-execute', () =>
2194
2800
  executeCompiledFrameGraph(
2195
2801
  internals,
@@ -2199,17 +2805,7 @@ export function recordFrame(
2199
2805
  profilePhase === undefined
2200
2806
  ? undefined
2201
2807
  : (pass, encode) => profilePhase(graphExecutionPhase(pass.name), encode),
2202
- gpuDriven === undefined
2203
- ? undefined
2204
- : {
2205
- afterGraphExecute: () => gpuDriven.occlusion?.recordResolve(encoder) ?? ok(undefined),
2206
- onSubmitted: () => {
2207
- gpuDriven.occlusion?.commit(true, frameState.frameNumber, profilePhase);
2208
- },
2209
- onAborted: () => {
2210
- gpuDriven.occlusion?.commit(false, frameState.frameNumber);
2211
- },
2212
- },
2808
+ frameHooks,
2213
2809
  cubeCaptureWork.length === 0 ? undefined : cubeCaptureWork.map((work) => work.faceIndex),
2214
2810
  (done) => {
2215
2811
  completion = done;
@@ -2231,6 +2827,20 @@ export function recordFrame(
2231
2827
  timingCapture,
2232
2828
  ),
2233
2829
  );
2830
+ if (submitted && preparedGpuDriven?.drawKeys !== undefined && onRenderableDraw !== undefined) {
2831
+ for (const entry of validated) {
2832
+ if (gpuDrivenRenderableConsumed(entry, preparedGpuDriven.drawKeys)) {
2833
+ onRenderableDraw(entry);
2834
+ }
2835
+ }
2836
+ }
2837
+ if (submitted) {
2838
+ // Feature plans are submission-sensitive: VFX keeps queued fixed ticks
2839
+ // until the graph that consumed them is actually submitted. Acknowledge
2840
+ // the exact accepted graph here so successful frames do not replay every
2841
+ // prior tick into the next topology.
2842
+ acknowledgeRenderFeatureSubmission(getRenderFeatureGraphState(internals).plans);
2843
+ }
2234
2844
  if (cubeCapture !== undefined && cubeCaptureWork.length > 0) {
2235
2845
  const completed = cubeCapture.scheduler.completeSubmission(submitted, completion);
2236
2846
  if (!completed.ok) internals.errorRegistry.fire(completed.error);
@@ -2238,21 +2848,20 @@ export function recordFrame(
2238
2848
  if (!submitted && fallbackReadbackRequest !== undefined) {
2239
2849
  internals.device.destroyBuffer(fallbackReadbackRequest.buffer);
2240
2850
  }
2851
+ const fallbackSource =
2852
+ frameState.reflectionFallbackObservationSource as RenderFrameState['reflectionFallbackObservationSource'];
2241
2853
  const reflectionFallbackCompletion = cubeCapture?.reflectionProbes?.completeSubmission(
2242
2854
  submitted,
2243
2855
  completion,
2244
- fallbackReadbackRequest === undefined || fallbackReadback === undefined
2856
+ fallbackSource === undefined
2245
2857
  ? undefined
2246
2858
  : {
2247
- format: fallbackReadbackRequest.expected.format,
2248
- size: {
2249
- width: fallbackReadbackRequest.expected.width,
2250
- height: fallbackReadbackRequest.expected.height,
2251
- },
2252
- frameId: fallbackReadbackRequest.expected.frameId,
2253
- graphGeneration: fallbackReadbackRequest.expected.graphGeneration,
2254
- textureIdentity: fallbackReadbackRequest.expected.textureIdentity,
2255
- readback: fallbackReadback,
2859
+ format: fallbackSource.descriptor.format,
2860
+ size: fallbackSource.descriptor.size,
2861
+ frameId: fallbackSource.frameId,
2862
+ graphGeneration: frameState.graphGeneration,
2863
+ textureIdentity: getTextureIdentity(fallbackSource.texture),
2864
+ ...(fallbackReadback === undefined ? {} : { readback: fallbackReadback }),
2256
2865
  },
2257
2866
  frameState.reflectionFallbackDemand,
2258
2867
  );
@@ -2312,6 +2921,10 @@ export function recordFrame(
2312
2921
  }
2313
2922
  return submitted;
2314
2923
  } finally {
2924
+ if (!submitted && frameState.ssrHistoryCandidate !== undefined) {
2925
+ frameState.ssrHistoryOwner?.abortFrame(frameState.ssrHistoryCandidate, 'submit');
2926
+ frameState.ssrHistoryCandidate = undefined;
2927
+ }
2315
2928
  // Volume topology replacements are provisional until the queue submission
2316
2929
  // succeeds. This runs for every early-return path as well, so a swapchain,
2317
2930
  // encoder, or graph failure cannot leave a non-submitted candidate as the
@@ -2422,6 +3035,15 @@ function prepareDirectionalShadowCache(
2422
3035
  ): DirectionalShadowCacheDecision {
2423
3036
  const cascadeCount = lights.cascadeCount;
2424
3037
  const lightViewProj = lights.lightViewProj;
3038
+ // GPU feature buffers can change without an ECS mesh mutation. Until a
3039
+ // producer provides a stable content revision, never reuse their shadow atlas.
3040
+ if (
3041
+ getRenderFeatureGraphState(internals).plans.some(({ plan }) =>
3042
+ plan.passes.some((pass) => pass.kind === 'shadow-caster'),
3043
+ )
3044
+ ) {
3045
+ return { reuse: false, next: null };
3046
+ }
2425
3047
  if (
2426
3048
  frameState.compiledFrameGraph === null ||
2427
3049
  frameState.compiledFrameGraphTopologyKey === null ||
@@ -2662,6 +3284,9 @@ export function validateRenderables(
2662
3284
  return validated;
2663
3285
  }
2664
3286
 
3287
+ // Keep the primary record-stage name explicit at its call site while sharing
3288
+ // the same owner implementation for the complete retained ShadowCaster view.
3289
+
2665
3290
  function prepareMorphMesh(
2666
3291
  internals: RenderSystemInternals,
2667
3292
  frameState: RenderFrameState,
@@ -2673,9 +3298,23 @@ function prepareMorphMesh(
2673
3298
  if (morph === undefined || frameState.morphBuffers === undefined) return undefined;
2674
3299
  const targets = mesh.morphTargets;
2675
3300
  if (targets === undefined || targets.length !== morph.targetCount) return undefined;
2676
- if (base.layoutProjection.arrayStride !== 48 || base.vertexCount <= 0) return undefined;
2677
- const stride = mesh.vertices.length / base.vertexCount;
2678
- if (!Number.isInteger(stride) || stride < 3) return undefined;
3301
+ const position = base.layoutProjection.attributes.find(
3302
+ (attribute) => attribute.key === 'position',
3303
+ );
3304
+ if (
3305
+ position === undefined ||
3306
+ position.offset !== 0 ||
3307
+ position.shaderLocation !== 0 ||
3308
+ position.format !== 'float32x3' ||
3309
+ position.byteLength !== 12
3310
+ ) {
3311
+ return undefined;
3312
+ }
3313
+ const vertexCount = deriveVertexCount(mesh.vertices, base.layoutProjection);
3314
+ if (vertexCount === undefined || vertexCount !== base.vertexCount || vertexCount <= 0) {
3315
+ return undefined;
3316
+ }
3317
+ const stride = base.layoutProjection.arrayStride / Float32Array.BYTES_PER_ELEMENT;
2679
3318
 
2680
3319
  let active = false;
2681
3320
  for (const weight of morph.weights) {
@@ -2753,17 +3392,20 @@ function prepareMorphMesh(
2753
3392
  *
2754
3393
  * @internal
2755
3394
  */
2756
- function buildDispatchPlan(
3395
+ export function buildDispatchPlan(
2757
3396
  internals: RenderSystemInternals,
2758
3397
  validated: readonly ValidatedRenderable[],
2759
3398
  transparentDispatch: readonly DispatchEntry[],
2760
3399
  foldBuckets: readonly FoldBucket[],
3400
+ shadowValidated: readonly ValidatedRenderable[] = [],
3401
+ materialBindingClasses?: ReadonlyMap<string, string>,
2761
3402
  ): {
2762
3403
  validatedOrdered: readonly ValidatedRenderable[];
3404
+ shadowValidatedOrdered: readonly ValidatedRenderable[];
2763
3405
  foldDispatchPlan: FoldDispatchPlan | null;
2764
3406
  materialSlotIndices: readonly (readonly number[])[];
2765
3407
  materialSlots: readonly MaterialSnapshot[];
2766
- materialSlotOwners: readonly number[];
3408
+ materialSlotOwners: readonly ValidatedRenderable[];
2767
3409
  materialSlotCount: number;
2768
3410
  } {
2769
3411
  // A production GPU-owned frame has no CPU validated rows by design. The
@@ -2771,9 +3413,16 @@ function buildDispatchPlan(
2771
3413
  // CPU plan, material table, or fold bucket is consumed in that lane. Return
2772
3414
  // the empty plan without walking that list so a steady 100k scene remains
2773
3415
  // bounded by the persistent GPU batches rather than per-entity record work.
2774
- if (validated.length === 0) {
3416
+ if (validated.length === 0 && shadowValidated.length === 0) {
3417
+ const meshSsboCapResult = ensureMeshSsboCapacity(internals, shadowValidated.length);
3418
+ const meshSsboSlotCapacity = meshSsboSlotCapacityAfterEnsure(
3419
+ internals,
3420
+ shadowValidated.length,
3421
+ meshSsboCapResult,
3422
+ );
2775
3423
  return {
2776
3424
  validatedOrdered: [],
3425
+ shadowValidatedOrdered: limitRowsToMeshSsboCapacity(shadowValidated, meshSsboSlotCapacity),
2777
3426
  foldDispatchPlan: null,
2778
3427
  materialSlotIndices: [],
2779
3428
  materialSlots: [],
@@ -2821,18 +3470,33 @@ function buildDispatchPlan(
2821
3470
  validatedOrdered = ordered;
2822
3471
  }
2823
3472
 
2824
- const materialGroups = validatedOrdered.map((entry): readonly MaterialSnapshot[] => {
3473
+ // Shadow-only/off-camera CPU residuals consume the same material UBO as
3474
+ // visible draws. Their preparation must not depend on main-view visibility.
3475
+ const materialRows = [...validatedOrdered, ...shadowValidated];
3476
+ const materialGroups = materialRows.map((entry): readonly MaterialSnapshot[] => {
2825
3477
  const shaderId = entry.source.material.materialShaderId;
2826
3478
  if (shaderId === 'forgeax::sprite' || shaderId === 'forgeax::sprite-lit') {
2827
3479
  return [entry.source.material];
2828
3480
  }
2829
3481
  return entry.source.materials.length > 0 ? entry.source.materials : [entry.source.material];
2830
3482
  });
2831
- const materialSlotPlan = buildMaterialSlotPlan(materialGroups);
3483
+ const materialSlotPlan = buildMaterialSlotPlan(materialGroups, (material, owner) => {
3484
+ const row = materialRows[owner];
3485
+ return row === undefined
3486
+ ? ''
3487
+ : (materialBindingClasses?.get(
3488
+ materialBindingKey(row.source, material.materialHandle ?? -1),
3489
+ ) ?? '');
3490
+ });
2832
3491
  let materialSlotIndices = materialSlotPlan.slotIndices;
2833
3492
  let materialSlots = materialSlotPlan.slots;
2834
- let materialSlotOwners = materialSlotPlan.slotOwners;
3493
+ let materialSlotOwners = materialSlotPlan.slotOwners.map((index) => {
3494
+ const owner = materialRows[index];
3495
+ if (owner === undefined) throw new Error('material slot has no validated owner');
3496
+ return owner;
3497
+ });
2835
3498
  let materialSlotCount = materialSlots.length;
3499
+ let materialRenderableCount = materialRows.length;
2836
3500
 
2837
3501
  // feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M3 / T-M3-04:
2838
3502
  // ensure the mesh-SSBO + material-UBO buffer pair is large enough to hold
@@ -2857,8 +3521,13 @@ function buildDispatchPlan(
2857
3521
  //
2858
3522
  // feat-20260624 M1' / t7: sprite-lit treated identically to sprite
2859
3523
  // for material-slot accounting (paramSchema mirror, t4).
2860
- const neededSlots = Math.max(validatedOrdered.length, materialSlotCount);
3524
+ const neededSlots = Math.max(validatedOrdered.length + shadowValidated.length, materialSlotCount);
2861
3525
  const meshSsboCapResult = ensureMeshSsboCapacity(internals, neededSlots);
3526
+ const meshSsboSlotCapacity = meshSsboSlotCapacityAfterEnsure(
3527
+ internals,
3528
+ neededSlots,
3529
+ meshSsboCapResult,
3530
+ );
2862
3531
  if (!meshSsboCapResult.ok) {
2863
3532
  // Graceful degradation: the controller reports slots, but this stage
2864
3533
  // consumes entities. Find a complete prefix instead of slicing at the
@@ -2867,12 +3536,17 @@ function buildDispatchPlan(
2867
3536
  materialSlotIndices,
2868
3537
  meshSsboCapResult.degradedToSlotCount,
2869
3538
  );
3539
+ materialRenderableCount = degradedRenderableCount;
2870
3540
  materialSlotCount = materialSlotCountForPrefix(materialSlotIndices, degradedRenderableCount);
2871
3541
  materialSlotIndices = materialSlotIndices.slice(0, degradedRenderableCount);
2872
3542
  materialSlots = materialSlots.slice(0, materialSlotCount);
2873
3543
  materialSlotOwners = materialSlotOwners.slice(0, materialSlotCount);
2874
3544
  validatedOrdered = validatedOrdered.slice(0, degradedRenderableCount);
2875
3545
  }
3546
+ const shadowValidatedOrdered = limitRowsToMeshSsboCapacity(
3547
+ shadowValidated,
3548
+ Math.min(meshSsboSlotCapacity, materialRenderableCount) - validatedOrdered.length,
3549
+ );
2876
3550
 
2877
3551
  // feat-20260622-chunk-gpu-instancing-sprite-tilemap M1 / w4-record-swap
2878
3552
  // (D-1): build the fold dispatch plan once `validatedOrdered` is final
@@ -2965,6 +3639,7 @@ function buildDispatchPlan(
2965
3639
 
2966
3640
  return {
2967
3641
  validatedOrdered,
3642
+ shadowValidatedOrdered,
2968
3643
  foldDispatchPlan,
2969
3644
  materialSlotIndices,
2970
3645
  materialSlots,
@@ -3042,13 +3717,6 @@ function cleanPerFrameCaches(
3042
3717
  }
3043
3718
  }
3044
3719
 
3045
- if (frameState.instanceResidency !== undefined) {
3046
- const liveCollections = new Set(validated.map((entry) => entry.source.instances?.collectionId));
3047
- for (const id of frameState.instanceResidency.keys()) {
3048
- if (!liveCollections.has(id)) frameState.instanceResidency.delete(id);
3049
- }
3050
- }
3051
-
3052
3720
  // Chunk keys are `${worldEntityKey}:${chunkStart}`. Keep the key opaque to
3053
3721
  // the authoring API, but retain the numeric owner prefix for eviction.
3054
3722
  if (frameState.instanceBufferChunks !== undefined) {