@forgeax/engine-render 0.1.20 → 0.1.21

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 (372) hide show
  1. package/README.md +215 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/assembly/device-feature-admission.d.ts +18 -0
  4. package/dist/assembly/device-feature-admission.d.ts.map +1 -0
  5. package/dist/assembly/directional-shadow-inspection.d.ts +38 -0
  6. package/dist/assembly/directional-shadow-inspection.d.ts.map +1 -0
  7. package/dist/assembly/factory.d.ts +21 -18
  8. package/dist/assembly/factory.d.ts.map +1 -1
  9. package/dist/assembly/host-contract.d.ts +9 -0
  10. package/dist/assembly/host-contract.d.ts.map +1 -1
  11. package/dist/assembly/render-target-host.d.ts +46 -0
  12. package/dist/assembly/render-target-host.d.ts.map +1 -0
  13. package/dist/assets/render-asset-port.d.ts.map +1 -1
  14. package/dist/authoring.mjs +3 -0
  15. package/dist/authoring.mjs.map +1 -1
  16. package/dist/capture/cube-views.d.ts +21 -0
  17. package/dist/capture/cube-views.d.ts.map +1 -0
  18. package/dist/capture/scheduler.d.ts +54 -0
  19. package/dist/capture/scheduler.d.ts.map +1 -0
  20. package/dist/{chunk-4XINDLAY.mjs → chunk-5YADUCA7.mjs} +1552 -1361
  21. package/dist/chunk-5YADUCA7.mjs.map +1 -0
  22. package/dist/{chunk-TBQJOK2L.mjs → chunk-6OPXDQ2E.mjs} +364 -51
  23. package/dist/chunk-6OPXDQ2E.mjs.map +1 -0
  24. package/dist/{chunk-M2MP4ZOU.mjs → chunk-BXBQQKFQ.mjs} +3 -9
  25. package/dist/chunk-BXBQQKFQ.mjs.map +1 -0
  26. package/dist/{chunk-CDTWTHZR.mjs → chunk-FS2KNUQH.mjs} +548 -6
  27. package/dist/chunk-FS2KNUQH.mjs.map +1 -0
  28. package/dist/components/camera.d.ts +1 -0
  29. package/dist/components/camera.d.ts.map +1 -1
  30. package/dist/components/cube-camera.d.ts +20 -0
  31. package/dist/components/cube-camera.d.ts.map +1 -0
  32. package/dist/components/directional-light.d.ts +11 -2
  33. package/dist/components/directional-light.d.ts.map +1 -1
  34. package/dist/components/directional-shadow-filter.d.ts +25 -0
  35. package/dist/components/directional-shadow-filter.d.ts.map +1 -0
  36. package/dist/components/index.d.ts +2 -0
  37. package/dist/components/index.d.ts.map +1 -1
  38. package/dist/components/light-helpers.d.ts.map +1 -1
  39. package/dist/components/reflection-probe.d.ts +17 -0
  40. package/dist/components/reflection-probe.d.ts.map +1 -0
  41. package/dist/construct-renderer.mjs +5826 -1003
  42. package/dist/construct-renderer.mjs.map +1 -1
  43. package/dist/environment/frame.d.ts.map +1 -1
  44. package/dist/environment/lifecycle.d.ts +5 -0
  45. package/dist/environment/lifecycle.d.ts.map +1 -1
  46. package/dist/errors/render.d.ts +141 -93
  47. package/dist/errors/render.d.ts.map +1 -1
  48. package/dist/extract/directional-shadow-projection.d.ts +23 -0
  49. package/dist/extract/directional-shadow-projection.d.ts.map +1 -0
  50. package/dist/extract/environment.d.ts +14 -0
  51. package/dist/extract/environment.d.ts.map +1 -1
  52. package/dist/extract/visibility.d.ts.map +1 -1
  53. package/dist/features/fullscreen.d.ts.map +1 -1
  54. package/dist/features/targets.d.ts.map +1 -1
  55. package/dist/features/types.d.ts +10 -0
  56. package/dist/features/types.d.ts.map +1 -1
  57. package/dist/fullscreen-post-process-pass.d.ts +13 -2
  58. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  59. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  60. package/dist/gpu-driven/production-raster.d.ts +0 -1
  61. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  62. package/dist/gpu-scene.d.ts.map +1 -1
  63. package/dist/hdrp-buffers.d.ts +19 -17
  64. package/dist/hdrp-buffers.d.ts.map +1 -1
  65. package/dist/ibl/IblPipelineCache.d.ts +4 -0
  66. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  67. package/dist/ibl/kernel-cache.d.ts +36 -0
  68. package/dist/ibl/kernel-cache.d.ts.map +1 -0
  69. package/dist/index.d.ts +28 -3
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.mjs +4 -4
  72. package/dist/index.mjs.map +1 -1
  73. package/dist/inspection-types.d.ts +51 -0
  74. package/dist/inspection-types.d.ts.map +1 -1
  75. package/dist/instances-derived-bounds.d.ts +50 -0
  76. package/dist/instances-derived-bounds.d.ts.map +1 -0
  77. package/dist/internal.mjs +1 -1
  78. package/dist/light-buffer-layout.d.ts +10 -1
  79. package/dist/light-buffer-layout.d.ts.map +1 -1
  80. package/dist/mesh-material-bindings.d.ts +54 -0
  81. package/dist/mesh-material-bindings.d.ts.map +1 -1
  82. package/dist/pbr-pipeline.d.ts +14 -4
  83. package/dist/pbr-pipeline.d.ts.map +1 -1
  84. package/dist/pipeline/standard-forward-lane.d.ts +2 -1
  85. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  86. package/dist/pipeline/standard-lighting/errors.d.ts +4 -0
  87. package/dist/pipeline/standard-lighting/errors.d.ts.map +1 -0
  88. package/dist/pipeline/standard-lighting/graph.d.ts +24 -0
  89. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -0
  90. package/dist/pipeline/standard-lighting/inspection.d.ts +38 -0
  91. package/dist/pipeline/standard-lighting/inspection.d.ts.map +1 -0
  92. package/dist/pipeline/standard-lighting/layout.d.ts +28 -0
  93. package/dist/pipeline/standard-lighting/layout.d.ts.map +1 -0
  94. package/dist/pipeline/standard-lighting/light-frame.d.ts +33 -0
  95. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -0
  96. package/dist/pipeline/standard-lighting/membership.d.ts +14 -0
  97. package/dist/pipeline/standard-lighting/membership.d.ts.map +1 -0
  98. package/dist/pipeline/standard-lighting/prepare.d.ts +16 -0
  99. package/dist/pipeline/standard-lighting/prepare.d.ts.map +1 -0
  100. package/dist/pipeline/standard-lighting/topology.d.ts +45 -0
  101. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -0
  102. package/dist/pipeline/standard-lighting/transport.d.ts +36 -0
  103. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -0
  104. package/dist/pipeline/standard-pipeline.d.ts +4 -13
  105. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  106. package/dist/pipeline/standard-post.d.ts.map +1 -1
  107. package/dist/pipeline/standard-profile.d.ts +5 -26
  108. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  109. package/dist/pipeline-spec.d.ts +40 -7
  110. package/dist/pipeline-spec.d.ts.map +1 -1
  111. package/dist/point-shadow-inspection.d.ts +21 -0
  112. package/dist/point-shadow-inspection.d.ts.map +1 -0
  113. package/dist/record/frame-lighting.d.ts +27 -32
  114. package/dist/record/frame-lighting.d.ts.map +1 -1
  115. package/dist/record/frame-snapshot.d.ts +28 -14
  116. package/dist/record/frame-snapshot.d.ts.map +1 -1
  117. package/dist/record/frame-targets.d.ts +5 -0
  118. package/dist/record/frame-targets.d.ts.map +1 -1
  119. package/dist/record/frame.d.ts +10 -4
  120. package/dist/record/frame.d.ts.map +1 -1
  121. package/dist/record/gpu-pass-timing/contract.d.ts +96 -0
  122. package/dist/record/gpu-pass-timing/contract.d.ts.map +1 -0
  123. package/dist/record/gpu-pass-timing/errors.d.ts +18 -0
  124. package/dist/record/gpu-pass-timing/errors.d.ts.map +1 -0
  125. package/dist/record/gpu-pass-timing/index.d.ts +8 -0
  126. package/dist/record/gpu-pass-timing/index.d.ts.map +1 -0
  127. package/dist/record/gpu-pass-timing/parser.d.ts +11 -0
  128. package/dist/record/gpu-pass-timing/parser.d.ts.map +1 -0
  129. package/dist/record/gpu-pass-timing/session.d.ts +43 -0
  130. package/dist/record/gpu-pass-timing/session.d.ts.map +1 -0
  131. package/dist/record/gpu-timing.d.ts +8 -3
  132. package/dist/record/gpu-timing.d.ts.map +1 -1
  133. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  134. package/dist/record/main-pass-material.d.ts +22 -1
  135. package/dist/record/main-pass-material.d.ts.map +1 -1
  136. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  137. package/dist/record/main-pass.d.ts +2 -0
  138. package/dist/record/main-pass.d.ts.map +1 -1
  139. package/dist/record/render-context.d.ts +43 -2
  140. package/dist/record/render-context.d.ts.map +1 -1
  141. package/dist/record/shadow-pass.d.ts.map +1 -1
  142. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  143. package/dist/record/typed-frame-graph.d.ts +72 -4
  144. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  145. package/dist/record/view-ubo.d.ts +7 -4
  146. package/dist/record/view-ubo.d.ts.map +1 -1
  147. package/dist/reflection/filter.d.ts +22 -0
  148. package/dist/reflection/filter.d.ts.map +1 -0
  149. package/dist/reflection/gpu-table.d.ts +25 -0
  150. package/dist/reflection/gpu-table.d.ts.map +1 -0
  151. package/dist/reflection/projection.d.ts +91 -0
  152. package/dist/reflection/projection.d.ts.map +1 -0
  153. package/dist/render-contract.d.ts +49 -7
  154. package/dist/render-contract.d.ts.map +1 -1
  155. package/dist/render-graph-primitives.d.ts +1 -0
  156. package/dist/render-graph-primitives.d.ts.map +1 -1
  157. package/dist/render-pipeline.d.ts +42 -2
  158. package/dist/render-pipeline.d.ts.map +1 -1
  159. package/dist/render-system-extract.d.ts +58 -17
  160. package/dist/render-system-extract.d.ts.map +1 -1
  161. package/dist/render-system.d.ts +17 -2
  162. package/dist/render-system.d.ts.map +1 -1
  163. package/dist/renderer.d.ts +43 -0
  164. package/dist/renderer.d.ts.map +1 -0
  165. package/dist/scene/render-scene.d.ts +14 -0
  166. package/dist/scene/render-scene.d.ts.map +1 -1
  167. package/dist/shadow-atlas.d.ts +3 -3
  168. package/dist/shadow-atlas.d.ts.map +1 -1
  169. package/dist/ssao-buffers.d.ts +3 -0
  170. package/dist/ssao-buffers.d.ts.map +1 -1
  171. package/dist/targets/contracts.d.ts +66 -0
  172. package/dist/targets/contracts.d.ts.map +1 -0
  173. package/dist/targets/graph-projection.d.ts +29 -0
  174. package/dist/targets/graph-projection.d.ts.map +1 -0
  175. package/dist/targets/material-source.d.ts +34 -0
  176. package/dist/targets/material-source.d.ts.map +1 -0
  177. package/dist/targets/owner.d.ts +32 -0
  178. package/dist/targets/owner.d.ts.map +1 -0
  179. package/dist/targets/physical.d.ts +18 -0
  180. package/dist/targets/physical.d.ts.map +1 -0
  181. package/dist/targets/readback.d.ts +39 -0
  182. package/dist/targets/readback.d.ts.map +1 -0
  183. package/dist/temporal/index.mjs +3 -3
  184. package/dist/typed-render-graph-primitives.d.ts +1 -1
  185. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  186. package/dist/typed-shadow-passes.d.ts +1 -1
  187. package/dist/typed-shadow-passes.d.ts.map +1 -1
  188. package/dist/volume/passes.d.ts +2 -2
  189. package/dist/volume/passes.d.ts.map +1 -1
  190. package/package.json +20 -20
  191. package/src/__tests__/bloom-exact-zero.unit.test.ts +48 -19
  192. package/src/__tests__/bloom-lifecycle.integration.test.ts +41 -13
  193. package/src/__tests__/camera-target-consumption.unit.test.ts +53 -0
  194. package/src/__tests__/cluster-binner.unit.test.ts +35 -0
  195. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +7 -2
  196. package/src/__tests__/cube-camera-scheduler.unit.test.ts +70 -0
  197. package/src/__tests__/cube-camera-single-submit.perf.test.ts +75 -0
  198. package/src/__tests__/cube-camera-view-matrices.unit.test.ts +41 -0
  199. package/src/__tests__/cube-camera-vocabulary.unit.test.ts +47 -0
  200. package/src/__tests__/cube-camera.integration.test.ts +42 -0
  201. package/src/__tests__/device-feature-admission.unit.test.ts +73 -0
  202. package/src/__tests__/direct-light-snapshot.unit.test.ts +3 -3
  203. package/src/__tests__/docs-gate-green.unit.test.ts +38 -0
  204. package/src/__tests__/factory-contract.integration.test.ts +255 -29
  205. package/src/__tests__/fog-component.unit.test.ts +67 -0
  206. package/src/__tests__/fog.integration.test.ts +86 -0
  207. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +37 -0
  208. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +10 -9
  209. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +17 -1
  210. package/src/__tests__/gpu-driven-production.integration.test.ts +16 -17
  211. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +12 -4
  212. package/src/__tests__/gpu-pass-timing-browser-runner.ts +281 -0
  213. package/src/__tests__/gpu-pass-timing-coexistence.integration.test.ts +80 -0
  214. package/src/__tests__/gpu-pass-timing-contract.unit.test.ts +243 -0
  215. package/src/__tests__/gpu-pass-timing-exact-zero.unit.test.ts +131 -0
  216. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +230 -0
  217. package/src/__tests__/gpu-pass-timing-observation.unit.test.ts +233 -0
  218. package/src/__tests__/gpu-pass-timing-session.unit.test.ts +438 -0
  219. package/src/__tests__/gpu-pass-timing.browser.test.ts +26 -0
  220. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +125 -0
  221. package/src/__tests__/hdrp-group2-visibility.unit.test.ts +24 -0
  222. package/src/__tests__/instances-culling.integration.test.ts +94 -0
  223. package/src/__tests__/instances-derived-bounds.unit.test.ts +118 -0
  224. package/src/__tests__/material-observation.integration.test.ts +101 -0
  225. package/src/__tests__/pbr-view-layout.unit.test.ts +16 -0
  226. package/src/__tests__/point-shadow-inspection.unit.test.ts +60 -0
  227. package/src/__tests__/public-surface.test-d.ts +10 -0
  228. package/src/__tests__/reflection-probe-authoring.unit.test.ts +71 -0
  229. package/src/__tests__/reflection-probe-filter.unit.test.ts +54 -0
  230. package/src/__tests__/reflection-probe-ibl-topology.unit.test.ts +45 -0
  231. package/src/__tests__/reflection-probe-pass-encode.integration.test.ts +101 -0
  232. package/src/__tests__/reflection-probe-performance.perf.test.ts +60 -0
  233. package/src/__tests__/reflection-probe-recovery.integration.test.ts +55 -0
  234. package/src/__tests__/reflection-probe-selection.unit.test.ts +46 -0
  235. package/src/__tests__/render-error-code-owner.test-d.ts +18 -3
  236. package/src/__tests__/render-feature-docs.unit.test.ts +1 -1
  237. package/src/__tests__/render-target-admission.unit.test.ts +80 -0
  238. package/src/__tests__/render-target-graph-projection.unit.test.ts +102 -0
  239. package/src/__tests__/render-target-inspection.unit.test.ts +52 -0
  240. package/src/__tests__/render-target-material-source.test-d.ts +30 -0
  241. package/src/__tests__/render-target-material-source.unit.test.ts +97 -0
  242. package/src/__tests__/render-target-owner-placement.unit.test.ts +203 -0
  243. package/src/__tests__/render-target-owner.unit.test.ts +122 -0
  244. package/src/__tests__/render-target-probe-soak.perf.test.ts +94 -0
  245. package/src/__tests__/render-target-public-schema.json +107 -0
  246. package/src/__tests__/render-target-public-schema.unit.test.ts +91 -0
  247. package/src/__tests__/render-target-public-surface.test-d.ts +83 -0
  248. package/src/__tests__/render-target-readback.integration.test.ts +90 -0
  249. package/src/__tests__/render-target-recovery.integration.test.ts +85 -0
  250. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +48 -0
  251. package/src/__tests__/renderer-lifecycle.integration.test.ts +14 -0
  252. package/src/__tests__/shader-stage-surface.unit.test.ts +19 -4
  253. package/src/__tests__/skin-motion-regression.unit.test.ts +132 -0
  254. package/src/__tests__/skinned-shadow-caster.test.ts +11 -2
  255. package/src/__tests__/standard-cluster-capability.integration.test.ts +63 -0
  256. package/src/__tests__/standard-cluster-transport.unit.test.ts +121 -0
  257. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +232 -0
  258. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +8 -5
  259. package/src/__tests__/standard-lighting-inspection.integration.test.ts +138 -0
  260. package/src/__tests__/standard-lighting-ssot.unit.test.ts +18 -0
  261. package/src/__tests__/standard-lighting-write.unit.test.ts +377 -0
  262. package/src/__tests__/standard-lighting.integration.test.ts +67 -0
  263. package/src/__tests__/standard-pipeline.dawn.test.ts +206 -5
  264. package/src/__tests__/standard-pipeline.integration.test.ts +181 -150
  265. package/src/__tests__/standard-probe-binding.unit.test.ts +25 -0
  266. package/src/__tests__/standard-profile-oracle.unit.test.ts +23 -38
  267. package/src/__tests__/typed-pipeline-topology.unit.test.ts +395 -14
  268. package/src/__tests__/urp-pipeline-targets.unit.test.ts +2 -2
  269. package/src/__tests__/visibility-resolver.unit.test.ts +15 -0
  270. package/src/assembly/device-feature-admission.ts +73 -0
  271. package/src/assembly/directional-shadow-inspection.ts +122 -0
  272. package/src/assembly/factory.ts +877 -286
  273. package/src/assembly/host-contract.ts +25 -0
  274. package/src/assembly/render-target-host.ts +847 -0
  275. package/src/assets/asset-decoders.ts +1 -1
  276. package/src/assets/render-asset-port.ts +7 -1
  277. package/src/capture/cube-views.ts +69 -0
  278. package/src/capture/scheduler.ts +265 -0
  279. package/src/components/__tests__/schema-contract.unit.test.ts +12 -0
  280. package/src/components/__tests__/schema-parity.unit.test.ts +9 -0
  281. package/src/components/camera.ts +4 -0
  282. package/src/components/cube-camera.ts +52 -0
  283. package/src/components/directional-light.ts +22 -2
  284. package/src/components/directional-shadow-filter.ts +57 -0
  285. package/src/components/index.ts +2 -0
  286. package/src/components/light-helpers.ts +81 -7
  287. package/src/components/reflection-probe.ts +49 -0
  288. package/src/environment/frame.ts +6 -24
  289. package/src/environment/lifecycle.ts +12 -0
  290. package/src/errors/render.ts +286 -142
  291. package/src/extract/directional-shadow-projection.ts +61 -0
  292. package/src/extract/environment.ts +57 -0
  293. package/src/extract/visibility.ts +25 -1
  294. package/src/features/fullscreen.ts +3 -0
  295. package/src/features/targets.ts +4 -1
  296. package/src/features/types.ts +10 -0
  297. package/src/fullscreen-post-process-pass.ts +43 -2
  298. package/src/gpu-driven/batch-topology.ts +1 -0
  299. package/src/gpu-driven/production-raster.ts +1 -3
  300. package/src/gpu-scene.ts +8 -1
  301. package/src/hdrp-buffers.ts +165 -59
  302. package/src/ibl/IblPipelineCache.ts +11 -0
  303. package/src/ibl/kernel-cache.ts +88 -0
  304. package/src/index.ts +148 -0
  305. package/src/inspection-types.ts +56 -0
  306. package/src/instances-derived-bounds.ts +171 -0
  307. package/src/light-buffer-layout.ts +20 -5
  308. package/src/mesh-material-bindings.ts +132 -0
  309. package/src/pbr-pipeline.ts +106 -17
  310. package/src/pipeline/standard-forward-lane.ts +46 -16
  311. package/src/pipeline/standard-lighting/errors.ts +6 -0
  312. package/src/pipeline/standard-lighting/graph.ts +157 -0
  313. package/src/pipeline/standard-lighting/inspection.ts +88 -0
  314. package/src/pipeline/standard-lighting/layout.ts +31 -0
  315. package/src/pipeline/standard-lighting/light-frame.ts +34 -0
  316. package/src/pipeline/standard-lighting/membership.ts +43 -0
  317. package/src/pipeline/standard-lighting/prepare.ts +37 -0
  318. package/src/pipeline/standard-lighting/topology.ts +120 -0
  319. package/src/pipeline/standard-lighting/transport.ts +77 -0
  320. package/src/pipeline/standard-pipeline.ts +122 -193
  321. package/src/pipeline/standard-post.ts +26 -7
  322. package/src/pipeline/standard-profile.ts +17 -47
  323. package/src/pipeline-spec.ts +84 -21
  324. package/src/point-shadow-inspection.ts +55 -0
  325. package/src/record/__tests__/temporal-payload.unit.test.ts +42 -1
  326. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +73 -0
  327. package/src/record/__tests__/view-ubo-layout.unit.test.ts +40 -0
  328. package/src/record/frame-lighting.ts +367 -427
  329. package/src/record/frame-snapshot.ts +35 -27
  330. package/src/record/frame-targets.ts +17 -0
  331. package/src/record/frame.ts +261 -74
  332. package/src/record/gpu-pass-timing/contract.ts +180 -0
  333. package/src/record/gpu-pass-timing/errors.ts +34 -0
  334. package/src/record/gpu-pass-timing/index.ts +37 -0
  335. package/src/record/gpu-pass-timing/parser.ts +97 -0
  336. package/src/record/gpu-pass-timing/session.ts +938 -0
  337. package/src/record/gpu-timing.ts +21 -12
  338. package/src/record/main-pass-geometry.ts +120 -80
  339. package/src/record/main-pass-material.ts +95 -9
  340. package/src/record/main-pass-sprite-draws.ts +54 -25
  341. package/src/record/main-pass.ts +71 -15
  342. package/src/record/render-context.ts +72 -0
  343. package/src/record/shadow-pass.ts +17 -28
  344. package/src/record/skybox-post-pass.ts +20 -4
  345. package/src/record/typed-frame-graph.ts +893 -44
  346. package/src/record/view-ubo.ts +63 -28
  347. package/src/reflection/filter.ts +81 -0
  348. package/src/reflection/gpu-table.ts +44 -0
  349. package/src/reflection/projection.ts +281 -0
  350. package/src/render-contract.ts +76 -5
  351. package/src/render-graph-primitives.ts +9 -4
  352. package/src/render-pipeline.ts +117 -2
  353. package/src/render-system-extract.ts +557 -100
  354. package/src/render-system.ts +1095 -66
  355. package/src/renderer.ts +222 -0
  356. package/src/scene/render-scene.ts +115 -9
  357. package/src/shadow-atlas.ts +5 -5
  358. package/src/ssao-buffers.ts +9 -2
  359. package/src/targets/contracts.ts +182 -0
  360. package/src/targets/graph-projection.ts +169 -0
  361. package/src/targets/material-source.ts +143 -0
  362. package/src/targets/owner.ts +214 -0
  363. package/src/targets/physical.ts +18 -0
  364. package/src/targets/readback.ts +153 -0
  365. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +59 -20
  366. package/src/typed-render-graph-primitives.ts +5 -3
  367. package/src/typed-shadow-passes.ts +46 -40
  368. package/src/volume/passes.ts +15 -9
  369. package/dist/chunk-4XINDLAY.mjs.map +0 -1
  370. package/dist/chunk-CDTWTHZR.mjs.map +0 -1
  371. package/dist/chunk-M2MP4ZOU.mjs.map +0 -1
  372. package/dist/chunk-TBQJOK2L.mjs.map +0 -1
@@ -21,10 +21,21 @@ import type { World } from '@forgeax/engine-ecs';
21
21
  import type { RenderReadLease } from '@forgeax/engine-ecs/projection';
22
22
  import type { RecorderSession } from '@forgeax/engine-profiler';
23
23
  import type { CompiledRenderGraphInfo } from '@forgeax/engine-render-graph';
24
- import type { BindGroupEntry, BindGroupLayout, Buffer, RenderPipeline } from '@forgeax/engine-rhi';
24
+ import type {
25
+ BindGroup,
26
+ BindGroupEntry,
27
+ BindGroupLayout,
28
+ Buffer,
29
+ RenderPipeline,
30
+ Sampler,
31
+ Texture,
32
+ TextureFormat,
33
+ TextureView,
34
+ } from '@forgeax/engine-rhi';
25
35
  import { err, ok, type Result, RhiError } from '@forgeax/engine-rhi';
26
36
  import {
27
37
  derive,
38
+ deriveTextureLayout,
28
39
  type Handle,
29
40
  type MaterialAsset,
30
41
  type MaterialTextureValue,
@@ -32,8 +43,14 @@ import {
32
43
  type RenderPipelineAsset,
33
44
  RenderQueue,
34
45
  type SamplerAsset,
46
+ type TextureAsset,
35
47
  toShared,
36
48
  } from '@forgeax/engine-types';
49
+ import {
50
+ type DirectionalShadowInspectionSource,
51
+ directionalShadowProfileFromQuality,
52
+ projectDirectionalShadowInspectionSource,
53
+ } from './assembly/directional-shadow-inspection';
37
54
  import { createClusterBinScratch } from './cluster-binner';
38
55
  import { EnvironmentLifecycle } from './environment/lifecycle';
39
56
  import {
@@ -57,11 +74,17 @@ import {
57
74
  DEPTH_MIN_PARAMS_BYTE_SIZE,
58
75
  entryHasDepthRead,
59
76
  type PostProcessShaderEntry,
77
+ postProcessShaderEntrySignature,
78
+ postProcessShaderModuleLabel,
79
+ postProcessShaderPipelineLabel,
60
80
  } from './fullscreen-post-process-pass';
61
81
  import { GpuDrivenProduction } from './gpu-driven/production-raster';
62
82
  import type {
63
83
  BloomInspection,
84
+ DirectionalShadowInspection,
64
85
  MotionBlurInspection,
86
+ ReflectionProbeInspection,
87
+ ReflectionProbeSelectionInspection,
65
88
  RenderSceneInspection,
66
89
  TemporalTargetInspection,
67
90
  TransmissionInspection,
@@ -85,29 +108,54 @@ import { inspectVolumetricFog } from './volume/inspection';
85
108
  export type { RenderSceneInspection } from './inspection-types';
86
109
 
87
110
  import { deriveVertexLayoutProjection } from '@forgeax/engine-geometry';
111
+ import { mat4, vec3 } from '@forgeax/engine-math';
112
+ import { buildCubeCameraFaceViews } from './capture/cube-views';
113
+ import { createCubeCaptureScheduler } from './capture/scheduler';
114
+ import {
115
+ createFaceUniformsBuffer,
116
+ createPrefilterUniformsBuffer,
117
+ writeAllFaceUniforms,
118
+ writeAllPrefilterUniforms,
119
+ } from './device/gpu-residency';
88
120
  import type { EnvironmentInspection } from './environment/inspection';
121
+ import {
122
+ GPU_TEXTURE_USAGE_COPY_SRC,
123
+ GPU_TEXTURE_USAGE_RENDER_ATTACHMENT_AND_TEXTURE_BINDING,
124
+ } from './gpu-texture-usage';
89
125
  import {
90
126
  GPU_BUFFER_USAGE_COPY_DST,
91
127
  GPU_BUFFER_USAGE_INDEX,
92
128
  GPU_BUFFER_USAGE_UNIFORM,
93
129
  GPU_BUFFER_USAGE_VERTEX,
94
130
  } from './gpu-usage';
131
+ import { resetHdrpBuffers } from './hdrp-buffers';
132
+ import {
133
+ CUBEMAP_FACE_VERTICES,
134
+ createIblPipelines,
135
+ getOrCreateIblCache,
136
+ PREFILTER_MIP_LEVELS,
137
+ PREFILTER_SIZE,
138
+ } from './ibl/IblPipelineCache';
95
139
  import { assembleMaterialWithSkylightEntries } from './ibl/skylight-bind-group';
96
140
  import { disposeInstanceBuffers, disposeTransientInstanceBuffers } from './instance-buffer-cache';
97
- import type { MeshMaterialBindingObservation } from './mesh-material-bindings';
98
- import { validateClusterGrid } from './pipeline/standard-pipeline';
99
141
  import {
100
- DEFAULT_CLUSTER_GRID,
101
- DEFAULT_STANDARD_PROFILE,
102
- resolveStandardLane,
103
- } from './pipeline/standard-profile';
142
+ type MeshMaterialBindingObservation,
143
+ type MeshMaterialBindingPreparationFailure,
144
+ type MeshMaterialBindingResidency,
145
+ projectMeshMaterialBindingObservation,
146
+ } from './mesh-material-bindings';
147
+ import type { StandardLightingInspection } from './pipeline/standard-lighting/inspection';
148
+ import { validateClusterGrid } from './pipeline/standard-pipeline';
149
+ import { DEFAULT_CLUSTER_GRID } from './pipeline/standard-profile';
104
150
  import { PipelineSpecError } from './pipeline-spec';
151
+ import type { PointShadowInspection } from './point-shadow-inspection';
105
152
  import { PostProcessError } from './post-process-errors';
106
153
  import {
107
154
  createPreparedGraphicsResolver,
108
155
  type PreparedGraphicsResolver,
109
156
  } from './prepare/prepared-graphics-resolver';
110
157
  import {
158
+ type CubeCaptureFrameInput,
111
159
  type FrameObservation,
112
160
  type FrameObservationOptions,
113
161
  observeCurrentFrame,
@@ -121,14 +169,35 @@ import type {
121
169
  RenderFrameState,
122
170
  } from './record/frame-snapshot';
123
171
  import { getTextureIdentity } from './record/frame-snapshot';
172
+ import type { GpuPassTimingReason } from './record/gpu-pass-timing/errors';
173
+ import type {
174
+ GpuPassTimingCapture,
175
+ GpuPassTimingFrameIdentity,
176
+ GpuPassTimingSession,
177
+ } from './record/gpu-pass-timing/session';
124
178
  import type { GpuTimingCapture } from './record/gpu-timing';
125
179
  import { applyParamSnapshotToUbo, residentTextureView } from './record/main-pass-material';
126
- import type { RenderSystemInternals } from './record/render-context';
180
+ import type { ReflectionProbeRecordState, RenderSystemInternals } from './record/render-context';
181
+ import type { CubeCaptureGraphState, ReflectionProbeGraphWork } from './record/typed-frame-graph';
127
182
  import {
128
183
  type RenderFeatureGraphCandidate,
129
184
  resetRenderFeatureGraphState,
130
185
  settleVolumetricFogGraphCandidate,
131
186
  } from './record/typed-frame-graph';
187
+ import { REFLECTION_PROBE_VIEW_SLOT_BASE, VIEW_UNIFORM_SLOT_STRIDE } from './record/view-ubo';
188
+ import {
189
+ advanceProbeFilter,
190
+ commitProbeFilterStep,
191
+ createProbeFilterState,
192
+ type ProbeFilterState,
193
+ } from './reflection/filter';
194
+ import { buildReflectionProbeTable } from './reflection/gpu-table';
195
+ import {
196
+ admitReflectionProbe,
197
+ DEFAULT_REFLECTION_PROBE_LIMITS,
198
+ type ReflectionProbeFact,
199
+ type ReflectionProbeSelectionResult,
200
+ } from './reflection/projection';
132
201
  import {
133
202
  type CameraSnapshot,
134
203
  type DrawOwnerOptions,
@@ -137,7 +206,13 @@ import {
137
206
  type RenderPhaseSkipReason,
138
207
  type RenderRecordPhase,
139
208
  } from './render-contract';
140
- import type { DispatchEntry, ExtractedFrame, RenderableSnapshot } from './render-system-extract';
209
+ import type {
210
+ DispatchEntry,
211
+ ExtractedFrame,
212
+ ExtractedLights,
213
+ MaterialSnapshot,
214
+ RenderableSnapshot,
215
+ } from './render-system-extract';
141
216
  import { extractFrames } from './render-system-extract';
142
217
  import { PersistentRenderScene } from './scene/render-scene';
143
218
  import { resolveSsaoParameters } from './ssao-config';
@@ -148,6 +223,7 @@ import {
148
223
  TRANSPARENT_SORT_MODE_LAYER_YZ,
149
224
  TRANSPARENT_SORT_MODE_LAYER_Z,
150
225
  } from './systems/transparent-sort-config';
226
+ import type { RenderTarget } from './targets/contracts';
151
227
  import { createTemporalFrameTransaction, type TemporalFrame } from './temporal/frame';
152
228
  import { retireTemporalGpuState } from './temporal/gpu';
153
229
  import { inspectTemporal, type TemporalInspection } from './temporal/inspection';
@@ -163,6 +239,21 @@ import {
163
239
  inspectTransmissionFromAdmission,
164
240
  } from './transmission/inspection';
165
241
 
242
+ type TimingSessionHost = RenderSystemInternals & {
243
+ gpuPassTimingSession?: GpuPassTimingSession | undefined;
244
+ gpuPassTimingCapture?: GpuPassTimingCapture | undefined;
245
+ gpuPassTimingFrameIdentity?: GpuPassTimingFrameIdentity | undefined;
246
+ gpuPassTimingBeginReason?: GpuPassTimingReason | undefined;
247
+ };
248
+
249
+ /** Attach the Render-owned timing session without widening the public renderer surface. */
250
+ export function attachGpuPassTimingSession(
251
+ internals: object,
252
+ session: GpuPassTimingSession | undefined,
253
+ ): void {
254
+ (internals as TimingSessionHost).gpuPassTimingSession = session;
255
+ }
256
+
166
257
  export type {
167
258
  _InternalRenderPipelineContext,
168
259
  _StandardForwardSceneView,
@@ -354,6 +445,9 @@ export interface RenderSystem {
354
445
  readonly visibilityStats: { explicitlyHidden: number };
355
446
  /** Persistent scene maintenance evidence from the ordinary single-World path. */
356
447
  readonly renderScene: RenderSceneInspection;
448
+ readonly reflectionProbes: ReflectionProbeInspection;
449
+ /** True while a progressive CubeCamera candidate still owns unfinished faces. */
450
+ isCubeCapturePending(target: RenderTarget): boolean;
357
451
  readonly environment: EnvironmentInspection;
358
452
  readonly temporal: TemporalInspection;
359
453
  readonly bloom: BloomInspection;
@@ -361,6 +455,12 @@ export interface RenderSystem {
361
455
  readonly temporalTargetInspection: TemporalTargetInspection | undefined;
362
456
  /** Last completed transmission candidate facts, detached from GPU handles. */
363
457
  readonly transmission: TransmissionInspection | undefined;
458
+ /** Single production projection of Directional author/extract/record facts. */
459
+ readonly directionalShadow: DirectionalShadowInspection;
460
+ /** Last prepared Standard transport facts, detached from GPU handles. */
461
+ readonly standardLightingInspection: StandardLightingInspection | undefined;
462
+ /** Last submitted point-shadow atlas budget facts, detached from GPU handles. */
463
+ readonly pointShadowInspection: PointShadowInspection | undefined;
364
464
  /** Retained Points/Lines authoring facts from the single scene projection. */
365
465
  readonly pointsLinesSnapshots: readonly PointsLinesRetainedSnapshot[];
366
466
  /** Current mesh-slot provenance and active diagnostics from the last frame. */
@@ -492,6 +592,175 @@ type PointsLinesGpuResources = {
492
592
  };
493
593
  type PointsLinesPreparationAdapter = PointsLinesLanePreparationAdapter<PointsLinesGpuResources>;
494
594
 
595
+ function preparationFailureFrom(error: unknown): MeshMaterialBindingPreparationFailure | undefined {
596
+ if (typeof error !== 'object' || error === null) return undefined;
597
+ const value = error as {
598
+ readonly code?: unknown;
599
+ readonly expected?: unknown;
600
+ readonly hint?: unknown;
601
+ readonly detail?: unknown;
602
+ };
603
+ if (
604
+ typeof value.code !== 'string' ||
605
+ typeof value.expected !== 'string' ||
606
+ typeof value.hint !== 'string'
607
+ ) {
608
+ return undefined;
609
+ }
610
+ const detail =
611
+ typeof value.detail === 'object' && value.detail !== null
612
+ ? { ...(value.detail as Record<string, unknown>) }
613
+ : undefined;
614
+ return {
615
+ code: value.code,
616
+ expected: value.expected,
617
+ hint: value.hint,
618
+ ...(detail === undefined ? {} : { detail }),
619
+ };
620
+ }
621
+
622
+ function materialTextureHandles(
623
+ material: MaterialSnapshot,
624
+ ): readonly Handle<'TextureAsset', 'shared'>[] {
625
+ const handles = new Map<number, Handle<'TextureAsset', 'shared'>>();
626
+ const add = (handle: Handle<'TextureAsset', 'shared'> | undefined): void => {
627
+ if (handle !== undefined) handles.set(Number(handle), handle);
628
+ };
629
+ for (const handle of material.textureHandles?.values() ?? []) add(handle);
630
+ add(material.baseColorTexture);
631
+ add(material.metallicRoughnessTexture);
632
+ add(material.normalTexture);
633
+ add(material.emissiveTexture);
634
+ add(material.occlusionTexture);
635
+ return [...handles.values()];
636
+ }
637
+
638
+ function materialSamplerHandles(
639
+ material: MaterialSnapshot,
640
+ ): readonly Handle<'SamplerAsset', 'shared'>[] {
641
+ const handles = new Map<number, Handle<'SamplerAsset', 'shared'>>();
642
+ for (const handle of material.samplerHandles?.values() ?? []) {
643
+ handles.set(Number(handle), handle);
644
+ }
645
+ return [...handles.values()];
646
+ }
647
+
648
+ /**
649
+ * Project the producer-owned material and GPU store state into the one
650
+ * renderer observation surface. This is intentionally recomputed from the
651
+ * current frame's detached material snapshots; no readiness map is retained
652
+ * by RenderSystem. A producer failure with an already resident resource is
653
+ * reported as last-known-good, preserving recovery context without copying
654
+ * the producer's lifecycle state.
655
+ */
656
+ function observeMaterialResidency(
657
+ world: World | undefined,
658
+ material: MaterialSnapshot,
659
+ internals: RenderSystemInternals,
660
+ ): MeshMaterialBindingResidency {
661
+ const textures: Array<{ readonly handle: number; readonly mipLevelCount: number }> = [];
662
+ const samplers: Array<{ readonly handle: number; readonly resident: boolean }> = [];
663
+ let pending = false;
664
+ let resident = false;
665
+ let failure: MeshMaterialBindingPreparationFailure | undefined;
666
+ const keepFailure = (error: unknown): void => {
667
+ if (failure === undefined) failure = preparationFailureFrom(error);
668
+ };
669
+
670
+ if (world === undefined) {
671
+ pending = true;
672
+ } else {
673
+ const materialHandle = material.materialHandle;
674
+ if (materialHandle !== undefined && materialHandle !== 0) {
675
+ const materialResult = resolveAssetHandle<MaterialAsset>(
676
+ world,
677
+ toShared<'MaterialAsset'>(materialHandle),
678
+ );
679
+ if (!materialResult.ok) {
680
+ keepFailure(materialResult.error);
681
+ } else {
682
+ const guid = internals.assets.guidOf(materialResult.value);
683
+ if (guid !== undefined) {
684
+ const readiness = internals.assets.getMaterialReadiness(guid);
685
+ if (readiness?.status === 'Error') keepFailure(readiness.error);
686
+ const load = internals.assets.loadState.get(guid);
687
+ if (load?.status === 'provisional') pending = true;
688
+ }
689
+ }
690
+ }
691
+
692
+ for (const handle of materialTextureHandles(material)) {
693
+ const existing = internals.gpuStore.getTextureGpuView(handle, world);
694
+ const pod = resolveAssetHandle<TextureAsset>(world, handle);
695
+ if (!pod.ok) {
696
+ keepFailure(pod.error);
697
+ continue;
698
+ }
699
+ let view = existing;
700
+ if (view === undefined) {
701
+ const prepared = internals.gpuStore.ensureResident(handle, pod.value, world);
702
+ if (!prepared.ok) keepFailure(prepared.error);
703
+ view = internals.gpuStore.getTextureGpuView(handle, world);
704
+ }
705
+ if (view === undefined) {
706
+ pending = failure === undefined;
707
+ continue;
708
+ }
709
+ const textureLayout = deriveTextureLayout({
710
+ shape: pod.value.shape,
711
+ format: pod.value.format,
712
+ mips: pod.value.mips,
713
+ });
714
+ if (!textureLayout.ok) {
715
+ keepFailure(textureLayout.error);
716
+ continue;
717
+ }
718
+ resident = true;
719
+ textures.push({
720
+ handle: Number(handle),
721
+ mipLevelCount: Math.max(1, textureLayout.value.levels.length),
722
+ });
723
+ }
724
+
725
+ for (const handle of materialSamplerHandles(material)) {
726
+ const pod = resolveAssetHandle<SamplerAsset>(world, handle);
727
+ if (!pod.ok) {
728
+ keepFailure(pod.error);
729
+ samplers.push({ handle: Number(handle), resident: false });
730
+ continue;
731
+ }
732
+ const prepared = internals.gpuStore.ensureSamplerResident(handle, pod.value, world);
733
+ if (!prepared.ok) {
734
+ keepFailure(prepared.error);
735
+ samplers.push({ handle: Number(handle), resident: false });
736
+ } else {
737
+ resident = true;
738
+ samplers.push({ handle: Number(handle), resident: true });
739
+ }
740
+ }
741
+ // Video views are supplied by the dynamic host store at record time. They
742
+ // are not static TextureAsset residents, so this bounded projection stays
743
+ // pending until a dynamic observation contract exists rather than guessing
744
+ // from the VideoAsset GUID.
745
+ if ((material.videoTextureFields?.size ?? 0) > 0) pending = true;
746
+ }
747
+
748
+ const readiness =
749
+ failure === undefined
750
+ ? pending
751
+ ? 'pending'
752
+ : 'ready'
753
+ : resident
754
+ ? 'last-known-good'
755
+ : 'failed';
756
+ return {
757
+ readiness,
758
+ samplers,
759
+ textures,
760
+ ...(failure === undefined ? {} : { preparationFailure: failure }),
761
+ };
762
+ }
763
+
495
764
  /**
496
765
  * The Standard renderer's single Points/Lines prepare owner.
497
766
  *
@@ -753,6 +1022,551 @@ class StandardPointsLinesOwner implements PointsLinesRecordOwner {
753
1022
  }
754
1023
  }
755
1024
 
1025
+ type ProbeCubeSet = {
1026
+ readonly texture: Texture;
1027
+ readonly cubeView: TextureView;
1028
+ readonly faceViews: readonly TextureView[];
1029
+ readonly mipViews: readonly (readonly TextureView[])[];
1030
+ readonly depthTexture: Texture;
1031
+ readonly depthView: TextureView;
1032
+ readonly size: number;
1033
+ };
1034
+
1035
+ type ProbeGpuResource = {
1036
+ fact: ReflectionProbeFact;
1037
+ index: number;
1038
+ filter: ProbeFilterState;
1039
+ rawFaceCursor: number;
1040
+ readonly raw: ProbeCubeSet;
1041
+ active: ProbeCubeSet | undefined;
1042
+ candidate: ProbeCubeSet;
1043
+ spare: ProbeCubeSet | undefined;
1044
+ readonly sampler: Sampler;
1045
+ readonly uniformBuffer: Buffer;
1046
+ readonly filterGroup1: BindGroup;
1047
+ pending?: {
1048
+ readonly rawCaptureFace: number | undefined;
1049
+ readonly step: ReturnType<typeof advanceProbeFilter>;
1050
+ };
1051
+ };
1052
+
1053
+ type ProbeSharedResources = {
1054
+ readonly faceUniforms: Buffer;
1055
+ readonly prefilterUniforms: Buffer;
1056
+ readonly vertexBuffer: Buffer;
1057
+ readonly sampler: Sampler;
1058
+ readonly filterPipeline: RenderPipeline;
1059
+ readonly cubeGroup1Bgl: BindGroupLayout;
1060
+ readonly prefilterGroup0Bgl: BindGroupLayout;
1061
+ };
1062
+
1063
+ /** Renderer-owned raw capture and incremental PMREM/LKG promotion owner. */
1064
+ class ReflectionProbeRecordOwner {
1065
+ private readonly resources = new Map<string, ProbeGpuResource>();
1066
+ private shared: ProbeSharedResources | undefined;
1067
+ private pipelineWarmup: Promise<void> | undefined;
1068
+ private pipelineWarmupFailure: string | undefined;
1069
+ private pipelineWarmupAttempts = 0;
1070
+ private lastFactCount = 0;
1071
+ private lastAcceptedCount = 0;
1072
+ private lastScheduledRawFaces = 0;
1073
+ private lastScheduledFilteredSteps = 0;
1074
+ private lastSelection: ReflectionProbeSelectionInspection = { kind: 'skylight' };
1075
+ private readonly outputFormat: TextureFormat;
1076
+
1077
+ constructor(private readonly internals: RenderSystemInternals) {
1078
+ this.outputFormat = internals.device.caps.rgba16floatRenderable ? 'rgba16float' : 'rgba8unorm';
1079
+ }
1080
+
1081
+ inspect(): ReflectionProbeInspection {
1082
+ let rawFacesCaptured = 0;
1083
+ let filteredStepsCompleted = 0;
1084
+ const filteredMipLevels = new Set<number>();
1085
+ let activeCount = 0;
1086
+ for (const resource of this.resources.values()) {
1087
+ rawFacesCaptured += resource.rawFaceCursor;
1088
+ filteredStepsCompleted += resource.filter.cursor;
1089
+ const completedMipCount = Math.min(
1090
+ PREFILTER_MIP_LEVELS,
1091
+ Math.floor(resource.filter.cursor / resource.filter.faceCount),
1092
+ );
1093
+ for (let mip = 0; mip < completedMipCount; mip += 1) filteredMipLevels.add(mip);
1094
+ if (resource.active !== undefined) activeCount += 1;
1095
+ }
1096
+ return {
1097
+ selection: this.lastSelection,
1098
+ factCount: this.lastFactCount,
1099
+ acceptedCount: this.lastAcceptedCount,
1100
+ activeCount,
1101
+ rawFacesCaptured,
1102
+ filteredStepsCompleted,
1103
+ filteredMipLevels: [...filteredMipLevels].sort((a, b) => a - b),
1104
+ scheduledRawFaces: this.lastScheduledRawFaces,
1105
+ scheduledFilteredSteps: this.lastScheduledFilteredSteps,
1106
+ pipelineReady: this.shared !== undefined,
1107
+ pipelineWarmupAttempts: this.pipelineWarmupAttempts,
1108
+ ...(this.pipelineWarmupFailure === undefined
1109
+ ? {}
1110
+ : { pipelineWarmupFailure: this.pipelineWarmupFailure }),
1111
+ };
1112
+ }
1113
+
1114
+ private ensurePipelineWarmup(): void {
1115
+ if (this.shared !== undefined || this.pipelineWarmup !== undefined) return;
1116
+ const factory = this.internals.shaderModuleFactory;
1117
+ if (factory === undefined) return;
1118
+ const attempt = this.pipelineWarmupAttempts + 1;
1119
+ this.pipelineWarmupAttempts = attempt;
1120
+ this.pipelineWarmup = createIblPipelines(
1121
+ this.internals.deviceScope,
1122
+ this.internals.device,
1123
+ (device, descriptor) => {
1124
+ const labeled = {
1125
+ ...descriptor,
1126
+ label: `${descriptor.label ?? 'ibl'}-reflection-probe-${attempt}`,
1127
+ };
1128
+ return this.internals.createShaderModule === undefined
1129
+ ? Promise.resolve(factory.createShaderModule(labeled))
1130
+ : this.internals.createShaderModule(device, labeled);
1131
+ },
1132
+ this.outputFormat,
1133
+ ).then((result) => {
1134
+ if (!result.ok) {
1135
+ this.pipelineWarmupFailure = `${result.error.code}: ${result.error.hint}`;
1136
+ console.warn('[forgeax] ReflectionProbe IBL warmup retry', this.pipelineWarmupFailure);
1137
+ this.pipelineWarmup = undefined;
1138
+ return;
1139
+ }
1140
+ this.shared = this.createSharedResources(result.value.prefilterPipeline);
1141
+ if (this.shared === undefined) {
1142
+ this.pipelineWarmupFailure = 'renderer-owned IBL resources could not be allocated';
1143
+ console.warn('[forgeax] ReflectionProbe IBL resource allocation retry');
1144
+ this.pipelineWarmup = undefined;
1145
+ }
1146
+ });
1147
+ }
1148
+
1149
+ prepare(
1150
+ facts: readonly ReflectionProbeFact[],
1151
+ selections: ReadonlyMap<string, ReflectionProbeSelectionResult>,
1152
+ displayCamera: CameraSnapshot | undefined,
1153
+ ): ReflectionProbeRecordState {
1154
+ this.lastFactCount = facts.length;
1155
+ this.lastScheduledRawFaces = 0;
1156
+ this.lastScheduledFilteredSteps = 0;
1157
+ const selectedProbe = [...selections.values()]
1158
+ .filter(
1159
+ (selection): selection is Extract<ReflectionProbeSelectionResult, { kind: 'probe' }> =>
1160
+ selection.kind === 'probe',
1161
+ )
1162
+ .sort((a, b) => a.worldId - b.worldId || a.entityKey - b.entityKey)[0];
1163
+ this.lastSelection =
1164
+ selectedProbe === undefined
1165
+ ? { kind: 'skylight' }
1166
+ : { kind: 'probe', worldId: selectedProbe.worldId, entityKey: selectedProbe.entityKey };
1167
+ if (facts.length > 0) this.ensurePipelineWarmup();
1168
+ void this.pipelineWarmup;
1169
+ const ordered = facts
1170
+ .slice()
1171
+ .sort((a, b) => a.worldId - b.worldId || a.entityKey - b.entityKey);
1172
+ let acceptedCount = 0;
1173
+ let acceptedBytes = 0;
1174
+ const accepted = new Map<string, ProbeGpuResource>();
1175
+ for (const fact of ordered) {
1176
+ const admission = admitReflectionProbe(
1177
+ fact,
1178
+ { acceptedCount, acceptedBytes },
1179
+ DEFAULT_REFLECTION_PROBE_LIMITS,
1180
+ );
1181
+ if (!admission.ok) continue;
1182
+ const key = `${fact.worldId}:${fact.entityKey}`;
1183
+ const resource = this.resourceFor(key, fact, acceptedCount);
1184
+ if (resource === undefined) continue;
1185
+ acceptedCount += 1;
1186
+ acceptedBytes = admission.acceptedBytes;
1187
+ accepted.set(key, resource);
1188
+ }
1189
+ for (const [key, resource] of this.resources) {
1190
+ if (!accepted.has(key)) {
1191
+ this.destroyProbeResource(resource);
1192
+ this.resources.delete(key);
1193
+ } else resource.index = [...accepted.keys()].indexOf(key);
1194
+ }
1195
+ const rows = [...accepted.values()]
1196
+ .map((resource) => {
1197
+ const active = resource.active;
1198
+ if (active === undefined) return undefined;
1199
+ return {
1200
+ primitiveKey: `${resource.fact.worldId}:${resource.fact.entityKey}`,
1201
+ index: resource.index,
1202
+ worldId: resource.fact.worldId,
1203
+ entityKey: resource.fact.entityKey,
1204
+ center: resource.fact.center,
1205
+ halfExtents: resource.fact.halfExtents,
1206
+ intensity: resource.fact.intensity,
1207
+ generation: resource.filter.activeGeneration,
1208
+ filteredView: active.cubeView,
1209
+ sampler: resource.sampler,
1210
+ uniformBuffer: resource.uniformBuffer,
1211
+ };
1212
+ })
1213
+ .filter((row): row is NonNullable<typeof row> => row !== undefined);
1214
+ const table = buildReflectionProbeTable(rows);
1215
+ const work: ReflectionProbeGraphWork[] = [];
1216
+ for (const resource of accepted.values()) {
1217
+ const rawCaptureFace =
1218
+ resource.rawFaceCursor < resource.raw.faceViews.length ? resource.rawFaceCursor : undefined;
1219
+ const step =
1220
+ resource.rawFaceCursor >= resource.raw.faceViews.length
1221
+ ? advanceProbeFilter(resource.filter)
1222
+ : undefined;
1223
+ if (this.shared === undefined) continue;
1224
+ if (rawCaptureFace === undefined && step === undefined) continue;
1225
+ const filterGroup0 =
1226
+ step === undefined
1227
+ ? undefined
1228
+ : this.internals.device.createBindGroup({
1229
+ label: `reflection-probe-${resource.index}-pmrem-${step.mipLevel}-${step.faceIndex}`,
1230
+ layout: this.shared.prefilterGroup0Bgl,
1231
+ entries: [
1232
+ {
1233
+ binding: 0,
1234
+ resource: {
1235
+ kind: 'buffer',
1236
+ value: {
1237
+ buffer: this.shared.faceUniforms,
1238
+ offset: step.faceIndex * 256,
1239
+ size: 64,
1240
+ },
1241
+ },
1242
+ },
1243
+ {
1244
+ binding: 1,
1245
+ resource: {
1246
+ kind: 'buffer',
1247
+ value: {
1248
+ buffer: this.shared.prefilterUniforms,
1249
+ offset: (step.mipLevel * 6 + step.faceIndex) * 256,
1250
+ size: 16,
1251
+ },
1252
+ },
1253
+ },
1254
+ ],
1255
+ });
1256
+ if (filterGroup0 !== undefined && !filterGroup0.ok) continue;
1257
+ if (resource.pending !== undefined) continue;
1258
+ resource.pending = { rawCaptureFace, step };
1259
+ if (rawCaptureFace !== undefined) this.lastScheduledRawFaces += 1;
1260
+ if (step !== undefined) this.lastScheduledFilteredSteps += 1;
1261
+ const face =
1262
+ rawCaptureFace === undefined
1263
+ ? undefined
1264
+ : buildCubeCameraFaceViews({
1265
+ position: resource.fact.center,
1266
+ near: 0.1,
1267
+ far: 100,
1268
+ })[rawCaptureFace];
1269
+ const faceCamera =
1270
+ face === undefined || displayCamera === undefined
1271
+ ? undefined
1272
+ : {
1273
+ ...displayCamera,
1274
+ position: vec3.create(
1275
+ resource.fact.center[0] ?? 0,
1276
+ resource.fact.center[1] ?? 0,
1277
+ resource.fact.center[2] ?? 0,
1278
+ ),
1279
+ world: mat4.invert(mat4.create(), face.view),
1280
+ fov: Math.PI / 2,
1281
+ aspect: 1,
1282
+ near: face.near,
1283
+ far: face.far,
1284
+ projection: 'perspective' as const,
1285
+ };
1286
+ work.push({
1287
+ probeIndex: resource.index,
1288
+ rawTexture: resource.raw.texture,
1289
+ rawCubeView: resource.raw.cubeView,
1290
+ rawFaceViews: resource.raw.faceViews,
1291
+ rawDepthTexture: resource.raw.depthTexture,
1292
+ filteredTexture: resource.candidate.texture,
1293
+ filteredCubeView: resource.candidate.cubeView,
1294
+ filteredFaceViewsByMip: resource.candidate.mipViews,
1295
+ outputFormat: this.outputFormat,
1296
+ sampler: resource.sampler,
1297
+ filterPipeline: this.shared.filterPipeline,
1298
+ filterGroup0: filterGroup0?.value,
1299
+ filterGroup1: resource.filterGroup1,
1300
+ cubeVertexBuffer: this.shared.vertexBuffer,
1301
+ rawCaptureFace,
1302
+ rawDepthView: resource.raw.depthView,
1303
+ rawSize: resource.raw.size,
1304
+ ...(faceCamera === undefined ? {} : { faceCamera }),
1305
+ ...(rawCaptureFace === undefined
1306
+ ? {}
1307
+ : {
1308
+ viewBindGroupDynamicOffset:
1309
+ (REFLECTION_PROBE_VIEW_SLOT_BASE + resource.index) * VIEW_UNIFORM_SLOT_STRIDE,
1310
+ }),
1311
+ filteredSize: resource.candidate.size,
1312
+ step,
1313
+ });
1314
+ }
1315
+ return {
1316
+ table,
1317
+ selections,
1318
+ graph: { work },
1319
+ completeSubmission: (submitted, completed) => this.completeSubmission(submitted, completed),
1320
+ };
1321
+ }
1322
+
1323
+ private completeSubmission(submitted: boolean, completed?: Promise<unknown>): void {
1324
+ if (completed !== undefined) {
1325
+ void completed.then(
1326
+ () => this.completeSubmission(submitted),
1327
+ () => this.completeSubmission(false),
1328
+ );
1329
+ return;
1330
+ }
1331
+ for (const resource of this.resources.values()) {
1332
+ const pending = resource.pending;
1333
+ delete resource.pending;
1334
+ if (!submitted || pending === undefined) continue;
1335
+ if (pending.rawCaptureFace !== undefined) {
1336
+ resource.rawFaceCursor = Math.min(
1337
+ resource.rawFaceCursor + 1,
1338
+ resource.raw.faceViews.length,
1339
+ );
1340
+ continue;
1341
+ }
1342
+ if (pending.step === undefined) continue;
1343
+ resource.filter = commitProbeFilterStep(resource.filter, true);
1344
+ if (resource.filter.cursor >= resource.filter.faceCount * resource.filter.mipCount) {
1345
+ const previous = resource.active;
1346
+ resource.active = resource.candidate;
1347
+ resource.candidate = resource.spare ?? previous ?? resource.candidate;
1348
+ resource.spare = undefined;
1349
+ }
1350
+ }
1351
+ }
1352
+
1353
+ dispose(): void {
1354
+ for (const resource of this.resources.values()) {
1355
+ this.destroyProbeResource(resource);
1356
+ }
1357
+ this.resources.clear();
1358
+ if (this.shared !== undefined) {
1359
+ this.internals.device.destroyBuffer(this.shared.faceUniforms);
1360
+ this.internals.device.destroyBuffer(this.shared.prefilterUniforms);
1361
+ this.internals.device.destroyBuffer(this.shared.vertexBuffer);
1362
+ }
1363
+ this.shared = undefined;
1364
+ this.pipelineWarmup = undefined;
1365
+ }
1366
+
1367
+ private destroyProbeResource(resource: ProbeGpuResource): void {
1368
+ const sets = new Set<ProbeCubeSet>([resource.raw, resource.candidate]);
1369
+ if (resource.active !== undefined) sets.add(resource.active);
1370
+ if (resource.spare !== undefined) sets.add(resource.spare);
1371
+ for (const set of sets) this.destroyCubeSet(set);
1372
+ this.internals.device.destroyBuffer(resource.uniformBuffer);
1373
+ }
1374
+
1375
+ private destroyCubeSet(set: ProbeCubeSet): void {
1376
+ this.internals.device.destroyTexture(set.texture);
1377
+ this.internals.device.destroyTexture(set.depthTexture);
1378
+ }
1379
+
1380
+ private resourceFor(
1381
+ key: string,
1382
+ fact: ReflectionProbeFact,
1383
+ index: number,
1384
+ ): ProbeGpuResource | undefined {
1385
+ const existing = this.resources.get(key);
1386
+ if (existing !== undefined) {
1387
+ if (existing.fact.revision !== fact.revision) {
1388
+ existing.fact = fact;
1389
+ existing.filter = createProbeFilterState({
1390
+ probeIndex: index,
1391
+ mipCount: PREFILTER_MIP_LEVELS,
1392
+ activeGeneration: existing.filter.activeGeneration,
1393
+ });
1394
+ existing.rawFaceCursor = 0;
1395
+ } else {
1396
+ existing.fact = fact;
1397
+ existing.index = index;
1398
+ }
1399
+ return existing;
1400
+ }
1401
+ if (this.shared === undefined) return undefined;
1402
+ const raw = this.createCubeSet(`reflection-probe-${index}-raw`, fact.resolution);
1403
+ const candidate = this.createCubeSet(`reflection-probe-${index}-candidate`, fact.resolution);
1404
+ const spare = this.createCubeSet(`reflection-probe-${index}-spare`, fact.resolution);
1405
+ if (raw === undefined || candidate === undefined || spare === undefined) return undefined;
1406
+ const uniformBuffer = this.internals.device.createBuffer({
1407
+ label: `reflection-probe-${index}-uniforms`,
1408
+ size: 32,
1409
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
1410
+ mappedAtCreation: false,
1411
+ });
1412
+ if (!uniformBuffer.ok) return undefined;
1413
+ const payload = new Float32Array([
1414
+ -Math.max(0, fact.intensity),
1415
+ fact.center[0],
1416
+ fact.center[1],
1417
+ fact.center[2],
1418
+ fact.halfExtents[0],
1419
+ fact.halfExtents[1],
1420
+ fact.halfExtents[2],
1421
+ 0,
1422
+ ]);
1423
+ const written = this.internals.device.queue.writeBuffer(uniformBuffer.value, 0, payload);
1424
+ if (!written.ok) return undefined;
1425
+ const filterGroup1 = this.internals.device.createBindGroup({
1426
+ label: `reflection-probe-${index}-filter-input`,
1427
+ layout: this.shared.cubeGroup1Bgl,
1428
+ entries: [
1429
+ { binding: 0, resource: { kind: 'textureView', value: raw.cubeView } },
1430
+ { binding: 1, resource: { kind: 'sampler', value: this.shared.sampler } },
1431
+ ],
1432
+ });
1433
+ if (!filterGroup1.ok) return undefined;
1434
+ const resource: ProbeGpuResource = {
1435
+ fact,
1436
+ index,
1437
+ filter: createProbeFilterState({ probeIndex: index, mipCount: PREFILTER_MIP_LEVELS }),
1438
+ rawFaceCursor: 0,
1439
+ raw,
1440
+ active: undefined,
1441
+ candidate,
1442
+ spare,
1443
+ sampler: this.shared.sampler,
1444
+ uniformBuffer: uniformBuffer.value,
1445
+ filterGroup1: filterGroup1.value,
1446
+ };
1447
+ this.resources.set(key, resource);
1448
+ return resource;
1449
+ }
1450
+
1451
+ private createSharedResources(filterPipeline: RenderPipeline): ProbeSharedResources | undefined {
1452
+ const faceUniforms = createFaceUniformsBuffer(this.internals.device);
1453
+ const prefilterUniforms = createPrefilterUniformsBuffer(this.internals.device);
1454
+ if (!faceUniforms.ok || !prefilterUniforms.ok) return undefined;
1455
+ if (!writeAllFaceUniforms(this.internals.device, faceUniforms.value).ok) return undefined;
1456
+ if (!writeAllPrefilterUniforms(this.internals.device, prefilterUniforms.value).ok)
1457
+ return undefined;
1458
+ const vertexBuffer = this.internals.device.createBuffer({
1459
+ label: 'reflection-probe-capture-cube-vertices',
1460
+ size: CUBEMAP_FACE_VERTICES.byteLength,
1461
+ usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
1462
+ mappedAtCreation: false,
1463
+ });
1464
+ if (!vertexBuffer.ok) return undefined;
1465
+ const vertexWrite = this.internals.device.queue.writeBuffer(
1466
+ vertexBuffer.value,
1467
+ 0,
1468
+ CUBEMAP_FACE_VERTICES,
1469
+ );
1470
+ if (!vertexWrite.ok) return undefined;
1471
+ const sampler = this.internals.device.createSampler({
1472
+ label: 'reflection-probe-pmrem-sampler',
1473
+ magFilter: 'linear',
1474
+ minFilter: 'linear',
1475
+ mipmapFilter: 'linear',
1476
+ addressModeU: 'clamp-to-edge',
1477
+ addressModeV: 'clamp-to-edge',
1478
+ addressModeW: 'clamp-to-edge',
1479
+ });
1480
+ const cache = getOrCreateIblCache(this.internals.deviceScope);
1481
+ if (
1482
+ !sampler.ok ||
1483
+ cache.cubeGroup1Bgl === undefined ||
1484
+ cache.prefilterGroup0Bgl === undefined
1485
+ ) {
1486
+ return undefined;
1487
+ }
1488
+ return {
1489
+ faceUniforms: faceUniforms.value,
1490
+ prefilterUniforms: prefilterUniforms.value,
1491
+ vertexBuffer: vertexBuffer.value,
1492
+ sampler: sampler.value,
1493
+ filterPipeline,
1494
+ cubeGroup1Bgl: cache.cubeGroup1Bgl,
1495
+ prefilterGroup0Bgl: cache.prefilterGroup0Bgl,
1496
+ };
1497
+ }
1498
+
1499
+ private createCubeSet(label: string, requestedSize: number): ProbeCubeSet | undefined {
1500
+ const size = Math.min(Math.max(1, Math.floor(requestedSize)), PREFILTER_SIZE);
1501
+ const mipCount = Math.min(PREFILTER_MIP_LEVELS, Math.floor(Math.log2(size)) + 1);
1502
+ const texture = this.internals.device.createTexture({
1503
+ label,
1504
+ size: { width: size, height: size, depthOrArrayLayers: 6 },
1505
+ mipLevelCount: mipCount,
1506
+ sampleCount: 1,
1507
+ dimension: '2d',
1508
+ format: this.outputFormat,
1509
+ usage: GPU_TEXTURE_USAGE_RENDER_ATTACHMENT_AND_TEXTURE_BINDING | GPU_TEXTURE_USAGE_COPY_SRC,
1510
+ viewFormats: [],
1511
+ textureBindingViewDimension: undefined,
1512
+ });
1513
+ if (!texture.ok) return undefined;
1514
+ const cubeView = this.internals.device.createTextureView(texture.value, {
1515
+ label: `${label}-cube`,
1516
+ dimension: 'cube',
1517
+ baseMipLevel: 0,
1518
+ mipLevelCount: mipCount,
1519
+ arrayLayerCount: 6,
1520
+ });
1521
+ if (!cubeView.ok) return undefined;
1522
+ const faceViews: TextureView[] = [];
1523
+ const mipViews: TextureView[][] = [];
1524
+ for (let mip = 0; mip < mipCount; mip += 1) {
1525
+ const faces: TextureView[] = [];
1526
+ for (let face = 0; face < 6; face += 1) {
1527
+ const view = this.internals.device.createTextureView(texture.value, {
1528
+ label: `${label}-mip${mip}-face${face}`,
1529
+ dimension: '2d',
1530
+ baseMipLevel: mip,
1531
+ mipLevelCount: 1,
1532
+ baseArrayLayer: face,
1533
+ arrayLayerCount: 1,
1534
+ });
1535
+ if (!view.ok) return undefined;
1536
+ if (mip === 0) faceViews.push(view.value);
1537
+ faces.push(view.value);
1538
+ }
1539
+ mipViews.push(faces);
1540
+ }
1541
+ const depthTexture = this.internals.device.createTexture({
1542
+ label: `${label}-depth`,
1543
+ size: { width: size, height: size, depthOrArrayLayers: 1 },
1544
+ mipLevelCount: 1,
1545
+ sampleCount: 1,
1546
+ dimension: '2d',
1547
+ format: 'depth24plus-stencil8',
1548
+ usage: 0x10,
1549
+ viewFormats: [],
1550
+ textureBindingViewDimension: undefined,
1551
+ });
1552
+ if (!depthTexture.ok) return undefined;
1553
+ const depthView = this.internals.device.createTextureView(depthTexture.value, {
1554
+ label: `${label}-depth-view`,
1555
+ dimension: '2d',
1556
+ });
1557
+ if (!depthView.ok) return undefined;
1558
+ return {
1559
+ texture: texture.value,
1560
+ cubeView: cubeView.value,
1561
+ faceViews,
1562
+ mipViews,
1563
+ depthTexture: depthTexture.value,
1564
+ depthView: depthView.value,
1565
+ size,
1566
+ };
1567
+ }
1568
+ }
1569
+
756
1570
  export function createRenderSystem(internals: RenderSystemInternals): RenderSystem {
757
1571
  const phaseCatalogRegistration = internals.profiler?.registerPhaseCatalog(
758
1572
  'render',
@@ -763,6 +1577,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
763
1577
  let preparedWorlds: readonly World[] = [];
764
1578
  let latestCamera: CameraSnapshot | undefined;
765
1579
  const pointsLinesOwner = new StandardPointsLinesOwner(internals);
1580
+ const reflectionProbeOwner = new ReflectionProbeRecordOwner(internals);
1581
+ const cubeCaptureScheduler = createCubeCaptureScheduler({ maxFacesPerFrame: 1 });
1582
+ const cubeCaptureState: CubeCaptureGraphState = { work: [] };
766
1583
  const persistentRenderScene = new PersistentRenderScene({
767
1584
  getDevice: () => internals.device,
768
1585
  onGpuError: (error) => internals.errorRegistry.fire(error),
@@ -964,6 +1781,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
964
1781
  directionalShadowCacheRecorded: false,
965
1782
  compiledFrameGraph: null,
966
1783
  compiledFrameGraphTopologyKey: null,
1784
+ compiledFrameGraphGeneration: 0,
967
1785
  volumetricFogPreviousGraph: null,
968
1786
  volumetricFogPreviousGraphKey: null,
969
1787
  volumetricFogCandidateGraph: null,
@@ -995,7 +1813,10 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
995
1813
  hdrpClusterBinScratch: createClusterBinScratch(),
996
1814
  hdrpClusterGridScratch: null,
997
1815
  hdrpLightIndexListScratch: null,
998
- hdrpClusterMembershipBindGroup: null,
1816
+ hdrpClusterMembership: null,
1817
+ standardLightingGraphSignature: '',
1818
+ standardLightingInspection: undefined,
1819
+ pointShadowInspection: undefined,
999
1820
  transientInstanceBuffers: [],
1000
1821
  warnedZeroLightStandard: false,
1001
1822
  warnedMultiLightDirectional: false,
@@ -1051,10 +1872,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1051
1872
  // (its topology is frame-invariant). Standard configuration overwrites this with the
1052
1873
  // resolved asset config on every swap so the active graph reads its current config.
1053
1874
  installedPipelineConfig: undefined,
1054
- // feat-20260608-cluster-lighting M2 / w10 + M5 / w20: HDRP active flag + once-per-frame
1055
- // warn dedup set for hdrp-light-budget-exceeded / hdrp-index-list-overflow.
1056
- isHdrpActive: false,
1057
- hdrpOncePerFrameFired: new Set(),
1875
+ standardOncePerFrameFired: new Set(),
1058
1876
  // feat-20260612-point-light-shadows-urp-hdrp M3 / T-M3-2 (plan-strategy §D-1):
1059
1877
  // cube_array shadow atlas + per-frame snapshot list. Atlas is null until the
1060
1878
  // first frame whose extracted lights.pointShadow is non-empty (zero-shadow
@@ -1073,6 +1891,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1073
1891
  spotShadowSnapshots: [],
1074
1892
  };
1075
1893
  let directFrameId = 0;
1894
+ let lastDirectionalShadowLights: ExtractedLights | undefined;
1895
+ let lastDirectionalShadowCandidate: 'accepted' | 'failed' = 'failed';
1896
+ let lastDirectionalShadowError: DirectionalShadowInspectionSource['error'];
1076
1897
 
1077
1898
  function beginProfilePhase(session: RecorderSession | undefined, phase: RenderPhase): boolean {
1078
1899
  if (session === undefined) return false;
@@ -1133,66 +1954,124 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1133
1954
  let activeFeaturePostProcessEntries: ReadonlyMap<string, PostProcessShaderEntry> = new Map();
1134
1955
  const lookupPostProcess = (id: string): PostProcessShaderEntry | undefined =>
1135
1956
  builtinPostProcessEntries.get(id) ?? activeFeaturePostProcessEntries.get(id);
1957
+ const invalidatePostProcessModule = (id: string): void => {
1958
+ const entry = lookupPostProcess(id);
1959
+ if (entry !== undefined) {
1960
+ internals.invalidateShaderModule?.(postProcessShaderModuleLabel(id, entry.source));
1961
+ }
1962
+ };
1136
1963
  // feat-20260621 M-A2 / w8: expose the eager-created per-id params UBO through
1137
1964
  // the narrow runtime surface so dispatchFullscreenPass can writeBuffer the
1138
1965
  // per-frame snapshot + bind it at group(1) binding(2).
1139
1966
  const getPostProcessParamsBuffer = (id: string): Buffer | undefined =>
1140
1967
  postProcessParamsBuffers.get(id);
1141
- const ensurePostProcessParamsResources = (
1968
+ interface PostProcessParamsCandidate {
1969
+ readonly buffers: ReadonlyMap<string, Buffer>;
1970
+ readonly created: readonly Buffer[];
1971
+ }
1972
+ const postProcessParamsByteSize = (entry: PostProcessShaderEntry): number =>
1973
+ entry.params?.byteSize ?? (entryHasDepthRead(entry) ? DEPTH_MIN_PARAMS_BYTE_SIZE : 0);
1974
+ const destroyPostProcessParams = (buffers: readonly Buffer[]): void => {
1975
+ for (const buffer of buffers) {
1976
+ const destroyed = internals.device.destroyBuffer(buffer);
1977
+ if (!destroyed.ok) internals.errorRegistry.fire(destroyed.error);
1978
+ }
1979
+ };
1980
+ const preparePostProcessParamsResources = (
1142
1981
  featureEntries: ReadonlyMap<string, PostProcessShaderEntry>,
1143
- ): void => {
1982
+ ): Result<PostProcessParamsCandidate, RhiError> => {
1144
1983
  const declarations = new Map<string, PostProcessShaderEntry>([
1145
1984
  ...builtinPostProcessEntries.entries(),
1146
1985
  ...featureEntries.entries(),
1147
1986
  ]);
1987
+ const next = new Map<string, Buffer>();
1988
+ const allocated: Buffer[] = [];
1989
+ const discardCreated = (): void => destroyPostProcessParams(allocated);
1148
1990
  for (const [id, entry] of declarations) {
1149
1991
  if (id === 'forgeax.taa-resolve') continue;
1150
- const byteSize =
1151
- entry.params?.byteSize ?? (entryHasDepthRead(entry) ? DEPTH_MIN_PARAMS_BYTE_SIZE : 0);
1152
- if (byteSize === 0 || postProcessParamsBuffers.has(id)) continue;
1992
+ const byteSize = postProcessParamsByteSize(entry);
1993
+ if (byteSize === 0) continue;
1994
+ const existing = postProcessParamsBuffers.get(id);
1995
+ const accepted = lookupPostProcess(id);
1996
+ if (
1997
+ existing !== undefined &&
1998
+ accepted !== undefined &&
1999
+ postProcessShaderEntrySignature(accepted) === postProcessShaderEntrySignature(entry)
2000
+ ) {
2001
+ next.set(id, existing);
2002
+ continue;
2003
+ }
1153
2004
  const created = internals.device.createBuffer({
1154
2005
  label: `post-process-params-${id}`,
1155
2006
  size: byteSize,
1156
2007
  usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
1157
2008
  mappedAtCreation: false,
1158
2009
  });
1159
- if (!created.ok) throw created.error;
2010
+ if (!created.ok) {
2011
+ discardCreated();
2012
+ return err(created.error);
2013
+ }
1160
2014
  const buffer = created.value;
2015
+ allocated.push(buffer);
1161
2016
  if (entry.params !== undefined) {
1162
2017
  const written = internals.device.queue.writeBuffer(buffer, 0, entry.params.defaultValue);
1163
2018
  if (!written.ok) {
1164
- internals.device.destroyBuffer(buffer);
1165
- throw written.error;
2019
+ discardCreated();
2020
+ return err(written.error);
1166
2021
  }
1167
2022
  }
1168
- postProcessParamsBuffers.set(id, buffer);
2023
+ next.set(id, buffer);
1169
2024
  }
1170
- for (const [id, buffer] of postProcessParamsBuffers) {
1171
- if (declarations.has(id)) continue;
1172
- internals.device.destroyBuffer(buffer);
1173
- postProcessParamsBuffers.delete(id);
2025
+ return ok({ buffers: next, created: Object.freeze([...allocated]) });
2026
+ };
2027
+ const discardPostProcessParamsCandidate = (candidate: PostProcessParamsCandidate): void => {
2028
+ destroyPostProcessParams(candidate.created);
2029
+ };
2030
+ const retirePostProcessParams = (buffers: readonly Buffer[]): void => {
2031
+ if (buffers.length === 0) return;
2032
+ const release = (): void => destroyPostProcessParams(buffers);
2033
+ try {
2034
+ void internals.device.queue.onSubmittedWorkDone().then(release, release);
2035
+ } catch {
2036
+ release();
2037
+ }
2038
+ };
2039
+ const acceptPostProcessParamsCandidate = (candidate: PostProcessParamsCandidate): void => {
2040
+ const previous = [...postProcessParamsBuffers.values()];
2041
+ const next = new Set(candidate.buffers.values());
2042
+ postProcessParamsBuffers.clear();
2043
+ for (const [id, buffer] of candidate.buffers) {
2044
+ postProcessParamsBuffers.set(id, buffer);
1174
2045
  }
2046
+ retirePostProcessParams(previous.filter((buffer) => !next.has(buffer)));
1175
2047
  };
1176
- // feat-20260609 M4 / T-10-a: post-process pipeline cache (id|colorFormat -> RhiRenderPipeline).
2048
+ // feat-20260609 M4 / T-10-a: post-process pipeline cache (declaration|colorFormat -> RhiRenderPipeline).
1177
2049
  // Solves CONCERN-1: dispatcher previously passed `pipeline=null` to
1178
2050
  // built.createHandle because per-frame execute closures cannot await async
1179
2051
  // shader compile. The cache here delegates the actual build to
1180
2052
  // `internals.buildPostProcessPipeline` (sync wrapper over the shared shader
1181
- // adapter; 1-frame warmup), then memoizes by `id|colorFormat`.
2053
+ // adapter; 1-frame warmup), then memoizes by declaration identity + format.
1182
2054
  const postProcessPipelineCache = new Map<string, RenderPipeline>();
1183
2055
  const getPostProcessPipeline = (
1184
2056
  id: string,
1185
2057
  bgl: BindGroupLayout,
1186
2058
  colorFormat: GPUTextureFormat,
2059
+ entryOverride?: PostProcessShaderEntry,
1187
2060
  ): RenderPipeline | null => {
1188
- const key = `${id}|${colorFormat}`;
2061
+ const entry = entryOverride ?? lookupPostProcess(id);
2062
+ if (entry === undefined) return null;
2063
+ const signature = postProcessShaderEntrySignature(entry);
2064
+ const key = `${id}|${colorFormat}|${signature}`;
1189
2065
  const cached = postProcessPipelineCache.get(key);
1190
2066
  if (cached !== undefined) return cached;
1191
- const entry = lookupPostProcess(id);
1192
- if (entry === undefined) return null;
1193
2067
  const factory = internals.buildPostProcessPipeline;
1194
2068
  if (factory === undefined) return null;
1195
- const built = factory(entry, bgl, colorFormat, `post-process-${id}`);
2069
+ const built = factory(
2070
+ entry,
2071
+ bgl,
2072
+ colorFormat,
2073
+ postProcessShaderPipelineLabel(id, entry.source),
2074
+ );
1196
2075
  if (built === null) return null;
1197
2076
  postProcessPipelineCache.set(key, built);
1198
2077
  return built;
@@ -1202,6 +2081,14 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1202
2081
  if (key.startsWith(`${id}|`)) postProcessPipelineCache.delete(key);
1203
2082
  }
1204
2083
  };
2084
+ const clearPostProcessPipelineEntry = (id: string, entry: PostProcessShaderEntry): void => {
2085
+ const suffix = `|${postProcessShaderEntrySignature(entry)}`;
2086
+ for (const key of postProcessPipelineCache.keys()) {
2087
+ if (key.startsWith(`${id}|`) && key.endsWith(suffix)) {
2088
+ postProcessPipelineCache.delete(key);
2089
+ }
2090
+ }
2091
+ };
1205
2092
  Object.assign(internals, {
1206
2093
  lookupPostProcess,
1207
2094
  getPostProcessParamsBuffer,
@@ -1376,7 +2263,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1376
2263
  if (buffer === undefined) return group;
1377
2264
  return ok({
1378
2265
  handle: group.value,
1379
- dynamicOffsets: [0],
2266
+ dynamicOffsets: [0, 0],
1380
2267
  release: () => internals.device.destroyBuffer(buffer),
1381
2268
  });
1382
2269
  };
@@ -1387,8 +2274,6 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1387
2274
  const preparedResolverFactory = (
1388
2275
  input: RenderFeaturePreparedGraphicsResolverInput,
1389
2276
  ): PreparedGraphicsResolver => {
1390
- activeFeaturePostProcessEntries = input.fullscreenEffects;
1391
- ensurePostProcessParamsResources(input.fullscreenEffects);
1392
2277
  return createPreparedGraphicsResolver({
1393
2278
  device: internals.device,
1394
2279
  featureIdentity: input.featureIdentity,
@@ -1412,6 +2297,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1412
2297
  descriptor.shader,
1413
2298
  fullscreen.bindGroupLayout,
1414
2299
  descriptor.colorFormats[0] as GPUTextureFormat,
2300
+ postProcessEntry,
1415
2301
  );
1416
2302
  if (pipeline !== null && pipeline !== undefined) {
1417
2303
  preparedPipelineIds.set(pipeline as object, descriptor.shader);
@@ -1580,6 +2466,12 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1580
2466
  gpuDriven: gpuDrivenProduction.inspect(),
1581
2467
  };
1582
2468
  },
2469
+ get reflectionProbes(): ReflectionProbeInspection {
2470
+ return reflectionProbeOwner.inspect();
2471
+ },
2472
+ isCubeCapturePending(target: RenderTarget): boolean {
2473
+ return cubeCaptureScheduler.inspect(target).candidateGeneration !== undefined;
2474
+ },
1583
2475
  get environment(): EnvironmentInspection {
1584
2476
  return environmentLifecycle.inspect();
1585
2477
  },
@@ -1672,6 +2564,49 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1672
2564
  get transmission(): TransmissionInspection | undefined {
1673
2565
  return lastTransmissionInspection;
1674
2566
  },
2567
+ get directionalShadow(): DirectionalShadowInspection {
2568
+ const lights = lastDirectionalShadowLights;
2569
+ const pipelineState = internals.getPipelineState();
2570
+ const graph = frameState.compiledFrameGraph?.inspect();
2571
+ const requested = directionalShadowProfileFromQuality(lights?.directionalShadowQuality);
2572
+ const mapSize = pipelineState?.perPassResources.shadowMapSize ?? 0;
2573
+ const cascadeCount = lights?.cascadeCount ?? 0;
2574
+ const shadowReady = frameState.currentDirectionalShadowView !== null;
2575
+ const atlasBytes =
2576
+ shadowReady && mapSize > 0 && cascadeCount > 0
2577
+ ? mapSize * mapSize * Math.ceil(Math.sqrt(cascadeCount)) ** 2 * 4
2578
+ : 0;
2579
+ return projectDirectionalShadowInspectionSource({
2580
+ requested,
2581
+ candidate: lastDirectionalShadowCandidate,
2582
+ ...(frameState.directionalShadowCache === null
2583
+ ? {}
2584
+ : {
2585
+ lastKnownGood: directionalShadowProfileFromQuality(
2586
+ frameState.directionalShadowCache.directionalShadowQuality,
2587
+ ),
2588
+ }),
2589
+ cascadeCount,
2590
+ mapSize,
2591
+ atlasBytes,
2592
+ writerPasses:
2593
+ graph?.passes.filter((pass) => pass.name.startsWith('shadowCascade')).length ?? 0,
2594
+ shadowAngularRadius: lights?.directionalCsmConfig?.shadowAngularRadius,
2595
+ maxPenumbraTexels: lights?.directionalCsmConfig?.maxPenumbraTexels,
2596
+ deviceGeneration: internals.deviceScope.generation,
2597
+ graphGeneration: frameState.perFrameGraph?.graphGeneration ?? frameState.graphGeneration,
2598
+ shadowReady,
2599
+ ...(lastDirectionalShadowError === undefined ? {} : { error: lastDirectionalShadowError }),
2600
+ });
2601
+ },
2602
+ get standardLightingInspection(): StandardLightingInspection | undefined {
2603
+ const inspection = frameState.standardLightingInspection;
2604
+ return inspection === undefined ? undefined : Object.freeze({ ...inspection });
2605
+ },
2606
+ get pointShadowInspection(): PointShadowInspection | undefined {
2607
+ const inspection = frameState.pointShadowInspection;
2608
+ return inspection === undefined ? undefined : Object.freeze({ ...inspection });
2609
+ },
1675
2610
  get pointsLinesSnapshots(): readonly PointsLinesRetainedSnapshot[] {
1676
2611
  return persistentRenderScene.pointsLinesSnapshots();
1677
2612
  },
@@ -1755,6 +2690,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1755
2690
  });
1756
2691
  const {
1757
2692
  cameras,
2693
+ cubeCameras,
1758
2694
  lights,
1759
2695
  environment,
1760
2696
  environmentReady,
@@ -1766,10 +2702,19 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1766
2702
  skybox,
1767
2703
  skyboxCount,
1768
2704
  fog,
2705
+ fogFailure,
1769
2706
  frustumStats,
1770
2707
  visibilityStats,
1771
2708
  postProcessParams,
1772
2709
  } = frame;
2710
+ lastDirectionalShadowLights = lights;
2711
+ lastDirectionalShadowCandidate = 'failed';
2712
+ lastDirectionalShadowError = lights.directionalShadowError;
2713
+ const reflectionProbeRecord = reflectionProbeOwner.prepare(
2714
+ frame.reflectionProbes ?? [],
2715
+ persistentRenderScene.reflectionProbeProjection().selected,
2716
+ cameras[0],
2717
+ );
1773
2718
  latestCamera = cameras[0];
1774
2719
  const temporalDemanded =
1775
2720
  cameras[0]?.antialias === 'taa' || (cameras[0]?.motionBlur?.shutterAngle ?? 0) > 0;
@@ -1777,6 +2722,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1777
2722
  persistentRenderScene.prepareTemporalFrame(renderables);
1778
2723
  }
1779
2724
  environmentLifecycle.bindScope(internals.deviceScope);
2725
+ if (fogFailure !== undefined) {
2726
+ environmentLifecycle.recordSelectionFailure(fogFailure);
2727
+ }
1780
2728
  if (!environmentReady || environment === undefined) {
1781
2729
  frameState.pendingTemporalCommit = { kind: 'none' };
1782
2730
  // A world with no camera still has a valid clear-only render path.
@@ -1805,15 +2753,21 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1805
2753
  }
1806
2754
  if (lastMeshMaterialBindingFrame !== frame) {
1807
2755
  lastMeshMaterialBindingFrame = frame;
1808
- lastMeshMaterialBindings = renderables.map((renderable) => ({
1809
- worldId: renderable.worldId,
1810
- entityKey: renderable.entityKey,
1811
- bindings: renderable.materials.map((material, slotIndex) => ({
1812
- handle: material.materialHandle ?? 0,
1813
- source: renderable.materialBindingSources[slotIndex] ?? 'engine-default',
1814
- })),
1815
- diagnostics: renderable.materialBindingDiagnostics ?? [],
1816
- }));
2756
+ lastMeshMaterialBindings = renderables.map((renderable) => {
2757
+ const materialWorld = preparedWorlds[renderable.worldId];
2758
+ return projectMeshMaterialBindingObservation({
2759
+ worldId: renderable.worldId,
2760
+ entityKey: renderable.entityKey,
2761
+ bindings: renderable.materials.map((material, slotIndex) => ({
2762
+ handle: material.materialHandle ?? 0,
2763
+ source: renderable.materialBindingSources[slotIndex] ?? 'engine-default',
2764
+ })),
2765
+ diagnostics: renderable.materialBindingDiagnostics ?? [],
2766
+ residency: renderable.materials.map((material) =>
2767
+ observeMaterialResidency(materialWorld, material, internals),
2768
+ ),
2769
+ });
2770
+ });
1817
2771
  }
1818
2772
 
1819
2773
  bindGroupCounts.createBindGroup = 0;
@@ -1879,14 +2833,23 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1879
2833
  createPreparedGraphicsResolver: preparedResolverFactory,
1880
2834
  gpuWork: featureGpuWork,
1881
2835
  });
1882
- activeFeaturePostProcessEntries = featureFrame.fullscreenEffects;
1883
- ensurePostProcessParamsResources(featureFrame.fullscreenEffects);
1884
2836
  lastVisibilityStats.explicitlyHidden = featureFrame.hiddenEntityReports.length;
1885
2837
  for (const featureError of featureFrame.errors) {
1886
2838
  if (!isPendingRenderFeaturePreparation(featureError)) {
1887
2839
  internals.errorRegistry.fire(featureError);
1888
2840
  }
1889
2841
  }
2842
+ const postProcessParamsCandidate = preparePostProcessParamsResources(
2843
+ featureFrame.fullscreenEffects,
2844
+ );
2845
+ if (!postProcessParamsCandidate.ok) {
2846
+ internals.errorRegistry.fire(postProcessParamsCandidate.error);
2847
+ for (const batch of featureFrame.preparedResourceBatches) {
2848
+ const released = batch.release();
2849
+ if (!released.ok) internals.errorRegistry.fire(released.error);
2850
+ }
2851
+ return [];
2852
+ }
1890
2853
  featureGraphCandidate = {
1891
2854
  plans: featureFrame.plans,
1892
2855
  fullscreenEffects: featureFrame.fullscreenEffects,
@@ -1900,6 +2863,29 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1900
2863
  internals.errorRegistry.fire(released.error);
1901
2864
  }
1902
2865
  }
2866
+ discardPostProcessParamsCandidate(postProcessParamsCandidate.value);
2867
+ },
2868
+ onAbandoned: () => {
2869
+ discardPostProcessParamsCandidate(postProcessParamsCandidate.value);
2870
+ },
2871
+ onAccepted: () => {
2872
+ for (const [id, previous] of activeFeaturePostProcessEntries) {
2873
+ const next = featureFrame.fullscreenEffects.get(id);
2874
+ if (next === undefined) {
2875
+ clearPostProcessPipelineCache(id);
2876
+ invalidatePostProcessModule(id);
2877
+ } else if (
2878
+ postProcessShaderEntrySignature(previous) !==
2879
+ postProcessShaderEntrySignature(next)
2880
+ ) {
2881
+ clearPostProcessPipelineEntry(id, previous);
2882
+ if (previous.source !== next.source) {
2883
+ invalidatePostProcessModule(id);
2884
+ }
2885
+ }
2886
+ }
2887
+ acceptPostProcessParamsCandidate(postProcessParamsCandidate.value);
2888
+ activeFeaturePostProcessEntries = featureFrame.fullscreenEffects;
1903
2889
  },
1904
2890
  };
1905
2891
  return featureFrame.preparedResourceBatches;
@@ -1976,14 +2962,20 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1976
2962
  {
1977
2963
  owner: gpuDrivenProduction,
1978
2964
  scene: persistentRenderScene.compositionGpuDrivenState(),
1979
- onCoverage: (ownsAll) => {
1980
- const key = `${internals.gpuStore.meshResidencyEpoch}:${frameState.isHdrpActive ? 'hdrp' : 'urp'}`;
2965
+ onCoverage: (ownsAll, clusteredLighting) => {
2966
+ const key = `${internals.gpuStore.meshResidencyEpoch}:${clusteredLighting ? 'clustered' : 'standard'}`;
1981
2967
  persistentRenderScene.setCompositionGpuDrivenCoverage(key, ownsAll);
1982
2968
  },
1983
2969
  },
1984
2970
  renderReadLeases,
1985
2971
  featureGraphCandidate,
1986
2972
  pointsLinesOwner,
2973
+ {
2974
+ snapshots: cubeCameras,
2975
+ scheduler: cubeCaptureScheduler,
2976
+ state: cubeCaptureState,
2977
+ reflectionProbes: reflectionProbeRecord,
2978
+ } satisfies CubeCaptureFrameInput,
1987
2979
  environment?.environmentSignature ?? '',
1988
2980
  environment?.fogSignature ?? '',
1989
2981
  environmentReady && environment !== undefined,
@@ -1992,6 +2984,28 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1992
2984
  timingCapture,
1993
2985
  ),
1994
2986
  );
2987
+ const directionalQuality = lights.directionalShadowQuality;
2988
+ const directionalShadowReady =
2989
+ directionalQuality === undefined ||
2990
+ (frameState.currentDirectionalShadowView !== null &&
2991
+ (lights.lightViewProj?.length ?? 0) > 0 &&
2992
+ (internals.getPipelineState()?.perPassResources.shadowMapSize ?? 0) > 0);
2993
+ if (submitted && directionalShadowReady) {
2994
+ lastDirectionalShadowCandidate = 'accepted';
2995
+ } else if (directionalQuality !== undefined) {
2996
+ lastDirectionalShadowError = {
2997
+ code: 'directional-shadow-candidate-failed',
2998
+ expected:
2999
+ 'the extracted Directional CSM candidate has a valid graph target and fitted matrices',
3000
+ hint: 'inspect the retained LKG and retry after the graph or resource owner recovers',
3001
+ detail: {
3002
+ submitted,
3003
+ hasShadowView: frameState.currentDirectionalShadowView !== null,
3004
+ matrixCount: lights.lightViewProj?.length ?? 0,
3005
+ shadowMapSize: internals.getPipelineState()?.perPassResources.shadowMapSize ?? 0,
3006
+ },
3007
+ };
3008
+ }
1995
3009
  if (submitted) {
1996
3010
  const temporalCommit = persistentRenderScene.commitTemporalFrame();
1997
3011
  if (!temporalCommit.ok) {
@@ -2127,9 +3141,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2127
3141
  profileConfig === undefined
2128
3142
  ? config
2129
3143
  : {
2130
- ...(profileConfig.lighting === 'clustered'
2131
- ? { clusterGrid: DEFAULT_CLUSTER_GRID }
2132
- : {}),
3144
+ clusterGrid: DEFAULT_CLUSTER_GRID,
2133
3145
  ...(profileConfig.ssao ? { ssao: { enabled: true } } : {}),
2134
3146
  ...config,
2135
3147
  };
@@ -2154,16 +3166,6 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2154
3166
  // is a rare boot/swap event, never a per-frame cost.
2155
3167
  installEpoch += 1;
2156
3168
  frameState.installedPipelineHandle = installEpoch;
2157
- // The installed profile may request clustered lighting, but the actual
2158
- // graph lane is capability-gated. On WebGL2 (including WebKit's wgpu
2159
- // fallback) the forward lane is selected when compute/storage buffers
2160
- // are unavailable; keep the frame bindings on that same lane instead of
2161
- // advertising HDRP's clustered bind-group layout to URP pipelines.
2162
- frameState.isHdrpActive =
2163
- resolveStandardLane(internals.standardProfile ?? DEFAULT_STANDARD_PROFILE, {
2164
- compute: internals.device.caps.compute,
2165
- storageBuffer: internals.device.caps.storageBuffer,
2166
- }) === 'clustered';
2167
3169
  frameState.installedPipelineConfig = resolvedConfig;
2168
3170
  },
2169
3171
  registerBuiltinPostProcess(id: string, entry: PostProcessShaderEntry): () => void {
@@ -2207,6 +3209,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2207
3209
  }
2208
3210
  builtinPostProcessEntries.set(id, entry);
2209
3211
  if (paramsBuffer !== undefined) postProcessParamsBuffers.set(id, paramsBuffer);
3212
+ const moduleLabel = postProcessShaderModuleLabel(id, entry.source);
2210
3213
  return () => {
2211
3214
  if (builtinPostProcessEntries.get(id) === entry) {
2212
3215
  builtinPostProcessEntries.delete(id);
@@ -2216,9 +3219,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2216
3219
  internals.device.destroyBuffer(current);
2217
3220
  postProcessParamsBuffers.delete(id);
2218
3221
  }
2219
- for (const key of postProcessPipelineCache.keys()) {
2220
- if (key.startsWith(`${id}|`)) postProcessPipelineCache.delete(key);
2221
- }
3222
+ clearPostProcessPipelineCache(id);
3223
+ internals.invalidateShaderModule?.(moduleLabel);
2222
3224
  };
2223
3225
  } catch (cause) {
2224
3226
  if (paramsBuffer !== undefined) internals.device.destroyBuffer(paramsBuffer);
@@ -2226,16 +3228,24 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2226
3228
  }
2227
3229
  },
2228
3230
  disposeFrameState(): void {
3231
+ (internals as TimingSessionHost).gpuPassTimingSession?.dispose();
3232
+ (internals as TimingSessionHost).gpuPassTimingSession = undefined;
3233
+ (internals as TimingSessionHost).gpuPassTimingCapture = undefined;
3234
+ (internals as TimingSessionHost).gpuPassTimingSubmittedWork = undefined;
3235
+ (internals as TimingSessionHost).gpuPassTimingFrameIdentity = undefined;
3236
+ (internals as TimingSessionHost).gpuPassTimingBeginReason = undefined;
2229
3237
  persistentRenderScene.dispose();
2230
3238
  gpuDrivenProduction.dispose();
2231
3239
  disposeFeatureGpuWork();
3240
+ reflectionProbeOwner.dispose();
2232
3241
  // Bloom generations are renderer-owned rather than graph-owned. Drain
2233
3242
  // candidate, active, and fence-retiring bundles before the rest of the
2234
3243
  // frame state is torn down; the owner callback is idempotent so the
2235
3244
  // outer Renderer.dispose cascade can safely call this once only.
2236
3245
  internals.getPipelineState()?.perPassResources.drainBloomResources?.();
2237
3246
  internals.clearPostProcessPipelineCache?.('forgeax.taa-resolve');
2238
- internals.invalidateShaderModule?.('post-process-forgeax.taa-resolve-module');
3247
+ invalidatePostProcessModule('forgeax.taa-resolve');
3248
+ resetHdrpBuffers(internals);
2239
3249
  // Retire graph-owned resources and dispose instance-buffer caches.
2240
3250
  // Both calls are idempotent + tolerate per-handle errors silently;
2241
3251
  // the Renderer.dispose() cascade owns the surrounding try/catch
@@ -2276,9 +3286,14 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2276
3286
  frameState.volumetricFogHistoryGraph = null;
2277
3287
  frameState.volumetricFogHistorySlot = null;
2278
3288
  frameState.volumetricFogHistorySignature = null;
3289
+ frameState.hdrpClusterMembership = null;
2279
3290
  const compiled = frameState.compiledFrameGraph;
2280
3291
  frameState.compiledFrameGraph = null;
2281
3292
  frameState.compiledFrameGraphTopologyKey = null;
3293
+ frameState.perFrameGraph = null;
3294
+ frameState.standardLightingGraphSignature = '';
3295
+ frameState.standardLightingInspection = undefined;
3296
+ frameState.pointShadowInspection = undefined;
2282
3297
  if (compiled !== null) compiled.retire().catch(() => undefined);
2283
3298
  for (const retired of frameState.retiredCompiledFrameGraphs) {
2284
3299
  retired.retire().catch(() => undefined);
@@ -2313,6 +3328,11 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2313
3328
  lastTransmissionKey = '';
2314
3329
  transmissionCapabilityGeneration = -1;
2315
3330
  transmissionCapability = undefined;
3331
+ // The replacement device cannot consume buffers minted by the lost
3332
+ // device. Drop the shared Standard Cluster bundle before the next
3333
+ // frame asks for a new generation; resetHdrpBuffers fences the bundle
3334
+ // using its own device/queue instead of the replacement runtime device.
3335
+ resetHdrpBuffers(internals);
2316
3336
  // Keep the persistent material demand observable across device loss.
2317
3337
  // The physical resource has already been fenced by markDeviceLost(); the
2318
3338
  // detached inspection now exposes the rebuild-required lifecycle until a
@@ -2322,6 +3342,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2322
3342
  gpuDrivenProduction.dispose();
2323
3343
  gpuDrivenProduction = new GpuDrivenProduction(internals.device, gpuDrivenShaderFactory);
2324
3344
  disposeFeatureGpuWork();
3345
+ reflectionProbeOwner.dispose();
2325
3346
  // A recovered device cannot retain handles from the lost device. The
2326
3347
  // Bloom owner abandons candidates and retires every committed generation
2327
3348
  // before the new PipelineState is rebuilt.
@@ -2351,6 +3372,13 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2351
3372
  frameState.volumetricFogParamsAcceptedSlot = null;
2352
3373
  frameState.volumetricFogAcceptedParams = undefined;
2353
3374
  frameState.volumetricFogPendingParams = undefined;
3375
+ // Do not expose graph-owned texture/descriptor accessors from the lost
3376
+ // device while recovery is between generations. Inspection and capture
3377
+ // must remain detached until the replacement graph is accepted.
3378
+ frameState.perFrameGraph = null;
3379
+ frameState.standardLightingGraphSignature = '';
3380
+ frameState.standardLightingInspection = undefined;
3381
+ frameState.pointShadowInspection = undefined;
2354
3382
  frameState.directionalShadowCache = null;
2355
3383
  frameState.directionalShadowCacheRecorded = false;
2356
3384
  frameState.currentFrameObservationSource = undefined;
@@ -2369,6 +3397,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2369
3397
  retireTemporalGpuState(retiring);
2370
3398
  }
2371
3399
  frameState.retiringTemporalGpuStates.clear();
3400
+ frameState.hdrpClusterMembership = null;
2372
3401
  for (const retired of frameState.retiredCompiledFrameGraphs) {
2373
3402
  retired.retire().catch(() => undefined);
2374
3403
  }