@forgeax/engine-render 0.1.23 → 0.1.25

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 (322) hide show
  1. package/README.md +231 -10
  2. package/dist/assembly/bundler-contract.d.ts +9 -0
  3. package/dist/assembly/bundler-contract.d.ts.map +1 -0
  4. package/dist/assembly/device-feature-admission.d.ts +8 -0
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -1
  6. package/dist/assembly/extended-lighting-fallback.d.ts +13 -0
  7. package/dist/assembly/extended-lighting-fallback.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +7 -243
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/material/assembly.d.ts +3 -2
  11. package/dist/assembly/material/assembly.d.ts.map +1 -1
  12. package/dist/assembly/material/standard-layer-projection.d.ts +10 -0
  13. package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -0
  14. package/dist/assembly/material/surface-projection.d.ts +24 -0
  15. package/dist/assembly/material/surface-projection.d.ts.map +1 -0
  16. package/dist/assembly/material-shader-policy.d.ts +188 -0
  17. package/dist/assembly/material-shader-policy.d.ts.map +1 -0
  18. package/dist/assembly/material-shader-prewarm.d.ts +21 -0
  19. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
  20. package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
  21. package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
  22. package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
  23. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
  24. package/dist/assembly/recovery/generation.d.ts +46 -0
  25. package/dist/assembly/recovery/generation.d.ts.map +1 -0
  26. package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
  27. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
  28. package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
  29. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
  30. package/dist/assembly/renderer-facade.d.ts +10 -0
  31. package/dist/assembly/renderer-facade.d.ts.map +1 -0
  32. package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
  33. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  34. package/dist/assembly/renderer-helpers.d.ts +19 -0
  35. package/dist/assembly/renderer-helpers.d.ts.map +1 -0
  36. package/dist/assembly/renderer-lifecycle.d.ts +34 -0
  37. package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
  38. package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
  39. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
  40. package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
  41. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
  42. package/dist/assembly/webgpu-ready.d.ts +55 -0
  43. package/dist/assembly/webgpu-ready.d.ts.map +1 -0
  44. package/dist/assembly/webgpu-renderer.d.ts +152 -0
  45. package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
  46. package/dist/assembly/webgpu-vertex-layouts.d.ts +89 -0
  47. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -0
  48. package/dist/assets/asset-decoders.d.ts.map +1 -1
  49. package/dist/authoring.mjs +1 -1
  50. package/dist/{chunk-FF7K3FSO.mjs → chunk-ADHHVYLW.mjs} +89 -89
  51. package/dist/chunk-ADHHVYLW.mjs.map +1 -0
  52. package/dist/{chunk-YALPS44A.mjs → chunk-E34VL5VI.mjs} +47 -47
  53. package/dist/chunk-E34VL5VI.mjs.map +1 -0
  54. package/dist/{chunk-4L3P6DT5.mjs → chunk-GE3SDD72.mjs} +37 -5
  55. package/dist/chunk-GE3SDD72.mjs.map +1 -0
  56. package/dist/{chunk-HZUPSYW7.mjs → chunk-HKXTW355.mjs} +105 -15
  57. package/dist/chunk-HKXTW355.mjs.map +1 -0
  58. package/dist/chunk-JSEDTZYS.mjs +413 -0
  59. package/dist/chunk-JSEDTZYS.mjs.map +1 -0
  60. package/dist/{chunk-FBC4R44J.mjs → chunk-KM2NOX2I.mjs} +57 -57
  61. package/dist/chunk-KM2NOX2I.mjs.map +1 -0
  62. package/dist/{chunk-DIYW3M5D.mjs → chunk-N3RT2EUR.mjs} +2288 -2238
  63. package/dist/chunk-N3RT2EUR.mjs.map +1 -0
  64. package/dist/{chunk-ZUWKAZ2I.mjs → chunk-TCLDX7SZ.mjs} +1167 -368
  65. package/dist/chunk-TCLDX7SZ.mjs.map +1 -0
  66. package/dist/{chunk-QODA5UKU.mjs → chunk-ZZ4YQ474.mjs} +2 -2
  67. package/dist/chunk-ZZ4YQ474.mjs.map +1 -0
  68. package/dist/components/instances.d.ts +13 -31
  69. package/dist/components/instances.d.ts.map +1 -1
  70. package/dist/construct-renderer.mjs +20688 -16314
  71. package/dist/construct-renderer.mjs.map +1 -1
  72. package/dist/device/device-scope.d.ts +2 -0
  73. package/dist/device/device-scope.d.ts.map +1 -1
  74. package/dist/device/gpu-residency.d.ts +24 -4
  75. package/dist/device/gpu-residency.d.ts.map +1 -1
  76. package/dist/environment/frame.d.ts +5 -0
  77. package/dist/environment/frame.d.ts.map +1 -1
  78. package/dist/environment/inspection.d.ts +7 -0
  79. package/dist/environment/inspection.d.ts.map +1 -1
  80. package/dist/environment/lifecycle.d.ts +20 -2
  81. package/dist/environment/lifecycle.d.ts.map +1 -1
  82. package/dist/errors/recover.d.ts +58 -2
  83. package/dist/errors/recover.d.ts.map +1 -1
  84. package/dist/errors/render.d.ts +13 -0
  85. package/dist/errors/render.d.ts.map +1 -1
  86. package/dist/extract/contracts.d.ts +899 -0
  87. package/dist/extract/contracts.d.ts.map +1 -0
  88. package/dist/features/host.d.ts +2 -0
  89. package/dist/features/host.d.ts.map +1 -1
  90. package/dist/features/prepared-gpu-work.d.ts +3 -0
  91. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  92. package/dist/features/prepared-graphics-store.d.ts +2 -0
  93. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  94. package/dist/gpu-driven/production-raster.d.ts +34 -1
  95. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  96. package/dist/gpu-scene.d.ts +5 -0
  97. package/dist/gpu-scene.d.ts.map +1 -1
  98. package/dist/ibl/IblPipelineCache.d.ts +7 -6
  99. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  100. package/dist/ibl/skylight-bind-group.d.ts +7 -1
  101. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  102. package/dist/index.d.ts +2 -1
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.mjs +27 -9
  105. package/dist/index.mjs.map +1 -1
  106. package/dist/instance-buffer-cache.d.ts +7 -1
  107. package/dist/instance-buffer-cache.d.ts.map +1 -1
  108. package/dist/instances.d.ts +101 -0
  109. package/dist/instances.d.ts.map +1 -0
  110. package/dist/internal.mjs +6 -6
  111. package/dist/light-buffer-layout.d.ts +3 -0
  112. package/dist/light-buffer-layout.d.ts.map +1 -1
  113. package/dist/materials.d.ts +96 -11
  114. package/dist/materials.d.ts.map +1 -1
  115. package/dist/mesh-material-bindings.d.ts +56 -0
  116. package/dist/mesh-material-bindings.d.ts.map +1 -1
  117. package/dist/occlusion-proxy.d.ts +4 -0
  118. package/dist/occlusion-proxy.d.ts.map +1 -0
  119. package/dist/pbr-pipeline.d.ts +32 -9
  120. package/dist/pbr-pipeline.d.ts.map +1 -1
  121. package/dist/pipeline-builder.d.ts.map +1 -1
  122. package/dist/pipeline-spec.d.ts.map +1 -1
  123. package/dist/points-lines/prepare.d.ts +3 -0
  124. package/dist/points-lines/prepare.d.ts.map +1 -1
  125. package/dist/points-lines/standard-owner.d.ts +33 -0
  126. package/dist/points-lines/standard-owner.d.ts.map +1 -0
  127. package/dist/record/frame-snapshot.d.ts +18 -2
  128. package/dist/record/frame-snapshot.d.ts.map +1 -1
  129. package/dist/record/frame-targets.d.ts.map +1 -1
  130. package/dist/record/frame.d.ts +14 -2
  131. package/dist/record/frame.d.ts.map +1 -1
  132. package/dist/record/main-pass-geometry.d.ts +36 -4
  133. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  134. package/dist/record/main-pass-material.d.ts +7 -17
  135. package/dist/record/main-pass-material.d.ts.map +1 -1
  136. package/dist/record/main-pass.d.ts.map +1 -1
  137. package/dist/record/mesh-ssbo.d.ts +11 -3
  138. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  139. package/dist/record/recovery-pipeline.d.ts +30 -0
  140. package/dist/record/recovery-pipeline.d.ts.map +1 -0
  141. package/dist/record/render-context.d.ts +28 -5
  142. package/dist/record/render-context.d.ts.map +1 -1
  143. package/dist/record/shadow-pass.d.ts +11 -1
  144. package/dist/record/shadow-pass.d.ts.map +1 -1
  145. package/dist/record/typed-frame-graph.d.ts +1 -1
  146. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  147. package/dist/recovery/render-system-candidate.d.ts +65 -0
  148. package/dist/recovery/render-system-candidate.d.ts.map +1 -0
  149. package/dist/render-contract.d.ts +68 -7
  150. package/dist/render-contract.d.ts.map +1 -1
  151. package/dist/render-system-extract-tail.d.ts +16 -0
  152. package/dist/render-system-extract-tail.d.ts.map +1 -0
  153. package/dist/render-system-extract.d.ts +180 -22
  154. package/dist/render-system-extract.d.ts.map +1 -1
  155. package/dist/render-system.d.ts +131 -4
  156. package/dist/render-system.d.ts.map +1 -1
  157. package/dist/scene/render-scene-types.d.ts +2 -0
  158. package/dist/scene/render-scene-types.d.ts.map +1 -1
  159. package/dist/scene/render-scene.d.ts +31 -1
  160. package/dist/scene/render-scene.d.ts.map +1 -1
  161. package/dist/systems/skin-palette-allocator.d.ts +6 -0
  162. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  163. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  164. package/dist/temporal/index.mjs +6 -6
  165. package/dist/temporal/taa-history-store.d.ts +15 -1
  166. package/dist/temporal/taa-history-store.d.ts.map +1 -1
  167. package/dist/temporal/transaction.d.ts +5 -0
  168. package/dist/temporal/transaction.d.ts.map +1 -1
  169. package/package.json +20 -20
  170. package/src/__tests__/device-feature-admission.unit.test.ts +6 -0
  171. package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
  172. package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
  173. package/src/__tests__/environment-generation.integration.test.ts +23 -0
  174. package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
  175. package/src/__tests__/factory-contract.integration.test.ts +122 -7
  176. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
  177. package/src/__tests__/gpu-scene.unit.test.ts +38 -2
  178. package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
  179. package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
  180. package/src/__tests__/instances-store.unit.test.ts +46 -0
  181. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  182. package/src/__tests__/material-contract-inventory.unit.test.ts +29 -22
  183. package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
  184. package/src/__tests__/material-pass-policy.unit.test.ts +69 -0
  185. package/src/__tests__/material-snapshot-mutation.integration.test.ts +69 -0
  186. package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
  187. package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
  188. package/src/__tests__/material-surface-authoring.test-d.ts +62 -0
  189. package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
  190. package/src/__tests__/materials-transmission-validation.unit.test.ts +24 -0
  191. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -5
  192. package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
  193. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +1 -1
  194. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
  195. package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
  196. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
  197. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +127 -18
  198. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
  199. package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
  200. package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
  201. package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
  202. package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
  203. package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
  204. package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
  205. package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
  206. package/src/__tests__/shader-manifest-fixture.ts +13 -16
  207. package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
  208. package/src/__tests__/skin-motion-regression.unit.test.ts +33 -0
  209. package/src/__tests__/skinned-shadow-caster.test.ts +30 -2
  210. package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
  211. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +60 -0
  212. package/src/__tests__/surface-standard-pipeline.browser.test.ts +20 -0
  213. package/src/__tests__/surface-standard-pipeline.dawn.test.ts +20 -0
  214. package/src/__tests__/surface-standard-pipeline.fixture.ts +132 -0
  215. package/src/__tests__/surface-variant-provenance.integration.test.ts +33 -0
  216. package/src/__tests__/transmission-prewarm.unit.test.ts +95 -85
  217. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +1 -1
  218. package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
  219. package/src/assembly/bundler-contract.ts +9 -0
  220. package/src/assembly/device-feature-admission.ts +15 -5
  221. package/src/assembly/extended-lighting-fallback.ts +216 -0
  222. package/src/assembly/factory.ts +83 -10088
  223. package/src/assembly/material/__tests__/standard-layer-plan-projection.integration.test.ts +18 -0
  224. package/src/assembly/material/__tests__/surface-pass-projection.integration.test.ts +62 -0
  225. package/src/assembly/material/assembly.ts +18 -2
  226. package/src/assembly/material/standard-layer-projection.ts +16 -0
  227. package/src/assembly/material/surface-projection.ts +90 -0
  228. package/src/assembly/material-shader-policy.ts +932 -0
  229. package/src/assembly/material-shader-prewarm.ts +88 -0
  230. package/src/assembly/mesh-ssbo-grow.ts +270 -0
  231. package/src/assembly/recovery/device-loss-fanout.ts +89 -0
  232. package/src/assembly/recovery/generation.ts +141 -0
  233. package/src/assembly/recovery/recovery-attempt.ts +800 -0
  234. package/src/assembly/recovery/renderer-recover.ts +906 -0
  235. package/src/assembly/renderer-facade.ts +342 -0
  236. package/src/assembly/renderer-frame-transaction.ts +45 -0
  237. package/src/assembly/renderer-helpers.ts +99 -0
  238. package/src/assembly/renderer-lifecycle.ts +159 -0
  239. package/src/assembly/shader-prewarm-policy.ts +157 -0
  240. package/src/assembly/webgpu-ready-contract.ts +14 -0
  241. package/src/assembly/webgpu-ready.ts +4066 -0
  242. package/src/assembly/webgpu-renderer.ts +4027 -0
  243. package/src/assembly/webgpu-vertex-layouts.ts +132 -0
  244. package/src/assets/asset-decoders.ts +27 -4
  245. package/src/components/camera.ts +1 -1
  246. package/src/components/instances.ts +14 -197
  247. package/src/debug-draw-glue.ts +1 -1
  248. package/src/device/device-scope.ts +35 -6
  249. package/src/device/gpu-residency.ts +194 -26
  250. package/src/environment/frame.ts +6 -0
  251. package/src/environment/inspection.ts +10 -0
  252. package/src/environment/lifecycle.ts +114 -8
  253. package/src/errors/recover.ts +76 -5
  254. package/src/errors/render.ts +17 -0
  255. package/src/extract/contracts.ts +974 -0
  256. package/src/features/host.ts +16 -0
  257. package/src/features/prepared-gpu-work.ts +20 -2
  258. package/src/features/prepared-graphics-store.ts +17 -0
  259. package/src/gpu-driven/production-raster.ts +131 -3
  260. package/src/gpu-scene.ts +87 -31
  261. package/src/ibl/IblPipelineCache.ts +134 -50
  262. package/src/ibl/skylight-bind-group.ts +26 -0
  263. package/src/index.ts +10 -1
  264. package/src/instance-buffer-cache.ts +14 -1
  265. package/src/instances.ts +288 -0
  266. package/src/light-buffer-layout.ts +3 -0
  267. package/src/materials.ts +365 -53
  268. package/src/mesh-material-bindings.ts +59 -0
  269. package/src/occlusion-proxy.ts +64 -0
  270. package/src/pbr-pipeline.ts +139 -15
  271. package/src/pipeline-builder.ts +14 -10
  272. package/src/pipeline-spec.ts +6 -14
  273. package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
  274. package/src/points-lines/prepare.ts +9 -0
  275. package/src/points-lines/standard-owner.ts +403 -0
  276. package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
  277. package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
  278. package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
  279. package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
  280. package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
  281. package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
  282. package/src/record/__tests__/temporal-payload.unit.test.ts +22 -43
  283. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -1
  284. package/src/record/frame-snapshot.ts +34 -2
  285. package/src/record/frame-targets.ts +11 -1
  286. package/src/record/frame.ts +37 -4
  287. package/src/record/helpers.ts +1 -1
  288. package/src/record/main-pass-geometry.ts +727 -63
  289. package/src/record/main-pass-material.ts +316 -209
  290. package/src/record/main-pass-sprite-draws.ts +7 -8
  291. package/src/record/main-pass.ts +94 -6
  292. package/src/record/mesh-ssbo.ts +32 -10
  293. package/src/record/recovery-pipeline.ts +695 -0
  294. package/src/record/render-context.ts +39 -4
  295. package/src/record/shadow-pass.ts +46 -99
  296. package/src/record/typed-frame-graph.ts +45 -1
  297. package/src/record/view-ubo.ts +3 -3
  298. package/src/recovery/render-system-candidate.ts +876 -0
  299. package/src/render-contract.ts +69 -7
  300. package/src/render-system-extract-tail.ts +3039 -0
  301. package/src/render-system-extract.ts +177 -3003
  302. package/src/render-system.ts +906 -662
  303. package/src/scene/render-scene-types.ts +2 -0
  304. package/src/scene/render-scene.ts +703 -37
  305. package/src/systems/skin-palette-allocator.ts +129 -7
  306. package/src/temporal/frame-coordinator.ts +4 -1
  307. package/src/temporal/taa-history-store.ts +46 -2
  308. package/src/temporal/temporal-view.ts +1 -1
  309. package/src/temporal/transaction.ts +9 -0
  310. package/src/tilemap-chunk-extract-system.ts +1 -1
  311. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
  312. package/src/transmission/__tests__/surface-compatibility.integration.test.ts +32 -0
  313. package/dist/chunk-4L3P6DT5.mjs.map +0 -1
  314. package/dist/chunk-4NPUASZR.mjs +0 -203
  315. package/dist/chunk-4NPUASZR.mjs.map +0 -1
  316. package/dist/chunk-DIYW3M5D.mjs.map +0 -1
  317. package/dist/chunk-FBC4R44J.mjs.map +0 -1
  318. package/dist/chunk-FF7K3FSO.mjs.map +0 -1
  319. package/dist/chunk-HZUPSYW7.mjs.map +0 -1
  320. package/dist/chunk-QODA5UKU.mjs.map +0 -1
  321. package/dist/chunk-YALPS44A.mjs.map +0 -1
  322. package/dist/chunk-ZUWKAZ2I.mjs.map +0 -1
@@ -0,0 +1,876 @@
1
+ import type { MipmapEncoderWork } from '@forgeax/engine-assets-runtime';
2
+ import { resolveAssetHandle } from '@forgeax/engine-assets-runtime';
3
+ import type { World } from '@forgeax/engine-ecs';
4
+ import {
5
+ type CommandBuffer,
6
+ err,
7
+ ok,
8
+ type RenderPipeline,
9
+ type Result,
10
+ RhiError,
11
+ } from '@forgeax/engine-rhi';
12
+ import type { Handle, MeshAsset, SamplerAsset, TextureAsset } from '@forgeax/engine-types';
13
+ import { toShared } from '@forgeax/engine-types';
14
+ import { createClusterBinScratch } from '../cluster-binner';
15
+ import type { EnvironmentLifecycle } from '../environment/lifecycle';
16
+ import {
17
+ getRenderFeaturePlanExecutionProjection,
18
+ type RenderFeatureHost,
19
+ type RenderFeaturePreparedGraphicsResolverInput,
20
+ runRenderFeatureFrame,
21
+ } from '../features/host';
22
+ import type { RenderFeatureGpuWorkOwner } from '../features/prepared-gpu-work';
23
+ import { resolveStandardRenderFeatureTargets } from '../features/targets';
24
+ import {
25
+ buildFullscreenPostProcessPass,
26
+ entryHasDepthRead,
27
+ type PostProcessShaderEntry,
28
+ postProcessShaderEntrySignature,
29
+ } from '../fullscreen-post-process-pass';
30
+ import { GpuDrivenProduction } from '../gpu-driven/production-raster';
31
+ import { disposeInstanceBufferChunks, disposeInstanceBuffers } from '../instance-buffer-cache';
32
+ import { inspectStandardLighting } from '../pipeline/standard-lighting/inspection';
33
+ import type { PipelineBuilderShaderModuleFactory } from '../pipeline-builder';
34
+ import { inspectPointShadow } from '../point-shadow-inspection';
35
+ import type { PreparedGraphicsResolver } from '../prepare/prepared-graphics-resolver';
36
+ import { validateRenderables } from '../record/frame';
37
+ import { prepareFrameLighting } from '../record/frame-lighting';
38
+ import type { RenderFrameState, ValidatedRenderable } from '../record/frame-snapshot';
39
+ import { resolveGeometryInstanceBuffer } from '../record/main-pass-geometry';
40
+ import { computeSplitLdrSprite } from '../record/main-pass-sprite-draws';
41
+ import {
42
+ prepareRecoveryPipelineReadiness,
43
+ type RecoveryPipelineReadiness,
44
+ } from '../record/recovery-pipeline';
45
+ import type { PointsLinesRecordOwner, RenderSystemInternals } from '../record/render-context';
46
+ import {
47
+ ensureCompiledFrameGraph,
48
+ getRenderFeatureGraphState,
49
+ type RenderFeatureGraphCandidate,
50
+ } from '../record/typed-frame-graph';
51
+ import { STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from '../render-contract';
52
+ import type {
53
+ RecoveryGraphCandidate,
54
+ RecoveryGraphCandidatePreparation,
55
+ RecoveryGraphCandidateRuntime,
56
+ RecoveryGraphSetupSubmission,
57
+ RecoveryPointsLinesCandidate,
58
+ } from '../render-system';
59
+ import type { ExtractedFrame, MaterialSnapshot } from '../render-system-extract';
60
+ import type { PersistentGpuDrivenCandidate, PersistentRenderScene } from '../scene/render-scene';
61
+ import { SHADOW_ATLAS_DEFAULT_LAYERS } from '../shadow-atlas';
62
+ import { createTemporalFrameTransaction } from '../temporal/frame';
63
+ import {
64
+ getTemporalBindGroupResources,
65
+ getTemporalGpuState,
66
+ getTemporalParamsBuffer,
67
+ } from '../temporal/gpu';
68
+
69
+ export interface PreparedResolverCaches {
70
+ readonly preparedPipelineIds: WeakMap<object, string>;
71
+ readonly preparedMaterialPipelineShaders: WeakMap<object, string>;
72
+ readonly preparedGroup0Pipelines: WeakSet<object>;
73
+ readonly preparedViewOnlyPipelines: WeakSet<object>;
74
+ readonly preparedRenderMaterialPipelines: WeakSet<object>;
75
+ readonly preparedAssetHandles: WeakMap<World, Map<string, number>>;
76
+ }
77
+
78
+ export interface RecoveryFrameSeed {
79
+ readonly frame: ExtractedFrame;
80
+ readonly worlds: readonly World[];
81
+ readonly cameraOwner: number;
82
+ readonly resourceOwner: number;
83
+ readonly width: number;
84
+ readonly height: number;
85
+ }
86
+
87
+ export interface RenderSystemRecoveryDependencies {
88
+ readonly frameState: RenderFrameState;
89
+ readonly internals: RenderSystemInternals;
90
+ readonly getEnvironmentLifecycle: () => EnvironmentLifecycle;
91
+ readonly getGpuDrivenProduction: () => GpuDrivenProduction;
92
+ readonly setGpuDrivenProduction: (production: GpuDrivenProduction) => void;
93
+ readonly disposeFeatureGpuWork: () => void;
94
+ readonly setFeatureGpuWork: (owner: RenderFeatureGpuWorkOwner) => void;
95
+ readonly getActiveFeaturePostProcessEntries: () => ReadonlyMap<string, PostProcessShaderEntry>;
96
+ readonly setActiveFeaturePostProcessEntries: (
97
+ entries: ReadonlyMap<string, PostProcessShaderEntry>,
98
+ ) => void;
99
+ readonly postProcessPipelineCache: Map<string, RenderPipeline>;
100
+ readonly clearPostProcessPipelineCache: (id: string) => void;
101
+ readonly clearPostProcessPipelineEntry: (id: string, entry: PostProcessShaderEntry) => void;
102
+ readonly invalidatePostProcessModule: (id: string) => void;
103
+ readonly persistentRenderScene: PersistentRenderScene;
104
+ readonly gpuDrivenShaderFactory: PipelineBuilderShaderModuleFactory;
105
+ readonly createFeatureGpuWorkOwner: (runtime: RenderSystemInternals) => RenderFeatureGpuWorkOwner;
106
+ readonly pointsLinesOwner: PointsLinesRecordOwner & {
107
+ prepareRecoveryCandidate: (
108
+ entries: readonly ValidatedRenderable[],
109
+ clustered: boolean,
110
+ runtime: RenderSystemInternals,
111
+ ) => RecoveryPointsLinesCandidate | undefined;
112
+ };
113
+ readonly createPreparedResolverFactory: (
114
+ runtime: RenderSystemInternals,
115
+ gpuWork: RenderFeatureGpuWorkOwner,
116
+ worlds: readonly World[],
117
+ caches: PreparedResolverCaches,
118
+ ) => (input: RenderFeaturePreparedGraphicsResolverInput) => PreparedGraphicsResolver;
119
+ readonly createPreparedResolverCaches: () => PreparedResolverCaches;
120
+ }
121
+
122
+ export interface RenderSystemRecoveryOwner {
123
+ setLastSuccessfulFrameSeed(seed: RecoveryFrameSeed): void;
124
+ prepareRecoveryGraphCandidate(
125
+ runtime: RecoveryGraphCandidateRuntime,
126
+ ): Promise<RecoveryGraphCandidatePreparation>;
127
+ submitCandidateSetup(
128
+ candidate: RecoveryGraphCandidate,
129
+ isValid: () => boolean,
130
+ ): Result<RecoveryGraphSetupSubmission, RhiError>;
131
+ publishRecoveryGraphCandidate(candidate: RecoveryGraphCandidate): void;
132
+ discardRecoveryGraphCandidate(candidate: RecoveryGraphCandidate): void;
133
+ }
134
+
135
+ export function materialTextureHandles(
136
+ material: MaterialSnapshot,
137
+ ): readonly Handle<'TextureAsset', 'shared'>[] {
138
+ const handles = new Map<number, Handle<'TextureAsset', 'shared'>>();
139
+ const add = (handle: Handle<'TextureAsset', 'shared'> | undefined): void => {
140
+ if (handle !== undefined) handles.set(Number(handle), handle);
141
+ };
142
+ for (const handle of material.textureHandles?.values() ?? []) add(handle);
143
+ add(material.baseColorTexture);
144
+ add(material.metallicRoughnessTexture);
145
+ add(material.normalTexture);
146
+ add(material.emissiveTexture);
147
+ add(material.occlusionTexture);
148
+ return [...handles.values()];
149
+ }
150
+
151
+ export function materialSamplerHandles(
152
+ material: MaterialSnapshot,
153
+ ): readonly Handle<'SamplerAsset', 'shared'>[] {
154
+ const handles = new Map<number, Handle<'SamplerAsset', 'shared'>>();
155
+ for (const handle of material.samplerHandles?.values() ?? []) {
156
+ handles.set(Number(handle), handle);
157
+ }
158
+ return [...handles.values()];
159
+ }
160
+
161
+ export function createRenderSystemRecovery(
162
+ deps: RenderSystemRecoveryDependencies,
163
+ ): RenderSystemRecoveryOwner {
164
+ const {
165
+ frameState,
166
+ internals,
167
+ getEnvironmentLifecycle,
168
+ getGpuDrivenProduction,
169
+ setGpuDrivenProduction,
170
+ disposeFeatureGpuWork,
171
+ setFeatureGpuWork,
172
+ getActiveFeaturePostProcessEntries,
173
+ setActiveFeaturePostProcessEntries,
174
+ postProcessPipelineCache,
175
+ clearPostProcessPipelineCache,
176
+ clearPostProcessPipelineEntry,
177
+ invalidatePostProcessModule,
178
+ persistentRenderScene,
179
+ gpuDrivenShaderFactory,
180
+ createFeatureGpuWorkOwner,
181
+ pointsLinesOwner,
182
+ createPreparedResolverFactory,
183
+ createPreparedResolverCaches,
184
+ } = deps;
185
+
186
+ // This is a detached CPU projection, retained only after queue submission.
187
+ // It is not a World/lease cache and it never carries target or scheduler state.
188
+ let lastSuccessfulFrameSeed: RecoveryFrameSeed | undefined;
189
+
190
+ const createRecoveryCandidateFrameState = (): RenderFrameState => {
191
+ const { probeBlendRecordBuffer: discardedProbeBlendRecordBuffer, ...baseFrameState } =
192
+ frameState;
193
+ void discardedProbeBlendRecordBuffer;
194
+ return {
195
+ ...baseFrameState,
196
+ successfulTemporalFrameIndex: 0,
197
+ temporalFrameTransaction: createTemporalFrameTransaction({ deviceEpoch: 0 }),
198
+ temporalFrameInput: undefined,
199
+ graphTargetCapture: undefined,
200
+ perFrameGraph: null,
201
+ directionalShadowCache: null,
202
+ directionalShadowCacheRecorded: false,
203
+ compiledFrameGraph: null,
204
+ compiledFrameGraphTopologyKey: null,
205
+ volumetricFogPreviousGraph: null,
206
+ volumetricFogPreviousGraphKey: null,
207
+ volumetricFogCandidateGraph: null,
208
+ volumetricFogParamsBuffers: [null, null],
209
+ volumetricFogParamsPendingSlot: null,
210
+ volumetricFogParamsAcceptedSlot: null,
211
+ volumetricFogAcceptedParams: undefined,
212
+ volumetricFogPendingParams: undefined,
213
+ volumetricFogAccepted: undefined,
214
+ volumetricFogAcceptedContext: undefined,
215
+ volumetricFogHistoryGraph: null,
216
+ volumetricFogHistorySlot: null,
217
+ volumetricFogHistorySignature: null,
218
+ retiredCompiledFrameGraphs: new Set(),
219
+ currentFrameObservationSource: undefined,
220
+ lastSuccessfulCameraAntialias: undefined,
221
+ temporalGpuState: undefined,
222
+ activeTemporalGpuState: undefined,
223
+ retiringTemporalGpuStates: new Set(),
224
+ lastSuccessfulTemporalView: undefined,
225
+ pendingTemporalCommit: { kind: 'none' },
226
+ environmentGeneration: undefined,
227
+ currentDirectionalShadowView: null,
228
+ currentSpotShadowView: null,
229
+ instanceBuffers: new Map(),
230
+ instanceBufferChunks: new Map(),
231
+ instanceResidency: new Map(),
232
+ morphBuffers: new Map(),
233
+ hdrpClusterBinScratch: createClusterBinScratch(),
234
+ hdrpClusterGridScratch: null,
235
+ hdrpLightIndexListScratch: null,
236
+ hdrpClusterMembership: null,
237
+ standardLightingGraphSignature: '',
238
+ standardLightingInspection: undefined,
239
+ pointShadowInspection: undefined,
240
+ transientInstanceBuffers: [],
241
+ pointShadowAtlas: null,
242
+ pointShadowSnapshots: [],
243
+ spotShadowSnapshots: [],
244
+ viewBindGroupCache: new WeakMap(),
245
+ meshBindGroupCache: new WeakMap(),
246
+ materialBgPerEntity: new Map(),
247
+ instancesBgPerEntity: new Map(),
248
+ instancesBgShared: new WeakMap(),
249
+ materialBgShared: new Map(),
250
+ materialBgAssemblyCache: new Map(),
251
+ singletonMaterialCache: new Map(),
252
+ postProcessBgCache: new WeakMap(),
253
+ probeBlendBuffers: new Map(),
254
+ probeBlendRecordBufferCapacity: 0,
255
+ standardOncePerFrameFired: new Set(),
256
+ } as RenderFrameState;
257
+ };
258
+
259
+ const publishRecoveryGraphCandidate = (candidate: RecoveryGraphCandidate): void => {
260
+ candidate.recoveryReadiness.assertFirstRecoveryFrame();
261
+ const previousFeatureHost = internals.featureHost;
262
+ // The caller enters this method only after the active generation swap and
263
+ // after resetForRecover() has shed every lost-device owner. Replace the
264
+ // whole device-bound frame projection so no stale cache/map/temporal view
265
+ // survives publication; the candidate was already built from the CPU/LKG
266
+ // facts before this synchronous boundary.
267
+ Object.assign(frameState, candidate.frameState);
268
+ // EnvironmentLifecycle is published by the candidate root bundle. The
269
+ // candidate frame state was created before that owner switch, so bind the
270
+ // freshly selected lifecycle explicitly at the same boundary.
271
+ Object.assign(frameState, {
272
+ environmentLifecycle: getEnvironmentLifecycle(),
273
+ temporalFrame: undefined,
274
+ bloomFrameReceipts: undefined,
275
+ });
276
+ candidate.gpuDrivenScene?.publish();
277
+ if (candidate.gpuDrivenProduction !== undefined) {
278
+ getGpuDrivenProduction().dispose();
279
+ setGpuDrivenProduction(candidate.gpuDrivenProduction);
280
+ }
281
+ if (candidate.featureGpuWork !== undefined) {
282
+ disposeFeatureGpuWork();
283
+ setFeatureGpuWork(candidate.featureGpuWork);
284
+ }
285
+ if (candidate.featureHost !== undefined) {
286
+ const setFeatureHost = internals.setFeatureHost;
287
+ if (setFeatureHost !== undefined) setFeatureHost(candidate.featureHost);
288
+ if (previousFeatureHost !== undefined && previousFeatureHost !== candidate.featureHost) {
289
+ const disposed = previousFeatureHost.dispose();
290
+ if (!disposed.ok) internals.errorRegistry.fire(disposed.error);
291
+ }
292
+ }
293
+ if (candidate.featureGraphCandidate !== undefined) {
294
+ for (const [id, previous] of getActiveFeaturePostProcessEntries()) {
295
+ const next = candidate.featureGraphCandidate.fullscreenEffects.get(id);
296
+ if (next === undefined) {
297
+ clearPostProcessPipelineCache(id);
298
+ invalidatePostProcessModule(id);
299
+ } else if (
300
+ postProcessShaderEntrySignature(previous) !== postProcessShaderEntrySignature(next)
301
+ ) {
302
+ clearPostProcessPipelineEntry(id, previous);
303
+ if (previous.source !== next.source) invalidatePostProcessModule(id);
304
+ }
305
+ }
306
+ setActiveFeaturePostProcessEntries(candidate.featureGraphCandidate.fullscreenEffects);
307
+ const graphState = getRenderFeatureGraphState(internals);
308
+ graphState.plans = candidate.featureGraphCandidate.plans;
309
+ graphState.fullscreenEffects = candidate.featureGraphCandidate.fullscreenEffects;
310
+ }
311
+ for (const [key, pipeline] of candidate.postProcessPipelines ?? []) {
312
+ postProcessPipelineCache.set(key, pipeline);
313
+ }
314
+ };
315
+
316
+ const discardRecoveryGraphCandidate = (candidate: RecoveryGraphCandidate): void => {
317
+ candidate.release();
318
+ };
319
+
320
+ const prepareRecoveryGraphCandidate = async (
321
+ runtime: RecoveryGraphCandidateRuntime,
322
+ ): Promise<RecoveryGraphCandidatePreparation> => {
323
+ const seed = lastSuccessfulFrameSeed;
324
+ if (seed === undefined) return { kind: 'no-seed' };
325
+ const camera = seed.frame.cameras[0];
326
+ // A successful clear-only draw can have no display camera. There is no
327
+ // frame graph to rehydrate in that case, but the device generation is
328
+ // still recoverable; the next camera-bearing draw will build its graph
329
+ // from the current World composition.
330
+ if (camera === undefined) return { kind: 'no-seed' };
331
+
332
+ const candidateFrameState = createRecoveryCandidateFrameState();
333
+ Object.assign(candidateFrameState, {
334
+ temporalFrame: undefined,
335
+ bloomFrameReceipts: undefined,
336
+ });
337
+ const setupWorks: MipmapEncoderWork[] = [];
338
+ const candidateGpuStore = runtime.internals.gpuStore;
339
+ let candidateGpuDrivenScene: PersistentGpuDrivenCandidate | undefined;
340
+ let candidateGpuDrivenProduction: GpuDrivenProduction | undefined;
341
+ let candidateFeatureHost: RenderFeatureHost | undefined;
342
+ let candidateFeatureGpuWork: RenderFeatureGpuWorkOwner | undefined;
343
+ let candidatePointsLines: RecoveryPointsLinesCandidate | undefined;
344
+ let candidateFeatureGraph: RenderFeatureGraphCandidate = {
345
+ plans: Object.freeze([]),
346
+ fullscreenEffects: new Map(),
347
+ };
348
+ let candidateValidated: ValidatedRenderable[] = [];
349
+ let recoveryReadiness: RecoveryPipelineReadiness | undefined;
350
+ let preparedGpuDriven:
351
+ | import('../gpu-driven/production-raster').PreparedGpuDrivenFrame
352
+ | undefined;
353
+ let recoveryGraphFailure: unknown;
354
+ let candidatePublished = false;
355
+ let candidateReleased = false;
356
+ const releaseCandidate = (): void => {
357
+ if (candidateReleased || candidatePublished) return;
358
+ candidateReleased = true;
359
+ for (const work of setupWorks) work.discard();
360
+ setupWorks.length = 0;
361
+ const graph = candidateFrameState.compiledFrameGraph;
362
+ candidateFrameState.compiledFrameGraph = null;
363
+ candidateFrameState.perFrameGraph = null;
364
+ try {
365
+ graph?.retire().catch(() => undefined);
366
+ } catch {
367
+ // Candidate cleanup continues through every independent owner.
368
+ }
369
+ try {
370
+ candidateFrameState.pointShadowAtlas?.dispose();
371
+ } catch {
372
+ // Candidate cleanup continues through every independent owner.
373
+ }
374
+ candidateFrameState.pointShadowAtlas = null;
375
+ candidateFeatureGraph.onAbandoned?.();
376
+ const featureHost = candidateFeatureHost;
377
+ candidateFeatureHost = undefined;
378
+ if (featureHost !== undefined) {
379
+ featureHost.dispose();
380
+ }
381
+ candidateFeatureGpuWork?.dispose();
382
+ candidateFeatureGpuWork = undefined;
383
+ candidateGpuDrivenProduction?.dispose();
384
+ candidateGpuDrivenProduction = undefined;
385
+ candidateGpuDrivenScene?.release();
386
+ candidateGpuDrivenScene = undefined;
387
+ candidatePointsLines?.release();
388
+ candidatePointsLines = undefined;
389
+ disposeInstanceBuffers(candidateFrameState.instanceBuffers, runtime.internals.errorRegistry);
390
+ if (candidateFrameState.instanceBufferChunks !== undefined) {
391
+ disposeInstanceBufferChunks(
392
+ candidateFrameState.instanceBufferChunks,
393
+ runtime.internals.errorRegistry,
394
+ );
395
+ }
396
+ candidateGpuStore.destroyAll();
397
+ };
398
+ const markPublished = (): void => {
399
+ candidatePointsLines?.publish();
400
+ candidatePublished = true;
401
+ };
402
+ const failed = (
403
+ reason: string,
404
+ cause: unknown = recoveryGraphFailure,
405
+ ): RecoveryGraphCandidatePreparation => {
406
+ releaseCandidate();
407
+ return cause === undefined ? { kind: 'failed', reason } : { kind: 'failed', reason, cause };
408
+ };
409
+
410
+ try {
411
+ const gpuScene = persistentRenderScene.prepareRecoveryGpuDrivenCandidate(
412
+ runtime.internals.device,
413
+ );
414
+ if (!gpuScene.ok) return failed('candidate GPU scene preparation failed', gpuScene.error);
415
+ candidateGpuDrivenScene = gpuScene.value;
416
+ if (candidateGpuDrivenScene.state !== undefined) {
417
+ const production = await GpuDrivenProduction.forRecoveryDevice(
418
+ runtime.internals.device,
419
+ gpuDrivenShaderFactory,
420
+ (descriptor) =>
421
+ internals.createShaderModule === undefined
422
+ ? Promise.resolve(gpuDrivenShaderFactory.createShaderModule(descriptor))
423
+ : internals.createShaderModule(runtime.internals.device, descriptor),
424
+ );
425
+ if (!production.ok)
426
+ return failed('candidate GPU-driven shader preparation failed', production.error);
427
+ candidateGpuDrivenProduction = production.value;
428
+ const prepared = candidateGpuDrivenProduction.prepare({
429
+ scene: candidateGpuDrivenScene.state,
430
+ camera,
431
+ meshes: runtime.pipelineState.meshes,
432
+ viewBindGroupLayout: runtime.pipelineState.viewBindGroupLayout,
433
+ meshResidencyEpoch: runtime.internals.gpuStore.meshResidencyEpoch,
434
+ hdrp: false,
435
+ });
436
+ if (!prepared.ok) return failed('candidate GPU-driven preparation failed', prepared.error);
437
+ preparedGpuDriven = prepared.value;
438
+ }
439
+ candidateFeatureHost = runtime.internals.featureHost;
440
+ if (candidateFeatureHost !== undefined && candidateFeatureHost.size > 0) {
441
+ candidateFeatureGpuWork = createFeatureGpuWorkOwner(runtime.internals);
442
+ const featureTargets = resolveStandardRenderFeatureTargets({
443
+ tonemap: camera.tonemap,
444
+ antialias: camera.antialias,
445
+ colorAttachmentFormat: runtime.pipelineState.colorAttachmentFormat,
446
+ storageBuffer: runtime.internals.device.caps.storageBuffer,
447
+ multisample: runtime.internals.device.caps.backendKind !== 'wgpu-webgl2',
448
+ });
449
+ const featureFrame = runRenderFeatureFrame(candidateFeatureHost, {
450
+ worlds: seed.worlds,
451
+ owner: seed.resourceOwner,
452
+ frameNumber: candidateFrameState.frameNumber,
453
+ visibilitySnapshots: seed.frame.featureVisibilitySnapshots,
454
+ hiddenEntityReports: seed.frame.hiddenEntityReports,
455
+ targets: featureTargets,
456
+ generation: runtime.internals.deviceScope.generation,
457
+ caps: runtime.internals.device.caps,
458
+ ...(runtime.internals.getMaterialShaderBindingContract === undefined
459
+ ? {}
460
+ : {
461
+ materialShaderBindingContract: runtime.internals.getMaterialShaderBindingContract,
462
+ }),
463
+ createPreparedGraphicsResolver: createPreparedResolverFactory(
464
+ runtime.internals,
465
+ candidateFeatureGpuWork,
466
+ seed.worlds,
467
+ createPreparedResolverCaches(),
468
+ ),
469
+ gpuWork: candidateFeatureGpuWork,
470
+ });
471
+ if (featureFrame.errors.length > 0) {
472
+ return failed('candidate render-feature preparation failed');
473
+ }
474
+ const featurePreparationIncomplete = featureFrame.plans.some((planned) => {
475
+ const execution = getRenderFeaturePlanExecutionProjection(planned);
476
+ return (
477
+ execution === undefined ||
478
+ execution.passes.some(
479
+ (pass) =>
480
+ (pass.graphics !== undefined && pass.resolvedGraphics === undefined) ||
481
+ (pass.gpuCompute !== undefined && pass.resolvedGpuCompute === undefined),
482
+ )
483
+ );
484
+ });
485
+ if (featurePreparationIncomplete) {
486
+ return failed('candidate render-feature resources are not fully prepared');
487
+ }
488
+ const batches = Object.freeze([...featureFrame.preparedResourceBatches]);
489
+ let batchesReleased = false;
490
+ const releaseBatches = (): void => {
491
+ if (batchesReleased) return;
492
+ batchesReleased = true;
493
+ for (const batch of batches) {
494
+ const released = batch.release();
495
+ if (!released.ok) runtime.internals.errorRegistry.fire(released.error);
496
+ }
497
+ };
498
+ candidateFeatureGraph = {
499
+ plans: featureFrame.plans,
500
+ fullscreenEffects: featureFrame.fullscreenEffects,
501
+ ...(batches.length === 0
502
+ ? {}
503
+ : { preparedResourceKey: `recovery-${runtime.internals.deviceScope.generation}` }),
504
+ onRejected: releaseBatches,
505
+ onAbandoned: releaseBatches,
506
+ };
507
+ }
508
+ for (const renderable of seed.frame.renderables) {
509
+ const world = seed.worlds[renderable.worldId];
510
+ if (world === undefined) return failed(`world ${renderable.worldId} is unavailable`);
511
+ if (!runtime.pipelineState.meshes.has(renderable.assetHandle)) {
512
+ const mesh = resolveAssetHandle<MeshAsset>(
513
+ world,
514
+ toShared<'MeshAsset'>(renderable.assetHandle),
515
+ );
516
+ if (!mesh.ok) return failed(`mesh ${renderable.assetHandle} resolution failed`);
517
+ const preparedMesh = runtime.internals.gpuStore.prepareResidentForRecovery(
518
+ toShared<'MeshAsset'>(renderable.assetHandle),
519
+ mesh.value,
520
+ world,
521
+ );
522
+ if (!preparedMesh.ok) return failed(`mesh ${renderable.assetHandle} residency failed`);
523
+ if (preparedMesh.value.mipmapWork !== undefined) {
524
+ setupWorks.push(preparedMesh.value.mipmapWork);
525
+ }
526
+ }
527
+ for (const material of renderable.materials) {
528
+ for (const textureHandle of materialTextureHandles(material)) {
529
+ const texture = resolveAssetHandle<TextureAsset>(world, textureHandle);
530
+ if (!texture.ok) return failed(`texture ${textureHandle} resolution failed`);
531
+ const preparedTexture = runtime.internals.gpuStore.prepareResidentForRecovery(
532
+ textureHandle,
533
+ texture.value,
534
+ world,
535
+ );
536
+ if (!preparedTexture.ok) return failed(`texture ${textureHandle} residency failed`);
537
+ if (preparedTexture.value.mipmapWork !== undefined) {
538
+ setupWorks.push(preparedTexture.value.mipmapWork);
539
+ }
540
+ }
541
+ for (const samplerHandle of materialSamplerHandles(material)) {
542
+ const sampler = resolveAssetHandle<SamplerAsset>(world, samplerHandle);
543
+ if (!sampler.ok) return failed(`sampler ${samplerHandle} resolution failed`);
544
+ const preparedSampler = runtime.internals.gpuStore.ensureSamplerResident(
545
+ samplerHandle,
546
+ sampler.value,
547
+ world,
548
+ );
549
+ if (!preparedSampler.ok) return failed(`sampler ${samplerHandle} residency failed`);
550
+ }
551
+ }
552
+ }
553
+ const resourceWorld = seed.worlds[seed.resourceOwner] ?? seed.worlds[0];
554
+ if (resourceWorld === undefined)
555
+ return failed('recovery resource-owner world is unavailable');
556
+ const volumetricFog = seed.frame.volumetricFog;
557
+ if (volumetricFog?.status === 'available') {
558
+ const volumeWorld =
559
+ seed.worlds[volumetricFog.worldId ?? seed.resourceOwner] ?? resourceWorld;
560
+ if (volumetricFog.densityHandle === undefined || volumetricFog.densityAsset === undefined) {
561
+ return failed('recovery volumetric density source is unavailable');
562
+ }
563
+ const preparedDensity = candidateGpuStore.prepareResidentForRecovery(
564
+ volumetricFog.densityHandle,
565
+ volumetricFog.densityAsset,
566
+ volumeWorld,
567
+ );
568
+ if (!preparedDensity.ok) return failed('volumetric density residency failed');
569
+ if (preparedDensity.value.mipmapWork !== undefined) {
570
+ setupWorks.push(preparedDensity.value.mipmapWork);
571
+ }
572
+ if (
573
+ volumetricFog.projectorHandle !== undefined &&
574
+ volumetricFog.projectorAsset !== undefined
575
+ ) {
576
+ const preparedProjector = candidateGpuStore.prepareResidentForRecovery(
577
+ volumetricFog.projectorHandle,
578
+ volumetricFog.projectorAsset,
579
+ volumeWorld,
580
+ );
581
+ if (!preparedProjector.ok) return failed('volumetric projector residency failed');
582
+ if (preparedProjector.value.mipmapWork !== undefined) {
583
+ setupWorks.push(preparedProjector.value.mipmapWork);
584
+ }
585
+ }
586
+ }
587
+ candidateValidated = validateRenderables(
588
+ runtime.internals,
589
+ resourceWorld,
590
+ seed.worlds,
591
+ runtime.pipelineState,
592
+ candidateFrameState,
593
+ seed.frame.renderables,
594
+ seed.frame.dispatch,
595
+ );
596
+ if (candidateValidated.length !== seed.frame.renderables.length) {
597
+ return failed('candidate validation did not retain every LKG-visible renderable');
598
+ }
599
+ for (const entry of candidateValidated) {
600
+ if (entry.source.instances === undefined) continue;
601
+ const prepared = resolveGeometryInstanceBuffer(
602
+ {
603
+ runtime: runtime.internals,
604
+ pipelineState: runtime.pipelineState,
605
+ frameState: candidateFrameState,
606
+ bindGroupCounts: { createBindGroup: 0, keys: [] },
607
+ },
608
+ entry,
609
+ [],
610
+ false,
611
+ );
612
+ if (prepared === null) return failed('candidate instance preparation failed');
613
+ }
614
+ // These uploads belong to recovery preparation, never to a rendered frame.
615
+ for (const [id, residency] of candidateFrameState.instanceResidency ?? []) {
616
+ candidateFrameState.instanceResidency?.set(id, { ...residency, frameNumber: -1 });
617
+ }
618
+ const lighting = prepareFrameLighting(
619
+ runtime.internals,
620
+ candidateFrameState,
621
+ seed.frame.lights,
622
+ camera,
623
+ runtime.pipelineState,
624
+ );
625
+ if (!lighting.ok) return failed('candidate lighting preparation failed');
626
+ candidatePointsLines = pointsLinesOwner.prepareRecoveryCandidate(
627
+ candidateValidated,
628
+ lighting.value.standard?.kind === 'clustered',
629
+ runtime.internals,
630
+ );
631
+ recoveryReadiness = prepareRecoveryPipelineReadiness({
632
+ internals: runtime.internals,
633
+ pipelineState: runtime.pipelineState,
634
+ camera,
635
+ standardLighting: lighting.value.standard,
636
+ validated: candidateValidated,
637
+ dispatch: seed.frame.dispatch,
638
+ shadowCastersActive:
639
+ seed.frame.lights.pointShadow.length > 0 ||
640
+ seed.frame.lights.spot.some((spot) => spot.castShadow === true) ||
641
+ (seed.frame.lights.lightViewProj?.length ?? 0) > 0,
642
+ splitLdrSprite: computeSplitLdrSprite(candidateValidated, camera.tonemap !== 'none'),
643
+ reflectionFallbackAvailable: candidateFrameState.reflectionFallbackDemand === true,
644
+ });
645
+ const candidatePostProcessIds = new Set<string>([
646
+ ...(candidateFrameState.installedPipelineConfig?.postEffects ?? []),
647
+ ...candidateFeatureGraph.fullscreenEffects.keys(),
648
+ ]);
649
+ const configuredPostEffects = candidateFrameState.installedPipelineConfig?.postEffects ?? [];
650
+ const postProcessPipeline = runtime.internals.getPostProcessPipeline;
651
+ const postProcessLookup = runtime.internals.lookupPostProcess;
652
+ if (postProcessPipeline === undefined || postProcessLookup === undefined) {
653
+ return failed('candidate post-process pipeline owner is unavailable');
654
+ }
655
+ const depthMultisampled =
656
+ camera.antialias === 'msaa' && runtime.internals.device.caps.backendKind !== 'wgpu-webgl2';
657
+ const warmPostProcessPipeline = (
658
+ id: string,
659
+ colorFormat: GPUTextureFormat,
660
+ entryOverride?: PostProcessShaderEntry,
661
+ ): void => {
662
+ const entry =
663
+ entryOverride ?? candidateFeatureGraph.fullscreenEffects.get(id) ?? postProcessLookup(id);
664
+ if (entry === undefined) {
665
+ throw new Error(`recovery post-process declaration is unavailable: ${id}`);
666
+ }
667
+ const built = buildFullscreenPostProcessPass(
668
+ { device: runtime.internals.device, errorRegistry: runtime.internals.errorRegistry },
669
+ entry,
670
+ depthMultisampled,
671
+ );
672
+ if (
673
+ built === null ||
674
+ built.sampler === null ||
675
+ (entryHasDepthRead(entry) && built.depthSampler === null)
676
+ ) {
677
+ throw new Error(`recovery post-process bindings are unavailable: ${id}`);
678
+ }
679
+ const pipeline = postProcessPipeline(id, built.bindGroupLayout, colorFormat, entry);
680
+ if (pipeline === null) {
681
+ throw new Error(`recovery post-process pipeline is unavailable: ${id}`);
682
+ }
683
+ };
684
+ const rawOnlyPostInput =
685
+ runtime.pipelineState.surfaceProfile === 'raw-only' &&
686
+ runtime.internals.device.caps.storageBuffer &&
687
+ configuredPostEffects.length > 0;
688
+ const outputFormat =
689
+ camera.antialias === 'fxaa' || rawOnlyPostInput
690
+ ? ('rgba16float' as GPUTextureFormat)
691
+ : (runtime.pipelineState.format as GPUTextureFormat);
692
+ // Standard's output transform is a graph pass even when no authored
693
+ // effect is installed. Warm its exact target format before publication;
694
+ // the first recovery frame must only resolve this candidate-owned cache.
695
+ warmPostProcessPipeline(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, outputFormat);
696
+ if (camera.antialias === 'taa') {
697
+ const temporal = getTemporalGpuState(
698
+ candidateFrameState,
699
+ runtime.internals.device,
700
+ runtime.internals.deviceScope,
701
+ seed.width,
702
+ seed.height,
703
+ );
704
+ getTemporalParamsBuffer(temporal);
705
+ const temporalBindings = getTemporalBindGroupResources(temporal);
706
+ const taaEntry = postProcessLookup('forgeax.taa-resolve');
707
+ if (taaEntry === undefined) {
708
+ throw new Error('recovery TAA post-process declaration is unavailable');
709
+ }
710
+ const taaPipeline = postProcessPipeline(
711
+ 'forgeax.taa-resolve',
712
+ temporalBindings.layout,
713
+ 'rgba16float',
714
+ taaEntry,
715
+ );
716
+ if (taaPipeline === null) {
717
+ throw new Error('recovery TAA pipeline is unavailable');
718
+ }
719
+ }
720
+ if (camera.motionBlur?.shutterAngle !== undefined && camera.motionBlur.shutterAngle > 0) {
721
+ warmPostProcessPipeline('forgeax.motion-blur', 'rgba16float');
722
+ }
723
+ for (const id of candidatePostProcessIds) {
724
+ warmPostProcessPipeline(id, runtime.pipelineState.format as GPUTextureFormat);
725
+ }
726
+ if (camera.bloom === 'on' && camera.tonemap !== 'none') {
727
+ runtime.pipelineState.perPassResources.ensureBloomResources?.();
728
+ const bloom = runtime.pipelineState.perPassResources.getBloomResources?.();
729
+ if (
730
+ bloom === null ||
731
+ bloom === undefined ||
732
+ bloom.bloomBrightPipeline === null ||
733
+ bloom.bloomBlurHPipeline === null ||
734
+ bloom.bloomBlurVPipeline === null ||
735
+ bloom.bloomCompositePipeline === null ||
736
+ bloom.bloomBrightBindGroupLayout === null ||
737
+ bloom.bloomBlurBindGroupLayout === null ||
738
+ bloom.bloomCompositeBindGroupLayout === null ||
739
+ bloom.bloomSampler === null ||
740
+ bloom.bloomBrightParamsBuffer === null ||
741
+ bloom.bloomBlurHParamsBuffer === null ||
742
+ bloom.bloomBlurVParamsBuffer === null ||
743
+ bloom.bloomCompositeParamsBuffer === null
744
+ ) {
745
+ throw new Error('recovery Bloom resources are incomplete');
746
+ }
747
+ }
748
+ const graph = ensureCompiledFrameGraph(
749
+ runtime.internals,
750
+ candidateFrameState,
751
+ runtime.pipelineState,
752
+ camera,
753
+ seed.frame.lights,
754
+ seed.width,
755
+ seed.height,
756
+ undefined,
757
+ preparedGpuDriven,
758
+ candidateFeatureGraph,
759
+ undefined,
760
+ false,
761
+ undefined,
762
+ undefined,
763
+ seed.frame.volumetricFog,
764
+ seed.frame.volumetricFog?.status === 'available',
765
+ lighting.value.standard,
766
+ (error) => {
767
+ recoveryGraphFailure ??= error;
768
+ },
769
+ );
770
+ if (graph === null) return failed('candidate frame graph compilation failed');
771
+ if (recoveryReadiness === undefined)
772
+ return failed('candidate pipeline readiness was not prepared');
773
+ // Keep recovery inspection derived from the exact Standard declaration
774
+ // admitted to the candidate graph. The point-shadow projection is
775
+ // likewise detached from the candidate atlas and never waits for the
776
+ // first post-recovery frame to rediscover the retained budget.
777
+ candidateFrameState.standardLightingInspection = inspectStandardLighting(
778
+ lighting.value.standard,
779
+ );
780
+ candidateFrameState.pointShadowInspection = inspectPointShadow(
781
+ seed.frame.lights.pointShadow,
782
+ SHADOW_ATLAS_DEFAULT_LAYERS,
783
+ );
784
+ return {
785
+ kind: 'ready',
786
+ candidate: {
787
+ frameState: candidateFrameState,
788
+ setupWorks: Object.freeze(setupWorks),
789
+ device: runtime.internals.device,
790
+ generation: runtime.internals.deviceScope.generation,
791
+ ...(candidateGpuDrivenProduction === undefined
792
+ ? {}
793
+ : { gpuDrivenProduction: candidateGpuDrivenProduction }),
794
+ ...(candidateGpuDrivenScene === undefined
795
+ ? {}
796
+ : { gpuDrivenScene: candidateGpuDrivenScene }),
797
+ ...(candidatePointsLines === undefined ? {} : { pointsLines: candidatePointsLines }),
798
+ ...(candidateFeatureHost === undefined ? {} : { featureHost: candidateFeatureHost }),
799
+ ...(candidateFeatureGpuWork === undefined
800
+ ? {}
801
+ : { featureGpuWork: candidateFeatureGpuWork }),
802
+ featureGraphCandidate: candidateFeatureGraph,
803
+ postProcessPipelines: new Map(
804
+ runtime.internals.getRecoveryPostProcessPipelines?.() ?? [],
805
+ ),
806
+ recoveryReadiness,
807
+ release: releaseCandidate,
808
+ markPublished,
809
+ },
810
+ };
811
+ } catch (cause) {
812
+ return failed(`candidate preparation threw: ${String(cause)}`, cause);
813
+ }
814
+ };
815
+
816
+ const submitCandidateSetup = (
817
+ candidate: RecoveryGraphCandidate,
818
+ isValid: () => boolean,
819
+ ): Result<RecoveryGraphSetupSubmission, RhiError> => {
820
+ const setupFailure = (hint: string): Result<never, RhiError> =>
821
+ err(
822
+ new RhiError({
823
+ code: 'webgpu-runtime-error',
824
+ expected: 'recovery setup remains candidate-only and valid before publication',
825
+ hint,
826
+ }),
827
+ );
828
+ const discardSetupWorks = (): void => {
829
+ for (const work of candidate.setupWorks) work.discard();
830
+ };
831
+ try {
832
+ if (!isValid()) {
833
+ discardSetupWorks();
834
+ return setupFailure('discard the recovery candidate after its setup deadline or loss');
835
+ }
836
+ const commandBuffers: CommandBuffer[] = [];
837
+ for (const work of candidate.setupWorks) {
838
+ const finished = work.finish();
839
+ if (!finished.ok) {
840
+ discardSetupWorks();
841
+ return err(finished.error);
842
+ }
843
+ if (finished.value !== undefined) commandBuffers.push(finished.value);
844
+ }
845
+ if (!isValid()) {
846
+ discardSetupWorks();
847
+ return setupFailure('discard the recovery candidate after mip setup became stale');
848
+ }
849
+ if (commandBuffers.length === 0) return ok({});
850
+ const submitted = candidate.device.queue.submit(commandBuffers);
851
+ if (!submitted.ok) {
852
+ discardSetupWorks();
853
+ return err(submitted.error);
854
+ }
855
+ if (!isValid()) {
856
+ discardSetupWorks();
857
+ return setupFailure('discard the recovery candidate after setup submission became stale');
858
+ }
859
+ return ok({ completion: candidate.device.queue.onSubmittedWorkDone() });
860
+ } catch (cause) {
861
+ discardSetupWorks();
862
+ if (cause instanceof RhiError) return err(cause);
863
+ return setupFailure(`recovery setup submission raised: ${String(cause)}`);
864
+ }
865
+ };
866
+
867
+ return {
868
+ setLastSuccessfulFrameSeed(seed: RecoveryFrameSeed): void {
869
+ lastSuccessfulFrameSeed = seed;
870
+ },
871
+ prepareRecoveryGraphCandidate,
872
+ submitCandidateSetup,
873
+ publishRecoveryGraphCandidate,
874
+ discardRecoveryGraphCandidate,
875
+ };
876
+ }