@forgeax/engine-render 0.1.19 → 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 (452) 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 +34 -20
  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-RJRUS55L.mjs → chunk-5YADUCA7.mjs} +1896 -1624
  21. package/dist/chunk-5YADUCA7.mjs.map +1 -0
  22. package/dist/{chunk-LOSO4CTV.mjs → chunk-6OPXDQ2E.mjs} +423 -46
  23. package/dist/chunk-6OPXDQ2E.mjs.map +1 -0
  24. package/dist/{chunk-IPXCABEP.mjs → chunk-BXBQQKFQ.mjs} +3 -9
  25. package/dist/chunk-BXBQQKFQ.mjs.map +1 -0
  26. package/dist/chunk-FS2KNUQH.mjs +1294 -0
  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/components/spot-light.d.ts +12 -0
  42. package/dist/components/spot-light.d.ts.map +1 -1
  43. package/dist/construct-renderer.mjs +8552 -1612
  44. package/dist/construct-renderer.mjs.map +1 -1
  45. package/dist/device/gpu-residency.d.ts +19 -2
  46. package/dist/device/gpu-residency.d.ts.map +1 -1
  47. package/dist/environment/frame.d.ts.map +1 -1
  48. package/dist/environment/lifecycle.d.ts +5 -0
  49. package/dist/environment/lifecycle.d.ts.map +1 -1
  50. package/dist/errors/render.d.ts +209 -93
  51. package/dist/errors/render.d.ts.map +1 -1
  52. package/dist/extract/directional-shadow-projection.d.ts +23 -0
  53. package/dist/extract/directional-shadow-projection.d.ts.map +1 -0
  54. package/dist/extract/environment.d.ts +14 -0
  55. package/dist/extract/environment.d.ts.map +1 -1
  56. package/dist/extract/visibility.d.ts.map +1 -1
  57. package/dist/features/fullscreen.d.ts.map +1 -1
  58. package/dist/features/targets.d.ts.map +1 -1
  59. package/dist/features/types.d.ts +10 -0
  60. package/dist/features/types.d.ts.map +1 -1
  61. package/dist/fullscreen-post-process-pass.d.ts +13 -2
  62. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  63. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  64. package/dist/gpu-driven/production-raster.d.ts +0 -1
  65. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  66. package/dist/gpu-scene.d.ts.map +1 -1
  67. package/dist/gpu-usage.d.ts +2 -0
  68. package/dist/gpu-usage.d.ts.map +1 -1
  69. package/dist/hdrp-buffers.d.ts +19 -17
  70. package/dist/hdrp-buffers.d.ts.map +1 -1
  71. package/dist/ibl/IblPipelineCache.d.ts +4 -0
  72. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  73. package/dist/ibl/kernel-cache.d.ts +36 -0
  74. package/dist/ibl/kernel-cache.d.ts.map +1 -0
  75. package/dist/index.d.ts +35 -4
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.mjs +4 -4
  78. package/dist/index.mjs.map +1 -1
  79. package/dist/inspection-types.d.ts +51 -0
  80. package/dist/inspection-types.d.ts.map +1 -1
  81. package/dist/instances-derived-bounds.d.ts +50 -0
  82. package/dist/instances-derived-bounds.d.ts.map +1 -0
  83. package/dist/internal.mjs +1 -1
  84. package/dist/light-buffer-layout.d.ts +30 -13
  85. package/dist/light-buffer-layout.d.ts.map +1 -1
  86. package/dist/mesh-material-bindings.d.ts +54 -0
  87. package/dist/mesh-material-bindings.d.ts.map +1 -1
  88. package/dist/pbr-pipeline.d.ts +20 -7
  89. package/dist/pbr-pipeline.d.ts.map +1 -1
  90. package/dist/pipeline/standard-forward-lane.d.ts +2 -1
  91. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  92. package/dist/pipeline/standard-lighting/errors.d.ts +4 -0
  93. package/dist/pipeline/standard-lighting/errors.d.ts.map +1 -0
  94. package/dist/pipeline/standard-lighting/graph.d.ts +24 -0
  95. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -0
  96. package/dist/pipeline/standard-lighting/inspection.d.ts +38 -0
  97. package/dist/pipeline/standard-lighting/inspection.d.ts.map +1 -0
  98. package/dist/pipeline/standard-lighting/layout.d.ts +28 -0
  99. package/dist/pipeline/standard-lighting/layout.d.ts.map +1 -0
  100. package/dist/pipeline/standard-lighting/light-frame.d.ts +33 -0
  101. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -0
  102. package/dist/pipeline/standard-lighting/membership.d.ts +14 -0
  103. package/dist/pipeline/standard-lighting/membership.d.ts.map +1 -0
  104. package/dist/pipeline/standard-lighting/prepare.d.ts +16 -0
  105. package/dist/pipeline/standard-lighting/prepare.d.ts.map +1 -0
  106. package/dist/pipeline/standard-lighting/topology.d.ts +45 -0
  107. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -0
  108. package/dist/pipeline/standard-lighting/transport.d.ts +36 -0
  109. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -0
  110. package/dist/pipeline/standard-pipeline.d.ts +4 -13
  111. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  112. package/dist/pipeline/standard-post.d.ts.map +1 -1
  113. package/dist/pipeline/standard-profile.d.ts +12 -20
  114. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  115. package/dist/pipeline-spec.d.ts +40 -7
  116. package/dist/pipeline-spec.d.ts.map +1 -1
  117. package/dist/point-shadow-inspection.d.ts +21 -0
  118. package/dist/point-shadow-inspection.d.ts.map +1 -0
  119. package/dist/record/frame-lighting.d.ts +29 -32
  120. package/dist/record/frame-lighting.d.ts.map +1 -1
  121. package/dist/record/frame-snapshot.d.ts +65 -15
  122. package/dist/record/frame-snapshot.d.ts.map +1 -1
  123. package/dist/record/frame-targets.d.ts +5 -0
  124. package/dist/record/frame-targets.d.ts.map +1 -1
  125. package/dist/record/frame.d.ts +12 -5
  126. package/dist/record/frame.d.ts.map +1 -1
  127. package/dist/record/gpu-pass-timing/contract.d.ts +96 -0
  128. package/dist/record/gpu-pass-timing/contract.d.ts.map +1 -0
  129. package/dist/record/gpu-pass-timing/errors.d.ts +18 -0
  130. package/dist/record/gpu-pass-timing/errors.d.ts.map +1 -0
  131. package/dist/record/gpu-pass-timing/index.d.ts +8 -0
  132. package/dist/record/gpu-pass-timing/index.d.ts.map +1 -0
  133. package/dist/record/gpu-pass-timing/parser.d.ts +11 -0
  134. package/dist/record/gpu-pass-timing/parser.d.ts.map +1 -0
  135. package/dist/record/gpu-pass-timing/session.d.ts +43 -0
  136. package/dist/record/gpu-pass-timing/session.d.ts.map +1 -0
  137. package/dist/record/gpu-timing.d.ts +37 -0
  138. package/dist/record/gpu-timing.d.ts.map +1 -0
  139. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  140. package/dist/record/main-pass-material.d.ts +23 -2
  141. package/dist/record/main-pass-material.d.ts.map +1 -1
  142. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  143. package/dist/record/main-pass.d.ts +2 -0
  144. package/dist/record/main-pass.d.ts.map +1 -1
  145. package/dist/record/render-context.d.ts +48 -3
  146. package/dist/record/render-context.d.ts.map +1 -1
  147. package/dist/record/shadow-pass.d.ts.map +1 -1
  148. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  149. package/dist/record/typed-frame-graph.d.ts +81 -5
  150. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  151. package/dist/record/view-ubo.d.ts +7 -4
  152. package/dist/record/view-ubo.d.ts.map +1 -1
  153. package/dist/reflection/filter.d.ts +22 -0
  154. package/dist/reflection/filter.d.ts.map +1 -0
  155. package/dist/reflection/gpu-table.d.ts +25 -0
  156. package/dist/reflection/gpu-table.d.ts.map +1 -0
  157. package/dist/reflection/projection.d.ts +91 -0
  158. package/dist/reflection/projection.d.ts.map +1 -0
  159. package/dist/render-contract.d.ts +114 -7
  160. package/dist/render-contract.d.ts.map +1 -1
  161. package/dist/render-data.d.ts +10 -6
  162. package/dist/render-data.d.ts.map +1 -1
  163. package/dist/render-graph-primitives.d.ts +1 -0
  164. package/dist/render-graph-primitives.d.ts.map +1 -1
  165. package/dist/render-pipeline.d.ts +78 -2
  166. package/dist/render-pipeline.d.ts.map +1 -1
  167. package/dist/render-system-extract.d.ts +113 -20
  168. package/dist/render-system-extract.d.ts.map +1 -1
  169. package/dist/render-system.d.ts +24 -3
  170. package/dist/render-system.d.ts.map +1 -1
  171. package/dist/renderer-inspect.d.ts +29 -0
  172. package/dist/renderer-inspect.d.ts.map +1 -0
  173. package/dist/renderer.d.ts +43 -0
  174. package/dist/renderer.d.ts.map +1 -0
  175. package/dist/scene/render-scene.d.ts +14 -0
  176. package/dist/scene/render-scene.d.ts.map +1 -1
  177. package/dist/shadow-atlas.d.ts +3 -3
  178. package/dist/shadow-atlas.d.ts.map +1 -1
  179. package/dist/ssao-buffers.d.ts +3 -0
  180. package/dist/ssao-buffers.d.ts.map +1 -1
  181. package/dist/targets/contracts.d.ts +66 -0
  182. package/dist/targets/contracts.d.ts.map +1 -0
  183. package/dist/targets/graph-projection.d.ts +29 -0
  184. package/dist/targets/graph-projection.d.ts.map +1 -0
  185. package/dist/targets/material-source.d.ts +34 -0
  186. package/dist/targets/material-source.d.ts.map +1 -0
  187. package/dist/targets/owner.d.ts +32 -0
  188. package/dist/targets/owner.d.ts.map +1 -0
  189. package/dist/targets/physical.d.ts +18 -0
  190. package/dist/targets/physical.d.ts.map +1 -0
  191. package/dist/targets/readback.d.ts +39 -0
  192. package/dist/targets/readback.d.ts.map +1 -0
  193. package/dist/temporal/frame.d.ts +44 -0
  194. package/dist/temporal/frame.d.ts.map +1 -0
  195. package/dist/temporal/index.mjs +3 -3
  196. package/dist/typed-render-graph-primitives.d.ts +6 -1
  197. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  198. package/dist/typed-shadow-atlas.d.ts +10 -0
  199. package/dist/typed-shadow-atlas.d.ts.map +1 -0
  200. package/dist/typed-shadow-passes.d.ts +1 -1
  201. package/dist/typed-shadow-passes.d.ts.map +1 -1
  202. package/dist/volume/capability.d.ts +57 -0
  203. package/dist/volume/capability.d.ts.map +1 -0
  204. package/dist/volume/component.d.ts +61 -0
  205. package/dist/volume/component.d.ts.map +1 -0
  206. package/dist/volume/extract.d.ts +11 -0
  207. package/dist/volume/extract.d.ts.map +1 -0
  208. package/dist/volume/inspection.d.ts +38 -0
  209. package/dist/volume/inspection.d.ts.map +1 -0
  210. package/dist/volume/passes.d.ts +38 -0
  211. package/dist/volume/passes.d.ts.map +1 -0
  212. package/dist/volume/recovery.d.ts +38 -0
  213. package/dist/volume/recovery.d.ts.map +1 -0
  214. package/dist/volume/resources.d.ts +57 -0
  215. package/dist/volume/resources.d.ts.map +1 -0
  216. package/dist/volume/temporal.d.ts +35 -0
  217. package/dist/volume/temporal.d.ts.map +1 -0
  218. package/package.json +20 -20
  219. package/src/__tests__/bloom-exact-zero.unit.test.ts +48 -19
  220. package/src/__tests__/bloom-lifecycle.integration.test.ts +41 -13
  221. package/src/__tests__/camera-target-consumption.unit.test.ts +53 -0
  222. package/src/__tests__/cluster-binner.unit.test.ts +35 -0
  223. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +7 -2
  224. package/src/__tests__/cube-camera-scheduler.unit.test.ts +70 -0
  225. package/src/__tests__/cube-camera-single-submit.perf.test.ts +75 -0
  226. package/src/__tests__/cube-camera-view-matrices.unit.test.ts +41 -0
  227. package/src/__tests__/cube-camera-vocabulary.unit.test.ts +47 -0
  228. package/src/__tests__/cube-camera.integration.test.ts +42 -0
  229. package/src/__tests__/device-feature-admission.unit.test.ts +73 -0
  230. package/src/__tests__/direct-light-snapshot.unit.test.ts +3 -3
  231. package/src/__tests__/docs-gate-green.unit.test.ts +38 -0
  232. package/src/__tests__/factory-contract.integration.test.ts +255 -29
  233. package/src/__tests__/fog-component.unit.test.ts +67 -0
  234. package/src/__tests__/fog.integration.test.ts +86 -0
  235. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +37 -0
  236. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +10 -9
  237. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +17 -1
  238. package/src/__tests__/gpu-driven-production.integration.test.ts +16 -17
  239. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +12 -4
  240. package/src/__tests__/gpu-pass-timing-browser-runner.ts +281 -0
  241. package/src/__tests__/gpu-pass-timing-coexistence.integration.test.ts +80 -0
  242. package/src/__tests__/gpu-pass-timing-contract.unit.test.ts +243 -0
  243. package/src/__tests__/gpu-pass-timing-exact-zero.unit.test.ts +131 -0
  244. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +230 -0
  245. package/src/__tests__/gpu-pass-timing-observation.unit.test.ts +233 -0
  246. package/src/__tests__/gpu-pass-timing-session.unit.test.ts +438 -0
  247. package/src/__tests__/gpu-pass-timing.browser.test.ts +26 -0
  248. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +125 -0
  249. package/src/__tests__/hdrp-group2-visibility.unit.test.ts +24 -0
  250. package/src/__tests__/instances-culling.integration.test.ts +94 -0
  251. package/src/__tests__/instances-derived-bounds.unit.test.ts +118 -0
  252. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +14 -14
  253. package/src/__tests__/material-observation.integration.test.ts +101 -0
  254. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
  255. package/src/__tests__/pbr-view-layout.unit.test.ts +16 -0
  256. package/src/__tests__/point-shadow-inspection.unit.test.ts +60 -0
  257. package/src/__tests__/public-surface.test-d.ts +10 -0
  258. package/src/__tests__/reflection-probe-authoring.unit.test.ts +71 -0
  259. package/src/__tests__/reflection-probe-filter.unit.test.ts +54 -0
  260. package/src/__tests__/reflection-probe-ibl-topology.unit.test.ts +45 -0
  261. package/src/__tests__/reflection-probe-pass-encode.integration.test.ts +101 -0
  262. package/src/__tests__/reflection-probe-performance.perf.test.ts +60 -0
  263. package/src/__tests__/reflection-probe-recovery.integration.test.ts +55 -0
  264. package/src/__tests__/reflection-probe-selection.unit.test.ts +46 -0
  265. package/src/__tests__/render-error-code-owner.test-d.ts +23 -3
  266. package/src/__tests__/render-feature-docs.unit.test.ts +1 -1
  267. package/src/__tests__/render-target-admission.unit.test.ts +80 -0
  268. package/src/__tests__/render-target-graph-projection.unit.test.ts +102 -0
  269. package/src/__tests__/render-target-inspection.unit.test.ts +52 -0
  270. package/src/__tests__/render-target-material-source.test-d.ts +30 -0
  271. package/src/__tests__/render-target-material-source.unit.test.ts +97 -0
  272. package/src/__tests__/render-target-owner-placement.unit.test.ts +203 -0
  273. package/src/__tests__/render-target-owner.unit.test.ts +122 -0
  274. package/src/__tests__/render-target-probe-soak.perf.test.ts +94 -0
  275. package/src/__tests__/render-target-public-schema.json +107 -0
  276. package/src/__tests__/render-target-public-schema.unit.test.ts +91 -0
  277. package/src/__tests__/render-target-public-surface.test-d.ts +83 -0
  278. package/src/__tests__/render-target-readback.integration.test.ts +90 -0
  279. package/src/__tests__/render-target-recovery.integration.test.ts +85 -0
  280. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +76 -0
  281. package/src/__tests__/renderer-frame-transaction.integration.test.ts +1 -1
  282. package/src/__tests__/renderer-lifecycle.integration.test.ts +14 -0
  283. package/src/__tests__/shader-stage-surface.unit.test.ts +19 -4
  284. package/src/__tests__/skin-motion-regression.unit.test.ts +132 -0
  285. package/src/__tests__/skinned-shadow-caster.test.ts +32 -2
  286. package/src/__tests__/standard-cluster-capability.integration.test.ts +63 -0
  287. package/src/__tests__/standard-cluster-transport.unit.test.ts +121 -0
  288. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +232 -0
  289. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +8 -5
  290. package/src/__tests__/standard-lighting-inspection.integration.test.ts +138 -0
  291. package/src/__tests__/standard-lighting-ssot.unit.test.ts +18 -0
  292. package/src/__tests__/standard-lighting-write.unit.test.ts +377 -0
  293. package/src/__tests__/standard-lighting.integration.test.ts +67 -0
  294. package/src/__tests__/standard-pipeline.dawn.test.ts +206 -5
  295. package/src/__tests__/standard-pipeline.integration.test.ts +181 -150
  296. package/src/__tests__/standard-probe-binding.unit.test.ts +25 -0
  297. package/src/__tests__/standard-profile-oracle.unit.test.ts +23 -36
  298. package/src/__tests__/typed-pipeline-topology.unit.test.ts +395 -14
  299. package/src/__tests__/urp-pipeline-targets.unit.test.ts +2 -2
  300. package/src/__tests__/visibility-resolver.unit.test.ts +15 -0
  301. package/src/__tests__/volume-optics-oracle.unit.test.ts +85 -0
  302. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +655 -0
  303. package/src/__tests__/volumetric-fog-component.unit.test.ts +71 -0
  304. package/src/__tests__/volumetric-fog-composite-stage-readback.dawn.test.ts +376 -0
  305. package/src/__tests__/volumetric-fog-errors.unit.test.ts +51 -0
  306. package/src/__tests__/volumetric-fog-inspection.unit.test.ts +77 -0
  307. package/src/__tests__/volumetric-fog-light-capability.unit.test.ts +19 -0
  308. package/src/__tests__/volumetric-fog-light-consumers.integration.test.ts +18 -0
  309. package/src/__tests__/volumetric-fog-light-errors.unit.test.ts +17 -0
  310. package/src/__tests__/volumetric-fog-light-topology.unit.test.ts +21 -0
  311. package/src/__tests__/volumetric-fog-light-transaction.integration.test.ts +53 -0
  312. package/src/__tests__/volumetric-fog-m9-watcher.integration.test.ts +27 -0
  313. package/src/__tests__/volumetric-fog-point-light-owner.integration.test.ts +44 -0
  314. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +49 -0
  315. package/src/__tests__/volumetric-fog-point-spot-contract.unit.test.ts +28 -0
  316. package/src/__tests__/volumetric-fog-point-spot-lkg.integration.test.ts +65 -0
  317. package/src/__tests__/volumetric-fog-point-spot-selection.integration.test.ts +49 -0
  318. package/src/__tests__/volumetric-fog-point-spot-temporal.unit.test.ts +55 -0
  319. package/src/__tests__/volumetric-fog-projector-contract.unit.test.ts +22 -0
  320. package/src/__tests__/volumetric-fog-projector-inspection.unit.test.ts +12 -0
  321. package/src/__tests__/volumetric-fog-projector-surface-volume.integration.test.ts +11 -0
  322. package/src/__tests__/volumetric-fog-projector-transaction.integration.test.ts +26 -0
  323. package/src/__tests__/volumetric-fog-recovery.integration.test.ts +47 -0
  324. package/src/__tests__/volumetric-fog-selected-light-resolution.integration.test.ts +63 -0
  325. package/src/__tests__/volumetric-fog-spot-closure.integration.test.ts +26 -0
  326. package/src/__tests__/volumetric-fog-spot-contract.test-d.ts +21 -0
  327. package/src/__tests__/volumetric-fog-spot-contract.unit.test.ts +27 -0
  328. package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +14 -0
  329. package/src/__tests__/volumetric-fog-spot-optics.integration.test.ts +29 -0
  330. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +550 -0
  331. package/src/__tests__/volumetric-fog-temporal-selected-light.unit.test.ts +33 -0
  332. package/src/__tests__/volumetric-fog-temporal.unit.test.ts +45 -0
  333. package/src/__tests__/volumetric-fog-topology.unit.test.ts +237 -0
  334. package/src/__tests__/volumetric-fog-transparent.integration.test.ts +27 -0
  335. package/src/assembly/device-feature-admission.ts +73 -0
  336. package/src/assembly/directional-shadow-inspection.ts +122 -0
  337. package/src/assembly/factory.ts +1093 -246
  338. package/src/assembly/host-contract.ts +25 -0
  339. package/src/assembly/render-target-host.ts +847 -0
  340. package/src/assets/asset-decoders.ts +1 -1
  341. package/src/assets/render-asset-port.ts +7 -1
  342. package/src/capture/cube-views.ts +69 -0
  343. package/src/capture/scheduler.ts +265 -0
  344. package/src/components/__tests__/schema-contract.unit.test.ts +12 -0
  345. package/src/components/__tests__/schema-parity.unit.test.ts +9 -0
  346. package/src/components/camera.ts +4 -0
  347. package/src/components/cube-camera.ts +52 -0
  348. package/src/components/directional-light.ts +22 -2
  349. package/src/components/directional-shadow-filter.ts +57 -0
  350. package/src/components/index.ts +2 -0
  351. package/src/components/light-helpers.ts +85 -7
  352. package/src/components/reflection-probe.ts +49 -0
  353. package/src/components/spot-light.ts +22 -3
  354. package/src/device/gpu-residency.ts +168 -58
  355. package/src/environment/frame.ts +6 -24
  356. package/src/environment/lifecycle.ts +12 -0
  357. package/src/errors/render.ts +405 -143
  358. package/src/extract/directional-shadow-projection.ts +61 -0
  359. package/src/extract/environment.ts +57 -0
  360. package/src/extract/visibility.ts +25 -1
  361. package/src/features/fullscreen.ts +3 -0
  362. package/src/features/targets.ts +4 -1
  363. package/src/features/types.ts +10 -0
  364. package/src/fullscreen-post-process-pass.ts +43 -2
  365. package/src/gpu-driven/batch-topology.ts +1 -0
  366. package/src/gpu-driven/production-raster.ts +1 -3
  367. package/src/gpu-scene.ts +8 -1
  368. package/src/gpu-usage.ts +2 -0
  369. package/src/hdrp-buffers.ts +165 -59
  370. package/src/ibl/IblPipelineCache.ts +11 -0
  371. package/src/ibl/kernel-cache.ts +88 -0
  372. package/src/index.ts +186 -1
  373. package/src/inspection-types.ts +56 -0
  374. package/src/instances-derived-bounds.ts +171 -0
  375. package/src/light-buffer-layout.ts +74 -26
  376. package/src/mesh-material-bindings.ts +132 -0
  377. package/src/pbr-pipeline.ts +139 -27
  378. package/src/pipeline/standard-forward-lane.ts +65 -15
  379. package/src/pipeline/standard-lighting/errors.ts +6 -0
  380. package/src/pipeline/standard-lighting/graph.ts +157 -0
  381. package/src/pipeline/standard-lighting/inspection.ts +88 -0
  382. package/src/pipeline/standard-lighting/layout.ts +31 -0
  383. package/src/pipeline/standard-lighting/light-frame.ts +34 -0
  384. package/src/pipeline/standard-lighting/membership.ts +43 -0
  385. package/src/pipeline/standard-lighting/prepare.ts +37 -0
  386. package/src/pipeline/standard-lighting/topology.ts +120 -0
  387. package/src/pipeline/standard-lighting/transport.ts +77 -0
  388. package/src/pipeline/standard-pipeline.ts +141 -192
  389. package/src/pipeline/standard-post.ts +26 -7
  390. package/src/pipeline/standard-profile.ts +23 -30
  391. package/src/pipeline-spec.ts +84 -21
  392. package/src/point-shadow-inspection.ts +55 -0
  393. package/src/record/__tests__/temporal-payload.unit.test.ts +42 -1
  394. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +73 -0
  395. package/src/record/__tests__/view-ubo-layout.unit.test.ts +40 -0
  396. package/src/record/frame-lighting.ts +470 -516
  397. package/src/record/frame-snapshot.ts +76 -28
  398. package/src/record/frame-targets.ts +23 -2
  399. package/src/record/frame.ts +1169 -79
  400. package/src/record/gpu-pass-timing/contract.ts +180 -0
  401. package/src/record/gpu-pass-timing/errors.ts +34 -0
  402. package/src/record/gpu-pass-timing/index.ts +37 -0
  403. package/src/record/gpu-pass-timing/parser.ts +97 -0
  404. package/src/record/gpu-pass-timing/session.ts +938 -0
  405. package/src/record/gpu-timing.ts +233 -0
  406. package/src/record/main-pass-geometry.ts +120 -80
  407. package/src/record/main-pass-material.ts +101 -11
  408. package/src/record/main-pass-sprite-draws.ts +54 -25
  409. package/src/record/main-pass.ts +71 -15
  410. package/src/record/render-context.ts +77 -0
  411. package/src/record/shadow-pass.ts +104 -66
  412. package/src/record/skybox-post-pass.ts +20 -4
  413. package/src/record/typed-frame-graph.ts +1016 -32
  414. package/src/record/view-ubo.ts +63 -28
  415. package/src/reflection/filter.ts +81 -0
  416. package/src/reflection/gpu-table.ts +44 -0
  417. package/src/reflection/projection.ts +281 -0
  418. package/src/render-contract.ts +147 -4
  419. package/src/render-data.ts +50 -22
  420. package/src/render-graph-primitives.ts +9 -4
  421. package/src/render-pipeline.ts +161 -2
  422. package/src/render-system-extract.ts +886 -103
  423. package/src/render-system.ts +1174 -66
  424. package/src/renderer-inspect.ts +66 -0
  425. package/src/renderer.ts +222 -0
  426. package/src/scene/render-scene.ts +117 -9
  427. package/src/shadow-atlas.ts +5 -5
  428. package/src/ssao-buffers.ts +9 -2
  429. package/src/targets/contracts.ts +182 -0
  430. package/src/targets/graph-projection.ts +169 -0
  431. package/src/targets/material-source.ts +143 -0
  432. package/src/targets/owner.ts +214 -0
  433. package/src/targets/physical.ts +18 -0
  434. package/src/targets/readback.ts +153 -0
  435. package/src/temporal/frame.ts +128 -0
  436. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +61 -23
  437. package/src/typed-render-graph-primitives.ts +34 -2
  438. package/src/typed-shadow-atlas.ts +17 -0
  439. package/src/typed-shadow-passes.ts +47 -40
  440. package/src/volume/capability.ts +173 -0
  441. package/src/volume/component.ts +110 -0
  442. package/src/volume/extract.ts +21 -0
  443. package/src/volume/inspection.ts +78 -0
  444. package/src/volume/passes.ts +686 -0
  445. package/src/volume/recovery.ts +86 -0
  446. package/src/volume/resources.ts +236 -0
  447. package/src/volume/temporal.ts +89 -0
  448. package/dist/chunk-IPXCABEP.mjs.map +0 -1
  449. package/dist/chunk-LOSO4CTV.mjs.map +0 -1
  450. package/dist/chunk-RJRUS55L.mjs.map +0 -1
  451. package/dist/chunk-YTQ5C7YQ.mjs +0 -584
  452. package/dist/chunk-YTQ5C7YQ.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,
@@ -80,33 +103,59 @@ import {
80
103
  import { createPointsLinesLaneAdapter, type PointsLinesBackend } from './points-lines/record';
81
104
  import type { PointsLinesRetainedSnapshot } from './points-lines/snapshot';
82
105
  import type { PointsLinesRecordOwner, PointsLinesRecordSubmission } from './record/render-context';
106
+ import { inspectVolumetricFog } from './volume/inspection';
83
107
 
84
108
  export type { RenderSceneInspection } from './inspection-types';
85
109
 
86
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';
87
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';
88
125
  import {
89
126
  GPU_BUFFER_USAGE_COPY_DST,
90
127
  GPU_BUFFER_USAGE_INDEX,
91
128
  GPU_BUFFER_USAGE_UNIFORM,
92
129
  GPU_BUFFER_USAGE_VERTEX,
93
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';
94
139
  import { assembleMaterialWithSkylightEntries } from './ibl/skylight-bind-group';
95
140
  import { disposeInstanceBuffers, disposeTransientInstanceBuffers } from './instance-buffer-cache';
96
- import type { MeshMaterialBindingObservation } from './mesh-material-bindings';
97
- import { validateClusterGrid } from './pipeline/standard-pipeline';
98
141
  import {
99
- DEFAULT_CLUSTER_GRID,
100
- DEFAULT_STANDARD_PROFILE,
101
- resolveStandardLane,
102
- } 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';
103
150
  import { PipelineSpecError } from './pipeline-spec';
151
+ import type { PointShadowInspection } from './point-shadow-inspection';
104
152
  import { PostProcessError } from './post-process-errors';
105
153
  import {
106
154
  createPreparedGraphicsResolver,
107
155
  type PreparedGraphicsResolver,
108
156
  } from './prepare/prepared-graphics-resolver';
109
157
  import {
158
+ type CubeCaptureFrameInput,
110
159
  type FrameObservation,
111
160
  type FrameObservationOptions,
112
161
  observeCurrentFrame,
@@ -120,12 +169,35 @@ import type {
120
169
  RenderFrameState,
121
170
  } from './record/frame-snapshot';
122
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';
178
+ import type { GpuTimingCapture } from './record/gpu-timing';
123
179
  import { applyParamSnapshotToUbo, residentTextureView } from './record/main-pass-material';
124
- 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';
125
182
  import {
126
183
  type RenderFeatureGraphCandidate,
127
184
  resetRenderFeatureGraphState,
185
+ settleVolumetricFogGraphCandidate,
128
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';
129
201
  import {
130
202
  type CameraSnapshot,
131
203
  type DrawOwnerOptions,
@@ -134,7 +206,13 @@ import {
134
206
  type RenderPhaseSkipReason,
135
207
  type RenderRecordPhase,
136
208
  } from './render-contract';
137
- 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';
138
216
  import { extractFrames } from './render-system-extract';
139
217
  import { PersistentRenderScene } from './scene/render-scene';
140
218
  import { resolveSsaoParameters } from './ssao-config';
@@ -145,6 +223,8 @@ import {
145
223
  TRANSPARENT_SORT_MODE_LAYER_YZ,
146
224
  TRANSPARENT_SORT_MODE_LAYER_Z,
147
225
  } from './systems/transparent-sort-config';
226
+ import type { RenderTarget } from './targets/contracts';
227
+ import { createTemporalFrameTransaction, type TemporalFrame } from './temporal/frame';
148
228
  import { retireTemporalGpuState } from './temporal/gpu';
149
229
  import { inspectTemporal, type TemporalInspection } from './temporal/inspection';
150
230
  import {
@@ -159,6 +239,21 @@ import {
159
239
  inspectTransmissionFromAdmission,
160
240
  } from './transmission/inspection';
161
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
+
162
257
  export type {
163
258
  _InternalRenderPipelineContext,
164
259
  _StandardForwardSceneView,
@@ -325,6 +420,7 @@ export interface RenderSystem {
325
420
  worlds: readonly World[],
326
421
  opts: DrawOwnerOptions,
327
422
  renderReadLeases?: readonly RenderReadLease[],
423
+ timingCapture?: GpuTimingCapture,
328
424
  ): boolean;
329
425
  /** Release renderer-owned persistent state for one detached World. */
330
426
  detachScene(world: World): void;
@@ -349,6 +445,9 @@ export interface RenderSystem {
349
445
  readonly visibilityStats: { explicitlyHidden: number };
350
446
  /** Persistent scene maintenance evidence from the ordinary single-World path. */
351
447
  readonly renderScene: RenderSceneInspection;
448
+ readonly reflectionProbes: ReflectionProbeInspection;
449
+ /** True while a progressive CubeCamera candidate still owns unfinished faces. */
450
+ isCubeCapturePending(target: RenderTarget): boolean;
352
451
  readonly environment: EnvironmentInspection;
353
452
  readonly temporal: TemporalInspection;
354
453
  readonly bloom: BloomInspection;
@@ -356,6 +455,12 @@ export interface RenderSystem {
356
455
  readonly temporalTargetInspection: TemporalTargetInspection | undefined;
357
456
  /** Last completed transmission candidate facts, detached from GPU handles. */
358
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;
359
464
  /** Retained Points/Lines authoring facts from the single scene projection. */
360
465
  readonly pointsLinesSnapshots: readonly PointsLinesRetainedSnapshot[];
361
466
  /** Current mesh-slot provenance and active diagnostics from the last frame. */
@@ -392,6 +497,10 @@ export interface RenderSystem {
392
497
  readonly createBindGroup: number;
393
498
  readonly keys: readonly string[];
394
499
  };
500
+ /** Latest accepted-submit temporal POD; absent until the first accepted frame. */
501
+ readonly temporalFrame: TemporalFrame | undefined;
502
+ /** Current renderer-owned volumetric fog inspection snapshot. */
503
+ readonly volumetricFog: import('./volume/inspection').VolumetricFogInspection;
395
504
  /** Configure the sole Standard graph owner before the first frame. */
396
505
  configureStandard(config: RenderPipelineAsset['config']): void;
397
506
  /** Register one engine-owned post-process shader used by the Standard lane. */
@@ -483,6 +592,175 @@ type PointsLinesGpuResources = {
483
592
  };
484
593
  type PointsLinesPreparationAdapter = PointsLinesLanePreparationAdapter<PointsLinesGpuResources>;
485
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
+
486
764
  /**
487
765
  * The Standard renderer's single Points/Lines prepare owner.
488
766
  *
@@ -744,6 +1022,551 @@ class StandardPointsLinesOwner implements PointsLinesRecordOwner {
744
1022
  }
745
1023
  }
746
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
+
747
1570
  export function createRenderSystem(internals: RenderSystemInternals): RenderSystem {
748
1571
  const phaseCatalogRegistration = internals.profiler?.registerPhaseCatalog(
749
1572
  'render',
@@ -754,6 +1577,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
754
1577
  let preparedWorlds: readonly World[] = [];
755
1578
  let latestCamera: CameraSnapshot | undefined;
756
1579
  const pointsLinesOwner = new StandardPointsLinesOwner(internals);
1580
+ const reflectionProbeOwner = new ReflectionProbeRecordOwner(internals);
1581
+ const cubeCaptureScheduler = createCubeCaptureScheduler({ maxFacesPerFrame: 1 });
1582
+ const cubeCaptureState: CubeCaptureGraphState = { work: [] };
757
1583
  const persistentRenderScene = new PersistentRenderScene({
758
1584
  getDevice: () => internals.device,
759
1585
  onGpuError: (error) => internals.errorRegistry.fire(error),
@@ -949,10 +1775,27 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
949
1775
  graphGeneration: 0,
950
1776
  successfulTemporalFrameIndex: 0,
951
1777
  lastSuccessfulBloom: 'off',
1778
+ temporalFrameTransaction: createTemporalFrameTransaction({ deviceEpoch: 0 }),
1779
+ temporalFrameInput: undefined,
952
1780
  directionalShadowCache: null,
953
1781
  directionalShadowCacheRecorded: false,
954
1782
  compiledFrameGraph: null,
955
1783
  compiledFrameGraphTopologyKey: null,
1784
+ compiledFrameGraphGeneration: 0,
1785
+ volumetricFogPreviousGraph: null,
1786
+ volumetricFogPreviousGraphKey: null,
1787
+ volumetricFogCandidateGraph: null,
1788
+ volumetricFogParamsBuffers: [null, null],
1789
+ volumetricFogParamsPendingSlot: null,
1790
+ volumetricFogParamsAcceptedSlot: null,
1791
+ volumetricFogAcceptedParams: undefined,
1792
+ volumetricFogPendingParams: undefined,
1793
+ volumetricFogInspection: inspectVolumetricFog({ authored: false, capability: 'available' }),
1794
+ volumetricFogAccepted: undefined,
1795
+ volumetricFogAcceptedContext: undefined,
1796
+ volumetricFogHistoryGraph: null,
1797
+ volumetricFogHistorySlot: null,
1798
+ volumetricFogHistorySignature: null,
956
1799
  retiredCompiledFrameGraphs: new Set(),
957
1800
  currentFrameObservationSource: undefined,
958
1801
  lastSuccessfulCameraAntialias: undefined,
@@ -970,7 +1813,10 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
970
1813
  hdrpClusterBinScratch: createClusterBinScratch(),
971
1814
  hdrpClusterGridScratch: null,
972
1815
  hdrpLightIndexListScratch: null,
973
- hdrpClusterMembershipBindGroup: null,
1816
+ hdrpClusterMembership: null,
1817
+ standardLightingGraphSignature: '',
1818
+ standardLightingInspection: undefined,
1819
+ pointShadowInspection: undefined,
974
1820
  transientInstanceBuffers: [],
975
1821
  warnedZeroLightStandard: false,
976
1822
  warnedMultiLightDirectional: false,
@@ -1026,10 +1872,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1026
1872
  // (its topology is frame-invariant). Standard configuration overwrites this with the
1027
1873
  // resolved asset config on every swap so the active graph reads its current config.
1028
1874
  installedPipelineConfig: undefined,
1029
- // feat-20260608-cluster-lighting M2 / w10 + M5 / w20: HDRP active flag + once-per-frame
1030
- // warn dedup set for hdrp-light-budget-exceeded / hdrp-index-list-overflow.
1031
- isHdrpActive: false,
1032
- hdrpOncePerFrameFired: new Set(),
1875
+ standardOncePerFrameFired: new Set(),
1033
1876
  // feat-20260612-point-light-shadows-urp-hdrp M3 / T-M3-2 (plan-strategy §D-1):
1034
1877
  // cube_array shadow atlas + per-frame snapshot list. Atlas is null until the
1035
1878
  // first frame whose extracted lights.pointShadow is non-empty (zero-shadow
@@ -1048,6 +1891,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1048
1891
  spotShadowSnapshots: [],
1049
1892
  };
1050
1893
  let directFrameId = 0;
1894
+ let lastDirectionalShadowLights: ExtractedLights | undefined;
1895
+ let lastDirectionalShadowCandidate: 'accepted' | 'failed' = 'failed';
1896
+ let lastDirectionalShadowError: DirectionalShadowInspectionSource['error'];
1051
1897
 
1052
1898
  function beginProfilePhase(session: RecorderSession | undefined, phase: RenderPhase): boolean {
1053
1899
  if (session === undefined) return false;
@@ -1108,66 +1954,124 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1108
1954
  let activeFeaturePostProcessEntries: ReadonlyMap<string, PostProcessShaderEntry> = new Map();
1109
1955
  const lookupPostProcess = (id: string): PostProcessShaderEntry | undefined =>
1110
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
+ };
1111
1963
  // feat-20260621 M-A2 / w8: expose the eager-created per-id params UBO through
1112
1964
  // the narrow runtime surface so dispatchFullscreenPass can writeBuffer the
1113
1965
  // per-frame snapshot + bind it at group(1) binding(2).
1114
1966
  const getPostProcessParamsBuffer = (id: string): Buffer | undefined =>
1115
1967
  postProcessParamsBuffers.get(id);
1116
- 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 = (
1117
1981
  featureEntries: ReadonlyMap<string, PostProcessShaderEntry>,
1118
- ): void => {
1982
+ ): Result<PostProcessParamsCandidate, RhiError> => {
1119
1983
  const declarations = new Map<string, PostProcessShaderEntry>([
1120
1984
  ...builtinPostProcessEntries.entries(),
1121
1985
  ...featureEntries.entries(),
1122
1986
  ]);
1987
+ const next = new Map<string, Buffer>();
1988
+ const allocated: Buffer[] = [];
1989
+ const discardCreated = (): void => destroyPostProcessParams(allocated);
1123
1990
  for (const [id, entry] of declarations) {
1124
1991
  if (id === 'forgeax.taa-resolve') continue;
1125
- const byteSize =
1126
- entry.params?.byteSize ?? (entryHasDepthRead(entry) ? DEPTH_MIN_PARAMS_BYTE_SIZE : 0);
1127
- 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
+ }
1128
2004
  const created = internals.device.createBuffer({
1129
2005
  label: `post-process-params-${id}`,
1130
2006
  size: byteSize,
1131
2007
  usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
1132
2008
  mappedAtCreation: false,
1133
2009
  });
1134
- if (!created.ok) throw created.error;
2010
+ if (!created.ok) {
2011
+ discardCreated();
2012
+ return err(created.error);
2013
+ }
1135
2014
  const buffer = created.value;
2015
+ allocated.push(buffer);
1136
2016
  if (entry.params !== undefined) {
1137
2017
  const written = internals.device.queue.writeBuffer(buffer, 0, entry.params.defaultValue);
1138
2018
  if (!written.ok) {
1139
- internals.device.destroyBuffer(buffer);
1140
- throw written.error;
2019
+ discardCreated();
2020
+ return err(written.error);
1141
2021
  }
1142
2022
  }
1143
- postProcessParamsBuffers.set(id, buffer);
2023
+ next.set(id, buffer);
2024
+ }
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();
1144
2037
  }
1145
- for (const [id, buffer] of postProcessParamsBuffers) {
1146
- if (declarations.has(id)) continue;
1147
- internals.device.destroyBuffer(buffer);
1148
- postProcessParamsBuffers.delete(id);
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);
1149
2045
  }
2046
+ retirePostProcessParams(previous.filter((buffer) => !next.has(buffer)));
1150
2047
  };
1151
- // 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).
1152
2049
  // Solves CONCERN-1: dispatcher previously passed `pipeline=null` to
1153
2050
  // built.createHandle because per-frame execute closures cannot await async
1154
2051
  // shader compile. The cache here delegates the actual build to
1155
2052
  // `internals.buildPostProcessPipeline` (sync wrapper over the shared shader
1156
- // adapter; 1-frame warmup), then memoizes by `id|colorFormat`.
2053
+ // adapter; 1-frame warmup), then memoizes by declaration identity + format.
1157
2054
  const postProcessPipelineCache = new Map<string, RenderPipeline>();
1158
2055
  const getPostProcessPipeline = (
1159
2056
  id: string,
1160
2057
  bgl: BindGroupLayout,
1161
2058
  colorFormat: GPUTextureFormat,
2059
+ entryOverride?: PostProcessShaderEntry,
1162
2060
  ): RenderPipeline | null => {
1163
- 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}`;
1164
2065
  const cached = postProcessPipelineCache.get(key);
1165
2066
  if (cached !== undefined) return cached;
1166
- const entry = lookupPostProcess(id);
1167
- if (entry === undefined) return null;
1168
2067
  const factory = internals.buildPostProcessPipeline;
1169
2068
  if (factory === undefined) return null;
1170
- 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
+ );
1171
2075
  if (built === null) return null;
1172
2076
  postProcessPipelineCache.set(key, built);
1173
2077
  return built;
@@ -1177,6 +2081,14 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1177
2081
  if (key.startsWith(`${id}|`)) postProcessPipelineCache.delete(key);
1178
2082
  }
1179
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
+ };
1180
2092
  Object.assign(internals, {
1181
2093
  lookupPostProcess,
1182
2094
  getPostProcessParamsBuffer,
@@ -1351,7 +2263,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1351
2263
  if (buffer === undefined) return group;
1352
2264
  return ok({
1353
2265
  handle: group.value,
1354
- dynamicOffsets: [0],
2266
+ dynamicOffsets: [0, 0],
1355
2267
  release: () => internals.device.destroyBuffer(buffer),
1356
2268
  });
1357
2269
  };
@@ -1362,8 +2274,6 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1362
2274
  const preparedResolverFactory = (
1363
2275
  input: RenderFeaturePreparedGraphicsResolverInput,
1364
2276
  ): PreparedGraphicsResolver => {
1365
- activeFeaturePostProcessEntries = input.fullscreenEffects;
1366
- ensurePostProcessParamsResources(input.fullscreenEffects);
1367
2277
  return createPreparedGraphicsResolver({
1368
2278
  device: internals.device,
1369
2279
  featureIdentity: input.featureIdentity,
@@ -1387,6 +2297,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1387
2297
  descriptor.shader,
1388
2298
  fullscreen.bindGroupLayout,
1389
2299
  descriptor.colorFormats[0] as GPUTextureFormat,
2300
+ postProcessEntry,
1390
2301
  );
1391
2302
  if (pipeline !== null && pipeline !== undefined) {
1392
2303
  preparedPipelineIds.set(pipeline as object, descriptor.shader);
@@ -1555,6 +2466,12 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1555
2466
  gpuDriven: gpuDrivenProduction.inspect(),
1556
2467
  };
1557
2468
  },
2469
+ get reflectionProbes(): ReflectionProbeInspection {
2470
+ return reflectionProbeOwner.inspect();
2471
+ },
2472
+ isCubeCapturePending(target: RenderTarget): boolean {
2473
+ return cubeCaptureScheduler.inspect(target).candidateGeneration !== undefined;
2474
+ },
1558
2475
  get environment(): EnvironmentInspection {
1559
2476
  return environmentLifecycle.inspect();
1560
2477
  },
@@ -1647,6 +2564,49 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1647
2564
  get transmission(): TransmissionInspection | undefined {
1648
2565
  return lastTransmissionInspection;
1649
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
+ },
1650
2610
  get pointsLinesSnapshots(): readonly PointsLinesRetainedSnapshot[] {
1651
2611
  return persistentRenderScene.pointsLinesSnapshots();
1652
2612
  },
@@ -1657,6 +2617,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1657
2617
  worlds: readonly World[],
1658
2618
  opts: DrawOwnerOptions,
1659
2619
  renderReadLeases?: readonly RenderReadLease[],
2620
+ timingCapture?: GpuTimingCapture,
1660
2621
  ): boolean {
1661
2622
  preparedWorlds = worlds;
1662
2623
  const profileSession = internals.profiler?.activeSession();
@@ -1729,9 +2690,11 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1729
2690
  });
1730
2691
  const {
1731
2692
  cameras,
2693
+ cubeCameras,
1732
2694
  lights,
1733
2695
  environment,
1734
2696
  environmentReady,
2697
+ volumetricFog,
1735
2698
  renderables,
1736
2699
  dispatch,
1737
2700
  skylight,
@@ -1739,10 +2702,19 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1739
2702
  skybox,
1740
2703
  skyboxCount,
1741
2704
  fog,
2705
+ fogFailure,
1742
2706
  frustumStats,
1743
2707
  visibilityStats,
1744
2708
  postProcessParams,
1745
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
+ );
1746
2718
  latestCamera = cameras[0];
1747
2719
  const temporalDemanded =
1748
2720
  cameras[0]?.antialias === 'taa' || (cameras[0]?.motionBlur?.shutterAngle ?? 0) > 0;
@@ -1750,6 +2722,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1750
2722
  persistentRenderScene.prepareTemporalFrame(renderables);
1751
2723
  }
1752
2724
  environmentLifecycle.bindScope(internals.deviceScope);
2725
+ if (fogFailure !== undefined) {
2726
+ environmentLifecycle.recordSelectionFailure(fogFailure);
2727
+ }
1753
2728
  if (!environmentReady || environment === undefined) {
1754
2729
  frameState.pendingTemporalCommit = { kind: 'none' };
1755
2730
  // A world with no camera still has a valid clear-only render path.
@@ -1778,15 +2753,21 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1778
2753
  }
1779
2754
  if (lastMeshMaterialBindingFrame !== frame) {
1780
2755
  lastMeshMaterialBindingFrame = frame;
1781
- lastMeshMaterialBindings = renderables.map((renderable) => ({
1782
- worldId: renderable.worldId,
1783
- entityKey: renderable.entityKey,
1784
- bindings: renderable.materials.map((material, slotIndex) => ({
1785
- handle: material.materialHandle ?? 0,
1786
- source: renderable.materialBindingSources[slotIndex] ?? 'engine-default',
1787
- })),
1788
- diagnostics: renderable.materialBindingDiagnostics ?? [],
1789
- }));
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
+ });
1790
2771
  }
1791
2772
 
1792
2773
  bindGroupCounts.createBindGroup = 0;
@@ -1852,14 +2833,23 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1852
2833
  createPreparedGraphicsResolver: preparedResolverFactory,
1853
2834
  gpuWork: featureGpuWork,
1854
2835
  });
1855
- activeFeaturePostProcessEntries = featureFrame.fullscreenEffects;
1856
- ensurePostProcessParamsResources(featureFrame.fullscreenEffects);
1857
2836
  lastVisibilityStats.explicitlyHidden = featureFrame.hiddenEntityReports.length;
1858
2837
  for (const featureError of featureFrame.errors) {
1859
2838
  if (!isPendingRenderFeaturePreparation(featureError)) {
1860
2839
  internals.errorRegistry.fire(featureError);
1861
2840
  }
1862
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
+ }
1863
2853
  featureGraphCandidate = {
1864
2854
  plans: featureFrame.plans,
1865
2855
  fullscreenEffects: featureFrame.fullscreenEffects,
@@ -1873,6 +2863,29 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1873
2863
  internals.errorRegistry.fire(released.error);
1874
2864
  }
1875
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;
1876
2889
  },
1877
2890
  };
1878
2891
  return featureFrame.preparedResourceBatches;
@@ -1949,20 +2962,50 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1949
2962
  {
1950
2963
  owner: gpuDrivenProduction,
1951
2964
  scene: persistentRenderScene.compositionGpuDrivenState(),
1952
- onCoverage: (ownsAll) => {
1953
- const key = `${internals.gpuStore.meshResidencyEpoch}:${frameState.isHdrpActive ? 'hdrp' : 'urp'}`;
2965
+ onCoverage: (ownsAll, clusteredLighting) => {
2966
+ const key = `${internals.gpuStore.meshResidencyEpoch}:${clusteredLighting ? 'clustered' : 'standard'}`;
1954
2967
  persistentRenderScene.setCompositionGpuDrivenCoverage(key, ownsAll);
1955
2968
  },
1956
2969
  },
1957
2970
  renderReadLeases,
1958
2971
  featureGraphCandidate,
1959
2972
  pointsLinesOwner,
2973
+ {
2974
+ snapshots: cubeCameras,
2975
+ scheduler: cubeCaptureScheduler,
2976
+ state: cubeCaptureState,
2977
+ reflectionProbes: reflectionProbeRecord,
2978
+ } satisfies CubeCaptureFrameInput,
1960
2979
  environment?.environmentSignature ?? '',
1961
2980
  environment?.fogSignature ?? '',
1962
2981
  environmentReady && environment !== undefined,
1963
2982
  persistentRenderScene.transmissionTopologyDemand(),
2983
+ volumetricFog,
2984
+ timingCapture,
1964
2985
  ),
1965
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
+ }
1966
3009
  if (submitted) {
1967
3010
  const temporalCommit = persistentRenderScene.commitTemporalFrame();
1968
3011
  if (!temporalCommit.ok) {
@@ -2061,6 +3104,20 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2061
3104
  frameState.graphTargetCapture = request;
2062
3105
  },
2063
3106
  bindGroupCounts: bindGroupCounts,
3107
+ get temporalFrame(): TemporalFrame | undefined {
3108
+ const accepted = frameState.temporalFrame;
3109
+ if (accepted === undefined) return undefined;
3110
+ return {
3111
+ ...accepted,
3112
+ currentViewProjection: new Float32Array(accepted.currentViewProjection),
3113
+ previousViewProjection:
3114
+ accepted.previousViewProjection === undefined
3115
+ ? undefined
3116
+ : new Float32Array(accepted.previousViewProjection),
3117
+ jitter: [accepted.jitter[0], accepted.jitter[1]],
3118
+ viewport: { ...accepted.viewport },
3119
+ };
3120
+ },
2064
3121
  frustumStats: lastFrustumStats,
2065
3122
  visibilityStats: lastVisibilityStats,
2066
3123
  get meshMaterialBindings(): readonly MeshMaterialBindingObservation[] {
@@ -2075,15 +3132,16 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2075
3132
  get perFrameGraphInfo(): CompiledRenderGraphInfo | undefined {
2076
3133
  return frameState.compiledFrameGraph?.inspect();
2077
3134
  },
3135
+ get volumetricFog(): import('./volume/inspection').VolumetricFogInspection {
3136
+ return frameState.volumetricFogInspection;
3137
+ },
2078
3138
  configureStandard(config: RenderPipelineAsset['config']): void {
2079
3139
  const profileConfig = internals.standardProfile;
2080
3140
  const resolvedConfig =
2081
3141
  profileConfig === undefined
2082
3142
  ? config
2083
3143
  : {
2084
- ...(profileConfig.lighting === 'clustered'
2085
- ? { clusterGrid: DEFAULT_CLUSTER_GRID }
2086
- : {}),
3144
+ clusterGrid: DEFAULT_CLUSTER_GRID,
2087
3145
  ...(profileConfig.ssao ? { ssao: { enabled: true } } : {}),
2088
3146
  ...config,
2089
3147
  };
@@ -2108,16 +3166,6 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2108
3166
  // is a rare boot/swap event, never a per-frame cost.
2109
3167
  installEpoch += 1;
2110
3168
  frameState.installedPipelineHandle = installEpoch;
2111
- // The installed profile may request clustered lighting, but the actual
2112
- // graph lane is capability-gated. On WebGL2 (including WebKit's wgpu
2113
- // fallback) the forward lane is selected when compute/storage buffers
2114
- // are unavailable; keep the frame bindings on that same lane instead of
2115
- // advertising HDRP's clustered bind-group layout to URP pipelines.
2116
- frameState.isHdrpActive =
2117
- resolveStandardLane(internals.standardProfile ?? DEFAULT_STANDARD_PROFILE, {
2118
- compute: internals.device.caps.compute,
2119
- storageBuffer: internals.device.caps.storageBuffer,
2120
- }) === 'clustered';
2121
3169
  frameState.installedPipelineConfig = resolvedConfig;
2122
3170
  },
2123
3171
  registerBuiltinPostProcess(id: string, entry: PostProcessShaderEntry): () => void {
@@ -2161,6 +3209,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2161
3209
  }
2162
3210
  builtinPostProcessEntries.set(id, entry);
2163
3211
  if (paramsBuffer !== undefined) postProcessParamsBuffers.set(id, paramsBuffer);
3212
+ const moduleLabel = postProcessShaderModuleLabel(id, entry.source);
2164
3213
  return () => {
2165
3214
  if (builtinPostProcessEntries.get(id) === entry) {
2166
3215
  builtinPostProcessEntries.delete(id);
@@ -2170,9 +3219,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2170
3219
  internals.device.destroyBuffer(current);
2171
3220
  postProcessParamsBuffers.delete(id);
2172
3221
  }
2173
- for (const key of postProcessPipelineCache.keys()) {
2174
- if (key.startsWith(`${id}|`)) postProcessPipelineCache.delete(key);
2175
- }
3222
+ clearPostProcessPipelineCache(id);
3223
+ internals.invalidateShaderModule?.(moduleLabel);
2176
3224
  };
2177
3225
  } catch (cause) {
2178
3226
  if (paramsBuffer !== undefined) internals.device.destroyBuffer(paramsBuffer);
@@ -2180,16 +3228,24 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2180
3228
  }
2181
3229
  },
2182
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;
2183
3237
  persistentRenderScene.dispose();
2184
3238
  gpuDrivenProduction.dispose();
2185
3239
  disposeFeatureGpuWork();
3240
+ reflectionProbeOwner.dispose();
2186
3241
  // Bloom generations are renderer-owned rather than graph-owned. Drain
2187
3242
  // candidate, active, and fence-retiring bundles before the rest of the
2188
3243
  // frame state is torn down; the owner callback is idempotent so the
2189
3244
  // outer Renderer.dispose cascade can safely call this once only.
2190
3245
  internals.getPipelineState()?.perPassResources.drainBloomResources?.();
2191
3246
  internals.clearPostProcessPipelineCache?.('forgeax.taa-resolve');
2192
- internals.invalidateShaderModule?.('post-process-forgeax.taa-resolve-module');
3247
+ invalidatePostProcessModule('forgeax.taa-resolve');
3248
+ resetHdrpBuffers(internals);
2193
3249
  // Retire graph-owned resources and dispose instance-buffer caches.
2194
3250
  // Both calls are idempotent + tolerate per-handle errors silently;
2195
3251
  // the Renderer.dispose() cascade owns the surrounding try/catch
@@ -2214,9 +3270,30 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2214
3270
  retireTemporalGpuState(retiring);
2215
3271
  }
2216
3272
  frameState.retiringTemporalGpuStates.clear();
3273
+ settleVolumetricFogGraphCandidate(frameState, false);
3274
+ for (const buffer of frameState.volumetricFogParamsBuffers) {
3275
+ if (buffer === null) continue;
3276
+ const destroyed = internals.device.destroyBuffer(buffer);
3277
+ if (!destroyed.ok) internals.errorRegistry.fire(destroyed.error);
3278
+ }
3279
+ frameState.volumetricFogParamsBuffers = [null, null];
3280
+ frameState.volumetricFogParamsPendingSlot = null;
3281
+ frameState.volumetricFogParamsAcceptedSlot = null;
3282
+ frameState.volumetricFogAcceptedParams = undefined;
3283
+ frameState.volumetricFogPendingParams = undefined;
3284
+ frameState.volumetricFogAccepted = undefined;
3285
+ frameState.volumetricFogAcceptedContext = undefined;
3286
+ frameState.volumetricFogHistoryGraph = null;
3287
+ frameState.volumetricFogHistorySlot = null;
3288
+ frameState.volumetricFogHistorySignature = null;
3289
+ frameState.hdrpClusterMembership = null;
2217
3290
  const compiled = frameState.compiledFrameGraph;
2218
3291
  frameState.compiledFrameGraph = null;
2219
3292
  frameState.compiledFrameGraphTopologyKey = null;
3293
+ frameState.perFrameGraph = null;
3294
+ frameState.standardLightingGraphSignature = '';
3295
+ frameState.standardLightingInspection = undefined;
3296
+ frameState.pointShadowInspection = undefined;
2220
3297
  if (compiled !== null) compiled.retire().catch(() => undefined);
2221
3298
  for (const retired of frameState.retiredCompiledFrameGraphs) {
2222
3299
  retired.retire().catch(() => undefined);
@@ -2251,6 +3328,11 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2251
3328
  lastTransmissionKey = '';
2252
3329
  transmissionCapabilityGeneration = -1;
2253
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);
2254
3336
  // Keep the persistent material demand observable across device loss.
2255
3337
  // The physical resource has already been fenced by markDeviceLost(); the
2256
3338
  // detached inspection now exposes the rebuild-required lifecycle until a
@@ -2260,6 +3342,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2260
3342
  gpuDrivenProduction.dispose();
2261
3343
  gpuDrivenProduction = new GpuDrivenProduction(internals.device, gpuDrivenShaderFactory);
2262
3344
  disposeFeatureGpuWork();
3345
+ reflectionProbeOwner.dispose();
2263
3346
  // A recovered device cannot retain handles from the lost device. The
2264
3347
  // Bloom owner abandons candidates and retires every committed generation
2265
3348
  // before the new PipelineState is rebuilt.
@@ -2269,9 +3352,33 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2269
3352
  // must be discarded, not merely marked for destruction: their opaque
2270
3353
  // handles cannot be used on the fresh device and the next draw must
2271
3354
  // lazily build a new graph from the preserved ECS / asset POD caches.
3355
+ settleVolumetricFogGraphCandidate(frameState, false);
2272
3356
  frameState.compiledFrameGraph?.retire().catch(() => undefined);
2273
3357
  frameState.compiledFrameGraph = null;
2274
3358
  frameState.compiledFrameGraphTopologyKey = null;
3359
+ frameState.volumetricFogPreviousGraph = null;
3360
+ frameState.volumetricFogPreviousGraphKey = null;
3361
+ frameState.volumetricFogCandidateGraph = null;
3362
+ frameState.volumetricFogAccepted = undefined;
3363
+ frameState.volumetricFogAcceptedContext = undefined;
3364
+ frameState.volumetricFogHistoryGraph = null;
3365
+ frameState.volumetricFogHistorySlot = null;
3366
+ frameState.volumetricFogHistorySignature = null;
3367
+ // The old device owns these transient params buffers. Discard their
3368
+ // handles without calling destroy() on the lost device; the next frame
3369
+ // must allocate fresh buffers on the replacement device.
3370
+ frameState.volumetricFogParamsBuffers = [null, null];
3371
+ frameState.volumetricFogParamsPendingSlot = null;
3372
+ frameState.volumetricFogParamsAcceptedSlot = null;
3373
+ frameState.volumetricFogAcceptedParams = undefined;
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;
2275
3382
  frameState.directionalShadowCache = null;
2276
3383
  frameState.directionalShadowCacheRecorded = false;
2277
3384
  frameState.currentFrameObservationSource = undefined;
@@ -2290,6 +3397,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2290
3397
  retireTemporalGpuState(retiring);
2291
3398
  }
2292
3399
  frameState.retiringTemporalGpuStates.clear();
3400
+ frameState.hdrpClusterMembership = null;
2293
3401
  for (const retired of frameState.retiredCompiledFrameGraphs) {
2294
3402
  retired.retire().catch(() => undefined);
2295
3403
  }