@forgeax/engine-render 0.1.23 → 0.1.24

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