@forgeax/engine-render 0.1.27 → 0.1.29

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 (655) hide show
  1. package/README.md +842 -124
  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/dynamic-geometry-host.d.ts +52 -0
  5. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -0
  6. package/dist/assembly/dynamic-geometry-runtime.d.ts +20 -0
  7. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +3 -1
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/gpu-driven-owner.d.ts +10 -0
  11. package/dist/assembly/gpu-driven-owner.d.ts.map +1 -0
  12. package/dist/assembly/host-contract.d.ts +19 -0
  13. package/dist/assembly/host-contract.d.ts.map +1 -1
  14. package/dist/assembly/material/assembly.d.ts +7 -0
  15. package/dist/assembly/material/assembly.d.ts.map +1 -1
  16. package/dist/assembly/material-shader-policy.d.ts +3 -1
  17. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  18. package/dist/assembly/recovery/failure-location.d.ts +12 -0
  19. package/dist/assembly/recovery/failure-location.d.ts.map +1 -0
  20. package/dist/assembly/recovery/renderer-recover.d.ts +6 -4
  21. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  22. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  23. package/dist/assembly/renderer-feature-inspection.d.ts +5 -0
  24. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -0
  25. package/dist/assembly/renderer-frame-transaction.d.ts +19 -0
  26. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  27. package/dist/assembly/renderer-helpers.d.ts +3 -0
  28. package/dist/assembly/renderer-helpers.d.ts.map +1 -1
  29. package/dist/assembly/renderer-inspection.d.ts +4 -0
  30. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  31. package/dist/assembly/shader-prewarm-policy.d.ts +6 -0
  32. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -1
  33. package/dist/assembly/skin-palette-owner.d.ts +8 -0
  34. package/dist/assembly/skin-palette-owner.d.ts.map +1 -0
  35. package/dist/assembly/ssr-shader-prewarm.d.ts +6 -0
  36. package/dist/assembly/ssr-shader-prewarm.d.ts.map +1 -0
  37. package/dist/assembly/webgpu-device-loss.d.ts +15 -0
  38. package/dist/assembly/webgpu-device-loss.d.ts.map +1 -0
  39. package/dist/assembly/webgpu-pbr-ready.d.ts +38 -0
  40. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -0
  41. package/dist/assembly/webgpu-ready-mipmap.d.ts +11 -0
  42. package/dist/assembly/webgpu-ready-mipmap.d.ts.map +1 -0
  43. package/dist/assembly/webgpu-ready.d.ts +7 -3
  44. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  45. package/dist/assembly/webgpu-renderer-bootstrap.d.ts +10 -0
  46. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -0
  47. package/dist/assembly/webgpu-renderer-contract.d.ts +62 -0
  48. package/dist/assembly/webgpu-renderer-contract.d.ts.map +1 -0
  49. package/dist/assembly/webgpu-renderer-guards.d.ts +9 -0
  50. package/dist/assembly/webgpu-renderer-guards.d.ts.map +1 -0
  51. package/dist/assembly/webgpu-renderer-observation.d.ts +6 -0
  52. package/dist/assembly/webgpu-renderer-observation.d.ts.map +1 -0
  53. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts +12 -0
  54. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts.map +1 -0
  55. package/dist/assembly/webgpu-renderer.d.ts +7 -141
  56. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  57. package/dist/assembly/webgpu-vertex-layouts.d.ts +28 -84
  58. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -1
  59. package/dist/authoring.mjs +3 -3
  60. package/dist/{chunk-ADHHVYLW.mjs → chunk-33XZFTQU.mjs} +245 -39
  61. package/dist/chunk-33XZFTQU.mjs.map +1 -0
  62. package/dist/{chunk-GF523LHF.mjs → chunk-BLXGONR2.mjs} +30 -12
  63. package/dist/chunk-BLXGONR2.mjs.map +1 -0
  64. package/dist/{chunk-KM2NOX2I.mjs → chunk-BQVQOQJH.mjs} +958 -254
  65. package/dist/chunk-BQVQOQJH.mjs.map +1 -0
  66. package/dist/{chunk-HKXTW355.mjs → chunk-BTWH7BP4.mjs} +386 -17
  67. package/dist/chunk-BTWH7BP4.mjs.map +1 -0
  68. package/dist/{chunk-TMIASV2N.mjs → chunk-FUODTTIA.mjs} +3934 -676
  69. package/dist/chunk-FUODTTIA.mjs.map +1 -0
  70. package/dist/{chunk-FJ6P52EE.mjs → chunk-ILQ5MEZF.mjs} +3 -17
  71. package/dist/chunk-ILQ5MEZF.mjs.map +1 -0
  72. package/dist/{chunk-E34VL5VI.mjs → chunk-JQTSEI3Z.mjs} +3 -3
  73. package/dist/{chunk-E34VL5VI.mjs.map → chunk-JQTSEI3Z.mjs.map} +1 -1
  74. package/dist/{chunk-GE3SDD72.mjs → chunk-NICXSHAT.mjs} +16 -38
  75. package/dist/chunk-NICXSHAT.mjs.map +1 -0
  76. package/dist/{chunk-ZZ4YQ474.mjs → chunk-NRNXBLIJ.mjs} +577 -7
  77. package/dist/chunk-NRNXBLIJ.mjs.map +1 -0
  78. package/dist/{chunk-JDWARUOI.mjs → chunk-PA5DRJNI.mjs} +17 -3
  79. package/dist/chunk-PA5DRJNI.mjs.map +1 -0
  80. package/dist/{chunk-XDP6XE5V.mjs → chunk-S4DNKQYQ.mjs} +1118 -446
  81. package/dist/chunk-S4DNKQYQ.mjs.map +1 -0
  82. package/dist/components/camera.d.ts +62 -2
  83. package/dist/components/camera.d.ts.map +1 -1
  84. package/dist/components/index.d.ts +1 -0
  85. package/dist/components/index.d.ts.map +1 -1
  86. package/dist/components/instances.d.ts +6 -20
  87. package/dist/components/instances.d.ts.map +1 -1
  88. package/dist/components/scene-instance.d.ts +2 -1
  89. package/dist/components/scene-instance.d.ts.map +1 -1
  90. package/dist/components/screen-space-reflection.d.ts +14 -0
  91. package/dist/components/screen-space-reflection.d.ts.map +1 -0
  92. package/dist/construct-renderer.mjs +13805 -7186
  93. package/dist/construct-renderer.mjs.map +1 -1
  94. package/dist/device/gpu-residency.d.ts +11 -17
  95. package/dist/device/gpu-residency.d.ts.map +1 -1
  96. package/dist/device/mesh-residency-lifetime.d.ts +21 -0
  97. package/dist/device/mesh-residency-lifetime.d.ts.map +1 -0
  98. package/dist/dynamic-geometry.d.ts +125 -0
  99. package/dist/dynamic-geometry.d.ts.map +1 -0
  100. package/dist/environment/background.d.ts +6 -0
  101. package/dist/environment/background.d.ts.map +1 -0
  102. package/dist/errors/gpu-driven.d.ts +6 -3
  103. package/dist/errors/gpu-driven.d.ts.map +1 -1
  104. package/dist/errors/render.d.ts +7 -4
  105. package/dist/errors/render.d.ts.map +1 -1
  106. package/dist/extract/camera.d.ts.map +1 -1
  107. package/dist/extract/gpu-driven.d.ts +33 -3
  108. package/dist/extract/gpu-driven.d.ts.map +1 -1
  109. package/dist/features/host.d.ts +23 -1
  110. package/dist/features/host.d.ts.map +1 -1
  111. package/dist/features/noise-texture.d.ts +17 -0
  112. package/dist/features/noise-texture.d.ts.map +1 -0
  113. package/dist/features/plan.d.ts +83 -2
  114. package/dist/features/plan.d.ts.map +1 -1
  115. package/dist/features/prepared-gpu-work.d.ts +37 -4
  116. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  117. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  118. package/dist/features/prepared-graphics.d.ts +5 -0
  119. package/dist/features/prepared-graphics.d.ts.map +1 -1
  120. package/dist/features/render-graph-compute.d.ts +5 -3
  121. package/dist/features/render-graph-compute.d.ts.map +1 -1
  122. package/dist/features/render-graph-contribution.d.ts +11 -3
  123. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  124. package/dist/features/render-graph-raster.d.ts +19 -3
  125. package/dist/features/render-graph-raster.d.ts.map +1 -1
  126. package/dist/features/types.d.ts +11 -0
  127. package/dist/features/types.d.ts.map +1 -1
  128. package/dist/fullscreen-post-process-pass.d.ts +2 -0
  129. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  130. package/dist/gpu-driven/batch-topology.d.ts +34 -1
  131. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  132. package/dist/gpu-driven/pbr-program.d.ts +10 -0
  133. package/dist/gpu-driven/pbr-program.d.ts.map +1 -0
  134. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  135. package/dist/gpu-driven/production-raster.d.ts +144 -23
  136. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  137. package/dist/gpu-driven/shadow-views.d.ts +126 -0
  138. package/dist/gpu-driven/shadow-views.d.ts.map +1 -0
  139. package/dist/gpu-driven/view-gpu.d.ts +18 -2
  140. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  141. package/dist/gpu-scene-schema.d.ts +2 -8
  142. package/dist/gpu-scene-schema.d.ts.map +1 -1
  143. package/dist/gpu-scene.d.ts +14 -0
  144. package/dist/gpu-scene.d.ts.map +1 -1
  145. package/dist/gpu-texture-usage.d.ts +1 -0
  146. package/dist/gpu-texture-usage.d.ts.map +1 -1
  147. package/dist/hdrp-buffers.d.ts +6 -0
  148. package/dist/hdrp-buffers.d.ts.map +1 -1
  149. package/dist/ibl/frame-resources.d.ts +8 -0
  150. package/dist/ibl/frame-resources.d.ts.map +1 -0
  151. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  152. package/dist/index.d.ts +21 -8
  153. package/dist/index.d.ts.map +1 -1
  154. package/dist/index.mjs +8 -7
  155. package/dist/index.mjs.map +1 -1
  156. package/dist/inspection-types.d.ts +164 -0
  157. package/dist/inspection-types.d.ts.map +1 -1
  158. package/dist/instances-derived-bounds.d.ts +11 -0
  159. package/dist/instances-derived-bounds.d.ts.map +1 -1
  160. package/dist/instances.d.ts +89 -44
  161. package/dist/instances.d.ts.map +1 -1
  162. package/dist/internal.d.ts +3 -0
  163. package/dist/internal.d.ts.map +1 -1
  164. package/dist/internal.mjs +118 -10
  165. package/dist/internal.mjs.map +1 -1
  166. package/dist/lifecycle.d.ts +2 -1
  167. package/dist/lifecycle.d.ts.map +1 -1
  168. package/dist/material-row.d.ts +5 -0
  169. package/dist/material-row.d.ts.map +1 -0
  170. package/dist/materials.d.ts +15 -0
  171. package/dist/materials.d.ts.map +1 -1
  172. package/dist/pbr-pipeline.d.ts +21 -5
  173. package/dist/pbr-pipeline.d.ts.map +1 -1
  174. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  175. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts +12 -0
  176. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts.map +1 -0
  177. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts +70 -0
  178. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts.map +1 -0
  179. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts +75 -0
  180. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts.map +1 -0
  181. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts +111 -0
  182. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts.map +1 -0
  183. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts +16 -0
  184. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts.map +1 -0
  185. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts +14 -0
  186. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts.map +1 -0
  187. package/dist/pipeline/standard-output/auto-exposure/state.d.ts +46 -0
  188. package/dist/pipeline/standard-output/auto-exposure/state.d.ts.map +1 -0
  189. package/dist/pipeline/standard-output/color-transform.d.ts +27 -0
  190. package/dist/pipeline/standard-output/color-transform.d.ts.map +1 -0
  191. package/dist/pipeline/standard-output/graph.d.ts +15 -0
  192. package/dist/pipeline/standard-output/graph.d.ts.map +1 -0
  193. package/dist/pipeline/standard-output/lut-admission.d.ts +29 -0
  194. package/dist/pipeline/standard-output/lut-admission.d.ts.map +1 -0
  195. package/dist/pipeline/standard-output/lut-gpu.d.ts +48 -0
  196. package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -0
  197. package/dist/pipeline/standard-output/lut-state.d.ts +68 -0
  198. package/dist/pipeline/standard-output/lut-state.d.ts.map +1 -0
  199. package/dist/pipeline/standard-output/resources.d.ts +10 -0
  200. package/dist/pipeline/standard-output/resources.d.ts.map +1 -0
  201. package/dist/pipeline/standard-output/types.d.ts +39 -0
  202. package/dist/pipeline/standard-output/types.d.ts.map +1 -0
  203. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  204. package/dist/pipeline/standard-post.d.ts +2 -2
  205. package/dist/pipeline/standard-post.d.ts.map +1 -1
  206. package/dist/pipeline-builder.d.ts +2 -1
  207. package/dist/pipeline-builder.d.ts.map +1 -1
  208. package/dist/pipeline-spec-types.d.ts +4 -0
  209. package/dist/pipeline-spec-types.d.ts.map +1 -1
  210. package/dist/pipeline-spec.d.ts +1 -9
  211. package/dist/pipeline-spec.d.ts.map +1 -1
  212. package/dist/points-lines/standard-owner.d.ts +1 -1
  213. package/dist/points-lines/standard-owner.d.ts.map +1 -1
  214. package/dist/prepare/prepared-graphics-resolver.d.ts +6 -1
  215. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  216. package/dist/record/dynamic-geometry-consumption.d.ts +15 -0
  217. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -0
  218. package/dist/record/frame-lighting.d.ts +3 -3
  219. package/dist/record/frame-lighting.d.ts.map +1 -1
  220. package/dist/record/frame-snapshot.d.ts +106 -5
  221. package/dist/record/frame-snapshot.d.ts.map +1 -1
  222. package/dist/record/frame-targets.d.ts.map +1 -1
  223. package/dist/record/frame.d.ts +39 -4
  224. package/dist/record/frame.d.ts.map +1 -1
  225. package/dist/record/main-pass-geometry.d.ts +7 -8
  226. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  227. package/dist/record/main-pass-material.d.ts +38 -1
  228. package/dist/record/main-pass-material.d.ts.map +1 -1
  229. package/dist/record/main-pass-sprite-draws.d.ts +2 -10
  230. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  231. package/dist/record/main-pass.d.ts +12 -2
  232. package/dist/record/main-pass.d.ts.map +1 -1
  233. package/dist/record/mesh-ssbo.d.ts +15 -6
  234. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  235. package/dist/record/prepared-material-bindings.d.ts +19 -0
  236. package/dist/record/prepared-material-bindings.d.ts.map +1 -0
  237. package/dist/record/recovery-pipeline.d.ts +2 -9
  238. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  239. package/dist/record/render-context.d.ts +76 -26
  240. package/dist/record/render-context.d.ts.map +1 -1
  241. package/dist/record/shadow-pass.d.ts +62 -7
  242. package/dist/record/shadow-pass.d.ts.map +1 -1
  243. package/dist/record/skybox-post-pass.d.ts +1 -1
  244. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  245. package/dist/record/typed-frame-graph.d.ts +35 -7
  246. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  247. package/dist/record/view-ubo.d.ts +1 -0
  248. package/dist/record/view-ubo.d.ts.map +1 -1
  249. package/dist/recovery/render-system-candidate.d.ts +10 -2
  250. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  251. package/dist/recovery/render-system-roots.d.ts +11 -0
  252. package/dist/recovery/render-system-roots.d.ts.map +1 -0
  253. package/dist/recovery/types.d.ts +76 -0
  254. package/dist/recovery/types.d.ts.map +1 -0
  255. package/dist/reflection/record-owner.d.ts +0 -2
  256. package/dist/reflection/record-owner.d.ts.map +1 -1
  257. package/dist/render-contract.d.ts +185 -30
  258. package/dist/render-contract.d.ts.map +1 -1
  259. package/dist/render-graph-primitives.d.ts +1 -0
  260. package/dist/render-graph-primitives.d.ts.map +1 -1
  261. package/dist/render-pipeline.d.ts +64 -5
  262. package/dist/render-pipeline.d.ts.map +1 -1
  263. package/dist/render-system-extract-tail.d.ts +7 -5
  264. package/dist/render-system-extract-tail.d.ts.map +1 -1
  265. package/dist/render-system-extract.d.ts +101 -41
  266. package/dist/render-system-extract.d.ts.map +1 -1
  267. package/dist/render-system-projections.d.ts +22 -0
  268. package/dist/render-system-projections.d.ts.map +1 -0
  269. package/dist/render-system.d.ts +34 -77
  270. package/dist/render-system.d.ts.map +1 -1
  271. package/dist/scene/render-scene-types.d.ts +14 -7
  272. package/dist/scene/render-scene-types.d.ts.map +1 -1
  273. package/dist/scene/render-scene.d.ts +29 -11
  274. package/dist/scene/render-scene.d.ts.map +1 -1
  275. package/dist/scene/shadow-visibility.d.ts +5 -0
  276. package/dist/scene/shadow-visibility.d.ts.map +1 -0
  277. package/dist/ssr/admission.d.ts +70 -1
  278. package/dist/ssr/admission.d.ts.map +1 -1
  279. package/dist/ssr/compose.d.ts +13 -0
  280. package/dist/ssr/compose.d.ts.map +1 -0
  281. package/dist/ssr/errors.d.ts +35 -0
  282. package/dist/ssr/errors.d.ts.map +1 -1
  283. package/dist/ssr/graph.d.ts +60 -0
  284. package/dist/ssr/graph.d.ts.map +1 -0
  285. package/dist/ssr/history.d.ts +97 -0
  286. package/dist/ssr/history.d.ts.map +1 -0
  287. package/dist/ssr/hiz.d.ts +37 -0
  288. package/dist/ssr/hiz.d.ts.map +1 -0
  289. package/dist/ssr/inspection.d.ts +42 -0
  290. package/dist/ssr/inspection.d.ts.map +1 -0
  291. package/dist/ssr/resources.d.ts +58 -0
  292. package/dist/ssr/resources.d.ts.map +1 -0
  293. package/dist/ssr/temporal.d.ts +38 -0
  294. package/dist/ssr/temporal.d.ts.map +1 -0
  295. package/dist/systems/skin-palette-allocator.d.ts +32 -10
  296. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  297. package/dist/systems/skin-palette-types.d.ts +15 -0
  298. package/dist/systems/skin-palette-types.d.ts.map +1 -1
  299. package/dist/systems/transparent-dispatch.d.ts +9 -0
  300. package/dist/systems/transparent-dispatch.d.ts.map +1 -0
  301. package/dist/targets/material-source.d.ts +7 -0
  302. package/dist/targets/material-source.d.ts.map +1 -1
  303. package/dist/temporal/frame-coordinator.d.ts +7 -0
  304. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  305. package/dist/temporal/gpu.d.ts +8 -0
  306. package/dist/temporal/gpu.d.ts.map +1 -1
  307. package/dist/temporal/history.d.ts.map +1 -1
  308. package/dist/temporal/index.mjs +6 -6
  309. package/dist/temporal/inspection.d.ts.map +1 -1
  310. package/dist/temporal/standard-scene-data.d.ts +2 -1
  311. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  312. package/dist/temporal/temporal-view.d.ts +2 -0
  313. package/dist/temporal/temporal-view.d.ts.map +1 -1
  314. package/dist/typed-render-graph-primitives.d.ts +35 -1
  315. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  316. package/dist/typed-shadow-passes.d.ts +3 -1
  317. package/dist/typed-shadow-passes.d.ts.map +1 -1
  318. package/dist/volume/temporal.d.ts +6 -0
  319. package/dist/volume/temporal.d.ts.map +1 -1
  320. package/package.json +21 -20
  321. package/src/__tests__/auto-exposure-gpu-evidence.ts +291 -0
  322. package/src/__tests__/auto-exposure-inspection.unit.test.ts +80 -0
  323. package/src/__tests__/auto-exposure-transaction.integration.test.ts +99 -0
  324. package/src/__tests__/auto-exposure.browser.test.ts +33 -0
  325. package/src/__tests__/auto-exposure.dawn.test.ts +33 -0
  326. package/src/__tests__/detached-inspection.integration.test.ts +5 -0
  327. package/src/__tests__/device-scope-lifecycle.unit.test.ts +18 -0
  328. package/src/__tests__/device-scope-stale-matrix.unit.test.ts +18 -0
  329. package/src/__tests__/docs-gate-green.unit.test.ts +3 -3
  330. package/src/__tests__/dynamic-geometry-host.unit.test.ts +1123 -0
  331. package/src/__tests__/dynamic-geometry.unit.test.ts +233 -0
  332. package/src/__tests__/factory-contract.integration.test.ts +73 -22
  333. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +19 -0
  334. package/src/__tests__/feature-depth-input.dawn.test.ts +121 -0
  335. package/src/__tests__/feature-noise.unit.test.ts +38 -0
  336. package/src/__tests__/fixtures/taa-resolve-pinned-baseline.wgsl +301 -0
  337. package/src/__tests__/frame-camera-selection.unit.test.ts +131 -0
  338. package/src/__tests__/frame-plan-contract.unit.test.ts +24 -0
  339. package/src/__tests__/frame-targets.unit.test.ts +24 -0
  340. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +3 -0
  341. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +79 -0
  342. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +287 -6
  343. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +122 -142
  344. package/src/__tests__/gpu-driven-indirect-raster.browser.test.ts +10 -1
  345. package/src/__tests__/gpu-driven-lod-cache.unit.test.ts +264 -0
  346. package/src/__tests__/gpu-driven-mesh-world-identity.unit.test.ts +349 -0
  347. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +1417 -0
  348. package/src/__tests__/gpu-driven-pbr.integration.test.ts +21 -0
  349. package/src/__tests__/gpu-driven-production-fixture.ts +96 -0
  350. package/src/__tests__/gpu-driven-production.integration.test.ts +1075 -93
  351. package/src/__tests__/gpu-driven-scaling.unit.test.ts +1 -3
  352. package/src/__tests__/gpu-driven-shadow-views.browser.test.ts +63 -0
  353. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +294 -0
  354. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +97 -0
  355. package/src/__tests__/gpu-driven-shadow-views.unit.test.ts +168 -0
  356. package/src/__tests__/gpu-driven-skin-contract.test-d.ts +11 -0
  357. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +72 -20
  358. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +30 -9
  359. package/src/__tests__/gpu-driven-view.unit.test.ts +159 -52
  360. package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -1
  361. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +42 -0
  362. package/src/__tests__/gpu-scene-render-graph-gpu.ts +19 -5
  363. package/src/__tests__/gpu-scene.dawn.test.ts +40 -13
  364. package/src/__tests__/gpu-scene.unit.test.ts +559 -33
  365. package/src/__tests__/instances-culling.integration.test.ts +36 -10
  366. package/src/__tests__/instances-derived-bounds.unit.test.ts +54 -0
  367. package/src/__tests__/instances-store.unit.test.ts +242 -39
  368. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +6 -0
  369. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +13 -16
  370. package/src/__tests__/lut-lkg-transaction.integration.test.ts +122 -0
  371. package/src/__tests__/material-contract-inventory.unit.test.ts +5 -6
  372. package/src/__tests__/material-dispatch-shader-id.unit.test.ts +51 -0
  373. package/src/__tests__/material-lane-selection.unit.test.ts +60 -0
  374. package/src/__tests__/material-numeric-upload.unit.test.ts +53 -0
  375. package/src/__tests__/material-texture-handles.unit.test.ts +18 -0
  376. package/src/__tests__/material-texture-source.unit.test.ts +87 -0
  377. package/src/__tests__/materials-authoring-validation.unit.test.ts +41 -0
  378. package/src/__tests__/materials-standard-contract.unit.test.ts +52 -2
  379. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +4 -0
  380. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +53 -0
  381. package/src/__tests__/normal-fallback.dawn.test.ts +122 -0
  382. package/src/__tests__/particle-material-input-layout.unit.test.ts +104 -0
  383. package/src/__tests__/physical-clearcoat.integration.test.ts +37 -3
  384. package/src/__tests__/pipeline-texture-specialization.unit.test.ts +138 -0
  385. package/src/__tests__/point-shadow-inspection.unit.test.ts +2 -0
  386. package/src/__tests__/prepared-gpu-driven-pbr-contract.test-d.ts +31 -0
  387. package/src/__tests__/prepared-gpu-driven-pbr-contract.unit.test.ts +39 -0
  388. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +35 -4
  389. package/src/__tests__/prepared-graphics-capability.unit.test.ts +21 -0
  390. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +6 -2
  391. package/src/__tests__/profiler-phase-catalog.test.ts +5 -0
  392. package/src/__tests__/public-surface.test-d.ts +3 -0
  393. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +30 -12
  394. package/src/__tests__/reflection-fallback-async-publication.unit.test.ts +104 -0
  395. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +2 -1
  396. package/src/__tests__/render-error-code-owner.test-d.ts +4 -0
  397. package/src/__tests__/render-error-detail-ssot.test-d.ts +22 -0
  398. package/src/__tests__/render-error-exhaustive.test-d.ts +15 -0
  399. package/src/__tests__/render-feature-gpu-work.integration.test.ts +285 -2
  400. package/src/__tests__/render-feature-plan.unit.test.ts +245 -0
  401. package/src/__tests__/render-feature-shadow-order.unit.test.ts +228 -0
  402. package/src/__tests__/render-feature-zero-work.unit.test.ts +35 -1
  403. package/src/__tests__/render-graph-fallback.integration.test.ts +2 -2
  404. package/src/__tests__/render-owner-cohesion.unit.test.ts +55 -0
  405. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +426 -0
  406. package/src/__tests__/render-scene-projection.unit.test.ts +914 -23
  407. package/src/__tests__/render-scene-resource-updates.integration.test.ts +214 -0
  408. package/src/__tests__/render-scene-skin-lifecycle.integration.test.ts +173 -0
  409. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +133 -0
  410. package/src/__tests__/render-target-material-source.test-d.ts +27 -3
  411. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +61 -1
  412. package/src/__tests__/renderer-frame-transaction.integration.test.ts +923 -5
  413. package/src/__tests__/shader-manifest-fixture.ts +39 -0
  414. package/src/__tests__/skin-motion-regression.unit.test.ts +0 -33
  415. package/src/__tests__/skin-palette-persistent.integration.test.ts +28 -0
  416. package/src/__tests__/skin-palette-persistent.unit.test.ts +247 -0
  417. package/src/__tests__/skinned-shadow-caster.test.ts +118 -14
  418. package/src/__tests__/specular-environment-scale.dawn.test.ts +156 -0
  419. package/src/__tests__/ssr-admission-matrix.integration.test.ts +7 -1
  420. package/src/__tests__/ssr-confidence-compose.unit.test.ts +84 -0
  421. package/src/__tests__/ssr-coordinate-oracle.unit.test.ts +28 -0
  422. package/src/__tests__/ssr-format-probe-gating.integration.test.ts +82 -0
  423. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +91 -0
  424. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +112 -0
  425. package/src/__tests__/ssr-gpu-dispatch.ts +946 -0
  426. package/src/__tests__/ssr-graph-topology.unit.test.ts +104 -0
  427. package/src/__tests__/ssr-history-filter.dawn.test.ts +198 -0
  428. package/src/__tests__/ssr-hit-color.dawn.test.ts +273 -0
  429. package/src/__tests__/ssr-hiz-odd-reduction.dawn.test.ts +569 -0
  430. package/src/__tests__/ssr-hiz-rescue.dawn.test.ts +279 -0
  431. package/src/__tests__/ssr-hiz.browser.test.ts +150 -0
  432. package/src/__tests__/ssr-hiz.dawn.test.ts +150 -0
  433. package/src/__tests__/ssr-hiz.unit.test.ts +56 -0
  434. package/src/__tests__/ssr-inspection.unit.test.ts +105 -0
  435. package/src/__tests__/ssr-memory-budget.unit.test.ts +45 -0
  436. package/src/__tests__/ssr-performance-sequence-aggregation.unit.test.ts +15 -0
  437. package/src/__tests__/ssr-public-surface.test-d.ts +37 -0
  438. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +387 -0
  439. package/src/__tests__/ssr-reflection-mip-reducer.dawn.test.ts +309 -0
  440. package/src/__tests__/ssr-reprojection-probe.ts +90 -0
  441. package/src/__tests__/ssr-roughness-mip.dawn.test.ts +163 -0
  442. package/src/__tests__/ssr-shader-prewarm.unit.test.ts +59 -0
  443. package/src/__tests__/ssr-spatial.integration.test.ts +141 -0
  444. package/src/__tests__/ssr-stable-lattice.dawn.test.ts +475 -0
  445. package/src/__tests__/ssr-temporal-history.unit.test.ts +202 -0
  446. package/src/__tests__/ssr-texel-crossing.dawn.test.ts +448 -0
  447. package/src/__tests__/ssr-zero-work.integration.test.ts +87 -0
  448. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +44 -12
  449. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +29 -0
  450. package/src/__tests__/standard-output-chain-contract.unit.test.ts +99 -0
  451. package/src/__tests__/standard-output-chain.integration.test.ts +73 -0
  452. package/src/__tests__/standard-output-gpu-evidence.ts +205 -0
  453. package/src/__tests__/standard-output.browser.test.ts +15 -0
  454. package/src/__tests__/standard-output.dawn.test.ts +15 -0
  455. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +38 -0
  456. package/src/__tests__/standard-pipeline.integration.test.ts +320 -3
  457. package/src/__tests__/standard-profile-oracle.unit.test.ts +22 -0
  458. package/src/__tests__/standard-surface-lighting.unit.test.ts +41 -0
  459. package/src/__tests__/standard-texture-specialization.dawn.test.ts +141 -0
  460. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +6 -3
  461. package/src/__tests__/taa-coverage-edge.dawn.test.ts +193 -0
  462. package/src/__tests__/taa-history-clipping.dawn.test.ts +328 -0
  463. package/src/__tests__/taa-neighborhood-equivalence.dawn.test.ts +778 -0
  464. package/src/__tests__/taa-stability-feedback.browser.test.ts +23 -0
  465. package/src/__tests__/taa-stability-feedback.dawn.test.ts +68 -0
  466. package/src/__tests__/taa-stability-feedback.ts +341 -0
  467. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +49 -7
  468. package/src/__tests__/temporal-reset-matrix.unit.test.ts +9 -0
  469. package/src/__tests__/temporal-transaction.integration.test.ts +28 -0
  470. package/src/__tests__/texture-usage-surface.unit.test.ts +4 -2
  471. package/src/__tests__/typed-pipeline-topology.unit.test.ts +109 -4
  472. package/src/__tests__/typed-scene-pass-material-cache.integration.test.ts +126 -0
  473. package/src/__tests__/vertex-layouts-ssot.unit.test.ts +47 -0
  474. package/src/__tests__/visibility-extract.perf.test.ts +20 -8
  475. package/src/__tests__/visibility-instances.integration.test.ts +19 -7
  476. package/src/__tests__/visibility-producer-matrix.integration.test.ts +20 -9
  477. package/src/assembly/atmosphere-shader-prewarm.ts +38 -0
  478. package/src/assembly/dynamic-geometry-host.ts +887 -0
  479. package/src/assembly/dynamic-geometry-runtime.ts +71 -0
  480. package/src/assembly/factory.ts +4 -0
  481. package/src/assembly/gpu-driven-owner.ts +29 -0
  482. package/src/assembly/host-contract.ts +36 -0
  483. package/src/assembly/material/assembly.ts +38 -0
  484. package/src/assembly/material-shader-policy.ts +71 -28
  485. package/src/assembly/recovery/failure-location.ts +62 -0
  486. package/src/assembly/recovery/renderer-recover.ts +19 -17
  487. package/src/assembly/renderer-facade.ts +29 -0
  488. package/src/assembly/renderer-feature-inspection.ts +13 -0
  489. package/src/assembly/renderer-frame-transaction.ts +63 -0
  490. package/src/assembly/renderer-helpers.ts +26 -0
  491. package/src/assembly/renderer-inspection.ts +6 -0
  492. package/src/assembly/shader-prewarm-policy.ts +32 -0
  493. package/src/assembly/skin-palette-owner.ts +21 -0
  494. package/src/assembly/ssr-shader-prewarm.ts +41 -0
  495. package/src/assembly/webgpu-device-loss.ts +75 -0
  496. package/src/assembly/webgpu-pbr-ready.ts +232 -0
  497. package/src/assembly/webgpu-ready-mipmap.ts +26 -0
  498. package/src/assembly/webgpu-ready.ts +142 -121
  499. package/src/assembly/webgpu-renderer-bootstrap.ts +85 -0
  500. package/src/assembly/webgpu-renderer-contract.ts +69 -0
  501. package/src/assembly/webgpu-renderer-guards.ts +32 -0
  502. package/src/assembly/webgpu-renderer-observation.ts +43 -0
  503. package/src/assembly/webgpu-renderer-recovery-failure.ts +62 -0
  504. package/src/assembly/webgpu-renderer.ts +675 -614
  505. package/src/assembly/webgpu-vertex-layouts.ts +233 -87
  506. package/src/components/__tests__/camera-exposure.contract.unit.test.ts +153 -0
  507. package/src/components/camera.ts +287 -7
  508. package/src/components/index.ts +1 -0
  509. package/src/components/instances.ts +45 -18
  510. package/src/components/scene-instance.ts +2 -1
  511. package/src/components/screen-space-reflection.ts +21 -0
  512. package/src/components/sprite-animation.ts +3 -3
  513. package/src/components/sprite-instances.ts +1 -1
  514. package/src/device/gpu-residency.ts +96 -93
  515. package/src/device/mesh-residency-lifetime.ts +73 -0
  516. package/src/dynamic-geometry.ts +736 -0
  517. package/src/environment/background.ts +263 -0
  518. package/src/errors/gpu-driven.ts +39 -5
  519. package/src/errors/render.ts +27 -3
  520. package/src/extract/camera.ts +26 -1
  521. package/src/extract/gpu-driven.ts +185 -10
  522. package/src/features/host.ts +237 -19
  523. package/src/features/noise-texture.ts +91 -0
  524. package/src/features/plan.ts +553 -24
  525. package/src/features/prepared-gpu-work.ts +631 -49
  526. package/src/features/prepared-graphics-store.ts +31 -0
  527. package/src/features/prepared-graphics.ts +5 -0
  528. package/src/features/render-graph-compute.ts +41 -8
  529. package/src/features/render-graph-contribution.ts +141 -3
  530. package/src/features/render-graph-raster.ts +143 -30
  531. package/src/features/types.ts +11 -0
  532. package/src/fullscreen-post-process-pass.ts +3 -0
  533. package/src/gpu-driven/batch-topology.ts +424 -102
  534. package/src/gpu-driven/pbr-program.ts +31 -0
  535. package/src/gpu-driven/prepared-draw.ts +29 -4
  536. package/src/gpu-driven/production-raster.ts +2267 -694
  537. package/src/gpu-driven/shadow-views.ts +539 -0
  538. package/src/gpu-driven/view-gpu.ts +188 -41
  539. package/src/gpu-scene-schema.ts +52 -11
  540. package/src/gpu-scene.ts +186 -99
  541. package/src/gpu-texture-usage.ts +1 -0
  542. package/src/hdrp-buffers.ts +72 -36
  543. package/src/ibl/frame-resources.ts +38 -0
  544. package/src/ibl/skylight-bind-group.ts +5 -4
  545. package/src/index.ts +125 -1
  546. package/src/inspection-types.ts +191 -0
  547. package/src/instances-derived-bounds.ts +258 -34
  548. package/src/instances.ts +459 -150
  549. package/src/internal.ts +22 -0
  550. package/src/lifecycle.ts +2 -0
  551. package/src/material-row.ts +145 -0
  552. package/src/materials.ts +44 -8
  553. package/src/pbr-pipeline.ts +159 -17
  554. package/src/pipeline/standard-forward-lane.ts +52 -24
  555. package/src/pipeline/standard-output/__tests__/lut-admission.unit.test.ts +103 -0
  556. package/src/pipeline/standard-output/auto-exposure/__tests__/capability.unit.test.ts +40 -0
  557. package/src/pipeline/standard-output/auto-exposure/__tests__/gpu.contract.unit.test.ts +125 -0
  558. package/src/pipeline/standard-output/auto-exposure/__tests__/graph.contract.unit.test.ts +94 -0
  559. package/src/pipeline/standard-output/auto-exposure/__tests__/oracle.test.ts +108 -0
  560. package/src/pipeline/standard-output/auto-exposure/__tests__/preset.test.ts +26 -0
  561. package/src/pipeline/standard-output/auto-exposure/capability.ts +38 -0
  562. package/src/pipeline/standard-output/auto-exposure/gpu.ts +567 -0
  563. package/src/pipeline/standard-output/auto-exposure/graph.ts +221 -0
  564. package/src/pipeline/standard-output/auto-exposure/inspection.ts +231 -0
  565. package/src/pipeline/standard-output/auto-exposure/oracle.ts +291 -0
  566. package/src/pipeline/standard-output/auto-exposure/preset.ts +28 -0
  567. package/src/pipeline/standard-output/auto-exposure/state.ts +156 -0
  568. package/src/pipeline/standard-output/color-transform.ts +124 -0
  569. package/src/pipeline/standard-output/graph.ts +139 -0
  570. package/src/pipeline/standard-output/lut-admission.ts +121 -0
  571. package/src/pipeline/standard-output/lut-gpu.ts +399 -0
  572. package/src/pipeline/standard-output/lut-state.ts +176 -0
  573. package/src/pipeline/standard-output/resources.ts +121 -0
  574. package/src/pipeline/standard-output/types.ts +58 -0
  575. package/src/pipeline/standard-pipeline.ts +192 -59
  576. package/src/pipeline/standard-post.ts +186 -27
  577. package/src/pipeline-builder.ts +16 -6
  578. package/src/pipeline-spec-types.ts +4 -0
  579. package/src/pipeline-spec.ts +37 -29
  580. package/src/points-lines/standard-owner.ts +6 -14
  581. package/src/prepare/prepared-graphics-resolver.ts +9 -2
  582. package/src/record/__tests__/material-slot-plan.unit.test.ts +48 -0
  583. package/src/record/__tests__/mesh-ssbo.unit.test.ts +40 -1
  584. package/src/record/__tests__/recovery-pipeline.unit.test.ts +2 -29
  585. package/src/record/__tests__/sprite-material-abi.unit.test.ts +36 -5
  586. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +45 -0
  587. package/src/record/__tests__/view-ubo-layout.unit.test.ts +5 -0
  588. package/src/record/dynamic-geometry-consumption.ts +59 -0
  589. package/src/record/frame-lighting.ts +12 -11
  590. package/src/record/frame-snapshot.ts +133 -23
  591. package/src/record/frame-targets.ts +9 -0
  592. package/src/record/frame.ts +727 -133
  593. package/src/record/main-pass-geometry.ts +162 -98
  594. package/src/record/main-pass-material.ts +115 -26
  595. package/src/record/main-pass-sprite-draws.ts +18 -107
  596. package/src/record/main-pass.ts +78 -8
  597. package/src/record/material-uniforms.ts +1 -1
  598. package/src/record/mesh-ssbo.ts +59 -15
  599. package/src/record/prepared-material-bindings.ts +234 -0
  600. package/src/record/recovery-pipeline.ts +59 -136
  601. package/src/record/render-context.ts +92 -22
  602. package/src/record/shadow-pass.ts +1231 -209
  603. package/src/record/skybox-post-pass.ts +14 -3
  604. package/src/record/typed-frame-graph.ts +1006 -53
  605. package/src/record/view-ubo.ts +11 -0
  606. package/src/recovery/render-system-candidate.ts +90 -66
  607. package/src/recovery/render-system-roots.ts +113 -0
  608. package/src/recovery/types.ts +77 -0
  609. package/src/reflection/record-owner.ts +26 -54
  610. package/src/render-contract.ts +219 -25
  611. package/src/render-graph-primitives.ts +6 -2
  612. package/src/render-pipeline.ts +70 -1
  613. package/src/render-system-extract-tail.ts +409 -160
  614. package/src/render-system-extract.ts +376 -102
  615. package/src/render-system-projections.ts +241 -0
  616. package/src/render-system.ts +692 -791
  617. package/src/scene/render-scene-types.ts +13 -4
  618. package/src/scene/render-scene.ts +569 -355
  619. package/src/scene/shadow-visibility.ts +21 -0
  620. package/src/ssr/admission.ts +380 -14
  621. package/src/ssr/compose.ts +144 -0
  622. package/src/ssr/errors.ts +48 -0
  623. package/src/ssr/graph.ts +777 -0
  624. package/src/ssr/history.ts +436 -0
  625. package/src/ssr/hiz.ts +124 -0
  626. package/src/ssr/inspection.ts +124 -0
  627. package/src/ssr/resources.ts +202 -0
  628. package/src/ssr/temporal.ts +164 -0
  629. package/src/systems/skin-palette-allocator.ts +495 -148
  630. package/src/systems/skin-palette-types.ts +17 -0
  631. package/src/systems/transparent-dispatch.ts +90 -0
  632. package/src/targets/material-source.ts +34 -0
  633. package/src/temporal/coverage.ts +1 -1
  634. package/src/temporal/frame-coordinator.ts +23 -2
  635. package/src/temporal/gpu.ts +74 -12
  636. package/src/temporal/history.ts +3 -1
  637. package/src/temporal/inspection.ts +2 -1
  638. package/src/temporal/standard-scene-data.ts +3 -1
  639. package/src/temporal/temporal-view.ts +3 -0
  640. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +204 -139
  641. package/src/typed-render-graph-primitives.ts +266 -74
  642. package/src/typed-shadow-passes.ts +122 -12
  643. package/src/volume/temporal.ts +10 -0
  644. package/dist/chunk-ADHHVYLW.mjs.map +0 -1
  645. package/dist/chunk-FJ6P52EE.mjs.map +0 -1
  646. package/dist/chunk-GE3SDD72.mjs.map +0 -1
  647. package/dist/chunk-GF523LHF.mjs.map +0 -1
  648. package/dist/chunk-HKXTW355.mjs.map +0 -1
  649. package/dist/chunk-JDWARUOI.mjs.map +0 -1
  650. package/dist/chunk-KM2NOX2I.mjs.map +0 -1
  651. package/dist/chunk-TMIASV2N.mjs.map +0 -1
  652. package/dist/chunk-XDP6XE5V.mjs.map +0 -1
  653. package/dist/chunk-ZZ4YQ474.mjs.map +0 -1
  654. package/src/__tests__/instances-world-ownership.integration.test.ts +0 -62
  655. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +0 -36
@@ -5,7 +5,8 @@ import { uploadMaterialUniforms } from './material-uniforms';
5
5
  import { HANDLE_NINESLICE_QUAD, resolveAssetHandle } from '@forgeax/engine-assets-runtime';
6
6
  import type { World } from '@forgeax/engine-ecs';
7
7
  import type { RenderReadLease } from '@forgeax/engine-ecs/projection';
8
- import { mat4, vec3 } from '@forgeax/engine-math';
8
+ import { deriveVertexCount } from '@forgeax/engine-geometry';
9
+ import { frustum, mat4, vec3 } from '@forgeax/engine-math';
9
10
  import {
10
11
  type CurrentFrameObservationLease,
11
12
  createCurrentFrameObservationLease,
@@ -21,7 +22,7 @@ import {
21
22
  type TextureView,
22
23
  } from '@forgeax/engine-rhi';
23
24
  import type { MaterialRenderState, MeshAsset } from '@forgeax/engine-types';
24
- import { handleSlot, toShared } from '@forgeax/engine-types';
25
+ import { BUILTIN_BASE, handleSlot, toShared } from '@forgeax/engine-types';
25
26
  import { CUBE_CAMERA_FACE_ORDER } from '../components/cube-camera';
26
27
  import type { DirectionalShadowQuality } from '../components/directional-shadow-filter';
27
28
  import type { MeshGpuHandles } from '../device/gpu-residency';
@@ -30,7 +31,9 @@ import {
30
31
  type ObservationUnavailableReason,
31
32
  RendererOperationError,
32
33
  } from '../errors/render';
34
+ import { gpuDrivenDrawKey, gpuDrivenSourceDrawItemIndex } from '../extract/gpu-driven';
33
35
  import type { GpuDrivenProduction, PreparedGpuDrivenFrame } from '../gpu-driven/production-raster';
36
+ import type { ShadowViewIdentity } from '../gpu-driven/shadow-views';
34
37
  import { GpuBuffer } from '../gpu-resource';
35
38
  import { GPU_TEXTURE_USAGE_COPY_SRC } from '../gpu-texture-usage';
36
39
  import {
@@ -56,12 +59,22 @@ import type {
56
59
  ExtractedVolumetricFog,
57
60
  MaterialSnapshot,
58
61
  RenderableSnapshot,
62
+ ShadowCasterMembership,
59
63
  SkyboxSnapshot,
60
64
  SkylightSnapshot,
61
65
  } from '../render-system-extract';
62
- import type { PersistentGpuDrivenState } from '../scene/render-scene';
66
+ import type {
67
+ PersistentGpuDrivenState,
68
+ PersistentShadowCasterProjection,
69
+ } from '../scene/render-scene';
63
70
  import type { OcclusionFrameProjection } from '../scene/visibility/occlusion-runtime';
64
71
  import { SHADOW_ATLAS_DEFAULT_LAYERS } from '../shadow-atlas';
72
+ import {
73
+ admitSsrSpatial,
74
+ type SsrDependenciesInspection,
75
+ type SsrSpatialAdmission,
76
+ } from '../ssr/admission';
77
+ import { createSsrHistoryOwner } from '../ssr/history';
65
78
  import {
66
79
  getTransparentSortConfig,
67
80
  TRANSPARENT_SORT_MODE_LAYER_Y,
@@ -123,6 +136,66 @@ export type FrameObservationReadback = (
123
136
  lease: CurrentFrameObservationLease,
124
137
  ) => Promise<Result<Uint8Array, Error>>;
125
138
 
139
+ /**
140
+ * Fold World identity into the renderer-owned validation token. The GPU
141
+ * production owner receives only this opaque key; ECS identity and ordering
142
+ * stay in the record owner where the World list is authoritative.
143
+ */
144
+ function residencyValidationCacheKey(world: World, worlds: readonly World[]): string {
145
+ return [world.identity, ...worlds.map((candidate) => candidate.identity)].join('\u001f');
146
+ }
147
+
148
+ function renderableIdentity(snapshot: Pick<RenderableSnapshot, 'worldId' | 'entityKey'>): string {
149
+ return `${snapshot.worldId}:${snapshot.entityKey}`;
150
+ }
151
+
152
+ /**
153
+ * A GPU-owned indirect draw has no CPU submit callback. Keep the same
154
+ * record-stage consumption proof for downstream lifecycle owners by checking
155
+ * the concrete draw-key projection that was actually encoded for this frame.
156
+ */
157
+ function gpuDrivenRenderableConsumed(
158
+ entry: ValidatedRenderable,
159
+ drawKeys: ReadonlySet<string>,
160
+ ): boolean {
161
+ const draws = entry.source.gpuDrivenDraws;
162
+ if (draws === undefined || draws.length === 0) return false;
163
+ const worldEntity = worldEntityKey(entry.source.worldId, entry.source.entityKey);
164
+ return draws.some((draw, compactIndex) => {
165
+ const material = entry.source.materials[draw.materialSlot] ?? entry.source.material;
166
+ return drawKeys.has(
167
+ gpuDrivenDrawKey(
168
+ worldEntity,
169
+ material.materialHandle ?? -1,
170
+ gpuDrivenSourceDrawItemIndex(draw, compactIndex),
171
+ ),
172
+ );
173
+ });
174
+ }
175
+
176
+ /**
177
+ * Project one shared validation result into a cull-specific renderable index
178
+ * domain. The retained ShadowCaster projection is the superset; the primary
179
+ * camera submission reuses its resolved mesh rows without a second residency
180
+ * walk or a second validation authority.
181
+ */
182
+ function projectValidatedRows(
183
+ validated: readonly ValidatedRenderable[],
184
+ target: readonly RenderableSnapshot[],
185
+ ): ValidatedRenderable[] {
186
+ const byIdentity = new Map<string, ValidatedRenderable>();
187
+ for (const row of validated) byIdentity.set(renderableIdentity(row.source), row);
188
+ const projected: ValidatedRenderable[] = [];
189
+ for (let index = 0; index < target.length; index += 1) {
190
+ const snapshot = target[index];
191
+ if (snapshot === undefined) continue;
192
+ const row = byIdentity.get(renderableIdentity(snapshot));
193
+ if (row === undefined) continue;
194
+ projected.push({ ...row, source: snapshot, renderableIndex: index });
195
+ }
196
+ return projected;
197
+ }
198
+
126
199
  export interface FrameObservationOptions {
127
200
  readonly semantic: 'linear-hdr';
128
201
  readonly readback: FrameObservationReadback;
@@ -219,7 +292,8 @@ export async function observeCurrentFrame(
219
292
  });
220
293
  }
221
294
 
222
- import { createTemporalView, resolveTemporalReset } from '../temporal/view';
295
+ import { acknowledgeRenderFeatureSubmission } from '../features/host';
296
+ import { createTemporalView, resolveTemporalReset, type TemporalView } from '../temporal/view';
223
297
  import { inspectVolumetricFog } from '../volume/inspection';
224
298
  import {
225
299
  acquireSwapChainTarget,
@@ -244,6 +318,7 @@ import {
244
318
  type CubeCaptureGraphState,
245
319
  ensureCompiledFrameGraph,
246
320
  executeCompiledFrameGraph,
321
+ getRenderFeatureGraphState,
247
322
  type RenderFeatureGraphCandidate,
248
323
  settleVolumetricFogGraphCandidate,
249
324
  } from './typed-frame-graph';
@@ -256,35 +331,34 @@ import {
256
331
 
257
332
  /**
258
333
  * 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.
334
+ * filtering pass runs. The result is keyed by the retained RenderScene slot,
335
+ * which preserves the World namespace already used by gpuStore. User meshes
336
+ * are pull-resident during record; relying only on the boot-time builtin map
337
+ * would make the first frame classify every imported mesh as a CPU fallback
338
+ * and would hide its LOD ranges from the GPU cull. Builtins are the only
339
+ * process-static entries and may be read from PipelineState.meshes.
264
340
  */
265
- function gpuDrivenMeshesForFrame(
341
+ export function gpuDrivenMeshesForFrame(
266
342
  internals: RenderSystemInternals,
267
343
  pipelineState: PipelineState,
268
344
  worlds: readonly World[],
269
345
  scene: PersistentGpuDrivenState | undefined,
270
346
  ): ReadonlyMap<number, MeshGpuHandles> {
271
- const meshes = new Map(pipelineState.meshes);
347
+ const meshes = new Map<number, MeshGpuHandles>();
272
348
  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
349
  for (const slot of scene.slots) {
282
- const world = worlds[slot.snapshot.worldId];
283
- if (world === undefined) continue;
284
350
  const assetHandle = slot.snapshot.assetHandle;
285
351
  const sharedHandle = toShared<'MeshAsset'>(assetHandle);
352
+ const assetSlot = handleSlot(sharedHandle);
353
+ if (assetSlot < BUILTIN_BASE) {
354
+ const gpu = pipelineState.meshes.get(assetSlot);
355
+ if (gpu !== undefined) meshes.set(slot.slot, gpu);
356
+ continue;
357
+ }
358
+ const world = worlds[slot.snapshot.worldId];
359
+ if (world === undefined) continue;
286
360
  let gpu = internals.gpuStore.getMeshGpuHandles(sharedHandle, world);
287
- if (gpu === undefined && !meshes.has(assetHandle)) {
361
+ if (gpu === undefined) {
288
362
  const asset = resolveAssetHandle<MeshAsset>(world, sharedHandle);
289
363
  if (asset.ok) {
290
364
  const lodMeshes = slot.snapshot.lods?.map((lod) =>
@@ -303,26 +377,11 @@ function gpuDrivenMeshesForFrame(
303
377
  if (resident.ok) gpu = resident.value;
304
378
  }
305
379
  }
306
- if (gpu !== undefined) meshes.set(assetHandle, gpu);
380
+ if (gpu !== undefined) meshes.set(slot.slot, gpu);
307
381
  }
308
- const value = meshes;
309
- gpuDrivenMeshCache.set(scene.scene, {
310
- meshResidencyEpoch: internals.gpuStore.meshResidencyEpoch,
311
- pipelineMeshes: pipelineState.meshes,
312
- value,
313
- });
314
- return value;
382
+ return meshes;
315
383
  }
316
384
 
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
385
  /**
327
386
  * A frame-local material table. Repeated snapshot objects share one UBO slot;
328
387
  * each renderable keeps only the slot index used by each authored material.
@@ -392,6 +451,34 @@ export function materialSlotCountForPrefix(
392
451
  return maxSlot + 1;
393
452
  }
394
453
 
454
+ type MeshSsboCapacityResult = ReturnType<typeof ensureMeshSsboCapacity>;
455
+
456
+ /**
457
+ * Resolve the actual mesh-SSBO ceiling after the shared grow contract ran.
458
+ * A missing grow controller is the legacy/test surface with no renderer-owned
459
+ * ceiling; a failed grow explicitly exposes the pre-grow capacity and must
460
+ * constrain every row written by this frame, including retained shadow rows.
461
+ */
462
+ function meshSsboSlotCapacityAfterEnsure(
463
+ internals: Pick<RenderSystemInternals, 'growMeshSsbo' | 'meshSsboState'>,
464
+ neededSlots: number,
465
+ result: MeshSsboCapacityResult,
466
+ ): number {
467
+ if (internals.growMeshSsbo === undefined) return Number.POSITIVE_INFINITY;
468
+ if (!result.ok) return Math.max(0, result.degradedToSlotCount);
469
+ return internals.meshSsboState?.slotCount ?? neededSlots;
470
+ }
471
+
472
+ /**
473
+ * Keep a row sequence inside the capacity published by the mesh-SSBO owner.
474
+ * The caller has already reserved earlier rows (the primary view), so this
475
+ * helper only accepts the remaining slots for the retained shadow sequence.
476
+ */
477
+ function limitRowsToMeshSsboCapacity<T>(rows: readonly T[], availableSlots: number): readonly T[] {
478
+ if (availableSlots === Number.POSITIVE_INFINITY) return rows;
479
+ return rows.slice(0, Math.max(0, Math.floor(availableSlots)));
480
+ }
481
+
395
482
  export type RecordProfileRunner = <T>(phase: RenderRecordPhase, action: () => T) => T;
396
483
 
397
484
  export interface CubeCaptureFrameInput {
@@ -456,6 +543,18 @@ function prepareCubeCapture(
456
543
  }
457
544
 
458
545
  const EMPTY_ENTITY_KEYS: ReadonlySet<number> = new Set<number>();
546
+ const EMPTY_DRAW_KEYS: ReadonlySet<string> = new Set<string>();
547
+
548
+ function cacheEntityKeysFromDrawKeys(drawKeys: ReadonlySet<string>): ReadonlySet<number> {
549
+ const entityKeys = new Set<number>();
550
+ for (const key of drawKeys) {
551
+ const separator = key.indexOf(':');
552
+ if (separator <= 0) continue;
553
+ const entityKey = Number(key.slice(0, separator));
554
+ if (Number.isSafeInteger(entityKey)) entityKeys.add(entityKey);
555
+ }
556
+ return entityKeys;
557
+ }
459
558
 
460
559
  function runRecordProfilePhase<T>(
461
560
  runner: RecordProfileRunner | undefined,
@@ -465,6 +564,14 @@ function runRecordProfilePhase<T>(
465
564
  return runner === undefined ? action() : runner(phase, action);
466
565
  }
467
566
 
567
+ function shadowPlanes(matrix: Float32Array): Float32Array {
568
+ return frustum.fromViewProjection(frustum.create(), matrix as never);
569
+ }
570
+
571
+ function shadowViewToken(matrix: Float32Array): string {
572
+ return Array.from(matrix).join(',');
573
+ }
574
+
468
575
  function halfFloatToNumber(bits: number): number {
469
576
  const sign = (bits & 0x8000) === 0 ? 1 : -1;
470
577
  const exponent = (bits >>> 10) & 0x1f;
@@ -1048,6 +1155,157 @@ function stageAcceptedVolumetricFogFrame(
1048
1155
  return { ...accepted, paramsBuffer };
1049
1156
  }
1050
1157
 
1158
+ function ssrRuntimeError(
1159
+ cause: unknown,
1160
+ ): RhiError | { code: string; message: string; name?: string } {
1161
+ if (cause instanceof RhiError) return cause;
1162
+ if (cause instanceof Error)
1163
+ return { code: 'ssr-history-failure', message: cause.message, name: cause.name };
1164
+ return { code: 'ssr-history-failure', message: String(cause) };
1165
+ }
1166
+
1167
+ function reportSsrHistoryFailure(
1168
+ internals: RenderSystemInternals,
1169
+ stage: 'build' | 'encode' | 'finish' | 'submit',
1170
+ cause: unknown,
1171
+ ): void {
1172
+ internals.errorRegistry.fire(
1173
+ new RhiError({
1174
+ code: 'webgpu-runtime-error',
1175
+ expected: `SSR history ${stage} succeeds before queue submission`,
1176
+ hint: 'inspect the SSR history owner failure and retry the next frame',
1177
+ detail: { error: ssrRuntimeError(cause) },
1178
+ }),
1179
+ );
1180
+ }
1181
+
1182
+ function mapSsrHistoryResetReason(
1183
+ reason: TemporalView['resetReason'],
1184
+ ): import('../ssr/history').SsrHistoryResetReason | undefined {
1185
+ switch (reason) {
1186
+ case 'first-frame':
1187
+ return 'first-enable';
1188
+ case 'resize':
1189
+ return 'resize';
1190
+ case 'camera-cut':
1191
+ case 'view-switch':
1192
+ return 'camera-cut';
1193
+ case 'history-version':
1194
+ return 'history-version';
1195
+ case 'environment-change':
1196
+ case 'fog-change':
1197
+ return 'reflection-generation';
1198
+ case 'device-recover':
1199
+ return 'device-recovery';
1200
+ case 'submit-failure':
1201
+ case undefined:
1202
+ return undefined;
1203
+ }
1204
+ }
1205
+
1206
+ function stageSsrHistoryCandidate(
1207
+ internals: RenderSystemInternals,
1208
+ frameState: RenderFrameState,
1209
+ camera: CameraSnapshot,
1210
+ width: number,
1211
+ height: number,
1212
+ admission: SsrSpatialAdmission,
1213
+ temporalView: TemporalView,
1214
+ ): boolean {
1215
+ const previousAdmission = frameState.ssrSpatialAdmission;
1216
+ frameState.ssrSpatialAdmission = admission;
1217
+ if (admission.status !== 'admitted') {
1218
+ if (frameState.ssrHistoryOwner !== undefined && previousAdmission?.status === 'admitted') {
1219
+ const owner = frameState.ssrHistoryOwner;
1220
+ owner.reset('disable');
1221
+ owner.retireAfterFence(internals.device.queue, (cause) => {
1222
+ reportSsrHistoryFailure(internals, 'submit', cause);
1223
+ });
1224
+ frameState.ssrHistoryOwner = undefined;
1225
+ }
1226
+ frameState.ssrHistoryCandidate = undefined;
1227
+ frameState.ssrLastCameraEntity = camera.entityKey;
1228
+ frameState.ssrLastHistoryVersion = camera.historyVersion;
1229
+ return true;
1230
+ }
1231
+
1232
+ let owner = frameState.ssrHistoryOwner;
1233
+ if (owner === undefined) {
1234
+ const created = createSsrHistoryOwner({
1235
+ device: internals.device,
1236
+ scope: internals.deviceScope,
1237
+ width,
1238
+ height,
1239
+ });
1240
+ if (!created.ok) {
1241
+ reportSsrHistoryFailure(internals, 'build', created.error);
1242
+ return false;
1243
+ }
1244
+ owner = created.value;
1245
+ frameState.ssrHistoryOwner = owner;
1246
+ } else if (owner.resources.width !== width || owner.resources.height !== height) {
1247
+ const resized = owner.resize(width, height);
1248
+ if (!resized.ok) {
1249
+ reportSsrHistoryFailure(internals, 'build', resized.error);
1250
+ return false;
1251
+ }
1252
+ owner.retireAfterFence(internals.device.queue, (cause) => {
1253
+ reportSsrHistoryFailure(internals, 'submit', cause);
1254
+ });
1255
+ }
1256
+
1257
+ let resetReason: import('../ssr/history').SsrHistoryResetReason | undefined;
1258
+ if (
1259
+ frameState.ssrLastCameraEntity !== undefined &&
1260
+ frameState.ssrLastCameraEntity !== camera.entityKey
1261
+ ) {
1262
+ resetReason = 'camera-cut';
1263
+ } else if (
1264
+ frameState.ssrLastHistoryVersion !== undefined &&
1265
+ frameState.ssrLastHistoryVersion !== camera.historyVersion
1266
+ ) {
1267
+ resetReason = 'history-version';
1268
+ } else {
1269
+ resetReason = mapSsrHistoryResetReason(temporalView.resetReason);
1270
+ }
1271
+ if (resetReason !== undefined) owner.reset(resetReason);
1272
+
1273
+ const started = owner.beginFrame();
1274
+ if (!started.ok) {
1275
+ reportSsrHistoryFailure(internals, 'build', started.error);
1276
+ return false;
1277
+ }
1278
+ const candidate = started.value;
1279
+ const payload =
1280
+ frameState.ssrTemporalParamsPayload?.byteLength === 32
1281
+ ? frameState.ssrTemporalParamsPayload
1282
+ : new Uint8Array(32);
1283
+ payload.fill(0);
1284
+ const values = new DataView(payload.buffer, payload.byteOffset, payload.byteLength);
1285
+ values.setUint32(0, candidate.historyValid ? 1 : 0, true);
1286
+ values.setFloat32(4, 0.9, true);
1287
+ values.setFloat32(8, admission.config.thickness, true);
1288
+ values.setFloat32(12, 0.5, true);
1289
+ // Reuse the shared successful-submit snapshot; SSR owns no jitter sequence
1290
+ // or independently advancing previous-camera state.
1291
+ const previousJitter = frameState.lastSuccessfulTemporalView?.currentJitterUv;
1292
+ values.setFloat32(16, temporalView.currentJitterUv?.[0] ?? 0, true);
1293
+ values.setFloat32(20, temporalView.currentJitterUv?.[1] ?? 0, true);
1294
+ values.setFloat32(24, previousJitter?.[0] ?? 0, true);
1295
+ values.setFloat32(28, previousJitter?.[1] ?? 0, true);
1296
+ const written = internals.device.queue.writeBuffer(owner.resources.paramsBuffer, 0, payload);
1297
+ if (!written.ok) {
1298
+ owner.abortFrame(candidate, 'encode');
1299
+ reportSsrHistoryFailure(internals, 'encode', written.error);
1300
+ return false;
1301
+ }
1302
+ frameState.ssrTemporalParamsPayload = payload;
1303
+ frameState.ssrHistoryCandidate = candidate;
1304
+ frameState.ssrLastCameraEntity = camera.entityKey;
1305
+ frameState.ssrLastHistoryVersion = camera.historyVersion;
1306
+ return true;
1307
+ }
1308
+
1051
1309
  function updateVolumetricFogInspection(
1052
1310
  frameState: RenderFrameState,
1053
1311
  fog: ExtractedVolumetricFog | undefined,
@@ -1303,7 +1561,7 @@ export function recordFrame(
1303
1561
  world: World,
1304
1562
  cameras: CameraSnapshot[],
1305
1563
  lights: ExtractedLights,
1306
- renderables: RenderableSnapshot[],
1564
+ renderables: readonly RenderableSnapshot[],
1307
1565
  transparentDispatch: readonly DispatchEntry[],
1308
1566
  frameState: RenderFrameState,
1309
1567
  dispatchCounts: DispatchCounts,
@@ -1339,9 +1597,18 @@ export function recordFrame(
1339
1597
  fogSignature = '',
1340
1598
  environmentReady = true,
1341
1599
  transmissionDemand?: TransmissionDemand,
1600
+ shadowCasterEntityKeys: ReadonlySet<number> = EMPTY_ENTITY_KEYS,
1601
+ shadowCasterDrawKeys: ReadonlySet<string> | undefined = undefined,
1342
1602
  volumetricFog?: ExtractedVolumetricFog,
1343
1603
  timingCapture?: GpuTimingCapture,
1604
+ shadowCasterMembership?: readonly ShadowCasterMembership[],
1605
+ shadowCasterProjection?: PersistentShadowCasterProjection,
1606
+ shadowCasterWorldKeys?: readonly number[],
1607
+ preparedLighting?: ReturnType<typeof prepareFrameLighting>,
1608
+ ssrDependencies?: SsrDependenciesInspection,
1609
+ onRenderableDraw?: (entry: ValidatedRenderable) => void,
1344
1610
  ): boolean {
1611
+ void shadowCasterEntityKeys;
1345
1612
  frameState.reflectionFallbackObservationSource = undefined;
1346
1613
  frameState.reflectionFallbackCompletion = undefined;
1347
1614
  // The `try / finally` wrapper advances `frameState.frameNumber` exactly
@@ -1391,6 +1658,7 @@ export function recordFrame(
1391
1658
  }
1392
1659
  const camera = activeCameras[0];
1393
1660
  if (!camera) return false;
1661
+ frameState.ssrRequested = camera.screenSpaceReflection !== undefined;
1394
1662
  const pipelineState = internals.getPipelineState();
1395
1663
  if (pipelineState === null) return false;
1396
1664
  if (pipelineState.device !== internals.device) {
@@ -1432,8 +1700,12 @@ export function recordFrame(
1432
1700
  // ensure + ExtractedLights three-arm destructure (directional fallback,
1433
1701
  // point/spot/Rect arrays, totalLightCount). Extracted to prepareFrameLighting
1434
1702
  // (M3/w18) so recordFrame stays a skeleton.
1435
- const lighting = runRecordProfilePhase(profilePhase, 'record/scene-state/lighting-prep', () =>
1436
- prepareFrameLighting(internals, frameState, lights, camera, pipelineState),
1703
+ const lighting = runRecordProfilePhase(
1704
+ profilePhase,
1705
+ 'record/scene-state/lighting-prep',
1706
+ () =>
1707
+ preparedLighting ??
1708
+ prepareFrameLighting(internals, frameState, lights, camera, pipelineState),
1437
1709
  );
1438
1710
  if (!lighting.ok) return lighting;
1439
1711
  const { light, totalLightCount, standard } = lighting.value;
@@ -1518,6 +1790,10 @@ export function recordFrame(
1518
1790
  const effectiveShadowMapSize = resolveShadowMapSize(internals, lights);
1519
1791
  const effectiveSpotShadowMapSize = resolveSpotShadowMapSize(internals, lights);
1520
1792
  const graphShadowMapSize = effectiveShadowMapSize ?? effectiveSpotShadowMapSize;
1793
+ const hasShadowViews =
1794
+ (effectiveShadowMapSize ?? 0) > 0 ||
1795
+ lights.pointShadow.length > 0 ||
1796
+ lights.spot.some((spot) => spot.castShadow);
1521
1797
  const volumeCapability = hasVolumetricFogCapability(
1522
1798
  internals.device.caps,
1523
1799
  internals.volumetricFogShaders,
@@ -1578,89 +1854,248 @@ export function recordFrame(
1578
1854
  (volumetricFog?.status === 'degraded' && acceptedFallback !== undefined);
1579
1855
 
1580
1856
  let preparedGpuDriven: PreparedGpuDrivenFrame | undefined;
1581
- if (gpuDriven !== undefined) {
1857
+ const abortPreparedGpuDriven = (): void => {
1858
+ preparedGpuDriven?._abortResourceReplacement?.();
1859
+ };
1860
+ const cubeCaptureWork = prepareCubeCapture(internals, cubeCapture, camera);
1861
+ const reflectionFallbackCandidate = cubeCapture?.reflectionProbes?.fallbackDemand === true;
1862
+ // The Standard PBR GPU raster owns one color target. Reflection fallback
1863
+ // adds an MRT attachment, so keep this frame on the ordinary CPU producer
1864
+ // instead of claiming rows that the one-target GPU pipeline cannot draw.
1865
+ const frameGpuDriven = reflectionFallbackCandidate ? undefined : gpuDriven;
1866
+
1867
+ // Validate first so user meshes are resident in the same frame. The GPU
1868
+ // owner consumes this producer-owned map together with the builtin mesh
1869
+ // entries; it never resolves an asset or creates a fallback residency.
1870
+ // Stable persistent-scene frames reuse this exact producer result. The
1871
+ // cache is fenced by source identities plus mesh/device/pipeline/catalog
1872
+ // generations, so recovery or any scene/resource replacement performs a
1873
+ // fresh owner validation before a capable submission.
1874
+ const shadowValidationActive = hasShadowViews && shadowCasterProjection !== undefined;
1875
+ const validationRenderables = shadowValidationActive
1876
+ ? shadowCasterProjection.renderables
1877
+ : renderables;
1878
+ const validationDispatch = shadowValidationActive
1879
+ ? shadowCasterProjection.dispatch
1880
+ : transparentDispatch;
1881
+ const validationInput = {
1882
+ cacheKey: residencyValidationCacheKey(world, worlds),
1883
+ renderables: validationRenderables,
1884
+ transparentDispatch: validationDispatch,
1885
+ pipelineState,
1886
+ pipelineHandle: frameState.installedPipelineHandle,
1887
+ meshResidencyEpoch: internals.gpuStore.meshResidencyEpoch,
1888
+ deviceGeneration: internals.deviceScope.generation,
1889
+ assetCatalogEpoch: internals.assets.catalogEpoch,
1890
+ } as const;
1891
+ const cachedValidation = frameGpuDriven?.owner.reuseResidencyValidation(validationInput);
1892
+ const validatedProjection =
1893
+ cachedValidation ??
1894
+ runRecordProfilePhase(profilePhase, 'record/validation', () =>
1895
+ validateRenderables(
1896
+ internals,
1897
+ world,
1898
+ worlds,
1899
+ pipelineState,
1900
+ frameState,
1901
+ validationRenderables,
1902
+ validationDispatch,
1903
+ EMPTY_ENTITY_KEYS,
1904
+ ),
1905
+ );
1906
+ if (cachedValidation === undefined) {
1907
+ frameGpuDriven?.owner.rememberResidencyValidation(validationInput, validatedProjection);
1908
+ }
1909
+ // Camera culling only changes the primary index domain. When shadows are
1910
+ // active, the retained projection is the validation superset; project the
1911
+ // same resolved rows into the camera-visible subset without another walk.
1912
+ const validatedShadowForResidency = shadowValidationActive ? validatedProjection : undefined;
1913
+ const validatedForResidency = shadowValidationActive
1914
+ ? projectValidatedRows(validatedProjection, renderables)
1915
+ : validatedProjection;
1916
+
1917
+ if (frameGpuDriven !== undefined) {
1918
+ const gpuDrivenMeshes = new Map(
1919
+ gpuDrivenMeshesForFrame(internals, pipelineState, worlds, frameGpuDriven.scene),
1920
+ );
1921
+ const meshSlotByIdentity = new Map(
1922
+ (frameGpuDriven.scene?.slots ?? []).map((slot) => [
1923
+ worldEntityKey(slot.snapshot.worldId, slot.snapshot.entityKey),
1924
+ slot.slot,
1925
+ ]),
1926
+ );
1927
+ for (const row of [...validatedForResidency, ...(validatedShadowForResidency ?? [])]) {
1928
+ const slot = meshSlotByIdentity.get(
1929
+ worldEntityKey(row.source.worldId, row.source.entityKey),
1930
+ );
1931
+ if (slot !== undefined) gpuDrivenMeshes.set(slot, row.mesh);
1932
+ }
1582
1933
  const standardPbrArtifact = internals.getMaterialShaderArtifact?.(
1583
1934
  'forgeax::default-standard-pbr',
1584
1935
  );
1585
- const prepared = gpuDriven.owner.prepare({
1586
- scene: gpuDriven.scene,
1936
+ const standardPbrSkinArtifact = internals.getMaterialShaderArtifact?.('forgeax::pbr-skin');
1937
+ const paletteUploadBytes = validatedForResidency.reduce(
1938
+ (total, row) => total + (row.source.skin?.uploadBytes ?? 0),
1939
+ 0,
1940
+ );
1941
+ const prepared = frameGpuDriven.owner.prepare({
1942
+ scene: frameGpuDriven.scene,
1587
1943
  camera,
1588
- meshes: gpuDrivenMeshesForFrame(internals, pipelineState, worlds, gpuDriven.scene),
1944
+ meshBySlot: gpuDrivenMeshes,
1589
1945
  viewBindGroupLayout: pipelineState.viewBindGroupLayout,
1590
1946
  meshResidencyEpoch: internals.gpuStore.meshResidencyEpoch,
1591
- // The built-in Standard graph (including its clustered transport) is
1592
- // eligible for the GPU-driven LOD lane. The old HDRP boolean no
1593
- // longer exists in the unified pipeline state, so it must not be
1594
- // inferred from `standard.kind`.
1595
- hdrp: false,
1596
- ...(gpuDriven.activeEntityKeys === undefined
1947
+ // GPU-driven Standard PBR currently owns the non-clustered path only.
1948
+ // Cluster bindings also carry screen-space ambient occlusion. Admit
1949
+ // the scene-index variant only when none of those surface inputs are
1950
+ // required; a zero local-light count alone does not establish that.
1951
+ hdrp:
1952
+ standard.kind === 'clustered' &&
1953
+ (standard.prepared.local.length > 0 ||
1954
+ frameState.installedPipelineConfig?.ssao?.enabled === true),
1955
+ ...(frameGpuDriven.activeEntityKeys === undefined
1956
+ ? {}
1957
+ : { activeEntityKeys: frameGpuDriven.activeEntityKeys }),
1958
+ ...(frameGpuDriven.activeEntityRevision === undefined
1597
1959
  ? {}
1598
- : { activeEntityKeys: gpuDriven.activeEntityKeys }),
1599
- ...(gpuDriven.activeEntityRevision === undefined
1960
+ : { activeEntityRevision: frameGpuDriven.activeEntityRevision }),
1961
+ ...(frameGpuDriven.telemetryCandidateCount === undefined
1600
1962
  ? {}
1601
- : { activeEntityRevision: gpuDriven.activeEntityRevision }),
1602
- ...(gpuDriven.telemetryCandidateCount === undefined
1963
+ : { telemetryCandidateCount: frameGpuDriven.telemetryCandidateCount }),
1964
+ ...(frameGpuDriven.occlusion === undefined ? {} : { occlusion: frameGpuDriven.occlusion }),
1965
+ ...(frameGpuDriven.scene?.structureMetrics === undefined
1603
1966
  ? {}
1604
- : { telemetryCandidateCount: gpuDriven.telemetryCandidateCount }),
1605
- ...(gpuDriven.occlusion === undefined ? {} : { occlusion: gpuDriven.occlusion }),
1967
+ : {
1968
+ structureMetrics: {
1969
+ ...frameGpuDriven.scene.structureMetrics,
1970
+ paletteUploadBytes,
1971
+ },
1972
+ }),
1606
1973
  ...(standardPbrArtifact === undefined ? {} : { standardPbrArtifact }),
1974
+ ...(standardPbrSkinArtifact === undefined ? {} : { standardPbrSkinArtifact }),
1607
1975
  standardPbrPipelineState: pipelineState,
1976
+ ...(shadowCasterDrawKeys === undefined ? {} : { shadowCasterDrawKeys }),
1977
+ ...(shadowCasterMembership === undefined ? {} : { shadowCasterMembership }),
1608
1978
  });
1609
- if (prepared.ok) preparedGpuDriven = prepared.value;
1610
- else if (prepared.error.code !== 'rhi-not-available')
1979
+ if (!prepared.ok) {
1980
+ // A capable GPU-owned candidate with missing producer resources is a
1981
+ // structured stop, not permission to enumerate the same draw through
1982
+ // the CPU semantic path. Preserve the last-known-good graph and retry
1983
+ // after the producer publishes a validated artifact.
1611
1984
  internals.errorRegistry.fire(prepared.error);
1985
+ abortPreparedGpuDriven();
1986
+ return false;
1987
+ } else if (prepared.value !== undefined) {
1988
+ preparedGpuDriven = prepared.value;
1989
+ const shadowViews: Array<{
1990
+ readonly identity: ShadowViewIdentity;
1991
+ readonly planes: Float32Array;
1992
+ readonly cacheToken: string;
1993
+ }> = [];
1994
+ for (const [index, matrix] of (lights.lightViewProj ?? []).entries()) {
1995
+ shadowViews.push({
1996
+ identity: { kind: 'directional', index },
1997
+ planes: shadowPlanes(matrix),
1998
+ cacheToken: `${shadowViewToken(matrix)}|target:${graphShadowMapSize}:${frameState.graphGeneration}`,
1999
+ });
2000
+ }
2001
+ for (const [index, point] of lights.pointShadow.entries()) {
2002
+ for (let face = 0; face < 6; face += 1) {
2003
+ const matrix = point.shadowMatrices.subarray(face * 16, face * 16 + 16);
2004
+ shadowViews.push({
2005
+ identity: { kind: 'point', index, face },
2006
+ planes: shadowPlanes(matrix),
2007
+ cacheToken: `${shadowViewToken(matrix)}|target:${graphShadowMapSize}:${frameState.graphGeneration}`,
2008
+ });
2009
+ }
2010
+ }
2011
+ let shadowSpotIndex = 0;
2012
+ for (const spot of lights.spot) {
2013
+ if (spot.lightViewProj === undefined) continue;
2014
+ shadowViews.push({
2015
+ identity: { kind: 'spot', index: shadowSpotIndex },
2016
+ planes: shadowPlanes(spot.lightViewProj),
2017
+ cacheToken: `${shadowViewToken(spot.lightViewProj)}|target:${graphShadowMapSize}:${frameState.graphGeneration}`,
2018
+ });
2019
+ shadowSpotIndex += 1;
2020
+ }
2021
+ if (prepared.value.updateShadowViews !== undefined) {
2022
+ const shadowUpdate = prepared.value.updateShadowViews(shadowViews);
2023
+ if (!shadowUpdate.ok) {
2024
+ internals.errorRegistry.fire(shadowUpdate.error);
2025
+ abortPreparedGpuDriven();
2026
+ return false;
2027
+ }
2028
+ }
2029
+ }
1612
2030
  }
1613
2031
 
2032
+ const gpuOwnedDrawKeys = preparedGpuDriven?.drawKeys ?? EMPTY_DRAW_KEYS;
1614
2033
  // The GPU lane owns only the primary opaque raster today. Directional,
1615
- // point, and spot shadow views still consume the CPU-validated draw plan.
1616
- const cpuShadowCastersActive =
1617
- (effectiveShadowMapSize ?? 0) > 0 ||
1618
- lights.pointShadow.length > 0 ||
1619
- lights.spot.some((spot) => spot.castShadow);
1620
- const gpuOwnsAllPrimaryRaster = preparedGpuDriven?.ownsAllRenderables === true;
1621
- gpuDriven?.onSceneDemand(gpuDriven.owner.requiresSceneRows());
1622
-
1623
- const cubeCaptureWork = prepareCubeCapture(internals, cubeCapture, camera);
1624
- const reflectionFallbackCandidate = cubeCapture?.reflectionProbes?.fallbackDemand === true;
1625
-
1626
- // Validate renderables before graph admission. Besides producing the draw
1627
- // plan, this is the owner of per-frame cache eviction; running it before a
1628
- // graph candidate can fail guarantees despawned instance buffers are
1629
- // released even when the frame cannot be submitted.
1630
- const gpuOwnedEntityKeys = preparedGpuDriven?.entityKeys ?? EMPTY_ENTITY_KEYS;
1631
- const gpuDrivenWorldKeys = gpuDriven?.scene?.worldKeys;
1632
- const validated = runRecordProfilePhase(profilePhase, 'record/validation', () =>
1633
- gpuOwnsAllPrimaryRaster && !cpuShadowCastersActive && cubeCaptureWork.length === 0
1634
- ? []
1635
- : validateRenderables(
1636
- internals,
1637
- world,
1638
- worlds,
1639
- pipelineState,
1640
- frameState,
1641
- renderables,
1642
- transparentDispatch,
1643
- cpuShadowCastersActive || cubeCaptureWork.length > 0
1644
- ? EMPTY_ENTITY_KEYS
1645
- : gpuOwnedEntityKeys,
1646
- gpuDrivenWorldKeys,
1647
- ),
2034
+ // point, and spot shadow views consume the same CPU-validated rows; they do
2035
+ // not require a second residency walk because the first validation already
2036
+ // resolved every renderable used by both lanes.
2037
+ // The GPU lane owns the actual geometry, but the main-pass material
2038
+ // producer still needs the validated snapshot rows to build the
2039
+ // frame-global bind-group table consumed by indirect draws. The record
2040
+ // loop skips claimed submeshes through `gpuDrivenDrawKeys`, so retaining
2041
+ // these rows does not issue duplicate CPU geometry. Reusing this one
2042
+ // validation result also keeps active shadow views from repeating the
2043
+ // residency walk.
2044
+ const validated = validatedForResidency;
2045
+ frameGpuDriven?.owner.recordCpuValidation(
2046
+ validated,
2047
+ gpuOwnedDrawKeys,
2048
+ validationInput.cacheKey,
1648
2049
  );
1649
- gpuDriven?.owner.recordCpuValidation(validated, gpuOwnedEntityKeys, gpuDrivenWorldKeys);
1650
- cleanPerFrameCaches(internals, frameState, validated, gpuOwnedEntityKeys);
2050
+ const cacheValidated =
2051
+ validatedShadowForResidency === undefined
2052
+ ? validated
2053
+ : [...validated, ...validatedShadowForResidency];
2054
+ cleanPerFrameCaches(
2055
+ internals,
2056
+ frameState,
2057
+ cacheValidated,
2058
+ cacheEntityKeysFromDrawKeys(gpuOwnedDrawKeys),
2059
+ );
2060
+
2061
+ // Compile the single frame graph only after GPU preparation has projected
2062
+ // the current frame. This keeps the graph topology and its execution
2063
+ // closure on the same prepared GPU-owned draw set.
1651
2064
 
1652
2065
  // S_fallback is the second output of the built-in Standard PBR BRDF. A
1653
2066
  // graph may still carry the detached owner tickets for inspection, but
1654
2067
  // the MRT is admitted only when every CPU-rendered primary row in this
1655
2068
  // frame uses that exact output contract. Unsupported compositions remain
1656
2069
  // fail-closed for SSR instead of submitting a device-invalid command.
1657
- frameState.reflectionFallbackDemand =
1658
- reflectionFallbackCandidate &&
2070
+ const sceneInputs =
1659
2071
  validated.length > 0 &&
1660
2072
  validated.every(
1661
2073
  (entry) => entry.source.material.materialShaderId === 'forgeax::default-standard-pbr',
1662
2074
  );
1663
-
2075
+ const ssrAdmission =
2076
+ ssrDependencies === undefined
2077
+ ? undefined
2078
+ : admitSsrSpatial({
2079
+ camera,
2080
+ environment: {
2081
+ lane: standard.prepared.renderPath === 'deferred' ? 'deferred' : 'forward',
2082
+ m0: ssrDependencies.admission,
2083
+ sceneInputs,
2084
+ // Standard geometry produces current motion/coverage in this
2085
+ // graph, including its first frame. Prior history is optional.
2086
+ temporal: sceneInputs,
2087
+ reflectionFallback: ssrDependencies.reflectionFallback !== undefined,
2088
+ capabilities: {
2089
+ compute: internals.device.caps.compute,
2090
+ storageTexture: internals.device.caps.storageTexture,
2091
+ rgba16floatRenderable: internals.device.caps.rgba16floatRenderable,
2092
+ r32floatSampledStorage: ssrDependencies.format?.verdict === 'admitted',
2093
+ },
2094
+ recovery: true,
2095
+ },
2096
+ });
2097
+ frameState.reflectionFallbackDemand =
2098
+ (reflectionFallbackCandidate || ssrAdmission?.status === 'admitted') && sceneInputs;
1664
2099
  const preflightGraph = runRecordProfilePhase(profilePhase, 'record/render-graph', () =>
1665
2100
  ensureCompiledFrameGraph(
1666
2101
  internals,
@@ -1676,13 +2111,18 @@ export function recordFrame(
1676
2111
  cubeCapture?.state,
1677
2112
  cameras.length === 0,
1678
2113
  transmissionDemand,
1679
- gpuDriven?.occlusion,
2114
+ frameGpuDriven?.occlusion,
1680
2115
  graphVolumetricFog,
1681
2116
  volumeTopologyCandidate,
1682
2117
  standard,
2118
+ undefined,
2119
+ ssrAdmission,
1683
2120
  ),
1684
2121
  );
1685
- if (preflightGraph === null) return false;
2122
+ if (preflightGraph === null) {
2123
+ abortPreparedGpuDriven();
2124
+ return false;
2125
+ }
1686
2126
 
1687
2127
  // The compiled graph is the acceptance boundary for the prepared
1688
2128
  // Standard lighting declaration. Upload the same declaration only after
@@ -1703,6 +2143,7 @@ export function recordFrame(
1703
2143
  },
1704
2144
  }),
1705
2145
  );
2146
+ abortPreparedGpuDriven();
1706
2147
  return false;
1707
2148
  }
1708
2149
  if (standard.kind === 'clustered') {
@@ -1731,6 +2172,7 @@ export function recordFrame(
1731
2172
  );
1732
2173
  if (!clusterWrite.ok) {
1733
2174
  internals.errorRegistry.fire(clusterWrite.error);
2175
+ abortPreparedGpuDriven();
1734
2176
  return false;
1735
2177
  }
1736
2178
  }
@@ -1743,7 +2185,10 @@ export function recordFrame(
1743
2185
  const swapTarget = runRecordProfilePhase(profilePhase, 'record/swapchain', () =>
1744
2186
  acquireSwapChainTarget(internals, pipelineState),
1745
2187
  );
1746
- if (swapTarget === null) return false;
2188
+ if (swapTarget === null) {
2189
+ abortPreparedGpuDriven();
2190
+ return false;
2191
+ }
1747
2192
  const currentTexture = swapTarget.currentTexture;
1748
2193
  const view = swapTarget.view;
1749
2194
  const targetW = swapTarget.targetW;
@@ -1801,6 +2246,23 @@ export function recordFrame(
1801
2246
  : { kind: 'off', view: temporalView };
1802
2247
  const recordCamera: CameraSnapshot =
1803
2248
  temporalView.mode === 'taa' ? { ...camera, temporal: temporalView } : camera;
2249
+ if (ssrAdmission !== undefined) {
2250
+ const historyWidth = Math.max(1, internals.canvas.width);
2251
+ const historyHeight = Math.max(1, internals.canvas.height);
2252
+ if (
2253
+ !stageSsrHistoryCandidate(
2254
+ internals,
2255
+ frameState,
2256
+ camera,
2257
+ historyWidth,
2258
+ historyHeight,
2259
+ ssrAdmission,
2260
+ temporalView,
2261
+ )
2262
+ ) {
2263
+ return false;
2264
+ }
2265
+ }
1804
2266
  let activeVolumetricFogContext: VolumetricFogFrameContext | undefined;
1805
2267
  let activeVolumetricFogSignature: VolumeTemporalSignature | undefined;
1806
2268
  if (graphVolumetricFog?.status === 'available' && baseVolumetricFogContext !== undefined) {
@@ -1827,6 +2289,7 @@ export function recordFrame(
1827
2289
  );
1828
2290
  if (!historyFlagWritten.ok) {
1829
2291
  internals.errorRegistry.fire(historyFlagWritten.error);
2292
+ abortPreparedGpuDriven();
1830
2293
  return false;
1831
2294
  }
1832
2295
  }
@@ -1902,9 +2365,18 @@ export function recordFrame(
1902
2365
  // order, run the mesh-SSBO capacity gate (graceful truncation), and build
1903
2366
  // the fold dispatch plan. Extracted to buildDispatchPlan (M3/w18).
1904
2367
  const dispatchPlan = runRecordProfilePhase(profilePhase, 'record/dispatch-plan', () =>
1905
- buildDispatchPlan(internals, validated, transparentDispatch, foldBuckets),
2368
+ buildDispatchPlan(
2369
+ internals,
2370
+ validated,
2371
+ transparentDispatch,
2372
+ foldBuckets,
2373
+ validatedShadowForResidency ?? [],
2374
+ ),
1906
2375
  );
1907
2376
  const validatedOrdered = dispatchPlan.validatedOrdered;
2377
+ const shadowValidatedOrdered = dispatchPlan.shadowValidatedOrdered;
2378
+ const shadowMeshSsboBase = validatedOrdered.length;
2379
+ const shadowDispatch = shadowCasterProjection?.dispatch;
1908
2380
  const foldDispatchPlan = dispatchPlan.foldDispatchPlan;
1909
2381
  const materialSlotIndices = dispatchPlan.materialSlotIndices;
1910
2382
  const materialSlots = dispatchPlan.materialSlots;
@@ -1936,12 +2408,13 @@ export function recordFrame(
1936
2408
  const unormViewRes = internals.device.createTextureView(currentTexture, {});
1937
2409
  if (!unormViewRes.ok) {
1938
2410
  internals.errorRegistry.fire(unormViewRes.error);
2411
+ abortPreparedGpuDriven();
1939
2412
  return false;
1940
2413
  }
1941
2414
  ldrSpritePassView = unormViewRes.value;
1942
2415
  }
1943
2416
 
1944
- // View / mesh uniform uploads are only needed when geometry will be drawn,
2417
+ // View / mesh uniform uploads are needed when geometry or a feature consumes View,
1945
2418
  // OR when a skybox is active (skybox pass reads inverseViewProj from
1946
2419
  // the View UBO). Skip the writeBuffer round-trips on the Case E
1947
2420
  // (clear-pass-only) path only when neither condition is met.
@@ -1956,7 +2429,10 @@ export function recordFrame(
1956
2429
  );
1957
2430
  if (
1958
2431
  validatedOrdered.length > 0 ||
2432
+ shadowValidatedOrdered.length > 0 ||
2433
+ getRenderFeatureGraphState(internals).plans.some(({ plan }) => plan.passes.length > 0) ||
1959
2434
  skyboxActive ||
2435
+ frameState.environmentFrame?.source.kind === 'atmosphere' ||
1960
2436
  activeVolumetricFogContext !== undefined ||
1961
2437
  preparedVolumetricFog !== undefined
1962
2438
  ) {
@@ -1999,6 +2475,15 @@ export function recordFrame(
1999
2475
  validatedOrdered,
2000
2476
  foldDispatchPlan,
2001
2477
  );
2478
+ if (shadowValidatedOrdered.length > 0) {
2479
+ uploadMeshSsboBatch(
2480
+ internals.device.queue,
2481
+ pipelineState.meshStorageBuffer,
2482
+ shadowValidatedOrdered,
2483
+ null,
2484
+ shadowMeshSsboBase,
2485
+ );
2486
+ }
2002
2487
  });
2003
2488
  }
2004
2489
 
@@ -2021,6 +2506,7 @@ export function recordFrame(
2021
2506
  const encoderResult = internals.device.createCommandEncoder({ label: 'render-system-frame' });
2022
2507
  if (!encoderResult.ok) {
2023
2508
  internals.errorRegistry.fire(encoderResult.error);
2509
+ abortPreparedGpuDriven();
2024
2510
  return false;
2025
2511
  }
2026
2512
  const encoder: RhiCommandEncoder = encoderResult.value;
@@ -2099,8 +2585,6 @@ export function recordFrame(
2099
2585
  const passCtx: _InternalRenderPipelineContext = {
2100
2586
  assets: internals.assets,
2101
2587
  world,
2102
- gpuDrivenEntityKeys: gpuOwnedEntityKeys,
2103
- ...(gpuDrivenWorldKeys === undefined ? {} : { gpuDrivenWorldKeys }),
2104
2588
  store: internals.gpuStore,
2105
2589
  pipelineState,
2106
2590
  bloomResources: pipelineState.perPassResources.getBloomResources?.() ?? null,
@@ -2136,6 +2620,10 @@ export function recordFrame(
2136
2620
  ...(internals.volumetricFogShaders === undefined
2137
2621
  ? {}
2138
2622
  : { volumetricFogShaders: internals.volumetricFogShaders }),
2623
+ ...(internals.ssrShaders === undefined ? {} : { ssrShaders: internals.ssrShaders }),
2624
+ ...(internals.atmosphereShaders === undefined
2625
+ ? {}
2626
+ : { atmosphereShaders: internals.atmosphereShaders }),
2139
2627
  ...(activeVolumetricFogContext === undefined
2140
2628
  ? {}
2141
2629
  : { volumetricFog: activeVolumetricFogContext }),
@@ -2145,19 +2633,39 @@ export function recordFrame(
2145
2633
  dispatch: transparentDispatch,
2146
2634
  hdrpClusterBindGroup,
2147
2635
  hdrpClusterMembershipBindGroup,
2148
- ...(gpuDriven?.occlusion === undefined ? {} : { occlusion: gpuDriven.occlusion }),
2636
+ ...(frameGpuDriven?.occlusion === undefined ? {} : { occlusion: frameGpuDriven.occlusion }),
2149
2637
  standardLighting: standard,
2150
2638
  foldDispatchPlan,
2151
2639
  materialSlotIndices,
2152
2640
  materialSlots,
2153
2641
  materialSlotOwners,
2154
2642
  materialSlotCount,
2643
+ ...(preparedGpuDriven === undefined
2644
+ ? {}
2645
+ : {
2646
+ gpuDrivenStandardPbrFrameResources: preparedGpuDriven.standardPbrFrameResources,
2647
+ gpuDrivenDrawKeys: preparedGpuDriven.drawKeys,
2648
+ gpuDrivenShadowViews: preparedGpuDriven.shadowViewPool,
2649
+ gpuDrivenShadowBatchProjections: preparedGpuDriven.shadowBatchProjections,
2650
+ gpuDrivenShadowDrawKeys: preparedGpuDriven.shadowDrawKeys,
2651
+ gpuDrivenShadowDrawKeysByView: preparedGpuDriven.shadowDrawKeysByView,
2652
+ }),
2653
+ ...(frameGpuDriven?.scene === undefined
2654
+ ? {}
2655
+ : { gpuDrivenSceneMaterialBuffer: frameGpuDriven.scene.scene.materialBuffer }),
2155
2656
  ...(cubeCapture?.reflectionProbes === undefined
2156
2657
  ? {}
2157
2658
  : { reflectionProbes: cubeCapture.reflectionProbes }),
2158
2659
  pointsLines: pointsLinesOwner,
2159
2660
  materialBgAssemblyCache: frameState.materialBgAssemblyCache,
2661
+ ...(onRenderableDraw === undefined ? {} : { onRenderableDraw }),
2160
2662
  directionalShadowCacheReuse: directionalShadowCache.reuse,
2663
+ ...(shadowCasterMembership === undefined ? {} : { shadowCasterMembership }),
2664
+ ...(shadowValidatedOrdered.length === 0
2665
+ ? {}
2666
+ : { shadowValidatedOrdered, shadowMeshSsboBase }),
2667
+ ...(shadowDispatch === undefined ? {} : { shadowDispatch }),
2668
+ ...(shadowCasterWorldKeys === undefined ? {} : { shadowCasterWorldKeys }),
2161
2669
  ...(profilePhase !== undefined ? { profilePhase } : {}),
2162
2670
  };
2163
2671
  uploadMaterialUniforms(passCtx);
@@ -2188,6 +2696,37 @@ export function recordFrame(
2188
2696
  frameState.temporalFrameInput = stagedTemporalFrame;
2189
2697
  frameState.temporalFrameTransaction.stage(stagedTemporalFrame);
2190
2698
  volumeSubmissionAttempted = volumeTopologyCandidate || activeVolumetricFogContext !== undefined;
2699
+ const ssrOwner = frameState.ssrHistoryOwner;
2700
+ const ssrCandidate = frameState.ssrHistoryCandidate;
2701
+ const frameHooks =
2702
+ preparedGpuDriven === undefined &&
2703
+ frameGpuDriven?.occlusion === undefined &&
2704
+ ssrCandidate === undefined
2705
+ ? undefined
2706
+ : {
2707
+ afterGraphExecute: () =>
2708
+ frameGpuDriven?.occlusion?.recordResolve(encoder) ?? ok(undefined),
2709
+ onSubmitted: () => {
2710
+ if (ssrOwner !== undefined && ssrCandidate !== undefined) {
2711
+ const committed = ssrOwner.commitFrame(ssrCandidate);
2712
+ if (!committed.ok) {
2713
+ ssrOwner.abortFrame(ssrCandidate, 'submit');
2714
+ reportSsrHistoryFailure(internals, 'submit', committed.error);
2715
+ }
2716
+ frameState.ssrHistoryCandidate = undefined;
2717
+ }
2718
+ preparedGpuDriven?._commitResourceReplacement();
2719
+ frameGpuDriven?.occlusion?.commit(true, frameState.frameNumber, profilePhase);
2720
+ },
2721
+ onAborted: () => {
2722
+ if (ssrOwner !== undefined && ssrCandidate !== undefined) {
2723
+ ssrOwner.abortFrame(ssrCandidate, 'submit');
2724
+ frameState.ssrHistoryCandidate = undefined;
2725
+ }
2726
+ preparedGpuDriven?._abortResourceReplacement?.();
2727
+ frameGpuDriven?.occlusion?.commit(false, frameState.frameNumber);
2728
+ },
2729
+ };
2191
2730
  submitted = runRecordProfilePhase(profilePhase, 'record/graph-execute', () =>
2192
2731
  executeCompiledFrameGraph(
2193
2732
  internals,
@@ -2197,17 +2736,7 @@ export function recordFrame(
2197
2736
  profilePhase === undefined
2198
2737
  ? undefined
2199
2738
  : (pass, encode) => profilePhase(graphExecutionPhase(pass.name), encode),
2200
- gpuDriven === undefined
2201
- ? undefined
2202
- : {
2203
- afterGraphExecute: () => gpuDriven.occlusion?.recordResolve(encoder) ?? ok(undefined),
2204
- onSubmitted: () => {
2205
- gpuDriven.occlusion?.commit(true, frameState.frameNumber, profilePhase);
2206
- },
2207
- onAborted: () => {
2208
- gpuDriven.occlusion?.commit(false, frameState.frameNumber);
2209
- },
2210
- },
2739
+ frameHooks,
2211
2740
  cubeCaptureWork.length === 0 ? undefined : cubeCaptureWork.map((work) => work.faceIndex),
2212
2741
  (done) => {
2213
2742
  completion = done;
@@ -2229,6 +2758,20 @@ export function recordFrame(
2229
2758
  timingCapture,
2230
2759
  ),
2231
2760
  );
2761
+ if (submitted && preparedGpuDriven?.drawKeys !== undefined && onRenderableDraw !== undefined) {
2762
+ for (const entry of validated) {
2763
+ if (gpuDrivenRenderableConsumed(entry, preparedGpuDriven.drawKeys)) {
2764
+ onRenderableDraw(entry);
2765
+ }
2766
+ }
2767
+ }
2768
+ if (submitted) {
2769
+ // Feature plans are submission-sensitive: VFX keeps queued fixed ticks
2770
+ // until the graph that consumed them is actually submitted. Acknowledge
2771
+ // the exact accepted graph here so successful frames do not replay every
2772
+ // prior tick into the next topology.
2773
+ acknowledgeRenderFeatureSubmission(getRenderFeatureGraphState(internals).plans);
2774
+ }
2232
2775
  if (cubeCapture !== undefined && cubeCaptureWork.length > 0) {
2233
2776
  const completed = cubeCapture.scheduler.completeSubmission(submitted, completion);
2234
2777
  if (!completed.ok) internals.errorRegistry.fire(completed.error);
@@ -2310,6 +2853,10 @@ export function recordFrame(
2310
2853
  }
2311
2854
  return submitted;
2312
2855
  } finally {
2856
+ if (!submitted && frameState.ssrHistoryCandidate !== undefined) {
2857
+ frameState.ssrHistoryOwner?.abortFrame(frameState.ssrHistoryCandidate, 'submit');
2858
+ frameState.ssrHistoryCandidate = undefined;
2859
+ }
2313
2860
  // Volume topology replacements are provisional until the queue submission
2314
2861
  // succeeds. This runs for every early-return path as well, so a swapchain,
2315
2862
  // encoder, or graph failure cannot leave a non-submitted candidate as the
@@ -2420,6 +2967,15 @@ function prepareDirectionalShadowCache(
2420
2967
  ): DirectionalShadowCacheDecision {
2421
2968
  const cascadeCount = lights.cascadeCount;
2422
2969
  const lightViewProj = lights.lightViewProj;
2970
+ // GPU feature buffers can change without an ECS mesh mutation. Until a
2971
+ // producer provides a stable content revision, never reuse their shadow atlas.
2972
+ if (
2973
+ getRenderFeatureGraphState(internals).plans.some(({ plan }) =>
2974
+ plan.passes.some((pass) => pass.kind === 'shadow-caster'),
2975
+ )
2976
+ ) {
2977
+ return { reuse: false, next: null };
2978
+ }
2423
2979
  if (
2424
2980
  frameState.compiledFrameGraph === null ||
2425
2981
  frameState.compiledFrameGraphTopologyKey === null ||
@@ -2660,6 +3216,9 @@ export function validateRenderables(
2660
3216
  return validated;
2661
3217
  }
2662
3218
 
3219
+ // Keep the primary record-stage name explicit at its call site while sharing
3220
+ // the same owner implementation for the complete retained ShadowCaster view.
3221
+
2663
3222
  function prepareMorphMesh(
2664
3223
  internals: RenderSystemInternals,
2665
3224
  frameState: RenderFrameState,
@@ -2671,9 +3230,23 @@ function prepareMorphMesh(
2671
3230
  if (morph === undefined || frameState.morphBuffers === undefined) return undefined;
2672
3231
  const targets = mesh.morphTargets;
2673
3232
  if (targets === undefined || targets.length !== morph.targetCount) return undefined;
2674
- if (base.layoutProjection.arrayStride !== 48 || base.vertexCount <= 0) return undefined;
2675
- const stride = mesh.vertices.length / base.vertexCount;
2676
- if (!Number.isInteger(stride) || stride < 3) return undefined;
3233
+ const position = base.layoutProjection.attributes.find(
3234
+ (attribute) => attribute.key === 'position',
3235
+ );
3236
+ if (
3237
+ position === undefined ||
3238
+ position.offset !== 0 ||
3239
+ position.shaderLocation !== 0 ||
3240
+ position.format !== 'float32x3' ||
3241
+ position.byteLength !== 12
3242
+ ) {
3243
+ return undefined;
3244
+ }
3245
+ const vertexCount = deriveVertexCount(mesh.vertices, base.layoutProjection);
3246
+ if (vertexCount === undefined || vertexCount !== base.vertexCount || vertexCount <= 0) {
3247
+ return undefined;
3248
+ }
3249
+ const stride = base.layoutProjection.arrayStride / Float32Array.BYTES_PER_ELEMENT;
2677
3250
 
2678
3251
  let active = false;
2679
3252
  for (const weight of morph.weights) {
@@ -2751,17 +3324,19 @@ function prepareMorphMesh(
2751
3324
  *
2752
3325
  * @internal
2753
3326
  */
2754
- function buildDispatchPlan(
3327
+ export function buildDispatchPlan(
2755
3328
  internals: RenderSystemInternals,
2756
3329
  validated: readonly ValidatedRenderable[],
2757
3330
  transparentDispatch: readonly DispatchEntry[],
2758
3331
  foldBuckets: readonly FoldBucket[],
3332
+ shadowValidated: readonly ValidatedRenderable[] = [],
2759
3333
  ): {
2760
3334
  validatedOrdered: readonly ValidatedRenderable[];
3335
+ shadowValidatedOrdered: readonly ValidatedRenderable[];
2761
3336
  foldDispatchPlan: FoldDispatchPlan | null;
2762
3337
  materialSlotIndices: readonly (readonly number[])[];
2763
3338
  materialSlots: readonly MaterialSnapshot[];
2764
- materialSlotOwners: readonly number[];
3339
+ materialSlotOwners: readonly ValidatedRenderable[];
2765
3340
  materialSlotCount: number;
2766
3341
  } {
2767
3342
  // A production GPU-owned frame has no CPU validated rows by design. The
@@ -2769,9 +3344,16 @@ function buildDispatchPlan(
2769
3344
  // CPU plan, material table, or fold bucket is consumed in that lane. Return
2770
3345
  // the empty plan without walking that list so a steady 100k scene remains
2771
3346
  // bounded by the persistent GPU batches rather than per-entity record work.
2772
- if (validated.length === 0) {
3347
+ if (validated.length === 0 && shadowValidated.length === 0) {
3348
+ const meshSsboCapResult = ensureMeshSsboCapacity(internals, shadowValidated.length);
3349
+ const meshSsboSlotCapacity = meshSsboSlotCapacityAfterEnsure(
3350
+ internals,
3351
+ shadowValidated.length,
3352
+ meshSsboCapResult,
3353
+ );
2773
3354
  return {
2774
3355
  validatedOrdered: [],
3356
+ shadowValidatedOrdered: limitRowsToMeshSsboCapacity(shadowValidated, meshSsboSlotCapacity),
2775
3357
  foldDispatchPlan: null,
2776
3358
  materialSlotIndices: [],
2777
3359
  materialSlots: [],
@@ -2819,7 +3401,10 @@ function buildDispatchPlan(
2819
3401
  validatedOrdered = ordered;
2820
3402
  }
2821
3403
 
2822
- const materialGroups = validatedOrdered.map((entry): readonly MaterialSnapshot[] => {
3404
+ // Shadow-only/off-camera CPU residuals consume the same material UBO as
3405
+ // visible draws. Their preparation must not depend on main-view visibility.
3406
+ const materialRows = [...validatedOrdered, ...shadowValidated];
3407
+ const materialGroups = materialRows.map((entry): readonly MaterialSnapshot[] => {
2823
3408
  const shaderId = entry.source.material.materialShaderId;
2824
3409
  if (shaderId === 'forgeax::sprite' || shaderId === 'forgeax::sprite-lit') {
2825
3410
  return [entry.source.material];
@@ -2829,8 +3414,13 @@ function buildDispatchPlan(
2829
3414
  const materialSlotPlan = buildMaterialSlotPlan(materialGroups);
2830
3415
  let materialSlotIndices = materialSlotPlan.slotIndices;
2831
3416
  let materialSlots = materialSlotPlan.slots;
2832
- let materialSlotOwners = materialSlotPlan.slotOwners;
3417
+ let materialSlotOwners = materialSlotPlan.slotOwners.map((index) => {
3418
+ const owner = materialRows[index];
3419
+ if (owner === undefined) throw new Error('material slot has no validated owner');
3420
+ return owner;
3421
+ });
2833
3422
  let materialSlotCount = materialSlots.length;
3423
+ let materialRenderableCount = materialRows.length;
2834
3424
 
2835
3425
  // feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M3 / T-M3-04:
2836
3426
  // ensure the mesh-SSBO + material-UBO buffer pair is large enough to hold
@@ -2855,8 +3445,13 @@ function buildDispatchPlan(
2855
3445
  //
2856
3446
  // feat-20260624 M1' / t7: sprite-lit treated identically to sprite
2857
3447
  // for material-slot accounting (paramSchema mirror, t4).
2858
- const neededSlots = Math.max(validatedOrdered.length, materialSlotCount);
3448
+ const neededSlots = Math.max(validatedOrdered.length + shadowValidated.length, materialSlotCount);
2859
3449
  const meshSsboCapResult = ensureMeshSsboCapacity(internals, neededSlots);
3450
+ const meshSsboSlotCapacity = meshSsboSlotCapacityAfterEnsure(
3451
+ internals,
3452
+ neededSlots,
3453
+ meshSsboCapResult,
3454
+ );
2860
3455
  if (!meshSsboCapResult.ok) {
2861
3456
  // Graceful degradation: the controller reports slots, but this stage
2862
3457
  // consumes entities. Find a complete prefix instead of slicing at the
@@ -2865,12 +3460,17 @@ function buildDispatchPlan(
2865
3460
  materialSlotIndices,
2866
3461
  meshSsboCapResult.degradedToSlotCount,
2867
3462
  );
3463
+ materialRenderableCount = degradedRenderableCount;
2868
3464
  materialSlotCount = materialSlotCountForPrefix(materialSlotIndices, degradedRenderableCount);
2869
3465
  materialSlotIndices = materialSlotIndices.slice(0, degradedRenderableCount);
2870
3466
  materialSlots = materialSlots.slice(0, materialSlotCount);
2871
3467
  materialSlotOwners = materialSlotOwners.slice(0, materialSlotCount);
2872
3468
  validatedOrdered = validatedOrdered.slice(0, degradedRenderableCount);
2873
3469
  }
3470
+ const shadowValidatedOrdered = limitRowsToMeshSsboCapacity(
3471
+ shadowValidated,
3472
+ Math.min(meshSsboSlotCapacity, materialRenderableCount) - validatedOrdered.length,
3473
+ );
2874
3474
 
2875
3475
  // feat-20260622-chunk-gpu-instancing-sprite-tilemap M1 / w4-record-swap
2876
3476
  // (D-1): build the fold dispatch plan once `validatedOrdered` is final
@@ -2963,6 +3563,7 @@ function buildDispatchPlan(
2963
3563
 
2964
3564
  return {
2965
3565
  validatedOrdered,
3566
+ shadowValidatedOrdered,
2966
3567
  foldDispatchPlan,
2967
3568
  materialSlotIndices,
2968
3569
  materialSlots,
@@ -3040,13 +3641,6 @@ function cleanPerFrameCaches(
3040
3641
  }
3041
3642
  }
3042
3643
 
3043
- if (frameState.instanceResidency !== undefined) {
3044
- const liveCollections = new Set(validated.map((entry) => entry.source.instances?.collectionId));
3045
- for (const id of frameState.instanceResidency.keys()) {
3046
- if (!liveCollections.has(id)) frameState.instanceResidency.delete(id);
3047
- }
3048
- }
3049
-
3050
3644
  // Chunk keys are `${worldEntityKey}:${chunkStart}`. Keep the key opaque to
3051
3645
  // the authoring API, but retain the numeric owner prefix for eviction.
3052
3646
  if (frameState.instanceBufferChunks !== undefined) {