@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
@@ -4,12 +4,13 @@
4
4
  import { HANDLE_NINESLICE_QUAD, resolveAssetHandle } from '@forgeax/engine-assets-runtime';
5
5
  import type { World } from '@forgeax/engine-ecs';
6
6
  import type { RenderReadLease } from '@forgeax/engine-ecs/projection';
7
- import { mat4 } from '@forgeax/engine-math';
7
+ import { mat4, vec3 } from '@forgeax/engine-math';
8
8
  import {
9
9
  type CurrentFrameObservationLease,
10
10
  createCurrentFrameObservationLease,
11
11
  } from '@forgeax/engine-render-graph';
12
12
  import {
13
+ type Buffer,
13
14
  err,
14
15
  ok,
15
16
  type Result,
@@ -20,36 +21,59 @@ import {
20
21
  } from '@forgeax/engine-rhi';
21
22
  import type { MaterialRenderState, MeshAsset } from '@forgeax/engine-types';
22
23
  import { handleSlot, toShared } from '@forgeax/engine-types';
24
+ import { CUBE_CAMERA_FACE_ORDER } from '../components/cube-camera';
25
+ import type { DirectionalShadowQuality } from '../components/directional-shadow-filter';
23
26
  import type { MeshGpuHandles } from '../device/gpu-residency';
24
- import { ObservationUnavailableError, type ObservationUnavailableReason } from '../errors/render';
27
+ import {
28
+ ObservationUnavailableError,
29
+ type ObservationUnavailableReason,
30
+ RendererOperationError,
31
+ } from '../errors/render';
25
32
  import type { FogFrame } from '../extract/environment';
26
33
  import type { GpuDrivenProduction, PreparedGpuDrivenFrame } from '../gpu-driven/production-raster';
27
34
  import { GpuBuffer } from '../gpu-resource';
28
- import { GPU_BUFFER_USAGE_COPY_DST, GPU_BUFFER_USAGE_VERTEX } from '../gpu-usage';
35
+ import {
36
+ GPU_BUFFER_USAGE_COPY_DST,
37
+ GPU_BUFFER_USAGE_UNIFORM,
38
+ GPU_BUFFER_USAGE_VERTEX,
39
+ } from '../gpu-usage';
29
40
  import { disposeTransientInstanceBuffers } from '../instance-buffer-cache';
30
- import type { CameraSnapshot, RenderRecordPhase } from '../render-contract';
41
+ import { inspectStandardLighting } from '../pipeline/standard-lighting/inspection';
42
+ import { standardLightingTopologySignature } from '../pipeline/standard-lighting/topology';
43
+ import { inspectPointShadow } from '../point-shadow-inspection';
44
+ import type {
45
+ CameraSnapshot,
46
+ CubeCameraSnapshot,
47
+ RenderRecordPhase,
48
+ SpotLightProjectorFrameContext,
49
+ VolumetricFogFrameContext,
50
+ } from '../render-contract';
31
51
  import type {
32
52
  DispatchEntry,
33
53
  ExtractedLights,
54
+ ExtractedVolumetricFog,
34
55
  MaterialSnapshot,
35
56
  RenderableSnapshot,
36
57
  SkyboxSnapshot,
37
58
  SkylightSnapshot,
38
59
  } from '../render-system-extract';
39
60
  import type { PersistentGpuDrivenState } from '../scene/render-scene';
61
+ import { SHADOW_ATLAS_DEFAULT_LAYERS } from '../shadow-atlas';
40
62
  import {
41
63
  getTransparentSortConfig,
42
64
  TRANSPARENT_SORT_MODE_LAYER_Y,
43
65
  TRANSPARENT_SORT_MODE_LAYER_Z,
44
66
  } from '../systems/transparent-sort-config';
45
67
  import type { TransmissionDemand } from '../transmission/projection';
68
+ import { hasVolumetricFogCapability } from '../volume/capability';
69
+ import { inspectVolumetricFogResources, VOLUMETRIC_FOG_PARAMS_BYTES } from '../volume/resources';
70
+ import { resolveVolumeTemporalReset, type VolumeTemporalSignature } from '../volume/temporal';
46
71
  import {
47
72
  buildPerFrameBindGroups,
48
73
  prepareFrameLighting,
49
74
  resolveSkyboxActive,
50
75
  warnZeroLightStandard,
51
76
  writeHdrpClusterAndSsaoBuffers,
52
- writePointSpotLightBuffers,
53
77
  writeShadowParamsBuffer,
54
78
  } from './frame-lighting';
55
79
  import {
@@ -188,12 +212,14 @@ export async function observeCurrentFrame(
188
212
  }
189
213
 
190
214
  import { createTemporalView, resolveTemporalReset } from '../temporal/view';
215
+ import { inspectVolumetricFog } from '../volume/inspection';
191
216
  import {
192
217
  acquireSwapChainTarget,
193
218
  graphExecutionPhase,
194
219
  resolveShadowMapSize,
195
220
  resolveSpotShadowMapSize,
196
221
  } from './frame-targets';
222
+ import type { GpuTimingCapture } from './gpu-timing';
197
223
  import {
198
224
  computeProjectionMatrix,
199
225
  computeViewMatrix,
@@ -207,11 +233,18 @@ import { computeSplitLdrSprite } from './main-pass-sprite-draws';
207
233
  import { cleanPerEntityCache, ensureMeshSsboCapacity, uploadMeshSsboBatch } from './mesh-ssbo';
208
234
  import { writeShadowCasterUniforms } from './shadow-pass';
209
235
  import {
236
+ type CubeCaptureGraphState,
210
237
  ensureCompiledFrameGraph,
211
238
  executeCompiledFrameGraph,
212
239
  type RenderFeatureGraphCandidate,
240
+ settleVolumetricFogGraphCandidate,
213
241
  } from './typed-frame-graph';
214
- import { writePointsLinesViewUbo, writeViewUbo } from './view-ubo';
242
+ import {
243
+ CUBE_CAPTURE_VIEW_SLOT_BASE,
244
+ VIEW_UNIFORM_SLOT_STRIDE,
245
+ writePointsLinesViewUbo,
246
+ writeViewUbo,
247
+ } from './view-ubo';
215
248
 
216
249
  /**
217
250
  * A frame-local material table. Repeated snapshot objects share one UBO slot;
@@ -284,6 +317,67 @@ export function materialSlotCountForPrefix(
284
317
 
285
318
  export type RecordProfileRunner = <T>(phase: RenderRecordPhase, action: () => T) => T;
286
319
 
320
+ export interface CubeCaptureFrameInput {
321
+ readonly snapshots: readonly CubeCameraSnapshot[];
322
+ readonly scheduler: import('../capture/scheduler').CubeCaptureScheduler;
323
+ readonly state: CubeCaptureGraphState;
324
+ readonly reflectionProbes?: import('./render-context').ReflectionProbeRecordState;
325
+ }
326
+
327
+ function prepareCubeCapture(
328
+ internals: RenderSystemInternals,
329
+ input: CubeCaptureFrameInput | undefined,
330
+ displayCamera: CameraSnapshot,
331
+ ): readonly import('./typed-frame-graph').CubeCaptureGraphWork[] {
332
+ if (input === undefined) return [];
333
+ if (input.reflectionProbes === undefined) delete input.state.reflectionProbes;
334
+ else input.state.reflectionProbes = input.reflectionProbes.graph;
335
+ input.scheduler.beginFrame();
336
+ for (const snapshot of input.snapshots) {
337
+ const inspection = input.scheduler.inspect(snapshot.target);
338
+ if (inspection.candidateGeneration !== undefined) continue;
339
+ const requested = input.scheduler.request({
340
+ target: snapshot.target,
341
+ position: snapshot.position,
342
+ near: snapshot.near,
343
+ far: snapshot.far,
344
+ updateIntent: snapshot.updateIntent,
345
+ requestVersion: snapshot.requestVersion,
346
+ faceBudget: snapshot.faceBudget,
347
+ });
348
+ if (!requested.ok) internals.errorRegistry.fire(requested.error);
349
+ }
350
+ const work = input.scheduler.nextWork();
351
+ const physicalWork: import('./typed-frame-graph').CubeCaptureGraphWork[] = [];
352
+ for (const item of work) {
353
+ const physical = internals.getRenderTargetPhysical?.(item.target);
354
+ if (physical === undefined) continue;
355
+ const faceIndex = CUBE_CAMERA_FACE_ORDER.indexOf(item.face);
356
+ if (faceIndex < 0) continue;
357
+ const faceWorld = mat4.create();
358
+ mat4.invert(faceWorld, item.view);
359
+ physicalWork.push({
360
+ target: item.target,
361
+ faceIndex,
362
+ physical,
363
+ faceCamera: {
364
+ ...displayCamera,
365
+ position: vec3.create(item.position[0] ?? 0, item.position[1] ?? 0, item.position[2] ?? 0),
366
+ world: faceWorld,
367
+ fov: Math.PI / 2,
368
+ aspect: 1,
369
+ near: item.near,
370
+ far: item.far,
371
+ projection: 'perspective',
372
+ },
373
+ viewBindGroupDynamicOffset:
374
+ (CUBE_CAPTURE_VIEW_SLOT_BASE + physicalWork.length) * VIEW_UNIFORM_SLOT_STRIDE,
375
+ });
376
+ }
377
+ input.state.work = physicalWork;
378
+ return physicalWork;
379
+ }
380
+
287
381
  const EMPTY_ENTITY_KEYS: ReadonlySet<number> = new Set<number>();
288
382
 
289
383
  function runRecordProfilePhase<T>(
@@ -294,6 +388,705 @@ function runRecordProfilePhase<T>(
294
388
  return runner === undefined ? action() : runner(phase, action);
295
389
  }
296
390
 
391
+ const VOLUMETRIC_FOG_PASS_COUNT = 4;
392
+
393
+ type VolumetricFogParamsSlot = 0 | 1;
394
+
395
+ function nextVolumetricFogParamsSlot(frameState: RenderFrameState): VolumetricFogParamsSlot {
396
+ const accepted = frameState.volumetricFogParamsAcceptedSlot;
397
+ const pending = frameState.volumetricFogParamsPendingSlot;
398
+ if (pending !== null && pending !== accepted) return pending;
399
+ return accepted === null ? 0 : accepted === 0 ? 1 : 0;
400
+ }
401
+
402
+ function stageVolumetricFogParams(
403
+ internals: RenderSystemInternals,
404
+ frameState: RenderFrameState,
405
+ params: Float32Array,
406
+ ): Buffer | undefined {
407
+ const slot = nextVolumetricFogParamsSlot(frameState);
408
+ let buffer = frameState.volumetricFogParamsBuffers[slot];
409
+ if (buffer === null) {
410
+ const created = internals.device.createBuffer({
411
+ label: `volumetric-fog-params-${slot}`,
412
+ size: VOLUMETRIC_FOG_PARAMS_BYTES,
413
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
414
+ mappedAtCreation: false,
415
+ });
416
+ if (!created.ok) {
417
+ internals.errorRegistry.fire(created.error);
418
+ return undefined;
419
+ }
420
+ buffer = created.value;
421
+ frameState.volumetricFogParamsBuffers[slot] = buffer;
422
+ }
423
+ const written = internals.device.queue.writeBuffer(buffer, 0, params);
424
+ if (!written.ok) {
425
+ internals.errorRegistry.fire(written.error);
426
+ return undefined;
427
+ }
428
+ frameState.volumetricFogParamsPendingSlot = slot;
429
+ frameState.volumetricFogPendingParams = new Float32Array(params);
430
+ return buffer;
431
+ }
432
+
433
+ /** Retire parameter buffers only after the submit that stopped using them. */
434
+ function retireVolumetricFogParams(
435
+ internals: RenderSystemInternals,
436
+ frameState: RenderFrameState,
437
+ ): void {
438
+ const buffers = frameState.volumetricFogParamsBuffers;
439
+ frameState.volumetricFogParamsBuffers = [null, null];
440
+ frameState.volumetricFogParamsPendingSlot = null;
441
+ frameState.volumetricFogParamsAcceptedSlot = null;
442
+ frameState.volumetricFogAcceptedParams = undefined;
443
+ frameState.volumetricFogPendingParams = undefined;
444
+ const live = buffers.filter((buffer): buffer is Buffer => buffer !== null);
445
+ if (live.length === 0) return;
446
+ const device = internals.device;
447
+ const release = (): void => {
448
+ for (const buffer of live) {
449
+ const destroyed = device.destroyBuffer(buffer);
450
+ if (!destroyed.ok) internals.errorRegistry.fire(destroyed.error);
451
+ }
452
+ };
453
+ try {
454
+ void device.queue.onSubmittedWorkDone().then(release, release);
455
+ } catch {
456
+ release();
457
+ }
458
+ }
459
+
460
+ function volumeRevision(values: readonly number[]): number {
461
+ let hash = 2166136261;
462
+ for (const value of values) {
463
+ const quantized = Number.isFinite(value) ? Math.round(value * 1_000_000) : 0;
464
+ hash = Math.imul(hash ^ quantized, 16777619) >>> 0;
465
+ }
466
+ return hash;
467
+ }
468
+
469
+ function volumeTemporalSignature(
470
+ fog: ExtractedVolumetricFog,
471
+ prepared: VolumetricFogFrameContext,
472
+ camera: CameraSnapshot,
473
+ lights: ExtractedLights,
474
+ width: number,
475
+ height: number,
476
+ ): VolumeTemporalSignature {
477
+ const authored = fog.fog;
478
+ const directional = lights.directional;
479
+ const cameraRevision = volumeRevision([
480
+ ...camera.world,
481
+ camera.fov,
482
+ camera.aspect,
483
+ camera.near,
484
+ camera.far,
485
+ camera.orthoLeft,
486
+ camera.orthoRight,
487
+ camera.orthoBottom,
488
+ camera.orthoTop,
489
+ ]);
490
+ const fogRevision =
491
+ authored === undefined
492
+ ? 0
493
+ : volumeRevision([
494
+ ...authored.bounds.min,
495
+ ...authored.bounds.max,
496
+ ...authored.extinction,
497
+ ...authored.albedo,
498
+ ...authored.emission,
499
+ authored.anisotropy,
500
+ authored.maxDistance,
501
+ ]);
502
+ const selectedSpot =
503
+ fog.spotLightEntity === undefined
504
+ ? fog.lightKind === 'spot'
505
+ ? lights.spot.find((spot) => spot.entity === fog.lightEntity)
506
+ : undefined
507
+ : lights.spot.find((spot) => spot.entity === fog.spotLightEntity);
508
+ const selectedPoint =
509
+ fog.pointLightEntity === undefined
510
+ ? fog.lightKind === 'point'
511
+ ? lights.point.find((point) => point.entity === fog.lightEntity)
512
+ : undefined
513
+ : lights.point.find((point) => point.entity === fog.pointLightEntity);
514
+ const selectedDirectional =
515
+ fog.lightKind === 'directional' && directional?.entity === fog.lightEntity
516
+ ? directional
517
+ : undefined;
518
+ const lightRevision =
519
+ selectedSpot === undefined && selectedPoint === undefined && selectedDirectional === undefined
520
+ ? 0
521
+ : volumeRevision(
522
+ selectedSpot === undefined && selectedPoint === undefined
523
+ ? selectedDirectional === undefined
524
+ ? []
525
+ : [
526
+ ...selectedDirectional.direction,
527
+ ...selectedDirectional.color,
528
+ selectedDirectional.intensity,
529
+ ...(lights.lightViewProj?.flatMap((matrix) => [...matrix]) ?? []),
530
+ ]
531
+ : [
532
+ ...(selectedPoint === undefined
533
+ ? []
534
+ : [
535
+ ...selectedPoint.position,
536
+ ...selectedPoint.color,
537
+ selectedPoint.intensity,
538
+ selectedPoint.invRangeSquared,
539
+ selectedPoint.shadowAtlasLayer ?? -1,
540
+ selectedPoint.shadowNear ?? 0,
541
+ selectedPoint.shadowFar ?? 0,
542
+ ]),
543
+ ...(selectedSpot === undefined
544
+ ? []
545
+ : [
546
+ ...selectedSpot.position,
547
+ ...selectedSpot.direction,
548
+ ...selectedSpot.color,
549
+ selectedSpot.intensity,
550
+ selectedSpot.invRangeSquared,
551
+ selectedSpot.cosInner,
552
+ selectedSpot.cosOuter,
553
+ selectedSpot.depthBias ?? 0.005,
554
+ selectedSpot.normalBias ?? 0.05,
555
+ selectedSpot.pcfKernelSize ?? 3,
556
+ selectedSpot.shadowAtlasTile,
557
+ ...(selectedSpot.lightViewProj === undefined
558
+ ? []
559
+ : [...selectedSpot.lightViewProj]),
560
+ ]),
561
+ ],
562
+ );
563
+ return {
564
+ cameraRevision,
565
+ fogRevision,
566
+ lightRevision,
567
+ densityGeneration: prepared.densityGeneration,
568
+ width,
569
+ height,
570
+ ...(fog.lightKind === undefined ? {} : { lightKind: fog.lightKind }),
571
+ ...(fog.lightEntity === undefined ? {} : { lightEntity: fog.lightEntity }),
572
+ ...(fog.pointLightEntity === undefined ? {} : { pointLightEntity: fog.pointLightEntity }),
573
+ ...(fog.spotLightEntity === undefined ? {} : { spotLightEntity: fog.spotLightEntity }),
574
+ ...(fog.projector === undefined ? {} : { projector: fog.projector }),
575
+ lightShadowRevision: lightRevision,
576
+ };
577
+ }
578
+
579
+ function resolveVolumetricFogHistoryContext(
580
+ frameState: RenderFrameState,
581
+ graph: import('@forgeax/engine-render-graph').CompiledRenderGraph<
582
+ import('../render-pipeline').RenderPipelineFrame
583
+ >,
584
+ fog: ExtractedVolumetricFog,
585
+ prepared: VolumetricFogFrameContext,
586
+ camera: CameraSnapshot,
587
+ lights: ExtractedLights,
588
+ width: number,
589
+ height: number,
590
+ ): { readonly context: VolumetricFogFrameContext; readonly signature: VolumeTemporalSignature } {
591
+ const signature = volumeTemporalSignature(fog, prepared, camera, lights, width, height);
592
+ const acceptedGraph = frameState.volumetricFogHistoryGraph;
593
+ const acceptedSlot = frameState.volumetricFogHistorySlot;
594
+ const previousSignature = frameState.volumetricFogHistorySignature;
595
+ let reset = acceptedGraph !== graph || acceptedSlot === null || previousSignature === null;
596
+ if (!reset && previousSignature !== null) {
597
+ const temporal = frameState.temporalFrame;
598
+ const previousResetReason = temporal?.resetReason;
599
+ const reprojection =
600
+ previousResetReason === 'out-of-screen'
601
+ ? 'out-of-screen'
602
+ : previousResetReason === 'depth-discontinuity'
603
+ ? 'depth-discontinuity'
604
+ : undefined;
605
+ reset = resolveVolumeTemporalReset(previousSignature, signature, {
606
+ taaEnabled: camera.antialias === 'taa',
607
+ ...(reprojection === undefined ? {} : { reprojection }),
608
+ }).reset;
609
+ }
610
+ const historyReadSlot = reset ? null : acceptedSlot;
611
+ const historyWriteSlot = reset || acceptedSlot === null ? 0 : acceptedSlot === 0 ? 1 : 0;
612
+ return {
613
+ context: {
614
+ ...prepared,
615
+ historyReadSlot,
616
+ historyWriteSlot,
617
+ historyValid: historyReadSlot !== null,
618
+ },
619
+ signature,
620
+ };
621
+ }
622
+
623
+ /**
624
+ * Resolve the authored density POD into the renderer-owned GPU resources used
625
+ * by the graph. This is deliberately a pull operation: a missing/stale
626
+ * handle or failed upload produces a structured error and leaves the graph
627
+ * candidate disabled for this frame rather than binding a synthetic density.
628
+ */
629
+ function prepareVolumetricFogFrame(
630
+ internals: RenderSystemInternals,
631
+ frameState: RenderFrameState,
632
+ world: World,
633
+ worlds: readonly World[],
634
+ fog: ExtractedVolumetricFog | undefined,
635
+ lights: ExtractedLights,
636
+ volumeCapability: boolean,
637
+ projector: SpotLightProjectorFrameContext | undefined,
638
+ ): VolumetricFogFrameContext | undefined {
639
+ if (
640
+ !volumeCapability ||
641
+ fog?.status !== 'available' ||
642
+ fog.fog === undefined ||
643
+ fog.densityHandle === undefined ||
644
+ fog.densityAsset === undefined ||
645
+ (fog.lightKind === 'spot'
646
+ ? lights.spot.find((light) => light.entity === fog.lightEntity) === undefined
647
+ : fog.lightKind === 'point'
648
+ ? lights.point.find((light) => light.entity === fog.lightEntity) === undefined
649
+ : lights.directional === undefined)
650
+ ) {
651
+ return undefined;
652
+ }
653
+ const ownerWorld = worlds[fog.worldId ?? 0] ?? world;
654
+ const resident = internals.gpuStore.ensureResident(
655
+ fog.densityHandle,
656
+ fog.densityAsset,
657
+ ownerWorld,
658
+ );
659
+ if (!resident.ok || !('texture' in resident.value)) {
660
+ if (!resident.ok) {
661
+ internals.errorRegistry.fire(
662
+ resident.error instanceof RhiError
663
+ ? resident.error
664
+ : new RhiError({
665
+ code: 'webgpu-runtime-error',
666
+ expected: 'volumetric density residency to resolve a GPU texture',
667
+ hint: 'repair the authored TextureAsset payload and retry the next frame',
668
+ detail: { error: resident.error },
669
+ }),
670
+ );
671
+ }
672
+ return undefined;
673
+ }
674
+ const params = new Float32Array(32);
675
+ params.set(fog.fog.bounds.min, 0);
676
+ params.set(fog.fog.bounds.max, 4);
677
+ params.set(fog.fog.extinction, 8);
678
+ params.set(fog.fog.albedo, 12);
679
+ params.set(fog.fog.emission, 16);
680
+ const selectedPoint =
681
+ fog.pointLightEntity === undefined
682
+ ? undefined
683
+ : lights.point.find((light) => light.entity === fog.pointLightEntity);
684
+ const selectedSpot =
685
+ fog.spotLightEntity === undefined
686
+ ? fog.lightKind === 'spot'
687
+ ? lights.spot.find((light) => light.entity === fog.lightEntity)
688
+ : undefined
689
+ : lights.spot.find((light) => light.entity === fog.spotLightEntity);
690
+ const lightDirection =
691
+ selectedSpot?.direction ?? lights.directional?.direction ?? vec3.create(0, -1, 0);
692
+ const lightColor = selectedSpot?.color ?? lights.directional?.color ?? vec3.create(0, 0, 0);
693
+ if (lightDirection === undefined || lightColor === undefined) return undefined;
694
+ params.set(lightDirection, 20);
695
+ // Reuse the vec4 padding lane as the renderer-owned frame phase for
696
+ // sub-froxel jitter. The temporal resolver consumes it without growing the
697
+ // public fog component or allocating another uniform buffer.
698
+ params[23] = frameState.frameNumber % 1024;
699
+ const selectedSpotIndex =
700
+ selectedSpot === undefined
701
+ ? -1
702
+ : lights.spot.findIndex((light) => light.entity === selectedSpot.entity);
703
+ params.set(lightColor, 24);
704
+ params[19] =
705
+ selectedPoint === undefined
706
+ ? -1
707
+ : lights.point.findIndex((light) => light.entity === selectedPoint.entity);
708
+ params[27] = selectedSpotIndex;
709
+ params[28] = fog.fog.maxDistance;
710
+ params[29] = fog.fog.anisotropy;
711
+ params[30] =
712
+ selectedPoint !== undefined && selectedSpot !== undefined
713
+ ? 3
714
+ : selectedPoint !== undefined
715
+ ? 1
716
+ : selectedSpot !== undefined
717
+ ? 2
718
+ : 0;
719
+ // The temporal shader reads this lane after the record stage resolves the
720
+ // accepted graph/slot. It is overwritten on the next queue-visible frame;
721
+ // the first candidate deliberately starts with history disabled.
722
+ params[31] =
723
+ frameState.temporalFrame !== undefined && frameState.temporalFrame.resetReason === 'none'
724
+ ? 1
725
+ : 0;
726
+
727
+ const paramsBuffer = stageVolumetricFogParams(internals, frameState, params);
728
+ if (paramsBuffer === undefined) return undefined;
729
+ return {
730
+ densityTexture: resident.value.texture.handle,
731
+ densityView: resident.value.view,
732
+ paramsBuffer,
733
+ densityGeneration: resident.value.receipt.generation,
734
+ historyReadSlot: null,
735
+ historyWriteSlot: 0,
736
+ historyValid: false,
737
+ ...(projector === undefined
738
+ ? {}
739
+ : {
740
+ projectorTexture: projector.texture,
741
+ projectorView: projector.view,
742
+ projectorSampler: projector.sampler,
743
+ }),
744
+ };
745
+ }
746
+
747
+ /**
748
+ * Resolve the SpotLight-owned cookie independently of volumetric fog. Surface
749
+ * lighting must keep the authored projector when the fog component is absent;
750
+ * otherwise the view bind group silently falls back to white and changes the
751
+ * direct-light energy on the floor. A fog-selected projector is preferred so
752
+ * the surface and volume lanes consume one identity, with the first authored
753
+ * projector as the deterministic scene fallback.
754
+ */
755
+ function prepareSpotLightProjectorFrame(
756
+ internals: RenderSystemInternals,
757
+ world: World,
758
+ worlds: readonly World[],
759
+ lights: ExtractedLights,
760
+ fog: ExtractedVolumetricFog | undefined,
761
+ ): SpotLightProjectorFrameContext | undefined {
762
+ const preferred =
763
+ fog?.projectorHandle === undefined
764
+ ? undefined
765
+ : lights.spot.find((light) => light.projectorHandle === fog.projectorHandle);
766
+ const source =
767
+ preferred ??
768
+ lights.spot.find(
769
+ (light) =>
770
+ light.projectorHandle !== undefined &&
771
+ light.projectorAsset !== undefined &&
772
+ light.lightViewProj !== undefined,
773
+ );
774
+ if (
775
+ source === undefined ||
776
+ source.projectorHandle === undefined ||
777
+ source.projectorAsset === undefined
778
+ ) {
779
+ return undefined;
780
+ }
781
+ const ownerWorld = worlds[fog?.worldId ?? 0] ?? world;
782
+ const resident = internals.gpuStore.ensureResident(
783
+ source.projectorHandle,
784
+ source.projectorAsset,
785
+ ownerWorld,
786
+ );
787
+ if (!resident.ok || !('texture' in resident.value)) {
788
+ if (!resident.ok) {
789
+ internals.errorRegistry.fire(
790
+ resident.error instanceof RhiError
791
+ ? resident.error
792
+ : new RhiError({
793
+ code: 'webgpu-runtime-error',
794
+ expected: 'SpotLight projector residency to resolve a GPU texture',
795
+ hint: 'repair the authored projector TextureAsset payload and retry the next frame',
796
+ detail: { error: resident.error },
797
+ }),
798
+ );
799
+ }
800
+ return undefined;
801
+ }
802
+ const sampler = internals.device.createSampler({
803
+ label: 'spot-projector-linear-clamp',
804
+ minFilter: 'linear',
805
+ magFilter: 'linear',
806
+ mipmapFilter: 'linear',
807
+ addressModeU: 'clamp-to-edge',
808
+ addressModeV: 'clamp-to-edge',
809
+ addressModeW: 'clamp-to-edge',
810
+ });
811
+ if (!sampler.ok) {
812
+ internals.errorRegistry.fire(sampler.error);
813
+ return undefined;
814
+ }
815
+ return {
816
+ texture: resident.value.texture.handle,
817
+ view: resident.value.view,
818
+ sampler: sampler.value,
819
+ spotIndex: lights.spot.indexOf(source),
820
+ lightSlotIndex: lights.point.length + lights.spot.indexOf(source),
821
+ };
822
+ }
823
+
824
+ /** Stage the accepted parameter payload into the writable slot for a degraded
825
+ * source frame. This keeps the accepted GPU buffer immutable until submit
826
+ * succeeds while still allowing the LKG graph to update its history flag. */
827
+ function stageAcceptedVolumetricFogFrame(
828
+ internals: RenderSystemInternals,
829
+ frameState: RenderFrameState,
830
+ accepted: VolumetricFogFrameContext | undefined,
831
+ ): VolumetricFogFrameContext | undefined {
832
+ if (accepted === undefined) return undefined;
833
+ const params = frameState.volumetricFogAcceptedParams;
834
+ if (params === undefined) return accepted;
835
+ const paramsBuffer = stageVolumetricFogParams(internals, frameState, new Float32Array(params));
836
+ if (paramsBuffer === undefined) return accepted;
837
+ return { ...accepted, paramsBuffer };
838
+ }
839
+
840
+ function updateVolumetricFogInspection(
841
+ frameState: RenderFrameState,
842
+ fog: ExtractedVolumetricFog | undefined,
843
+ lights: ExtractedLights,
844
+ prepared: VolumetricFogFrameContext | undefined,
845
+ submitted: boolean,
846
+ volumeCapability: boolean,
847
+ volumeSubmissionAttempted: boolean,
848
+ ): void {
849
+ const resourceFacts =
850
+ frameState.compiledFrameGraph === undefined || frameState.compiledFrameGraph === null
851
+ ? undefined
852
+ : inspectVolumetricFogResources(frameState.compiledFrameGraph.inspect(), {
853
+ parameterBufferCount: frameState.volumetricFogParamsBuffers.filter(
854
+ (buffer) => buffer !== null,
855
+ ).length,
856
+ });
857
+ const graphSampleCount = resourceFacts?.sampleCount ?? 0;
858
+ const graphMemoryBytes = resourceFacts?.totalBytes ?? 0;
859
+ const previous = frameState.volumetricFogInspection;
860
+ const accepted = frameState.volumetricFogAccepted;
861
+ const acceptedLkg =
862
+ accepted === undefined
863
+ ? undefined
864
+ : {
865
+ guid: accepted.guid ?? previous.guid,
866
+ generation: accepted.generation ?? previous.generation,
867
+ digest: accepted.digest ?? previous.digest,
868
+ deviceEpoch: previous.deviceEpoch,
869
+ format: previous.format ?? accepted.densityAsset?.format ?? fog?.densityAsset?.format,
870
+ passCount: previous.passCount > 0 ? previous.passCount : VOLUMETRIC_FOG_PASS_COUNT,
871
+ sampleCount: previous.sampleCount > 0 ? previous.sampleCount : graphSampleCount,
872
+ memoryBytes: previous.memoryBytes > 0 ? previous.memoryBytes : graphMemoryBytes,
873
+ resourceFacts: previous.resourceFacts ?? resourceFacts,
874
+ lightEntity: accepted.lightEntity,
875
+ lightKind: accepted.lightKind,
876
+ pointLightEntity: accepted.pointLightEntity,
877
+ spotLightEntity: accepted.spotLightEntity,
878
+ projector: accepted.projector,
879
+ };
880
+
881
+ const candidateIdentity =
882
+ fog?.guid === undefined || fog.generation === undefined
883
+ ? undefined
884
+ : {
885
+ guid: fog.guid,
886
+ generation: fog.generation,
887
+ digest: fog.digest,
888
+ };
889
+ const selectedLightAvailable =
890
+ fog?.lightKind === 'spot'
891
+ ? lights.spot.some((light) => light.entity === fog.lightEntity)
892
+ : fog?.lightKind === 'point'
893
+ ? lights.point.some(
894
+ (light) => light.entity === (fog.pointLightEntity ?? fog.lightEntity),
895
+ ) &&
896
+ (fog.spotLightEntity === undefined ||
897
+ lights.spot.some((light) => light.entity === fog.spotLightEntity))
898
+ : fog?.lightKind === 'directional'
899
+ ? lights.directional?.entity === fog.lightEntity
900
+ : lights.directional !== undefined;
901
+ const capability = volumeCapability && selectedLightAvailable ? 'available' : 'unavailable';
902
+
903
+ const inspectAccepted = (status: 'accepted' | 'degraded', candidateFailure?: 'submit-failed') => {
904
+ if (
905
+ acceptedLkg === undefined ||
906
+ acceptedLkg.guid === undefined ||
907
+ acceptedLkg.generation === undefined
908
+ ) {
909
+ return false;
910
+ }
911
+ const recovery = {
912
+ guid: acceptedLkg.guid,
913
+ generation: acceptedLkg.generation,
914
+ deviceEpoch: acceptedLkg.deviceEpoch,
915
+ status,
916
+ ...(status === 'degraded' ? { lkgGeneration: acceptedLkg.generation } : {}),
917
+ ...(candidateIdentity === undefined
918
+ ? {}
919
+ : {
920
+ candidateGeneration: candidateIdentity.generation,
921
+ ...(candidateIdentity.digest === undefined
922
+ ? {}
923
+ : { candidateDigest: candidateIdentity.digest }),
924
+ }),
925
+ ...(acceptedLkg.lightEntity === undefined || acceptedLkg.lightKind === undefined
926
+ ? {}
927
+ : {
928
+ selectedLight: {
929
+ entity: acceptedLkg.lightEntity,
930
+ kind: acceptedLkg.lightKind,
931
+ revision: 0,
932
+ shadowTile: -1,
933
+ ...(acceptedLkg.pointLightEntity === undefined
934
+ ? {}
935
+ : { pointLightEntity: acceptedLkg.pointLightEntity }),
936
+ ...(acceptedLkg.spotLightEntity === undefined
937
+ ? {}
938
+ : { spotLightEntity: acceptedLkg.spotLightEntity }),
939
+ ...(acceptedLkg.projector === undefined ? {} : { projector: acceptedLkg.projector }),
940
+ },
941
+ }),
942
+ ...(candidateFailure === undefined ? {} : { candidateFailure }),
943
+ } satisfies import('../volume/recovery').VolumeRecoveryState;
944
+ frameState.volumetricFogInspection = inspectVolumetricFog({
945
+ authored: true,
946
+ capability: 'available',
947
+ degraded: status === 'degraded',
948
+ recovery,
949
+ ...(acceptedLkg.digest === undefined ? {} : { acceptedDigest: acceptedLkg.digest }),
950
+ ...(acceptedLkg.format === undefined ? {} : { format: acceptedLkg.format }),
951
+ passCount: acceptedLkg.passCount,
952
+ sampleCount: acceptedLkg.sampleCount,
953
+ memoryBytes: acceptedLkg.memoryBytes,
954
+ ...(acceptedLkg.resourceFacts === undefined
955
+ ? {}
956
+ : { resourceFacts: acceptedLkg.resourceFacts }),
957
+ });
958
+ return true;
959
+ };
960
+
961
+ if (fog === undefined || fog.status === 'off') {
962
+ // A failed fog-off candidate must not erase the inspection/LKG that is
963
+ // still being rendered. The accepted projection is retired only after a
964
+ // successful no-volume submission below.
965
+ if (!submitted && volumeSubmissionAttempted && inspectAccepted('degraded', 'submit-failed')) {
966
+ return;
967
+ }
968
+ if (!submitted && volumeSubmissionAttempted) return;
969
+ if (!submitted) return;
970
+ frameState.volumetricFogInspection = inspectVolumetricFog({
971
+ authored: false,
972
+ capability: 'available',
973
+ });
974
+ return;
975
+ }
976
+
977
+ const authored = fog.status === 'available' || fog.status === 'degraded';
978
+
979
+ // A successfully submitted available candidate is the only path that may
980
+ // become the new accepted projection. This call runs after graph settlement
981
+ // so the inspection and the graph/context observe the same transaction.
982
+ if (
983
+ submitted &&
984
+ fog.status === 'available' &&
985
+ prepared !== undefined &&
986
+ capability === 'available'
987
+ ) {
988
+ const recovery =
989
+ candidateIdentity === undefined
990
+ ? undefined
991
+ : {
992
+ guid: candidateIdentity.guid,
993
+ generation: candidateIdentity.generation,
994
+ deviceEpoch: previous.deviceEpoch,
995
+ status: 'accepted' as const,
996
+ ...(fog.lightEntity === undefined || fog.lightKind === undefined
997
+ ? {}
998
+ : {
999
+ selectedLight: {
1000
+ entity: fog.lightEntity,
1001
+ kind: fog.lightKind,
1002
+ revision: 0,
1003
+ shadowTile:
1004
+ lights.spot.find((spot) => spot.entity === fog.spotLightEntity)
1005
+ ?.shadowAtlasTile ??
1006
+ lights.spot.find((spot) => spot.entity === fog.lightEntity)
1007
+ ?.shadowAtlasTile ??
1008
+ -1,
1009
+ ...(fog.pointLightEntity === undefined
1010
+ ? {}
1011
+ : { pointLightEntity: fog.pointLightEntity }),
1012
+ ...(fog.spotLightEntity === undefined
1013
+ ? {}
1014
+ : { spotLightEntity: fog.spotLightEntity }),
1015
+ ...(fog.projector === undefined ? {} : { projector: fog.projector }),
1016
+ },
1017
+ }),
1018
+ };
1019
+ frameState.volumetricFogInspection = inspectVolumetricFog({
1020
+ authored: true,
1021
+ capability,
1022
+ ...(recovery === undefined ? {} : { recovery }),
1023
+ ...(candidateIdentity?.digest === undefined
1024
+ ? {}
1025
+ : { acceptedDigest: candidateIdentity.digest }),
1026
+ ...(fog.densityAsset?.format === undefined ? {} : { format: fog.densityAsset.format }),
1027
+ passCount: VOLUMETRIC_FOG_PASS_COUNT,
1028
+ sampleCount: graphSampleCount,
1029
+ memoryBytes: graphMemoryBytes,
1030
+ ...(resourceFacts === undefined ? {} : { resourceFacts }),
1031
+ });
1032
+ return;
1033
+ }
1034
+
1035
+ // A submitted degraded source (or an available source whose residency
1036
+ // preparation failed) is rendered through the previously accepted graph.
1037
+ // Keep that LKG resource fact visible instead of projecting a zero-resource
1038
+ // candidate merely because the current source could not be prepared.
1039
+ if (
1040
+ submitted &&
1041
+ acceptedLkg !== undefined &&
1042
+ (fog.status === 'degraded' || prepared === undefined)
1043
+ ) {
1044
+ if (inspectAccepted('degraded')) return;
1045
+ }
1046
+
1047
+ // Any candidate that reached encode/submit but failed must project the LKG,
1048
+ // never the unsubmitted candidate. The candidate identity remains visible so
1049
+ // recovery tooling can distinguish the rejected bytes from the frame being
1050
+ // rendered.
1051
+ if (!submitted && volumeSubmissionAttempted && inspectAccepted('degraded', 'submit-failed')) {
1052
+ return;
1053
+ }
1054
+
1055
+ // A degraded source that has no LKG is still a diagnostic, but it must not
1056
+ // claim accepted resources. Keep its identity in the candidate stage.
1057
+ const candidateRecovery =
1058
+ candidateIdentity === undefined
1059
+ ? undefined
1060
+ : {
1061
+ guid: candidateIdentity.guid,
1062
+ generation: candidateIdentity.generation,
1063
+ deviceEpoch: previous.deviceEpoch,
1064
+ status: 'candidate' as const,
1065
+ candidateGeneration: candidateIdentity.generation,
1066
+ ...(candidateIdentity.digest === undefined
1067
+ ? {}
1068
+ : { candidateDigest: candidateIdentity.digest }),
1069
+ ...(!submitted && volumeSubmissionAttempted
1070
+ ? { candidateFailure: 'submit-failed' as const }
1071
+ : {}),
1072
+ };
1073
+ frameState.volumetricFogInspection = inspectVolumetricFog({
1074
+ authored,
1075
+ capability,
1076
+ degraded: fog.status === 'degraded' || capability === 'unavailable',
1077
+ ...(candidateRecovery === undefined ? {} : { recovery: candidateRecovery }),
1078
+ ...(acceptedLkg?.digest === undefined ? {} : { acceptedDigest: acceptedLkg.digest }),
1079
+ ...(fog.densityAsset?.format === undefined
1080
+ ? acceptedLkg?.format === undefined
1081
+ ? {}
1082
+ : { format: acceptedLkg.format }
1083
+ : { format: fog.densityAsset.format }),
1084
+ passCount: 0,
1085
+ sampleCount: 0,
1086
+ memoryBytes: 0,
1087
+ });
1088
+ }
1089
+
297
1090
  export function recordFrame(
298
1091
  internals: RenderSystemInternals,
299
1092
  world: World,
@@ -322,15 +1115,18 @@ export function recordFrame(
322
1115
  gpuDriven?: {
323
1116
  readonly owner: GpuDrivenProduction;
324
1117
  readonly scene: PersistentGpuDrivenState | undefined;
325
- readonly onCoverage: (ownsAll: boolean) => void;
1118
+ readonly onCoverage: (ownsAll: boolean, clusteredLighting: boolean) => void;
326
1119
  },
327
1120
  renderReadLeases?: readonly RenderReadLease[],
328
1121
  featureGraphCandidate?: RenderFeatureGraphCandidate,
329
1122
  pointsLinesOwner?: import('./render-context').PointsLinesRecordOwner,
1123
+ cubeCapture?: CubeCaptureFrameInput,
330
1124
  environmentSignature = '',
331
1125
  fogSignature = '',
332
1126
  environmentReady = true,
333
1127
  transmissionDemand?: TransmissionDemand,
1128
+ volumetricFog?: ExtractedVolumetricFog,
1129
+ timingCapture?: GpuTimingCapture,
334
1130
  ): boolean {
335
1131
  // The `try / finally` wrapper advances `frameState.frameNumber` exactly
336
1132
  // once per `recordFrame` invocation regardless of which early-return
@@ -361,6 +1157,11 @@ export function recordFrame(
361
1157
  );
362
1158
  activeCameras = [makeZeroCameraFallbackSnapshot()];
363
1159
  }
1160
+ let submitted = false;
1161
+ let volumeInspectionReady = false;
1162
+ let volumeSubmissionAttempted = false;
1163
+ let preparedVolumetricFogForInspection: VolumetricFogFrameContext | undefined;
1164
+ let volumeCapabilityForInspection = false;
364
1165
  try {
365
1166
  // Case D: N>1 Camera => fire onError, use first hit (D-S7).
366
1167
  if (activeCameras.length > 1) {
@@ -376,6 +1177,23 @@ export function recordFrame(
376
1177
  if (!camera) return false;
377
1178
  const pipelineState = internals.getPipelineState();
378
1179
  if (pipelineState === null) return false;
1180
+ if (pipelineState.device !== internals.device) {
1181
+ internals.errorRegistry.fire(
1182
+ new RendererOperationError('device-operation-failed', {
1183
+ operation: 'draw',
1184
+ cause: {
1185
+ code: 'pipeline-device-mismatch',
1186
+ expected: 'pipelineState.device === runtime.device',
1187
+ hint: 'discard stale pipeline state before recording the next frame',
1188
+ detail: {
1189
+ expected: 'pipelineState.device === runtime.device',
1190
+ hint: 'discard stale pipeline state before recording the next frame',
1191
+ },
1192
+ },
1193
+ }),
1194
+ );
1195
+ return false;
1196
+ }
379
1197
  frameState.currentFrameObservationSource = undefined;
380
1198
  frameState.currentDirectionalShadowView = null;
381
1199
  frameState.currentSpotShadowView = null;
@@ -383,7 +1201,7 @@ export function recordFrame(
383
1201
  // Record-stage fold operator linear scan (groups transparent-sort entries
384
1202
  // into fold buckets + records the fold-eligible count metric). Extracted to
385
1203
  // computeFoldBuckets (M3/w18).
386
- const { foldBuckets, light } = runRecordProfilePhase(profilePhase, 'record/scene-state', () => {
1204
+ const lightingPreparation = runRecordProfilePhase(profilePhase, 'record/scene-state', () => {
387
1205
  const foldBuckets = runRecordProfilePhase(
388
1206
  profilePhase,
389
1207
  'record/scene-state/fold-buckets',
@@ -394,11 +1212,11 @@ export function recordFrame(
394
1212
  // ensure + ExtractedLights three-arm destructure (directional fallback,
395
1213
  // point/spot arrays, totalLightCount). Extracted to prepareFrameLighting
396
1214
  // (M3/w18) so recordFrame stays a skeleton.
397
- const { light, pointLights, spotLights, totalLightCount } = runRecordProfilePhase(
398
- profilePhase,
399
- 'record/scene-state/lighting-prep',
400
- () => prepareFrameLighting(internals, frameState, lights),
1215
+ const lighting = runRecordProfilePhase(profilePhase, 'record/scene-state/lighting-prep', () =>
1216
+ prepareFrameLighting(internals, frameState, lights, camera, pipelineState),
401
1217
  );
1218
+ if (!lighting.ok) return lighting;
1219
+ const { light, totalLightCount, standard } = lighting.value;
402
1220
 
403
1221
  runRecordProfilePhase(profilePhase, 'record/scene-state/ambient-resolution', () => {
404
1222
  // feat-20260520-skylight-ibl-cubemap M4 / t27 (AC-10 + F-4 nit):
@@ -445,31 +1263,17 @@ export function recordFrame(
445
1263
  }
446
1264
  });
447
1265
 
448
- // feat-20260608-cluster-lighting M5 / w21 + M6 / w23: HDRP per-frame CPU
449
- // cluster binning + light-data / cluster-grid / SSAO buffer uploads. Runs
450
- // only when the HDRP pipeline is active and there is at least one punctual
451
- // light. Extracted to a sub-function (M3/w18) so recordFrame stays a
452
- // skeleton; fail-soft error semantics are preserved verbatim.
453
- runRecordProfilePhase(profilePhase, 'record/scene-state/hdrp-cluster', () => {
454
- writeHdrpClusterAndSsaoBuffers(
455
- internals,
456
- frameState,
457
- camera,
458
- pointLights,
459
- spotLights,
460
- profilePhase,
461
- pipelineState.hdrpClusterMembershipPipeline !== null &&
462
- internals.device.caps?.compute === true,
463
- pipelineState.hdrpClusterMembershipBindGroupLayout,
464
- );
465
- });
466
-
467
1266
  // Zero-light standard-material once-warn (no Skylight + 0 direct light +
468
1267
  // >=1 lit material -> black). Extracted to warnZeroLightStandard (M3/w18).
469
1268
  warnZeroLightStandard(frameState, renderables, skylight, totalLightCount);
470
1269
 
471
- return { foldBuckets, light };
1270
+ return { ok: true as const, value: { foldBuckets, light, standard } };
472
1271
  });
1272
+ if (!lightingPreparation.ok) {
1273
+ internals.errorRegistry.fire(lightingPreparation.error);
1274
+ return false;
1275
+ }
1276
+ const { foldBuckets, light, standard } = lightingPreparation.value;
473
1277
 
474
1278
  // Case E (this commit): 0 renderables = legitimate scene (LO §1.1
475
1279
  // hello-window minimum semantic). Mirrors the Case C softening for
@@ -494,6 +1298,64 @@ export function recordFrame(
494
1298
  const effectiveShadowMapSize = resolveShadowMapSize(internals, lights);
495
1299
  const effectiveSpotShadowMapSize = resolveSpotShadowMapSize(internals, lights);
496
1300
  const graphShadowMapSize = effectiveShadowMapSize ?? effectiveSpotShadowMapSize;
1301
+ const volumeCapability = hasVolumetricFogCapability(
1302
+ internals.device.caps,
1303
+ internals.volumetricFogShaders,
1304
+ );
1305
+ volumeInspectionReady = true;
1306
+ volumeCapabilityForInspection = volumeCapability;
1307
+
1308
+ const preparedSpotLightProjector = prepareSpotLightProjectorFrame(
1309
+ internals,
1310
+ world,
1311
+ worlds,
1312
+ lights,
1313
+ volumetricFog,
1314
+ );
1315
+
1316
+ const preparedVolumetricFog = prepareVolumetricFogFrame(
1317
+ internals,
1318
+ frameState,
1319
+ world,
1320
+ worlds,
1321
+ volumetricFog,
1322
+ lights,
1323
+ volumeCapability,
1324
+ preparedSpotLightProjector,
1325
+ );
1326
+ preparedVolumetricFogForInspection = preparedVolumetricFog;
1327
+ // A degraded authored snapshot must keep consuming the last accepted GPU
1328
+ // projection. The current source is still surfaced as a diagnostic, but
1329
+ // it must not replace the graph/context that produced the last good frame.
1330
+ // An absent component is different: a successful no-volume frame retires
1331
+ // the accepted projection and compiles the normal graph.
1332
+ const acceptedFallback =
1333
+ volumeCapability && volumetricFog !== undefined && preparedVolumetricFog === undefined
1334
+ ? frameState.volumetricFogAccepted
1335
+ : undefined;
1336
+ const acceptedFallbackContext =
1337
+ acceptedFallback === undefined
1338
+ ? undefined
1339
+ : stageAcceptedVolumetricFogFrame(
1340
+ internals,
1341
+ frameState,
1342
+ frameState.volumetricFogAcceptedContext,
1343
+ );
1344
+ const graphVolumetricFog = volumeCapability
1345
+ ? preparedVolumetricFog === undefined
1346
+ ? acceptedFallback
1347
+ : volumetricFog
1348
+ : undefined;
1349
+ const baseVolumetricFogContext = preparedVolumetricFog ?? acceptedFallbackContext;
1350
+ // A volume graph replacement is provisional whenever it can affect the
1351
+ // accepted volume projection. This includes authoring degradation (which
1352
+ // consumes the LKG graph), an authored replacement, and an explicit
1353
+ // fog-off transition. Non-volume frames retain the legacy immediate graph
1354
+ // retirement path.
1355
+ const volumeTopologyCandidate =
1356
+ (graphVolumetricFog?.status === 'available' && baseVolumetricFogContext !== undefined) ||
1357
+ (graphVolumetricFog === undefined && frameState.volumetricFogAccepted !== undefined) ||
1358
+ (volumetricFog?.status === 'degraded' && acceptedFallback !== undefined);
497
1359
 
498
1360
  let preparedGpuDriven: PreparedGpuDrivenFrame | undefined;
499
1361
  if (gpuDriven !== undefined) {
@@ -503,7 +1365,6 @@ export function recordFrame(
503
1365
  meshes: pipelineState.meshes,
504
1366
  viewBindGroupLayout: pipelineState.viewBindGroupLayout,
505
1367
  meshResidencyEpoch: internals.gpuStore.meshResidencyEpoch,
506
- hdrp: frameState.isHdrpActive,
507
1368
  });
508
1369
  if (prepared.ok) preparedGpuDriven = prepared.value;
509
1370
  else if (prepared.error.code !== 'rhi-not-available')
@@ -517,7 +1378,36 @@ export function recordFrame(
517
1378
  lights.pointShadow.length > 0 ||
518
1379
  lights.spot.some((spot) => spot.castShadow);
519
1380
  const gpuOwnsAllPrimaryRaster = preparedGpuDriven?.ownsAllRenderables === true;
520
- gpuDriven?.onCoverage(gpuOwnsAllPrimaryRaster && !cpuShadowCastersActive);
1381
+ gpuDriven?.onCoverage(
1382
+ gpuOwnsAllPrimaryRaster && !cpuShadowCastersActive,
1383
+ standard.kind === 'clustered',
1384
+ );
1385
+
1386
+ const cubeCaptureWork = prepareCubeCapture(internals, cubeCapture, camera);
1387
+
1388
+ // Validate renderables before graph admission. Besides producing the draw
1389
+ // plan, this is the owner of per-frame cache eviction; running it before a
1390
+ // graph candidate can fail guarantees despawned instance buffers are
1391
+ // released even when the frame cannot be submitted.
1392
+ const gpuOwnedEntityKeys = preparedGpuDriven?.entityKeys ?? EMPTY_ENTITY_KEYS;
1393
+ const validated = runRecordProfilePhase(profilePhase, 'record/validation', () =>
1394
+ gpuOwnsAllPrimaryRaster && !cpuShadowCastersActive && cubeCaptureWork.length === 0
1395
+ ? []
1396
+ : validateRenderables(
1397
+ internals,
1398
+ world,
1399
+ worlds,
1400
+ pipelineState,
1401
+ frameState,
1402
+ renderables,
1403
+ transparentDispatch,
1404
+ cpuShadowCastersActive || cubeCaptureWork.length > 0
1405
+ ? EMPTY_ENTITY_KEYS
1406
+ : gpuOwnedEntityKeys,
1407
+ ),
1408
+ );
1409
+ gpuDriven?.owner.recordCpuValidation(validated, gpuOwnedEntityKeys);
1410
+ cleanPerFrameCaches(internals, frameState, validated, gpuOwnedEntityKeys);
521
1411
 
522
1412
  const preflightGraph = runRecordProfilePhase(profilePhase, 'record/render-graph', () =>
523
1413
  ensureCompiledFrameGraph(
@@ -531,12 +1421,60 @@ export function recordFrame(
531
1421
  graphShadowMapSize,
532
1422
  preparedGpuDriven,
533
1423
  featureGraphCandidate,
1424
+ cubeCapture?.state,
534
1425
  cameras.length === 0,
535
1426
  transmissionDemand,
1427
+ graphVolumetricFog,
1428
+ volumeTopologyCandidate,
1429
+ standard,
536
1430
  ),
537
1431
  );
538
1432
  if (preflightGraph === null) return false;
539
1433
 
1434
+ // The compiled graph is the acceptance boundary for the prepared
1435
+ // Standard lighting declaration. Upload the same declaration only after
1436
+ // that graph is current; a failed candidate therefore cannot leave an old
1437
+ // graph consuming a new buffer generation.
1438
+ if (frameState.standardLightingGraphSignature !== standardLightingTopologySignature(standard)) {
1439
+ internals.errorRegistry.fire(
1440
+ new RendererOperationError('device-operation-failed', {
1441
+ operation: 'draw',
1442
+ cause: {
1443
+ code: 'standard-graph-generation-mismatch',
1444
+ expected: 'compiled graph and prepared Standard lighting share one topology signature',
1445
+ hint: 'retry the frame after the Standard graph candidate is accepted',
1446
+ detail: {
1447
+ expected: standardLightingTopologySignature(standard),
1448
+ actual: frameState.standardLightingGraphSignature,
1449
+ },
1450
+ },
1451
+ }),
1452
+ );
1453
+ return false;
1454
+ }
1455
+ if (standard.kind === 'clustered') {
1456
+ const clusterWrite = runRecordProfilePhase(
1457
+ profilePhase,
1458
+ 'record/scene-state/hdrp-cluster',
1459
+ () =>
1460
+ writeHdrpClusterAndSsaoBuffers(
1461
+ internals,
1462
+ frameState,
1463
+ camera,
1464
+ standard.prepared,
1465
+ standard.transport,
1466
+ profilePhase,
1467
+ pipelineState.hdrpClusterMembershipPipeline !== null,
1468
+ pipelineState.hdrpClusterMembershipBindGroupLayout,
1469
+ preparedSpotLightProjector?.lightSlotIndex,
1470
+ ),
1471
+ );
1472
+ if (!clusterWrite.ok) {
1473
+ internals.errorRegistry.fire(clusterWrite.error);
1474
+ return false;
1475
+ }
1476
+ }
1477
+
540
1478
  // Acquire the swap-chain texture + colour view + target dimensions (with
541
1479
  // one reconfigure-and-retry on surface-outdated). Extracted to
542
1480
  // acquireSwapChainTarget (M3/w18); returns null on unrecoverable failure
@@ -623,6 +1561,35 @@ export function recordFrame(
623
1561
  : { kind: 'taa', view: temporalView };
624
1562
  const recordCamera: CameraSnapshot =
625
1563
  temporalView === undefined ? camera : { ...camera, temporal: temporalView };
1564
+ let activeVolumetricFogContext: VolumetricFogFrameContext | undefined;
1565
+ let activeVolumetricFogSignature: VolumeTemporalSignature | undefined;
1566
+ if (graphVolumetricFog?.status === 'available' && baseVolumetricFogContext !== undefined) {
1567
+ const resolvedHistory = resolveVolumetricFogHistoryContext(
1568
+ frameState,
1569
+ preflightGraph,
1570
+ graphVolumetricFog,
1571
+ baseVolumetricFogContext,
1572
+ camera,
1573
+ lights,
1574
+ targetW,
1575
+ targetH,
1576
+ );
1577
+ activeVolumetricFogContext = resolvedHistory.context;
1578
+ activeVolumetricFogSignature = resolvedHistory.signature;
1579
+ const historyFlag = new Float32Array([activeVolumetricFogContext.historyValid ? 1 : 0]);
1580
+ if (frameState.volumetricFogPendingParams !== undefined) {
1581
+ frameState.volumetricFogPendingParams[31] = historyFlag[0] ?? 0;
1582
+ }
1583
+ const historyFlagWritten = internals.device.queue.writeBuffer(
1584
+ activeVolumetricFogContext.paramsBuffer,
1585
+ 31 * Float32Array.BYTES_PER_ELEMENT,
1586
+ historyFlag,
1587
+ );
1588
+ if (!historyFlagWritten.ok) {
1589
+ internals.errorRegistry.fire(historyFlagWritten.error);
1590
+ return false;
1591
+ }
1592
+ }
626
1593
 
627
1594
  const depthView: TextureView | null = null;
628
1595
  // feat-20260709 M3 / D-3: clear-color read from the active CameraSnapshot's
@@ -691,41 +1658,6 @@ export function recordFrame(
691
1658
  const geometryColorResolveView: TextureView | null = null;
692
1659
  const ldrSpriteColorView: TextureView | null = null;
693
1660
 
694
- // Validate renderable handles + collect render plan. Empty `renderables`
695
- // input or all-unregistered handles both produce `validatedOrdered.length === 0`,
696
- // which is the Case E (clear-pass-only) path.
697
- //
698
- // feat-20260520-2d-sprite-layer-mvp M-3 / w25 (@new-surface): per-entry
699
- // `renderableIndex` tracks the position into the original renderables[]
700
- // array so the bucket-aware reordering below can correlate
701
- // transparent-sort output (TransparentEntry.renderableIndex) back to
702
- // the validated row.
703
- // bug-20260527-renderstate-pipeline-dispatch-gap D-4:
704
- // build a renderableIndex -> renderState map from dispatch entries
705
- // so each ValidatedRenderable carries its per-material renderState
706
- // override without an O(n^2) back-scan in the draw loop.
707
- const gpuOwnedEntityKeys = preparedGpuDriven?.entityKeys ?? EMPTY_ENTITY_KEYS;
708
- const validated = runRecordProfilePhase(profilePhase, 'record/validation', () =>
709
- gpuOwnsAllPrimaryRaster && !cpuShadowCastersActive
710
- ? []
711
- : validateRenderables(
712
- internals,
713
- world,
714
- worlds,
715
- pipelineState,
716
- frameState,
717
- renderables,
718
- transparentDispatch,
719
- cpuShadowCastersActive ? EMPTY_ENTITY_KEYS : gpuOwnedEntityKeys,
720
- ),
721
- );
722
- gpuDriven?.owner.recordCpuValidation(validated, gpuOwnedEntityKeys);
723
-
724
- // Per-frame cache clean-up: drop per-entity BG cache entries + instance
725
- // buffers whose entityKey is absent from the current validated set (despawn
726
- // eviction). Extracted to cleanPerFrameCaches (M3/w18).
727
- cleanPerFrameCaches(internals, frameState, validated, gpuOwnedEntityKeys);
728
-
729
1661
  // Dispatch-ordered render plan: reorder validated renderables to dispatch
730
1662
  // order, run the mesh-SSBO capacity gate (graceful truncation), and build
731
1663
  // the fold dispatch plan. Extracted to buildDispatchPlan (M3/w18).
@@ -772,7 +1704,18 @@ export function recordFrame(
772
1704
  // feat-20260531-skybox-env-background M2 / w6: gate relaxed from
773
1705
  // `validatedOrdered.length > 0` to include skybox-only frames
774
1706
  // (plan-strategy D-3, R-3).
775
- if (validatedOrdered.length > 0 || skyboxActive) {
1707
+ const currentViewProjection = mat4.create();
1708
+ mat4.multiply(
1709
+ currentViewProjection,
1710
+ computeProjectionMatrix(camera),
1711
+ computeViewMatrix(camera),
1712
+ );
1713
+ if (
1714
+ validatedOrdered.length > 0 ||
1715
+ skyboxActive ||
1716
+ activeVolumetricFogContext !== undefined ||
1717
+ preparedVolumetricFog !== undefined
1718
+ ) {
776
1719
  runRecordProfilePhase(profilePhase, 'record/uploads', () => {
777
1720
  // View UBO + CSM/spot-shadow matrix pack: assembled + uploaded in one
778
1721
  // queue.writeBuffer round-trip. Extracted to view-ubo.ts (M3/w18) so
@@ -786,6 +1729,7 @@ export function recordFrame(
786
1729
  frameState.spotShadowSnapshots,
787
1730
  fog,
788
1731
  temporalView,
1732
+ preparedSpotLightProjector?.spotIndex,
789
1733
  );
790
1734
  if (pipelineState.pointsLinesViewBuffer !== undefined) {
791
1735
  writePointsLinesViewUbo(
@@ -802,11 +1746,6 @@ export function recordFrame(
802
1746
  lights,
803
1747
  );
804
1748
 
805
- // Per-frame full rewrite of the PointLight + SpotLight std430 storage
806
- // buffers (header + first-slice cap N=4 slots). Extracted to
807
- // writePointSpotLightBuffers (M3/w18).
808
- writePointSpotLightBuffers(internals, pipelineState, lights);
809
-
810
1749
  // Per-renderable entity_world upload (batched): all N mat4+normalMatrix
811
1750
  // slots assembled into a single contiguous scratch buffer, then flushed
812
1751
  // as one writeBuffer call. Extracted to mesh-ssbo.ts (M3/w21) so its
@@ -855,10 +1794,40 @@ export function recordFrame(
855
1794
  validated.length > 0 || preparedGpuDriven !== undefined,
856
1795
  bindGroupCounts,
857
1796
  undefined,
858
- false,
1797
+ true,
1798
+ standard,
859
1799
  ),
860
1800
  );
861
1801
 
1802
+ for (const capture of cubeCaptureWork) {
1803
+ writeViewUbo(
1804
+ internals.device.queue,
1805
+ pipelineState.viewUniformBuffer,
1806
+ capture.faceCamera,
1807
+ light,
1808
+ lights,
1809
+ frameState.spotShadowSnapshots,
1810
+ capture.viewBindGroupDynamicOffset,
1811
+ undefined,
1812
+ preparedSpotLightProjector?.spotIndex,
1813
+ );
1814
+ }
1815
+ for (const probe of cubeCapture?.reflectionProbes?.graph.work ?? []) {
1816
+ if (probe.faceCamera === undefined || probe.viewBindGroupDynamicOffset === undefined)
1817
+ continue;
1818
+ writeViewUbo(
1819
+ internals.device.queue,
1820
+ pipelineState.viewUniformBuffer,
1821
+ probe.faceCamera,
1822
+ light,
1823
+ lights,
1824
+ frameState.spotShadowSnapshots,
1825
+ probe.viewBindGroupDynamicOffset,
1826
+ undefined,
1827
+ preparedSpotLightProjector?.spotIndex,
1828
+ );
1829
+ }
1830
+
862
1831
  // ── feat-20260529-rendergraph-pass-abstraction M4 / w13b ───────────
863
1832
  // The compiled typed graph is the sole per-frame pass owner.
864
1833
  //
@@ -922,20 +1891,48 @@ export function recordFrame(
922
1891
  geometryColorResolveView,
923
1892
  ldrSpriteColorView,
924
1893
  postProcessParams: framePostProcessParams,
1894
+ ...(internals.volumetricFogShaders === undefined
1895
+ ? {}
1896
+ : { volumetricFogShaders: internals.volumetricFogShaders }),
1897
+ ...(activeVolumetricFogContext === undefined
1898
+ ? {}
1899
+ : { volumetricFog: activeVolumetricFogContext }),
1900
+ ...(preparedSpotLightProjector === undefined
1901
+ ? {}
1902
+ : { spotLightProjector: preparedSpotLightProjector }),
925
1903
  dispatch: transparentDispatch,
926
1904
  hdrpClusterBindGroup,
927
1905
  hdrpClusterMembershipBindGroup,
1906
+ standardLighting: standard,
928
1907
  foldDispatchPlan,
929
1908
  materialSlotIndices,
930
1909
  materialSlots,
931
1910
  materialSlotOwners,
932
1911
  materialSlotCount,
1912
+ ...(cubeCapture?.reflectionProbes === undefined
1913
+ ? {}
1914
+ : { reflectionProbes: cubeCapture.reflectionProbes }),
933
1915
  pointsLines: pointsLinesOwner,
934
1916
  materialBgAssemblyCache: frameState.materialBgAssemblyCache,
935
1917
  directionalShadowCacheReuse: directionalShadowCache.reuse,
936
1918
  ...(profilePhase !== undefined ? { profilePhase } : {}),
937
1919
  };
938
- const submitted = runRecordProfilePhase(profilePhase, 'record/graph-execute', () =>
1920
+ let completion: Promise<unknown> | undefined;
1921
+ const stagedTemporalFrame = {
1922
+ frameId: frameState.frameNumber,
1923
+ currentViewProjection: new Float32Array(currentViewProjection),
1924
+ jitter: [0, 0] as [number, number],
1925
+ viewport: { width: targetW, height: targetH },
1926
+ cameraPosition: [
1927
+ camera.position[0] ?? 0,
1928
+ camera.position[1] ?? 0,
1929
+ camera.position[2] ?? 0,
1930
+ ] as [number, number, number],
1931
+ };
1932
+ frameState.temporalFrameInput = stagedTemporalFrame;
1933
+ frameState.temporalFrameTransaction.stage(stagedTemporalFrame);
1934
+ volumeSubmissionAttempted = volumeTopologyCandidate || activeVolumetricFogContext !== undefined;
1935
+ submitted = runRecordProfilePhase(profilePhase, 'record/graph-execute', () =>
939
1936
  executeCompiledFrameGraph(
940
1937
  internals,
941
1938
  frameState,
@@ -944,19 +1941,92 @@ export function recordFrame(
944
1941
  profilePhase === undefined
945
1942
  ? undefined
946
1943
  : (pass, encode) => profilePhase(graphExecutionPhase(pass.name), encode),
1944
+ cubeCaptureWork.length === 0 ? undefined : cubeCaptureWork.map((work) => work.faceIndex),
1945
+ (done) => {
1946
+ completion = done;
1947
+ },
1948
+ timingCapture,
947
1949
  ),
948
1950
  );
1951
+ if (cubeCapture !== undefined && cubeCaptureWork.length > 0) {
1952
+ const completed = cubeCapture.scheduler.completeSubmission(submitted, completion);
1953
+ if (!completed.ok) internals.errorRegistry.fire(completed.error);
1954
+ }
1955
+ cubeCapture?.reflectionProbes?.completeSubmission(submitted, completion);
949
1956
  if (submitted && !directionalShadowCache.reuse) {
950
1957
  frameState.directionalShadowCache = frameState.directionalShadowCacheRecorded
951
1958
  ? directionalShadowCache.next
952
1959
  : null;
953
1960
  }
1961
+ if (submitted) {
1962
+ if (graphVolumetricFog === undefined) {
1963
+ frameState.volumetricFogAccepted = undefined;
1964
+ frameState.volumetricFogAcceptedContext = undefined;
1965
+ frameState.volumetricFogHistoryGraph = null;
1966
+ frameState.volumetricFogHistorySlot = null;
1967
+ frameState.volumetricFogHistorySignature = null;
1968
+ retireVolumetricFogParams(internals, frameState);
1969
+ } else if (
1970
+ activeVolumetricFogContext !== undefined &&
1971
+ activeVolumetricFogSignature !== undefined
1972
+ ) {
1973
+ // Temporal history is graph-owned state. Promote the write slot only
1974
+ // after queue submission succeeds; a failed candidate keeps the
1975
+ // accepted graph/context/slot and retries into the same pending slot.
1976
+ frameState.volumetricFogHistoryGraph = preflightGraph;
1977
+ frameState.volumetricFogHistorySlot = activeVolumetricFogContext.historyWriteSlot;
1978
+ frameState.volumetricFogHistorySignature = activeVolumetricFogSignature;
1979
+ if (frameState.volumetricFogParamsPendingSlot !== null) {
1980
+ frameState.volumetricFogParamsAcceptedSlot = frameState.volumetricFogParamsPendingSlot;
1981
+ frameState.volumetricFogAcceptedParams =
1982
+ frameState.volumetricFogPendingParams === undefined
1983
+ ? undefined
1984
+ : new Float32Array(frameState.volumetricFogPendingParams);
1985
+ frameState.volumetricFogParamsPendingSlot = null;
1986
+ frameState.volumetricFogPendingParams = undefined;
1987
+ }
1988
+ if (volumetricFog?.status === 'available' && preparedVolumetricFog !== undefined) {
1989
+ frameState.volumetricFogAccepted = volumetricFog;
1990
+ frameState.volumetricFogAcceptedContext = activeVolumetricFogContext;
1991
+ } else if (acceptedFallback !== undefined) {
1992
+ // The current authored source is degraded, so keep the old accepted
1993
+ // POD while advancing the LKG graph's ping-pong slot.
1994
+ frameState.volumetricFogAcceptedContext = activeVolumetricFogContext;
1995
+ }
1996
+ } else if (volumetricFog?.status === 'available' && preparedVolumetricFog !== undefined) {
1997
+ // Defensive fallback for a volume graph that was accepted before the
1998
+ // temporal context could be resolved; do not manufacture history.
1999
+ frameState.volumetricFogAccepted = volumetricFog;
2000
+ frameState.volumetricFogAcceptedContext = preparedVolumetricFog;
2001
+ }
2002
+ frameState.standardLightingInspection = inspectStandardLighting(standard);
2003
+ frameState.pointShadowInspection = inspectPointShadow(
2004
+ lights.pointShadow,
2005
+ SHADOW_ATLAS_DEFAULT_LAYERS,
2006
+ );
2007
+ }
954
2008
  return submitted;
955
2009
  } finally {
2010
+ // Volume topology replacements are provisional until the queue submission
2011
+ // succeeds. This runs for every early-return path as well, so a swapchain,
2012
+ // encoder, or graph failure cannot leave a non-submitted candidate as the
2013
+ // next frame's accepted graph.
2014
+ settleVolumetricFogGraphCandidate(frameState, submitted);
2015
+ if (volumeInspectionReady && (submitted || volumeSubmissionAttempted)) {
2016
+ updateVolumetricFogInspection(
2017
+ frameState,
2018
+ volumetricFog,
2019
+ lights,
2020
+ preparedVolumetricFogForInspection,
2021
+ submitted,
2022
+ volumeCapabilityForInspection,
2023
+ volumeSubmissionAttempted,
2024
+ );
2025
+ }
956
2026
  frameState.frameNumber += 1;
957
- // feat-20260608-cluster-lighting M5 / w22: clear HDRP once-per-frame fired
2027
+ // feat-20260608-cluster-lighting M5 / w22: clear Standard once-per-frame fired
958
2028
  // set so the next frame re-fires if the condition persists.
959
- frameState.hdrpOncePerFrameFired.clear();
2029
+ frameState.standardOncePerFrameFired.clear();
960
2030
  }
961
2031
  }
962
2032
 
@@ -1024,6 +2094,20 @@ function sameLightViewProj(
1024
2094
  return true;
1025
2095
  }
1026
2096
 
2097
+ function sameDirectionalShadowQuality(
2098
+ previous: DirectionalShadowQuality,
2099
+ next: DirectionalShadowQuality,
2100
+ ): boolean {
2101
+ if (previous.kind !== next.kind) return false;
2102
+ if (previous.kind === 'pcf' && next.kind === 'pcf') return previous.kernel === next.kernel;
2103
+ if (previous.kind !== 'pcss' || next.kind !== 'pcss') return false;
2104
+ return (
2105
+ previous.preset === next.preset &&
2106
+ previous.angularRadiusRadians === next.angularRadiusRadians &&
2107
+ previous.maxPenumbraTexels === next.maxPenumbraTexels
2108
+ );
2109
+ }
2110
+
1027
2111
  function prepareDirectionalShadowCache(
1028
2112
  internals: RenderSystemInternals,
1029
2113
  frameState: RenderFrameState,
@@ -1043,6 +2127,7 @@ function prepareDirectionalShadowCache(
1043
2127
  cascadeCount === undefined ||
1044
2128
  cascadeCount <= 0 ||
1045
2129
  lightViewProj === undefined ||
2130
+ lights.directionalShadowQuality === undefined ||
1046
2131
  validatedOrdered.length === 0
1047
2132
  ) {
1048
2133
  return { reuse: false, next: null };
@@ -1055,6 +2140,10 @@ function prepareDirectionalShadowCache(
1055
2140
  cached !== null &&
1056
2141
  cached.shadowMapSize === shadowMapSize &&
1057
2142
  cached.cascadeCount === cascadeCount &&
2143
+ sameDirectionalShadowQuality(
2144
+ cached.directionalShadowQuality,
2145
+ lights.directionalShadowQuality,
2146
+ ) &&
1058
2147
  cached.pipelineHandle === frameState.installedPipelineHandle &&
1059
2148
  cached.graphTopologyKey === frameState.compiledFrameGraphTopologyKey &&
1060
2149
  cached.assetCatalogEpoch === assetCatalogEpoch &&
@@ -1083,6 +2172,7 @@ function prepareDirectionalShadowCache(
1083
2172
  graphTopologyKey: frameState.compiledFrameGraphTopologyKey,
1084
2173
  shadowMapSize,
1085
2174
  cascadeCount,
2175
+ directionalShadowQuality: lights.directionalShadowQuality,
1086
2176
  lightViewProj: lightViewProj.map((matrix) => new Float32Array(matrix)),
1087
2177
  meshResidencyEpoch,
1088
2178
  },