@forgeax/engine-render 0.1.28 → 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 (642) hide show
  1. package/README.md +627 -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 +21 -0
  5. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  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 +8 -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 +2 -2
  60. package/dist/{chunk-JAASCMXA.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-J52NMQKR.mjs → chunk-BQVQOQJH.mjs} +1086 -838
  65. package/dist/chunk-BQVQOQJH.mjs.map +1 -0
  66. package/dist/{chunk-IQMKJLNU.mjs → chunk-BTWH7BP4.mjs} +386 -17
  67. package/dist/chunk-BTWH7BP4.mjs.map +1 -0
  68. package/dist/{chunk-MBU4ZVE7.mjs → chunk-FUODTTIA.mjs} +3921 -671
  69. package/dist/chunk-FUODTTIA.mjs.map +1 -0
  70. package/dist/{chunk-MYJ5RPJO.mjs → chunk-JQTSEI3Z.mjs} +3 -3
  71. package/dist/{chunk-MYJ5RPJO.mjs.map → chunk-JQTSEI3Z.mjs.map} +1 -1
  72. package/dist/{chunk-GE3SDD72.mjs → chunk-NICXSHAT.mjs} +16 -38
  73. package/dist/chunk-NICXSHAT.mjs.map +1 -0
  74. package/dist/{chunk-J6KACNOH.mjs → chunk-NRNXBLIJ.mjs} +577 -7
  75. package/dist/chunk-NRNXBLIJ.mjs.map +1 -0
  76. package/dist/{chunk-RBDBZ5R7.mjs → chunk-PA5DRJNI.mjs} +2 -2
  77. package/dist/chunk-PA5DRJNI.mjs.map +1 -0
  78. package/dist/{chunk-VSL23LZE.mjs → chunk-S4DNKQYQ.mjs} +1117 -445
  79. package/dist/chunk-S4DNKQYQ.mjs.map +1 -0
  80. package/dist/components/camera.d.ts +62 -2
  81. package/dist/components/camera.d.ts.map +1 -1
  82. package/dist/components/index.d.ts +1 -0
  83. package/dist/components/index.d.ts.map +1 -1
  84. package/dist/components/instances.d.ts +6 -20
  85. package/dist/components/instances.d.ts.map +1 -1
  86. package/dist/components/scene-instance.d.ts +2 -1
  87. package/dist/components/scene-instance.d.ts.map +1 -1
  88. package/dist/components/screen-space-reflection.d.ts +14 -0
  89. package/dist/components/screen-space-reflection.d.ts.map +1 -0
  90. package/dist/construct-renderer.mjs +12888 -7025
  91. package/dist/construct-renderer.mjs.map +1 -1
  92. package/dist/device/gpu-residency.d.ts +1 -12
  93. package/dist/device/gpu-residency.d.ts.map +1 -1
  94. package/dist/environment/background.d.ts +6 -0
  95. package/dist/environment/background.d.ts.map +1 -0
  96. package/dist/errors/gpu-driven.d.ts +6 -3
  97. package/dist/errors/gpu-driven.d.ts.map +1 -1
  98. package/dist/errors/render.d.ts +7 -4
  99. package/dist/errors/render.d.ts.map +1 -1
  100. package/dist/extract/camera.d.ts.map +1 -1
  101. package/dist/extract/gpu-driven.d.ts +33 -3
  102. package/dist/extract/gpu-driven.d.ts.map +1 -1
  103. package/dist/features/host.d.ts +23 -1
  104. package/dist/features/host.d.ts.map +1 -1
  105. package/dist/features/noise-texture.d.ts +17 -0
  106. package/dist/features/noise-texture.d.ts.map +1 -0
  107. package/dist/features/plan.d.ts +83 -2
  108. package/dist/features/plan.d.ts.map +1 -1
  109. package/dist/features/prepared-gpu-work.d.ts +37 -4
  110. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  111. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  112. package/dist/features/prepared-graphics.d.ts +5 -0
  113. package/dist/features/prepared-graphics.d.ts.map +1 -1
  114. package/dist/features/render-graph-compute.d.ts +5 -3
  115. package/dist/features/render-graph-compute.d.ts.map +1 -1
  116. package/dist/features/render-graph-contribution.d.ts +11 -3
  117. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  118. package/dist/features/render-graph-raster.d.ts +19 -3
  119. package/dist/features/render-graph-raster.d.ts.map +1 -1
  120. package/dist/features/types.d.ts +11 -0
  121. package/dist/features/types.d.ts.map +1 -1
  122. package/dist/fullscreen-post-process-pass.d.ts +2 -0
  123. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  124. package/dist/gpu-driven/batch-topology.d.ts +34 -1
  125. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  126. package/dist/gpu-driven/pbr-program.d.ts +10 -0
  127. package/dist/gpu-driven/pbr-program.d.ts.map +1 -0
  128. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  129. package/dist/gpu-driven/production-raster.d.ts +144 -23
  130. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  131. package/dist/gpu-driven/shadow-views.d.ts +126 -0
  132. package/dist/gpu-driven/shadow-views.d.ts.map +1 -0
  133. package/dist/gpu-driven/view-gpu.d.ts +18 -2
  134. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  135. package/dist/gpu-scene-schema.d.ts +2 -8
  136. package/dist/gpu-scene-schema.d.ts.map +1 -1
  137. package/dist/gpu-scene.d.ts +14 -0
  138. package/dist/gpu-scene.d.ts.map +1 -1
  139. package/dist/gpu-texture-usage.d.ts +1 -0
  140. package/dist/gpu-texture-usage.d.ts.map +1 -1
  141. package/dist/hdrp-buffers.d.ts +6 -0
  142. package/dist/hdrp-buffers.d.ts.map +1 -1
  143. package/dist/ibl/frame-resources.d.ts +8 -0
  144. package/dist/ibl/frame-resources.d.ts.map +1 -0
  145. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  146. package/dist/index.d.ts +18 -8
  147. package/dist/index.d.ts.map +1 -1
  148. package/dist/index.mjs +8 -8
  149. package/dist/inspection-types.d.ts +164 -0
  150. package/dist/inspection-types.d.ts.map +1 -1
  151. package/dist/instances-derived-bounds.d.ts +11 -0
  152. package/dist/instances-derived-bounds.d.ts.map +1 -1
  153. package/dist/instances.d.ts +89 -44
  154. package/dist/instances.d.ts.map +1 -1
  155. package/dist/internal.d.ts +3 -0
  156. package/dist/internal.d.ts.map +1 -1
  157. package/dist/internal.mjs +117 -9
  158. package/dist/internal.mjs.map +1 -1
  159. package/dist/lifecycle.d.ts +2 -1
  160. package/dist/lifecycle.d.ts.map +1 -1
  161. package/dist/material-row.d.ts +5 -0
  162. package/dist/material-row.d.ts.map +1 -0
  163. package/dist/materials.d.ts +15 -0
  164. package/dist/materials.d.ts.map +1 -1
  165. package/dist/pbr-pipeline.d.ts +21 -5
  166. package/dist/pbr-pipeline.d.ts.map +1 -1
  167. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  168. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts +12 -0
  169. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts.map +1 -0
  170. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts +70 -0
  171. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts.map +1 -0
  172. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts +75 -0
  173. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts.map +1 -0
  174. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts +111 -0
  175. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts.map +1 -0
  176. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts +16 -0
  177. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts.map +1 -0
  178. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts +14 -0
  179. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts.map +1 -0
  180. package/dist/pipeline/standard-output/auto-exposure/state.d.ts +46 -0
  181. package/dist/pipeline/standard-output/auto-exposure/state.d.ts.map +1 -0
  182. package/dist/pipeline/standard-output/color-transform.d.ts +27 -0
  183. package/dist/pipeline/standard-output/color-transform.d.ts.map +1 -0
  184. package/dist/pipeline/standard-output/graph.d.ts +15 -0
  185. package/dist/pipeline/standard-output/graph.d.ts.map +1 -0
  186. package/dist/pipeline/standard-output/lut-admission.d.ts +29 -0
  187. package/dist/pipeline/standard-output/lut-admission.d.ts.map +1 -0
  188. package/dist/pipeline/standard-output/lut-gpu.d.ts +48 -0
  189. package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -0
  190. package/dist/pipeline/standard-output/lut-state.d.ts +68 -0
  191. package/dist/pipeline/standard-output/lut-state.d.ts.map +1 -0
  192. package/dist/pipeline/standard-output/resources.d.ts +10 -0
  193. package/dist/pipeline/standard-output/resources.d.ts.map +1 -0
  194. package/dist/pipeline/standard-output/types.d.ts +39 -0
  195. package/dist/pipeline/standard-output/types.d.ts.map +1 -0
  196. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  197. package/dist/pipeline/standard-post.d.ts +2 -2
  198. package/dist/pipeline/standard-post.d.ts.map +1 -1
  199. package/dist/pipeline-builder.d.ts +2 -1
  200. package/dist/pipeline-builder.d.ts.map +1 -1
  201. package/dist/pipeline-spec-types.d.ts +4 -0
  202. package/dist/pipeline-spec-types.d.ts.map +1 -1
  203. package/dist/pipeline-spec.d.ts +1 -9
  204. package/dist/pipeline-spec.d.ts.map +1 -1
  205. package/dist/points-lines/standard-owner.d.ts +1 -1
  206. package/dist/points-lines/standard-owner.d.ts.map +1 -1
  207. package/dist/prepare/prepared-graphics-resolver.d.ts +6 -1
  208. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  209. package/dist/record/dynamic-geometry-consumption.d.ts +15 -0
  210. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -0
  211. package/dist/record/frame-lighting.d.ts +3 -3
  212. package/dist/record/frame-lighting.d.ts.map +1 -1
  213. package/dist/record/frame-snapshot.d.ts +106 -5
  214. package/dist/record/frame-snapshot.d.ts.map +1 -1
  215. package/dist/record/frame-targets.d.ts.map +1 -1
  216. package/dist/record/frame.d.ts +39 -4
  217. package/dist/record/frame.d.ts.map +1 -1
  218. package/dist/record/main-pass-geometry.d.ts +7 -8
  219. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  220. package/dist/record/main-pass-material.d.ts +38 -1
  221. package/dist/record/main-pass-material.d.ts.map +1 -1
  222. package/dist/record/main-pass-sprite-draws.d.ts +2 -10
  223. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  224. package/dist/record/main-pass.d.ts +12 -2
  225. package/dist/record/main-pass.d.ts.map +1 -1
  226. package/dist/record/mesh-ssbo.d.ts +15 -6
  227. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  228. package/dist/record/prepared-material-bindings.d.ts +19 -0
  229. package/dist/record/prepared-material-bindings.d.ts.map +1 -0
  230. package/dist/record/recovery-pipeline.d.ts +2 -9
  231. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  232. package/dist/record/render-context.d.ts +74 -26
  233. package/dist/record/render-context.d.ts.map +1 -1
  234. package/dist/record/shadow-pass.d.ts +62 -7
  235. package/dist/record/shadow-pass.d.ts.map +1 -1
  236. package/dist/record/skybox-post-pass.d.ts +1 -1
  237. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  238. package/dist/record/typed-frame-graph.d.ts +35 -7
  239. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  240. package/dist/record/view-ubo.d.ts +1 -0
  241. package/dist/record/view-ubo.d.ts.map +1 -1
  242. package/dist/recovery/render-system-candidate.d.ts +10 -2
  243. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  244. package/dist/recovery/render-system-roots.d.ts +11 -0
  245. package/dist/recovery/render-system-roots.d.ts.map +1 -0
  246. package/dist/recovery/types.d.ts +76 -0
  247. package/dist/recovery/types.d.ts.map +1 -0
  248. package/dist/reflection/record-owner.d.ts +0 -2
  249. package/dist/reflection/record-owner.d.ts.map +1 -1
  250. package/dist/render-contract.d.ts +173 -30
  251. package/dist/render-contract.d.ts.map +1 -1
  252. package/dist/render-graph-primitives.d.ts +1 -0
  253. package/dist/render-graph-primitives.d.ts.map +1 -1
  254. package/dist/render-pipeline.d.ts +64 -5
  255. package/dist/render-pipeline.d.ts.map +1 -1
  256. package/dist/render-system-extract-tail.d.ts +7 -5
  257. package/dist/render-system-extract-tail.d.ts.map +1 -1
  258. package/dist/render-system-extract.d.ts +101 -41
  259. package/dist/render-system-extract.d.ts.map +1 -1
  260. package/dist/render-system-projections.d.ts +22 -0
  261. package/dist/render-system-projections.d.ts.map +1 -0
  262. package/dist/render-system.d.ts +27 -77
  263. package/dist/render-system.d.ts.map +1 -1
  264. package/dist/scene/render-scene-types.d.ts +14 -7
  265. package/dist/scene/render-scene-types.d.ts.map +1 -1
  266. package/dist/scene/render-scene.d.ts +29 -11
  267. package/dist/scene/render-scene.d.ts.map +1 -1
  268. package/dist/scene/shadow-visibility.d.ts +5 -0
  269. package/dist/scene/shadow-visibility.d.ts.map +1 -0
  270. package/dist/ssr/admission.d.ts +70 -1
  271. package/dist/ssr/admission.d.ts.map +1 -1
  272. package/dist/ssr/compose.d.ts +13 -0
  273. package/dist/ssr/compose.d.ts.map +1 -0
  274. package/dist/ssr/errors.d.ts +35 -0
  275. package/dist/ssr/errors.d.ts.map +1 -1
  276. package/dist/ssr/graph.d.ts +60 -0
  277. package/dist/ssr/graph.d.ts.map +1 -0
  278. package/dist/ssr/history.d.ts +97 -0
  279. package/dist/ssr/history.d.ts.map +1 -0
  280. package/dist/ssr/hiz.d.ts +37 -0
  281. package/dist/ssr/hiz.d.ts.map +1 -0
  282. package/dist/ssr/inspection.d.ts +42 -0
  283. package/dist/ssr/inspection.d.ts.map +1 -0
  284. package/dist/ssr/resources.d.ts +58 -0
  285. package/dist/ssr/resources.d.ts.map +1 -0
  286. package/dist/ssr/temporal.d.ts +38 -0
  287. package/dist/ssr/temporal.d.ts.map +1 -0
  288. package/dist/systems/skin-palette-allocator.d.ts +32 -10
  289. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  290. package/dist/systems/skin-palette-types.d.ts +15 -0
  291. package/dist/systems/skin-palette-types.d.ts.map +1 -1
  292. package/dist/systems/transparent-dispatch.d.ts +9 -0
  293. package/dist/systems/transparent-dispatch.d.ts.map +1 -0
  294. package/dist/targets/material-source.d.ts +7 -0
  295. package/dist/targets/material-source.d.ts.map +1 -1
  296. package/dist/temporal/frame-coordinator.d.ts +7 -0
  297. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  298. package/dist/temporal/gpu.d.ts +8 -0
  299. package/dist/temporal/gpu.d.ts.map +1 -1
  300. package/dist/temporal/history.d.ts.map +1 -1
  301. package/dist/temporal/index.mjs +6 -6
  302. package/dist/temporal/inspection.d.ts.map +1 -1
  303. package/dist/temporal/standard-scene-data.d.ts +2 -1
  304. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  305. package/dist/temporal/temporal-view.d.ts +2 -0
  306. package/dist/temporal/temporal-view.d.ts.map +1 -1
  307. package/dist/typed-render-graph-primitives.d.ts +35 -1
  308. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  309. package/dist/typed-shadow-passes.d.ts +3 -1
  310. package/dist/typed-shadow-passes.d.ts.map +1 -1
  311. package/dist/volume/temporal.d.ts +6 -0
  312. package/dist/volume/temporal.d.ts.map +1 -1
  313. package/package.json +21 -20
  314. package/src/__tests__/auto-exposure-gpu-evidence.ts +291 -0
  315. package/src/__tests__/auto-exposure-inspection.unit.test.ts +80 -0
  316. package/src/__tests__/auto-exposure-transaction.integration.test.ts +99 -0
  317. package/src/__tests__/auto-exposure.browser.test.ts +33 -0
  318. package/src/__tests__/auto-exposure.dawn.test.ts +33 -0
  319. package/src/__tests__/detached-inspection.integration.test.ts +5 -0
  320. package/src/__tests__/device-scope-lifecycle.unit.test.ts +18 -0
  321. package/src/__tests__/device-scope-stale-matrix.unit.test.ts +18 -0
  322. package/src/__tests__/docs-gate-green.unit.test.ts +3 -3
  323. package/src/__tests__/factory-contract.integration.test.ts +67 -8
  324. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +19 -0
  325. package/src/__tests__/feature-depth-input.dawn.test.ts +121 -0
  326. package/src/__tests__/feature-noise.unit.test.ts +38 -0
  327. package/src/__tests__/fixtures/taa-resolve-pinned-baseline.wgsl +301 -0
  328. package/src/__tests__/frame-camera-selection.unit.test.ts +131 -0
  329. package/src/__tests__/frame-plan-contract.unit.test.ts +24 -0
  330. package/src/__tests__/frame-targets.unit.test.ts +24 -0
  331. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +3 -0
  332. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +79 -0
  333. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +287 -6
  334. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +122 -142
  335. package/src/__tests__/gpu-driven-indirect-raster.browser.test.ts +10 -1
  336. package/src/__tests__/gpu-driven-lod-cache.unit.test.ts +264 -0
  337. package/src/__tests__/gpu-driven-mesh-world-identity.unit.test.ts +349 -0
  338. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +1417 -0
  339. package/src/__tests__/gpu-driven-pbr.integration.test.ts +21 -0
  340. package/src/__tests__/gpu-driven-production-fixture.ts +96 -0
  341. package/src/__tests__/gpu-driven-production.integration.test.ts +1075 -93
  342. package/src/__tests__/gpu-driven-scaling.unit.test.ts +1 -3
  343. package/src/__tests__/gpu-driven-shadow-views.browser.test.ts +63 -0
  344. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +294 -0
  345. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +97 -0
  346. package/src/__tests__/gpu-driven-shadow-views.unit.test.ts +168 -0
  347. package/src/__tests__/gpu-driven-skin-contract.test-d.ts +11 -0
  348. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +72 -20
  349. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +30 -9
  350. package/src/__tests__/gpu-driven-view.unit.test.ts +159 -52
  351. package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -1
  352. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +42 -0
  353. package/src/__tests__/gpu-scene-render-graph-gpu.ts +19 -5
  354. package/src/__tests__/gpu-scene.dawn.test.ts +40 -13
  355. package/src/__tests__/gpu-scene.unit.test.ts +559 -33
  356. package/src/__tests__/instances-culling.integration.test.ts +36 -10
  357. package/src/__tests__/instances-derived-bounds.unit.test.ts +54 -0
  358. package/src/__tests__/instances-store.unit.test.ts +242 -39
  359. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +6 -0
  360. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +13 -16
  361. package/src/__tests__/lut-lkg-transaction.integration.test.ts +122 -0
  362. package/src/__tests__/material-contract-inventory.unit.test.ts +5 -6
  363. package/src/__tests__/material-dispatch-shader-id.unit.test.ts +51 -0
  364. package/src/__tests__/material-lane-selection.unit.test.ts +60 -0
  365. package/src/__tests__/material-numeric-upload.unit.test.ts +53 -0
  366. package/src/__tests__/material-texture-handles.unit.test.ts +18 -0
  367. package/src/__tests__/material-texture-source.unit.test.ts +87 -0
  368. package/src/__tests__/materials-authoring-validation.unit.test.ts +41 -0
  369. package/src/__tests__/materials-standard-contract.unit.test.ts +52 -2
  370. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +4 -0
  371. package/src/__tests__/normal-fallback.dawn.test.ts +122 -0
  372. package/src/__tests__/particle-material-input-layout.unit.test.ts +104 -0
  373. package/src/__tests__/physical-clearcoat.integration.test.ts +37 -3
  374. package/src/__tests__/pipeline-texture-specialization.unit.test.ts +138 -0
  375. package/src/__tests__/point-shadow-inspection.unit.test.ts +2 -0
  376. package/src/__tests__/prepared-gpu-driven-pbr-contract.test-d.ts +31 -0
  377. package/src/__tests__/prepared-gpu-driven-pbr-contract.unit.test.ts +39 -0
  378. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +35 -4
  379. package/src/__tests__/prepared-graphics-capability.unit.test.ts +21 -0
  380. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +6 -2
  381. package/src/__tests__/profiler-phase-catalog.test.ts +5 -0
  382. package/src/__tests__/public-surface.test-d.ts +3 -0
  383. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +30 -12
  384. package/src/__tests__/reflection-fallback-async-publication.unit.test.ts +104 -0
  385. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +2 -1
  386. package/src/__tests__/render-error-code-owner.test-d.ts +4 -0
  387. package/src/__tests__/render-error-detail-ssot.test-d.ts +22 -0
  388. package/src/__tests__/render-error-exhaustive.test-d.ts +15 -0
  389. package/src/__tests__/render-feature-gpu-work.integration.test.ts +285 -2
  390. package/src/__tests__/render-feature-plan.unit.test.ts +245 -0
  391. package/src/__tests__/render-feature-shadow-order.unit.test.ts +228 -0
  392. package/src/__tests__/render-feature-zero-work.unit.test.ts +35 -1
  393. package/src/__tests__/render-graph-fallback.integration.test.ts +2 -2
  394. package/src/__tests__/render-owner-cohesion.unit.test.ts +55 -0
  395. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +426 -0
  396. package/src/__tests__/render-scene-projection.unit.test.ts +914 -23
  397. package/src/__tests__/render-scene-resource-updates.integration.test.ts +214 -0
  398. package/src/__tests__/render-scene-skin-lifecycle.integration.test.ts +173 -0
  399. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +133 -0
  400. package/src/__tests__/render-target-material-source.test-d.ts +27 -3
  401. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +60 -0
  402. package/src/__tests__/renderer-frame-transaction.integration.test.ts +923 -5
  403. package/src/__tests__/shader-manifest-fixture.ts +39 -0
  404. package/src/__tests__/skin-motion-regression.unit.test.ts +0 -33
  405. package/src/__tests__/skin-palette-persistent.integration.test.ts +28 -0
  406. package/src/__tests__/skin-palette-persistent.unit.test.ts +247 -0
  407. package/src/__tests__/skinned-shadow-caster.test.ts +118 -14
  408. package/src/__tests__/specular-environment-scale.dawn.test.ts +156 -0
  409. package/src/__tests__/ssr-admission-matrix.integration.test.ts +7 -1
  410. package/src/__tests__/ssr-confidence-compose.unit.test.ts +84 -0
  411. package/src/__tests__/ssr-coordinate-oracle.unit.test.ts +28 -0
  412. package/src/__tests__/ssr-format-probe-gating.integration.test.ts +82 -0
  413. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +91 -0
  414. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +112 -0
  415. package/src/__tests__/ssr-gpu-dispatch.ts +946 -0
  416. package/src/__tests__/ssr-graph-topology.unit.test.ts +104 -0
  417. package/src/__tests__/ssr-history-filter.dawn.test.ts +198 -0
  418. package/src/__tests__/ssr-hit-color.dawn.test.ts +273 -0
  419. package/src/__tests__/ssr-hiz-odd-reduction.dawn.test.ts +569 -0
  420. package/src/__tests__/ssr-hiz-rescue.dawn.test.ts +279 -0
  421. package/src/__tests__/ssr-hiz.browser.test.ts +150 -0
  422. package/src/__tests__/ssr-hiz.dawn.test.ts +150 -0
  423. package/src/__tests__/ssr-hiz.unit.test.ts +56 -0
  424. package/src/__tests__/ssr-inspection.unit.test.ts +105 -0
  425. package/src/__tests__/ssr-memory-budget.unit.test.ts +45 -0
  426. package/src/__tests__/ssr-performance-sequence-aggregation.unit.test.ts +15 -0
  427. package/src/__tests__/ssr-public-surface.test-d.ts +37 -0
  428. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +387 -0
  429. package/src/__tests__/ssr-reflection-mip-reducer.dawn.test.ts +309 -0
  430. package/src/__tests__/ssr-reprojection-probe.ts +90 -0
  431. package/src/__tests__/ssr-roughness-mip.dawn.test.ts +163 -0
  432. package/src/__tests__/ssr-shader-prewarm.unit.test.ts +59 -0
  433. package/src/__tests__/ssr-spatial.integration.test.ts +141 -0
  434. package/src/__tests__/ssr-stable-lattice.dawn.test.ts +475 -0
  435. package/src/__tests__/ssr-temporal-history.unit.test.ts +202 -0
  436. package/src/__tests__/ssr-texel-crossing.dawn.test.ts +448 -0
  437. package/src/__tests__/ssr-zero-work.integration.test.ts +87 -0
  438. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +44 -12
  439. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +29 -0
  440. package/src/__tests__/standard-output-chain-contract.unit.test.ts +99 -0
  441. package/src/__tests__/standard-output-chain.integration.test.ts +73 -0
  442. package/src/__tests__/standard-output-gpu-evidence.ts +205 -0
  443. package/src/__tests__/standard-output.browser.test.ts +15 -0
  444. package/src/__tests__/standard-output.dawn.test.ts +15 -0
  445. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +38 -0
  446. package/src/__tests__/standard-pipeline.integration.test.ts +320 -3
  447. package/src/__tests__/standard-profile-oracle.unit.test.ts +22 -0
  448. package/src/__tests__/standard-surface-lighting.unit.test.ts +41 -0
  449. package/src/__tests__/standard-texture-specialization.dawn.test.ts +141 -0
  450. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +6 -3
  451. package/src/__tests__/taa-coverage-edge.dawn.test.ts +193 -0
  452. package/src/__tests__/taa-history-clipping.dawn.test.ts +328 -0
  453. package/src/__tests__/taa-neighborhood-equivalence.dawn.test.ts +778 -0
  454. package/src/__tests__/taa-stability-feedback.browser.test.ts +23 -0
  455. package/src/__tests__/taa-stability-feedback.dawn.test.ts +68 -0
  456. package/src/__tests__/taa-stability-feedback.ts +341 -0
  457. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +49 -7
  458. package/src/__tests__/temporal-reset-matrix.unit.test.ts +9 -0
  459. package/src/__tests__/temporal-transaction.integration.test.ts +28 -0
  460. package/src/__tests__/texture-usage-surface.unit.test.ts +4 -2
  461. package/src/__tests__/typed-pipeline-topology.unit.test.ts +109 -4
  462. package/src/__tests__/typed-scene-pass-material-cache.integration.test.ts +126 -0
  463. package/src/__tests__/vertex-layouts-ssot.unit.test.ts +47 -0
  464. package/src/__tests__/visibility-extract.perf.test.ts +20 -8
  465. package/src/__tests__/visibility-instances.integration.test.ts +19 -7
  466. package/src/__tests__/visibility-producer-matrix.integration.test.ts +20 -9
  467. package/src/assembly/atmosphere-shader-prewarm.ts +38 -0
  468. package/src/assembly/dynamic-geometry-host.ts +48 -0
  469. package/src/assembly/dynamic-geometry-runtime.ts +71 -0
  470. package/src/assembly/factory.ts +4 -0
  471. package/src/assembly/gpu-driven-owner.ts +29 -0
  472. package/src/assembly/host-contract.ts +10 -0
  473. package/src/assembly/material/assembly.ts +38 -0
  474. package/src/assembly/material-shader-policy.ts +71 -28
  475. package/src/assembly/recovery/failure-location.ts +62 -0
  476. package/src/assembly/recovery/renderer-recover.ts +19 -17
  477. package/src/assembly/renderer-facade.ts +12 -0
  478. package/src/assembly/renderer-feature-inspection.ts +13 -0
  479. package/src/assembly/renderer-frame-transaction.ts +63 -0
  480. package/src/assembly/renderer-helpers.ts +26 -0
  481. package/src/assembly/renderer-inspection.ts +6 -0
  482. package/src/assembly/shader-prewarm-policy.ts +32 -0
  483. package/src/assembly/skin-palette-owner.ts +21 -0
  484. package/src/assembly/ssr-shader-prewarm.ts +41 -0
  485. package/src/assembly/webgpu-device-loss.ts +75 -0
  486. package/src/assembly/webgpu-pbr-ready.ts +232 -0
  487. package/src/assembly/webgpu-ready-mipmap.ts +26 -0
  488. package/src/assembly/webgpu-ready.ts +142 -121
  489. package/src/assembly/webgpu-renderer-bootstrap.ts +85 -0
  490. package/src/assembly/webgpu-renderer-contract.ts +69 -0
  491. package/src/assembly/webgpu-renderer-guards.ts +32 -0
  492. package/src/assembly/webgpu-renderer-observation.ts +43 -0
  493. package/src/assembly/webgpu-renderer-recovery-failure.ts +62 -0
  494. package/src/assembly/webgpu-renderer.ts +666 -652
  495. package/src/assembly/webgpu-vertex-layouts.ts +233 -87
  496. package/src/components/__tests__/camera-exposure.contract.unit.test.ts +153 -0
  497. package/src/components/camera.ts +287 -7
  498. package/src/components/index.ts +1 -0
  499. package/src/components/instances.ts +45 -18
  500. package/src/components/scene-instance.ts +2 -1
  501. package/src/components/screen-space-reflection.ts +21 -0
  502. package/src/components/sprite-animation.ts +2 -2
  503. package/src/components/sprite-instances.ts +1 -1
  504. package/src/device/gpu-residency.ts +34 -31
  505. package/src/environment/background.ts +263 -0
  506. package/src/errors/gpu-driven.ts +39 -5
  507. package/src/errors/render.ts +27 -3
  508. package/src/extract/camera.ts +26 -1
  509. package/src/extract/gpu-driven.ts +185 -10
  510. package/src/features/host.ts +237 -19
  511. package/src/features/noise-texture.ts +91 -0
  512. package/src/features/plan.ts +553 -24
  513. package/src/features/prepared-gpu-work.ts +631 -49
  514. package/src/features/prepared-graphics-store.ts +31 -0
  515. package/src/features/prepared-graphics.ts +5 -0
  516. package/src/features/render-graph-compute.ts +41 -8
  517. package/src/features/render-graph-contribution.ts +141 -3
  518. package/src/features/render-graph-raster.ts +143 -30
  519. package/src/features/types.ts +11 -0
  520. package/src/fullscreen-post-process-pass.ts +3 -0
  521. package/src/gpu-driven/batch-topology.ts +424 -102
  522. package/src/gpu-driven/pbr-program.ts +31 -0
  523. package/src/gpu-driven/prepared-draw.ts +29 -4
  524. package/src/gpu-driven/production-raster.ts +2267 -694
  525. package/src/gpu-driven/shadow-views.ts +539 -0
  526. package/src/gpu-driven/view-gpu.ts +188 -41
  527. package/src/gpu-scene-schema.ts +52 -11
  528. package/src/gpu-scene.ts +186 -99
  529. package/src/gpu-texture-usage.ts +1 -0
  530. package/src/hdrp-buffers.ts +72 -36
  531. package/src/ibl/frame-resources.ts +38 -0
  532. package/src/ibl/skylight-bind-group.ts +5 -4
  533. package/src/index.ts +109 -1
  534. package/src/inspection-types.ts +191 -0
  535. package/src/instances-derived-bounds.ts +258 -34
  536. package/src/instances.ts +459 -150
  537. package/src/internal.ts +22 -0
  538. package/src/lifecycle.ts +2 -0
  539. package/src/material-row.ts +145 -0
  540. package/src/materials.ts +44 -8
  541. package/src/pbr-pipeline.ts +159 -17
  542. package/src/pipeline/standard-forward-lane.ts +52 -24
  543. package/src/pipeline/standard-output/__tests__/lut-admission.unit.test.ts +103 -0
  544. package/src/pipeline/standard-output/auto-exposure/__tests__/capability.unit.test.ts +40 -0
  545. package/src/pipeline/standard-output/auto-exposure/__tests__/gpu.contract.unit.test.ts +125 -0
  546. package/src/pipeline/standard-output/auto-exposure/__tests__/graph.contract.unit.test.ts +94 -0
  547. package/src/pipeline/standard-output/auto-exposure/__tests__/oracle.test.ts +108 -0
  548. package/src/pipeline/standard-output/auto-exposure/__tests__/preset.test.ts +26 -0
  549. package/src/pipeline/standard-output/auto-exposure/capability.ts +38 -0
  550. package/src/pipeline/standard-output/auto-exposure/gpu.ts +567 -0
  551. package/src/pipeline/standard-output/auto-exposure/graph.ts +221 -0
  552. package/src/pipeline/standard-output/auto-exposure/inspection.ts +231 -0
  553. package/src/pipeline/standard-output/auto-exposure/oracle.ts +291 -0
  554. package/src/pipeline/standard-output/auto-exposure/preset.ts +28 -0
  555. package/src/pipeline/standard-output/auto-exposure/state.ts +156 -0
  556. package/src/pipeline/standard-output/color-transform.ts +124 -0
  557. package/src/pipeline/standard-output/graph.ts +139 -0
  558. package/src/pipeline/standard-output/lut-admission.ts +121 -0
  559. package/src/pipeline/standard-output/lut-gpu.ts +399 -0
  560. package/src/pipeline/standard-output/lut-state.ts +176 -0
  561. package/src/pipeline/standard-output/resources.ts +121 -0
  562. package/src/pipeline/standard-output/types.ts +58 -0
  563. package/src/pipeline/standard-pipeline.ts +192 -59
  564. package/src/pipeline/standard-post.ts +186 -27
  565. package/src/pipeline-builder.ts +16 -6
  566. package/src/pipeline-spec-types.ts +4 -0
  567. package/src/pipeline-spec.ts +37 -29
  568. package/src/points-lines/standard-owner.ts +6 -14
  569. package/src/prepare/prepared-graphics-resolver.ts +9 -2
  570. package/src/record/__tests__/material-slot-plan.unit.test.ts +48 -0
  571. package/src/record/__tests__/mesh-ssbo.unit.test.ts +40 -1
  572. package/src/record/__tests__/recovery-pipeline.unit.test.ts +2 -29
  573. package/src/record/__tests__/sprite-material-abi.unit.test.ts +36 -5
  574. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +45 -0
  575. package/src/record/__tests__/view-ubo-layout.unit.test.ts +5 -0
  576. package/src/record/dynamic-geometry-consumption.ts +59 -0
  577. package/src/record/frame-lighting.ts +12 -11
  578. package/src/record/frame-snapshot.ts +133 -23
  579. package/src/record/frame-targets.ts +9 -0
  580. package/src/record/frame.ts +725 -133
  581. package/src/record/main-pass-geometry.ts +149 -94
  582. package/src/record/main-pass-material.ts +115 -26
  583. package/src/record/main-pass-sprite-draws.ts +16 -107
  584. package/src/record/main-pass.ts +78 -8
  585. package/src/record/material-uniforms.ts +1 -1
  586. package/src/record/mesh-ssbo.ts +59 -15
  587. package/src/record/prepared-material-bindings.ts +234 -0
  588. package/src/record/recovery-pipeline.ts +59 -136
  589. package/src/record/render-context.ts +90 -22
  590. package/src/record/shadow-pass.ts +1231 -209
  591. package/src/record/skybox-post-pass.ts +14 -3
  592. package/src/record/typed-frame-graph.ts +1004 -44
  593. package/src/record/view-ubo.ts +11 -0
  594. package/src/recovery/render-system-candidate.ts +90 -66
  595. package/src/recovery/render-system-roots.ts +113 -0
  596. package/src/recovery/types.ts +77 -0
  597. package/src/reflection/record-owner.ts +26 -54
  598. package/src/render-contract.ts +195 -25
  599. package/src/render-graph-primitives.ts +6 -2
  600. package/src/render-pipeline.ts +70 -1
  601. package/src/render-system-extract-tail.ts +409 -160
  602. package/src/render-system-extract.ts +376 -102
  603. package/src/render-system-projections.ts +241 -0
  604. package/src/render-system.ts +681 -822
  605. package/src/scene/render-scene-types.ts +13 -4
  606. package/src/scene/render-scene.ts +569 -355
  607. package/src/scene/shadow-visibility.ts +21 -0
  608. package/src/ssr/admission.ts +380 -14
  609. package/src/ssr/compose.ts +144 -0
  610. package/src/ssr/errors.ts +48 -0
  611. package/src/ssr/graph.ts +777 -0
  612. package/src/ssr/history.ts +436 -0
  613. package/src/ssr/hiz.ts +124 -0
  614. package/src/ssr/inspection.ts +124 -0
  615. package/src/ssr/resources.ts +202 -0
  616. package/src/ssr/temporal.ts +164 -0
  617. package/src/systems/skin-palette-allocator.ts +495 -148
  618. package/src/systems/skin-palette-types.ts +17 -0
  619. package/src/systems/transparent-dispatch.ts +90 -0
  620. package/src/targets/material-source.ts +34 -0
  621. package/src/temporal/coverage.ts +1 -1
  622. package/src/temporal/frame-coordinator.ts +23 -2
  623. package/src/temporal/gpu.ts +74 -12
  624. package/src/temporal/history.ts +3 -1
  625. package/src/temporal/inspection.ts +2 -1
  626. package/src/temporal/standard-scene-data.ts +3 -1
  627. package/src/temporal/temporal-view.ts +3 -0
  628. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +204 -139
  629. package/src/typed-render-graph-primitives.ts +266 -74
  630. package/src/typed-shadow-passes.ts +122 -12
  631. package/src/volume/temporal.ts +10 -0
  632. package/dist/chunk-GE3SDD72.mjs.map +0 -1
  633. package/dist/chunk-GF523LHF.mjs.map +0 -1
  634. package/dist/chunk-IQMKJLNU.mjs.map +0 -1
  635. package/dist/chunk-J52NMQKR.mjs.map +0 -1
  636. package/dist/chunk-J6KACNOH.mjs.map +0 -1
  637. package/dist/chunk-JAASCMXA.mjs.map +0 -1
  638. package/dist/chunk-MBU4ZVE7.mjs.map +0 -1
  639. package/dist/chunk-RBDBZ5R7.mjs.map +0 -1
  640. package/dist/chunk-VSL23LZE.mjs.map +0 -1
  641. package/src/__tests__/instances-world-ownership.integration.test.ts +0 -62
  642. 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,10 +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,
1344
1609
  onRenderableDraw?: (entry: ValidatedRenderable) => void,
1345
1610
  ): boolean {
1611
+ void shadowCasterEntityKeys;
1346
1612
  frameState.reflectionFallbackObservationSource = undefined;
1347
1613
  frameState.reflectionFallbackCompletion = undefined;
1348
1614
  // The `try / finally` wrapper advances `frameState.frameNumber` exactly
@@ -1392,6 +1658,7 @@ export function recordFrame(
1392
1658
  }
1393
1659
  const camera = activeCameras[0];
1394
1660
  if (!camera) return false;
1661
+ frameState.ssrRequested = camera.screenSpaceReflection !== undefined;
1395
1662
  const pipelineState = internals.getPipelineState();
1396
1663
  if (pipelineState === null) return false;
1397
1664
  if (pipelineState.device !== internals.device) {
@@ -1433,8 +1700,12 @@ export function recordFrame(
1433
1700
  // ensure + ExtractedLights three-arm destructure (directional fallback,
1434
1701
  // point/spot/Rect arrays, totalLightCount). Extracted to prepareFrameLighting
1435
1702
  // (M3/w18) so recordFrame stays a skeleton.
1436
- const lighting = runRecordProfilePhase(profilePhase, 'record/scene-state/lighting-prep', () =>
1437
- 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),
1438
1709
  );
1439
1710
  if (!lighting.ok) return lighting;
1440
1711
  const { light, totalLightCount, standard } = lighting.value;
@@ -1519,6 +1790,10 @@ export function recordFrame(
1519
1790
  const effectiveShadowMapSize = resolveShadowMapSize(internals, lights);
1520
1791
  const effectiveSpotShadowMapSize = resolveSpotShadowMapSize(internals, lights);
1521
1792
  const graphShadowMapSize = effectiveShadowMapSize ?? effectiveSpotShadowMapSize;
1793
+ const hasShadowViews =
1794
+ (effectiveShadowMapSize ?? 0) > 0 ||
1795
+ lights.pointShadow.length > 0 ||
1796
+ lights.spot.some((spot) => spot.castShadow);
1522
1797
  const volumeCapability = hasVolumetricFogCapability(
1523
1798
  internals.device.caps,
1524
1799
  internals.volumetricFogShaders,
@@ -1579,89 +1854,248 @@ export function recordFrame(
1579
1854
  (volumetricFog?.status === 'degraded' && acceptedFallback !== undefined);
1580
1855
 
1581
1856
  let preparedGpuDriven: PreparedGpuDrivenFrame | undefined;
1582
- 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
+ }
1583
1933
  const standardPbrArtifact = internals.getMaterialShaderArtifact?.(
1584
1934
  'forgeax::default-standard-pbr',
1585
1935
  );
1586
- const prepared = gpuDriven.owner.prepare({
1587
- 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,
1588
1943
  camera,
1589
- meshes: gpuDrivenMeshesForFrame(internals, pipelineState, worlds, gpuDriven.scene),
1944
+ meshBySlot: gpuDrivenMeshes,
1590
1945
  viewBindGroupLayout: pipelineState.viewBindGroupLayout,
1591
1946
  meshResidencyEpoch: internals.gpuStore.meshResidencyEpoch,
1592
- // The built-in Standard graph (including its clustered transport) is
1593
- // eligible for the GPU-driven LOD lane. The old HDRP boolean no
1594
- // longer exists in the unified pipeline state, so it must not be
1595
- // inferred from `standard.kind`.
1596
- hdrp: false,
1597
- ...(gpuDriven.activeEntityKeys === undefined
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
1598
1959
  ? {}
1599
- : { activeEntityKeys: gpuDriven.activeEntityKeys }),
1600
- ...(gpuDriven.activeEntityRevision === undefined
1960
+ : { activeEntityRevision: frameGpuDriven.activeEntityRevision }),
1961
+ ...(frameGpuDriven.telemetryCandidateCount === undefined
1601
1962
  ? {}
1602
- : { activeEntityRevision: gpuDriven.activeEntityRevision }),
1603
- ...(gpuDriven.telemetryCandidateCount === undefined
1963
+ : { telemetryCandidateCount: frameGpuDriven.telemetryCandidateCount }),
1964
+ ...(frameGpuDriven.occlusion === undefined ? {} : { occlusion: frameGpuDriven.occlusion }),
1965
+ ...(frameGpuDriven.scene?.structureMetrics === undefined
1604
1966
  ? {}
1605
- : { telemetryCandidateCount: gpuDriven.telemetryCandidateCount }),
1606
- ...(gpuDriven.occlusion === undefined ? {} : { occlusion: gpuDriven.occlusion }),
1967
+ : {
1968
+ structureMetrics: {
1969
+ ...frameGpuDriven.scene.structureMetrics,
1970
+ paletteUploadBytes,
1971
+ },
1972
+ }),
1607
1973
  ...(standardPbrArtifact === undefined ? {} : { standardPbrArtifact }),
1974
+ ...(standardPbrSkinArtifact === undefined ? {} : { standardPbrSkinArtifact }),
1608
1975
  standardPbrPipelineState: pipelineState,
1976
+ ...(shadowCasterDrawKeys === undefined ? {} : { shadowCasterDrawKeys }),
1977
+ ...(shadowCasterMembership === undefined ? {} : { shadowCasterMembership }),
1609
1978
  });
1610
- if (prepared.ok) preparedGpuDriven = prepared.value;
1611
- 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.
1612
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
+ }
1613
2030
  }
1614
2031
 
2032
+ const gpuOwnedDrawKeys = preparedGpuDriven?.drawKeys ?? EMPTY_DRAW_KEYS;
1615
2033
  // The GPU lane owns only the primary opaque raster today. Directional,
1616
- // point, and spot shadow views still consume the CPU-validated draw plan.
1617
- const cpuShadowCastersActive =
1618
- (effectiveShadowMapSize ?? 0) > 0 ||
1619
- lights.pointShadow.length > 0 ||
1620
- lights.spot.some((spot) => spot.castShadow);
1621
- const gpuOwnsAllPrimaryRaster = preparedGpuDriven?.ownsAllRenderables === true;
1622
- gpuDriven?.onSceneDemand(gpuDriven.owner.requiresSceneRows());
1623
-
1624
- const cubeCaptureWork = prepareCubeCapture(internals, cubeCapture, camera);
1625
- const reflectionFallbackCandidate = cubeCapture?.reflectionProbes?.fallbackDemand === true;
1626
-
1627
- // Validate renderables before graph admission. Besides producing the draw
1628
- // plan, this is the owner of per-frame cache eviction; running it before a
1629
- // graph candidate can fail guarantees despawned instance buffers are
1630
- // released even when the frame cannot be submitted.
1631
- const gpuOwnedEntityKeys = preparedGpuDriven?.entityKeys ?? EMPTY_ENTITY_KEYS;
1632
- const gpuDrivenWorldKeys = gpuDriven?.scene?.worldKeys;
1633
- const validated = runRecordProfilePhase(profilePhase, 'record/validation', () =>
1634
- gpuOwnsAllPrimaryRaster && !cpuShadowCastersActive && cubeCaptureWork.length === 0
1635
- ? []
1636
- : validateRenderables(
1637
- internals,
1638
- world,
1639
- worlds,
1640
- pipelineState,
1641
- frameState,
1642
- renderables,
1643
- transparentDispatch,
1644
- cpuShadowCastersActive || cubeCaptureWork.length > 0
1645
- ? EMPTY_ENTITY_KEYS
1646
- : gpuOwnedEntityKeys,
1647
- gpuDrivenWorldKeys,
1648
- ),
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,
1649
2049
  );
1650
- gpuDriven?.owner.recordCpuValidation(validated, gpuOwnedEntityKeys, gpuDrivenWorldKeys);
1651
- 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.
1652
2064
 
1653
2065
  // S_fallback is the second output of the built-in Standard PBR BRDF. A
1654
2066
  // graph may still carry the detached owner tickets for inspection, but
1655
2067
  // the MRT is admitted only when every CPU-rendered primary row in this
1656
2068
  // frame uses that exact output contract. Unsupported compositions remain
1657
2069
  // fail-closed for SSR instead of submitting a device-invalid command.
1658
- frameState.reflectionFallbackDemand =
1659
- reflectionFallbackCandidate &&
2070
+ const sceneInputs =
1660
2071
  validated.length > 0 &&
1661
2072
  validated.every(
1662
2073
  (entry) => entry.source.material.materialShaderId === 'forgeax::default-standard-pbr',
1663
2074
  );
1664
-
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;
1665
2099
  const preflightGraph = runRecordProfilePhase(profilePhase, 'record/render-graph', () =>
1666
2100
  ensureCompiledFrameGraph(
1667
2101
  internals,
@@ -1677,13 +2111,18 @@ export function recordFrame(
1677
2111
  cubeCapture?.state,
1678
2112
  cameras.length === 0,
1679
2113
  transmissionDemand,
1680
- gpuDriven?.occlusion,
2114
+ frameGpuDriven?.occlusion,
1681
2115
  graphVolumetricFog,
1682
2116
  volumeTopologyCandidate,
1683
2117
  standard,
2118
+ undefined,
2119
+ ssrAdmission,
1684
2120
  ),
1685
2121
  );
1686
- if (preflightGraph === null) return false;
2122
+ if (preflightGraph === null) {
2123
+ abortPreparedGpuDriven();
2124
+ return false;
2125
+ }
1687
2126
 
1688
2127
  // The compiled graph is the acceptance boundary for the prepared
1689
2128
  // Standard lighting declaration. Upload the same declaration only after
@@ -1704,6 +2143,7 @@ export function recordFrame(
1704
2143
  },
1705
2144
  }),
1706
2145
  );
2146
+ abortPreparedGpuDriven();
1707
2147
  return false;
1708
2148
  }
1709
2149
  if (standard.kind === 'clustered') {
@@ -1732,6 +2172,7 @@ export function recordFrame(
1732
2172
  );
1733
2173
  if (!clusterWrite.ok) {
1734
2174
  internals.errorRegistry.fire(clusterWrite.error);
2175
+ abortPreparedGpuDriven();
1735
2176
  return false;
1736
2177
  }
1737
2178
  }
@@ -1744,7 +2185,10 @@ export function recordFrame(
1744
2185
  const swapTarget = runRecordProfilePhase(profilePhase, 'record/swapchain', () =>
1745
2186
  acquireSwapChainTarget(internals, pipelineState),
1746
2187
  );
1747
- if (swapTarget === null) return false;
2188
+ if (swapTarget === null) {
2189
+ abortPreparedGpuDriven();
2190
+ return false;
2191
+ }
1748
2192
  const currentTexture = swapTarget.currentTexture;
1749
2193
  const view = swapTarget.view;
1750
2194
  const targetW = swapTarget.targetW;
@@ -1802,6 +2246,23 @@ export function recordFrame(
1802
2246
  : { kind: 'off', view: temporalView };
1803
2247
  const recordCamera: CameraSnapshot =
1804
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
+ }
1805
2266
  let activeVolumetricFogContext: VolumetricFogFrameContext | undefined;
1806
2267
  let activeVolumetricFogSignature: VolumeTemporalSignature | undefined;
1807
2268
  if (graphVolumetricFog?.status === 'available' && baseVolumetricFogContext !== undefined) {
@@ -1828,6 +2289,7 @@ export function recordFrame(
1828
2289
  );
1829
2290
  if (!historyFlagWritten.ok) {
1830
2291
  internals.errorRegistry.fire(historyFlagWritten.error);
2292
+ abortPreparedGpuDriven();
1831
2293
  return false;
1832
2294
  }
1833
2295
  }
@@ -1903,9 +2365,18 @@ export function recordFrame(
1903
2365
  // order, run the mesh-SSBO capacity gate (graceful truncation), and build
1904
2366
  // the fold dispatch plan. Extracted to buildDispatchPlan (M3/w18).
1905
2367
  const dispatchPlan = runRecordProfilePhase(profilePhase, 'record/dispatch-plan', () =>
1906
- buildDispatchPlan(internals, validated, transparentDispatch, foldBuckets),
2368
+ buildDispatchPlan(
2369
+ internals,
2370
+ validated,
2371
+ transparentDispatch,
2372
+ foldBuckets,
2373
+ validatedShadowForResidency ?? [],
2374
+ ),
1907
2375
  );
1908
2376
  const validatedOrdered = dispatchPlan.validatedOrdered;
2377
+ const shadowValidatedOrdered = dispatchPlan.shadowValidatedOrdered;
2378
+ const shadowMeshSsboBase = validatedOrdered.length;
2379
+ const shadowDispatch = shadowCasterProjection?.dispatch;
1909
2380
  const foldDispatchPlan = dispatchPlan.foldDispatchPlan;
1910
2381
  const materialSlotIndices = dispatchPlan.materialSlotIndices;
1911
2382
  const materialSlots = dispatchPlan.materialSlots;
@@ -1937,12 +2408,13 @@ export function recordFrame(
1937
2408
  const unormViewRes = internals.device.createTextureView(currentTexture, {});
1938
2409
  if (!unormViewRes.ok) {
1939
2410
  internals.errorRegistry.fire(unormViewRes.error);
2411
+ abortPreparedGpuDriven();
1940
2412
  return false;
1941
2413
  }
1942
2414
  ldrSpritePassView = unormViewRes.value;
1943
2415
  }
1944
2416
 
1945
- // 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,
1946
2418
  // OR when a skybox is active (skybox pass reads inverseViewProj from
1947
2419
  // the View UBO). Skip the writeBuffer round-trips on the Case E
1948
2420
  // (clear-pass-only) path only when neither condition is met.
@@ -1957,7 +2429,10 @@ export function recordFrame(
1957
2429
  );
1958
2430
  if (
1959
2431
  validatedOrdered.length > 0 ||
2432
+ shadowValidatedOrdered.length > 0 ||
2433
+ getRenderFeatureGraphState(internals).plans.some(({ plan }) => plan.passes.length > 0) ||
1960
2434
  skyboxActive ||
2435
+ frameState.environmentFrame?.source.kind === 'atmosphere' ||
1961
2436
  activeVolumetricFogContext !== undefined ||
1962
2437
  preparedVolumetricFog !== undefined
1963
2438
  ) {
@@ -2000,6 +2475,15 @@ export function recordFrame(
2000
2475
  validatedOrdered,
2001
2476
  foldDispatchPlan,
2002
2477
  );
2478
+ if (shadowValidatedOrdered.length > 0) {
2479
+ uploadMeshSsboBatch(
2480
+ internals.device.queue,
2481
+ pipelineState.meshStorageBuffer,
2482
+ shadowValidatedOrdered,
2483
+ null,
2484
+ shadowMeshSsboBase,
2485
+ );
2486
+ }
2003
2487
  });
2004
2488
  }
2005
2489
 
@@ -2022,6 +2506,7 @@ export function recordFrame(
2022
2506
  const encoderResult = internals.device.createCommandEncoder({ label: 'render-system-frame' });
2023
2507
  if (!encoderResult.ok) {
2024
2508
  internals.errorRegistry.fire(encoderResult.error);
2509
+ abortPreparedGpuDriven();
2025
2510
  return false;
2026
2511
  }
2027
2512
  const encoder: RhiCommandEncoder = encoderResult.value;
@@ -2100,8 +2585,6 @@ export function recordFrame(
2100
2585
  const passCtx: _InternalRenderPipelineContext = {
2101
2586
  assets: internals.assets,
2102
2587
  world,
2103
- gpuDrivenEntityKeys: gpuOwnedEntityKeys,
2104
- ...(gpuDrivenWorldKeys === undefined ? {} : { gpuDrivenWorldKeys }),
2105
2588
  store: internals.gpuStore,
2106
2589
  pipelineState,
2107
2590
  bloomResources: pipelineState.perPassResources.getBloomResources?.() ?? null,
@@ -2137,6 +2620,10 @@ export function recordFrame(
2137
2620
  ...(internals.volumetricFogShaders === undefined
2138
2621
  ? {}
2139
2622
  : { volumetricFogShaders: internals.volumetricFogShaders }),
2623
+ ...(internals.ssrShaders === undefined ? {} : { ssrShaders: internals.ssrShaders }),
2624
+ ...(internals.atmosphereShaders === undefined
2625
+ ? {}
2626
+ : { atmosphereShaders: internals.atmosphereShaders }),
2140
2627
  ...(activeVolumetricFogContext === undefined
2141
2628
  ? {}
2142
2629
  : { volumetricFog: activeVolumetricFogContext }),
@@ -2146,13 +2633,26 @@ export function recordFrame(
2146
2633
  dispatch: transparentDispatch,
2147
2634
  hdrpClusterBindGroup,
2148
2635
  hdrpClusterMembershipBindGroup,
2149
- ...(gpuDriven?.occlusion === undefined ? {} : { occlusion: gpuDriven.occlusion }),
2636
+ ...(frameGpuDriven?.occlusion === undefined ? {} : { occlusion: frameGpuDriven.occlusion }),
2150
2637
  standardLighting: standard,
2151
2638
  foldDispatchPlan,
2152
2639
  materialSlotIndices,
2153
2640
  materialSlots,
2154
2641
  materialSlotOwners,
2155
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 }),
2156
2656
  ...(cubeCapture?.reflectionProbes === undefined
2157
2657
  ? {}
2158
2658
  : { reflectionProbes: cubeCapture.reflectionProbes }),
@@ -2160,6 +2660,12 @@ export function recordFrame(
2160
2660
  materialBgAssemblyCache: frameState.materialBgAssemblyCache,
2161
2661
  ...(onRenderableDraw === undefined ? {} : { onRenderableDraw }),
2162
2662
  directionalShadowCacheReuse: directionalShadowCache.reuse,
2663
+ ...(shadowCasterMembership === undefined ? {} : { shadowCasterMembership }),
2664
+ ...(shadowValidatedOrdered.length === 0
2665
+ ? {}
2666
+ : { shadowValidatedOrdered, shadowMeshSsboBase }),
2667
+ ...(shadowDispatch === undefined ? {} : { shadowDispatch }),
2668
+ ...(shadowCasterWorldKeys === undefined ? {} : { shadowCasterWorldKeys }),
2163
2669
  ...(profilePhase !== undefined ? { profilePhase } : {}),
2164
2670
  };
2165
2671
  uploadMaterialUniforms(passCtx);
@@ -2190,6 +2696,37 @@ export function recordFrame(
2190
2696
  frameState.temporalFrameInput = stagedTemporalFrame;
2191
2697
  frameState.temporalFrameTransaction.stage(stagedTemporalFrame);
2192
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
+ };
2193
2730
  submitted = runRecordProfilePhase(profilePhase, 'record/graph-execute', () =>
2194
2731
  executeCompiledFrameGraph(
2195
2732
  internals,
@@ -2199,17 +2736,7 @@ export function recordFrame(
2199
2736
  profilePhase === undefined
2200
2737
  ? undefined
2201
2738
  : (pass, encode) => profilePhase(graphExecutionPhase(pass.name), encode),
2202
- gpuDriven === undefined
2203
- ? undefined
2204
- : {
2205
- afterGraphExecute: () => gpuDriven.occlusion?.recordResolve(encoder) ?? ok(undefined),
2206
- onSubmitted: () => {
2207
- gpuDriven.occlusion?.commit(true, frameState.frameNumber, profilePhase);
2208
- },
2209
- onAborted: () => {
2210
- gpuDriven.occlusion?.commit(false, frameState.frameNumber);
2211
- },
2212
- },
2739
+ frameHooks,
2213
2740
  cubeCaptureWork.length === 0 ? undefined : cubeCaptureWork.map((work) => work.faceIndex),
2214
2741
  (done) => {
2215
2742
  completion = done;
@@ -2231,6 +2758,20 @@ export function recordFrame(
2231
2758
  timingCapture,
2232
2759
  ),
2233
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
+ }
2234
2775
  if (cubeCapture !== undefined && cubeCaptureWork.length > 0) {
2235
2776
  const completed = cubeCapture.scheduler.completeSubmission(submitted, completion);
2236
2777
  if (!completed.ok) internals.errorRegistry.fire(completed.error);
@@ -2312,6 +2853,10 @@ export function recordFrame(
2312
2853
  }
2313
2854
  return submitted;
2314
2855
  } finally {
2856
+ if (!submitted && frameState.ssrHistoryCandidate !== undefined) {
2857
+ frameState.ssrHistoryOwner?.abortFrame(frameState.ssrHistoryCandidate, 'submit');
2858
+ frameState.ssrHistoryCandidate = undefined;
2859
+ }
2315
2860
  // Volume topology replacements are provisional until the queue submission
2316
2861
  // succeeds. This runs for every early-return path as well, so a swapchain,
2317
2862
  // encoder, or graph failure cannot leave a non-submitted candidate as the
@@ -2422,6 +2967,15 @@ function prepareDirectionalShadowCache(
2422
2967
  ): DirectionalShadowCacheDecision {
2423
2968
  const cascadeCount = lights.cascadeCount;
2424
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
+ }
2425
2979
  if (
2426
2980
  frameState.compiledFrameGraph === null ||
2427
2981
  frameState.compiledFrameGraphTopologyKey === null ||
@@ -2662,6 +3216,9 @@ export function validateRenderables(
2662
3216
  return validated;
2663
3217
  }
2664
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
+
2665
3222
  function prepareMorphMesh(
2666
3223
  internals: RenderSystemInternals,
2667
3224
  frameState: RenderFrameState,
@@ -2673,9 +3230,23 @@ function prepareMorphMesh(
2673
3230
  if (morph === undefined || frameState.morphBuffers === undefined) return undefined;
2674
3231
  const targets = mesh.morphTargets;
2675
3232
  if (targets === undefined || targets.length !== morph.targetCount) return undefined;
2676
- if (base.layoutProjection.arrayStride !== 48 || base.vertexCount <= 0) return undefined;
2677
- const stride = mesh.vertices.length / base.vertexCount;
2678
- if (!Number.isInteger(stride) || stride < 3) return undefined;
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;
2679
3250
 
2680
3251
  let active = false;
2681
3252
  for (const weight of morph.weights) {
@@ -2753,17 +3324,19 @@ function prepareMorphMesh(
2753
3324
  *
2754
3325
  * @internal
2755
3326
  */
2756
- function buildDispatchPlan(
3327
+ export function buildDispatchPlan(
2757
3328
  internals: RenderSystemInternals,
2758
3329
  validated: readonly ValidatedRenderable[],
2759
3330
  transparentDispatch: readonly DispatchEntry[],
2760
3331
  foldBuckets: readonly FoldBucket[],
3332
+ shadowValidated: readonly ValidatedRenderable[] = [],
2761
3333
  ): {
2762
3334
  validatedOrdered: readonly ValidatedRenderable[];
3335
+ shadowValidatedOrdered: readonly ValidatedRenderable[];
2763
3336
  foldDispatchPlan: FoldDispatchPlan | null;
2764
3337
  materialSlotIndices: readonly (readonly number[])[];
2765
3338
  materialSlots: readonly MaterialSnapshot[];
2766
- materialSlotOwners: readonly number[];
3339
+ materialSlotOwners: readonly ValidatedRenderable[];
2767
3340
  materialSlotCount: number;
2768
3341
  } {
2769
3342
  // A production GPU-owned frame has no CPU validated rows by design. The
@@ -2771,9 +3344,16 @@ function buildDispatchPlan(
2771
3344
  // CPU plan, material table, or fold bucket is consumed in that lane. Return
2772
3345
  // the empty plan without walking that list so a steady 100k scene remains
2773
3346
  // bounded by the persistent GPU batches rather than per-entity record work.
2774
- 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
+ );
2775
3354
  return {
2776
3355
  validatedOrdered: [],
3356
+ shadowValidatedOrdered: limitRowsToMeshSsboCapacity(shadowValidated, meshSsboSlotCapacity),
2777
3357
  foldDispatchPlan: null,
2778
3358
  materialSlotIndices: [],
2779
3359
  materialSlots: [],
@@ -2821,7 +3401,10 @@ function buildDispatchPlan(
2821
3401
  validatedOrdered = ordered;
2822
3402
  }
2823
3403
 
2824
- 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[] => {
2825
3408
  const shaderId = entry.source.material.materialShaderId;
2826
3409
  if (shaderId === 'forgeax::sprite' || shaderId === 'forgeax::sprite-lit') {
2827
3410
  return [entry.source.material];
@@ -2831,8 +3414,13 @@ function buildDispatchPlan(
2831
3414
  const materialSlotPlan = buildMaterialSlotPlan(materialGroups);
2832
3415
  let materialSlotIndices = materialSlotPlan.slotIndices;
2833
3416
  let materialSlots = materialSlotPlan.slots;
2834
- 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
+ });
2835
3422
  let materialSlotCount = materialSlots.length;
3423
+ let materialRenderableCount = materialRows.length;
2836
3424
 
2837
3425
  // feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M3 / T-M3-04:
2838
3426
  // ensure the mesh-SSBO + material-UBO buffer pair is large enough to hold
@@ -2857,8 +3445,13 @@ function buildDispatchPlan(
2857
3445
  //
2858
3446
  // feat-20260624 M1' / t7: sprite-lit treated identically to sprite
2859
3447
  // for material-slot accounting (paramSchema mirror, t4).
2860
- const neededSlots = Math.max(validatedOrdered.length, materialSlotCount);
3448
+ const neededSlots = Math.max(validatedOrdered.length + shadowValidated.length, materialSlotCount);
2861
3449
  const meshSsboCapResult = ensureMeshSsboCapacity(internals, neededSlots);
3450
+ const meshSsboSlotCapacity = meshSsboSlotCapacityAfterEnsure(
3451
+ internals,
3452
+ neededSlots,
3453
+ meshSsboCapResult,
3454
+ );
2862
3455
  if (!meshSsboCapResult.ok) {
2863
3456
  // Graceful degradation: the controller reports slots, but this stage
2864
3457
  // consumes entities. Find a complete prefix instead of slicing at the
@@ -2867,12 +3460,17 @@ function buildDispatchPlan(
2867
3460
  materialSlotIndices,
2868
3461
  meshSsboCapResult.degradedToSlotCount,
2869
3462
  );
3463
+ materialRenderableCount = degradedRenderableCount;
2870
3464
  materialSlotCount = materialSlotCountForPrefix(materialSlotIndices, degradedRenderableCount);
2871
3465
  materialSlotIndices = materialSlotIndices.slice(0, degradedRenderableCount);
2872
3466
  materialSlots = materialSlots.slice(0, materialSlotCount);
2873
3467
  materialSlotOwners = materialSlotOwners.slice(0, materialSlotCount);
2874
3468
  validatedOrdered = validatedOrdered.slice(0, degradedRenderableCount);
2875
3469
  }
3470
+ const shadowValidatedOrdered = limitRowsToMeshSsboCapacity(
3471
+ shadowValidated,
3472
+ Math.min(meshSsboSlotCapacity, materialRenderableCount) - validatedOrdered.length,
3473
+ );
2876
3474
 
2877
3475
  // feat-20260622-chunk-gpu-instancing-sprite-tilemap M1 / w4-record-swap
2878
3476
  // (D-1): build the fold dispatch plan once `validatedOrdered` is final
@@ -2965,6 +3563,7 @@ function buildDispatchPlan(
2965
3563
 
2966
3564
  return {
2967
3565
  validatedOrdered,
3566
+ shadowValidatedOrdered,
2968
3567
  foldDispatchPlan,
2969
3568
  materialSlotIndices,
2970
3569
  materialSlots,
@@ -3042,13 +3641,6 @@ function cleanPerFrameCaches(
3042
3641
  }
3043
3642
  }
3044
3643
 
3045
- if (frameState.instanceResidency !== undefined) {
3046
- const liveCollections = new Set(validated.map((entry) => entry.source.instances?.collectionId));
3047
- for (const id of frameState.instanceResidency.keys()) {
3048
- if (!liveCollections.has(id)) frameState.instanceResidency.delete(id);
3049
- }
3050
- }
3051
-
3052
3644
  // Chunk keys are `${worldEntityKey}:${chunkStart}`. Keep the key opaque to
3053
3645
  // the authoring API, but retain the numeric owner prefix for eviction.
3054
3646
  if (frameState.instanceBufferChunks !== undefined) {