@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,906 @@
1
+ import type { AssetRegistry, MipmapShaderModuleFactory } from '@forgeax/engine-assets-runtime';
2
+ import { adaptDynamicTextureDevice, DynamicTextureStore } from '@forgeax/engine-assets-runtime';
3
+ import type {
4
+ BindGroupLayout,
5
+ PipelineLayout,
6
+ RenderPipeline,
7
+ RhiCanvasContext,
8
+ RhiDevice,
9
+ } from '@forgeax/engine-rhi';
10
+ import { err, ok, type Result, RhiError } from '@forgeax/engine-rhi';
11
+ import type { MaterialShaderManifestEntry, ShaderCatalog } from '@forgeax/engine-shader';
12
+ import type { Handle, ParamSchemaEntry } from '@forgeax/engine-types';
13
+ import { DeviceScope } from '../../device/device-scope';
14
+ import { GpuResidencyCache } from '../../device/gpu-residency';
15
+ import { RecoverError, type RecoverFailure } from '../../errors/recover';
16
+ import { createRenderFeatureHost, type RenderFeatureHost } from '../../features/host';
17
+ import {
18
+ type PostProcessShaderEntry,
19
+ postProcessShaderEntrySignature,
20
+ postProcessShaderPipelineLabel,
21
+ } from '../../fullscreen-post-process-pass';
22
+ import { createRecoveryColdWorkGuard } from '../../record/recovery-pipeline';
23
+ import type {
24
+ PipelineState,
25
+ RecoveryColdWorkGuard,
26
+ RenderSystemInternals,
27
+ } from '../../record/render-context';
28
+ import type {
29
+ RecoveryGraphCandidate,
30
+ RecoveryPostProcessResources,
31
+ RecoveryRootBundle,
32
+ RenderSystem,
33
+ } from '../../render-system';
34
+ import type { RhiBackendPack } from '../backend-contract';
35
+ import {
36
+ type LayoutKind,
37
+ type MaterialShaderBindingContract,
38
+ prepareMaterialShaders,
39
+ type ShaderDeviceAdapterInternal,
40
+ } from '../material-shader-policy';
41
+ import type { MeshSsboGrowResult, MeshSsboState } from '../mesh-ssbo-grow';
42
+ import { structuredRendererCause } from '../renderer-facade';
43
+ import { ensureContextConfigured } from '../renderer-helpers';
44
+ import type { RecoveryContinuation, RecoveryDeadline, RecoveryPhase } from '../renderer-lifecycle';
45
+ import type { RecoveryFailureLocation, WebGPURendererInternals } from '../webgpu-renderer';
46
+ import { attachDeviceLostFanout } from './device-loss-fanout';
47
+ import { buildGenerationAggregate, type GenerationAggregate } from './generation';
48
+ import {
49
+ acquireDeviceGeneration,
50
+ probeCandidateDeviceExecution,
51
+ probeCandidateGraphExecution,
52
+ runRecoveryStep,
53
+ } from './recovery-attempt';
54
+
55
+ type RendererShaderState = {
56
+ readonly device: RhiDevice;
57
+ shaderInstance: ShaderCatalog | null;
58
+ sharedShaderModuleAdapter: ShaderDeviceAdapterInternal | null;
59
+ sharedImmediateShaderModuleAdapter: ShaderDeviceAdapterInternal | null;
60
+ };
61
+ type PerShaderMaterialLayoutCacheEntry = {
62
+ readonly source: string;
63
+ readonly paramSchema: readonly ParamSchemaEntry[];
64
+ readonly layoutKind: LayoutKind;
65
+ readonly layout: { materialBgl: BindGroupLayout; pipelineLayout: PipelineLayout } | null;
66
+ };
67
+ type RendererPipelineCacheState = {
68
+ materialShaderPipelineCache: Map<string, RenderPipeline>;
69
+ materialShaderManifestEntryCache: Map<string, MaterialShaderManifestEntry>;
70
+ materialShaderVariantResolutionCache: WeakMap<object, Map<string, string | undefined>>;
71
+ group0MaterialLayout: { materialBgl: BindGroupLayout; pipelineLayout: PipelineLayout } | null;
72
+ viewOnlyMaterialPipelineLayout: PipelineLayout | null;
73
+ viewAndSceneDepthMaterialPipelineLayout: PipelineLayout | null;
74
+ group0ResourceLayouts: Map<
75
+ string,
76
+ { materialBgl: BindGroupLayout; pipelineLayout: PipelineLayout }
77
+ >;
78
+ preparedMaterialPipelineLayoutCache: Map<string, PipelineLayout>;
79
+ perShaderMaterialLayoutCache: Map<string, PerShaderMaterialLayoutCacheEntry>;
80
+ materialShaderBindingContractCache: Map<
81
+ string,
82
+ { source: string; contract: MaterialShaderBindingContract }
83
+ >;
84
+ };
85
+ type RendererCandidateState = {
86
+ readonly device: RhiDevice;
87
+ readonly context: RhiCanvasContext;
88
+ readonly scope: DeviceScope;
89
+ readonly gpuStore: GpuResidencyCache;
90
+ readonly dynamicTextureStore: DynamicTextureStore;
91
+ readonly shaderState: RendererShaderState;
92
+ readonly pipelineCacheState: RendererPipelineCacheState;
93
+ readonly materialShaderUvSetCounts: Map<string, number>;
94
+ pipelineState: PipelineState | null;
95
+ emptyPostProcessBgl: BindGroupLayout | null;
96
+ growMeshSsbo: ((neededSlots: number) => MeshSsboGrowResult) | undefined;
97
+ meshSsboState: MeshSsboState | undefined;
98
+ };
99
+ type RendererGenerationBindings = {
100
+ readonly gpuStore: GpuResidencyCache;
101
+ readonly dynamicTextureStore: DynamicTextureStore;
102
+ readonly shaderState: RendererShaderState;
103
+ readonly pipelineCacheState: RendererPipelineCacheState;
104
+ readonly materialShaderUvSetCounts: Map<string, number>;
105
+ readonly emptyPostProcessBgl: BindGroupLayout | null;
106
+ readonly growMeshSsbo: ((neededSlots: number) => MeshSsboGrowResult) | undefined;
107
+ readonly meshSsboState: MeshSsboState | undefined;
108
+ };
109
+ type RendererGeneration = GenerationAggregate<
110
+ RhiDevice,
111
+ RhiCanvasContext,
112
+ PipelineState,
113
+ RendererGenerationBindings,
114
+ undefined
115
+ >;
116
+
117
+ export interface RendererRecoveryDependencies {
118
+ readonly isDisposed: () => boolean;
119
+ readonly internals: WebGPURendererInternals;
120
+ readonly getActiveDeviceScope: () => DeviceScope;
121
+ readonly getActiveShaderState: () => RendererShaderState;
122
+ readonly getMaterialShaderUvSetCounts: () => Map<string, number>;
123
+ readonly createRendererPipelineCacheState: () => RendererPipelineCacheState;
124
+ readonly getCandidateBuildState: () => RendererCandidateState | undefined;
125
+ readonly setCandidateBuildState: (state: RendererCandidateState | undefined) => void;
126
+ readonly getCandidateShaderState: () => RendererShaderState | undefined;
127
+ readonly setCandidateShaderState: (state: RendererShaderState | undefined) => void;
128
+ readonly getCandidatePipelineCacheState: () => RendererPipelineCacheState | undefined;
129
+ readonly setCandidatePipelineCacheState: (state: RendererPipelineCacheState | undefined) => void;
130
+ readonly getCandidateMaterialShaderUvSetCounts: () => Map<string, number> | undefined;
131
+ readonly setCandidateMaterialShaderUvSetCounts: (state: Map<string, number> | undefined) => void;
132
+ readonly setCandidateEmptyPostProcessBgl: (layout: BindGroupLayout | null | undefined) => void;
133
+ readonly getShader: () => ShaderCatalog;
134
+ readonly assets: AssetRegistry;
135
+ readonly adaptMipmapShaderModuleFactory: (
136
+ factory: RhiBackendPack['createShaderModule'],
137
+ ) => MipmapShaderModuleFactory | undefined;
138
+ readonly buildPipeline: (
139
+ scope?: DeviceScope,
140
+ device?: RhiDevice,
141
+ residencyStore?: GpuResidencyCache,
142
+ ) => Promise<PipelineState>;
143
+ readonly getMaterialShaderPipeline: NonNullable<
144
+ RenderSystemInternals['getMaterialShaderPipeline']
145
+ >;
146
+ readonly getMaterialShaderPipelineEntry: NonNullable<
147
+ RenderSystemInternals['getMaterialShaderPipelineEntry']
148
+ >;
149
+ readonly getCachedMaterialShaderBindingContract: NonNullable<
150
+ RenderSystemInternals['getMaterialShaderBindingContract']
151
+ >;
152
+ readonly getParamSchema: NonNullable<RenderSystemInternals['getParamSchema']>;
153
+ readonly getMaterialBindGroupLayout: NonNullable<
154
+ RenderSystemInternals['getMaterialBindGroupLayout']
155
+ >;
156
+ readonly metrics: RenderSystemInternals['metrics'];
157
+ readonly buildPostProcessPipeline: NonNullable<RenderSystemInternals['buildPostProcessPipeline']>;
158
+ readonly renderSystem: RenderSystem;
159
+ readonly createRecoveryFailureLocation: (
160
+ phase: RecoveryPhase,
161
+ cause: unknown,
162
+ overrides?: Partial<Omit<RecoveryFailureLocation, 'phase' | 'cause'>>,
163
+ ) => RecoveryFailureLocation;
164
+ readonly publishAndRetireRendererGeneration: (
165
+ candidate: RendererGeneration,
166
+ candidatePostProcessResources: RecoveryPostProcessResources,
167
+ recoveryRootBundle: RecoveryRootBundle,
168
+ recoveryGraphCandidate: RecoveryGraphCandidate | undefined,
169
+ recoveryColdWorkGuard: RecoveryColdWorkGuard,
170
+ ) => void;
171
+ }
172
+
173
+ export interface RendererRecovery {
174
+ recoverOnce(
175
+ deadline: RecoveryDeadline,
176
+ deadlineExpired: (phase: RecoveryPhase) => boolean,
177
+ setCandidateCleanup: (cleanup: () => void) => void,
178
+ continuation: RecoveryContinuation,
179
+ setRecoveryPhase: (phase: RecoveryPhase) => void,
180
+ onRootsCommitted: (count: number) => void,
181
+ onFailure: (failure: RecoveryFailureLocation) => void,
182
+ ): Promise<Result<void, RecoverFailure>>;
183
+ }
184
+
185
+ export function createRendererRecovery(deps: RendererRecoveryDependencies): RendererRecovery {
186
+ const {
187
+ isDisposed,
188
+ internals,
189
+ getActiveDeviceScope,
190
+ getActiveShaderState,
191
+ getMaterialShaderUvSetCounts,
192
+ createRendererPipelineCacheState,
193
+ getCandidateBuildState,
194
+ setCandidateBuildState,
195
+ getCandidateShaderState,
196
+ setCandidateShaderState,
197
+ getCandidatePipelineCacheState,
198
+ setCandidatePipelineCacheState,
199
+ getCandidateMaterialShaderUvSetCounts,
200
+ setCandidateMaterialShaderUvSetCounts,
201
+ setCandidateEmptyPostProcessBgl,
202
+ getShader,
203
+ assets,
204
+ adaptMipmapShaderModuleFactory,
205
+ buildPipeline,
206
+ getMaterialShaderPipeline,
207
+ getMaterialShaderPipelineEntry,
208
+ getCachedMaterialShaderBindingContract,
209
+ getParamSchema,
210
+ getMaterialBindGroupLayout,
211
+ metrics,
212
+ buildPostProcessPipeline,
213
+ renderSystem,
214
+ createRecoveryFailureLocation,
215
+ publishAndRetireRendererGeneration,
216
+ } = deps;
217
+ const isRendererDisposed = isDisposed;
218
+ async function recoverOnce(
219
+ deadline: RecoveryDeadline,
220
+ deadlineExpired: (phase: RecoveryPhase) => boolean,
221
+ setCandidateCleanup: (cleanup: () => void) => void,
222
+ continuation: RecoveryContinuation,
223
+ setRecoveryPhase: (phase: RecoveryPhase) => void,
224
+ onRootsCommitted: (count: number) => void,
225
+ onFailure: (failure: RecoveryFailureLocation) => void,
226
+ ): Promise<Result<void, RecoverFailure>> {
227
+ // feat-20260622-s5 M3 / w17 (A-IN-3 / D-1): single idempotent device
228
+ // rebuild. One attempt only — no loop, no backoff, no timer, no extra
229
+ // in-flight health state (A-OOS-1); the host owns the cadence of calling
230
+ // this again. Fail-Fast entry guards (architecture-principles #5):
231
+ // isRendererDisposed() renderer or non-device-lost state short-circuits before a
232
+ // GPU work.
233
+ if (isRendererDisposed()) {
234
+ // A-IN-6: isRendererDisposed() latch wins; recover() never rebuilds a dead
235
+ // renderer. `recover-not-needed` is the sentinel (no degraded state to
236
+ // recover from on a isRendererDisposed() renderer).
237
+ return err(new RecoverError('recover-not-needed'));
238
+ }
239
+ const snapshot = internals.healthRegistry.getLastSnapshot();
240
+ // A-AC-08: `alive` (including the alive state after a prior successful
241
+ // recover) is a no-op signal — idempotent second call.
242
+ if (snapshot.reason !== 'device-lost') {
243
+ return err(new RecoverError('recover-not-needed'));
244
+ }
245
+
246
+ const recordFailure = (
247
+ phase: RecoveryPhase,
248
+ cause: unknown,
249
+ overrides: Partial<Omit<RecoveryFailureLocation, 'phase' | 'cause'>> = {},
250
+ ): void => onFailure(createRecoveryFailureLocation(phase, cause, overrides));
251
+
252
+ // Candidate device-bound state is isolated from the active aggregate. The
253
+ // old roots remain authoritative until the final synchronous publication.
254
+ const candidateGpuStore = new GpuResidencyCache();
255
+ const candidateDynamicTextureStore = new DynamicTextureStore();
256
+ const recoveryColdWorkGuard = createRecoveryColdWorkGuard();
257
+ // Re-acquire through the SAME backend pack (the
258
+ // idempotent factory primitives tryCreateWebGPURenderer uses). On the
259
+ // adapter / device failure paths, health stays `device-lost` (A-AC-07):
260
+ // recover() never fakes the renderer back to `alive`.
261
+ const acquired = await acquireDeviceGeneration(
262
+ internals.pack,
263
+ internals.canvas,
264
+ deadline,
265
+ continuation,
266
+ setRecoveryPhase,
267
+ );
268
+ if (!acquired.ok) {
269
+ const phase = acquired.error.stage === 'adapter' ? 'acquire-adapter' : 'acquire-device';
270
+ recordFailure(phase, acquired.error.error, {
271
+ retryable: true,
272
+ guidance: 'retry',
273
+ owner: 'backend',
274
+ resourceKind: 'surface',
275
+ });
276
+ return err(
277
+ new RecoverError(
278
+ acquired.error.stage === 'adapter'
279
+ ? 'recover-adapter-unavailable'
280
+ : 'recover-device-unavailable',
281
+ ),
282
+ );
283
+ }
284
+ const { device, context } = acquired.value;
285
+ setRecoveryPhase('rehydrate');
286
+ if (deadlineExpired('rehydrate')) {
287
+ const cause = new RecoverError('recover-device-unavailable');
288
+ recordFailure('rehydrate', cause);
289
+ return err(cause);
290
+ }
291
+ if (isRendererDisposed()) return err(new RecoverError('recover-not-needed'));
292
+
293
+ // Build the replacement graph under a fresh renderer-owned scope. The old
294
+ // committed scope remains authoritative until every rebuild step succeeds.
295
+ const recoveryScope = DeviceScope.create(
296
+ getActiveDeviceScope().generation + 1,
297
+ getActiveDeviceScope().owner,
298
+ );
299
+ candidateGpuStore.bindDeviceScope(recoveryScope);
300
+ const candidateState: RendererCandidateState = {
301
+ device,
302
+ context,
303
+ scope: recoveryScope,
304
+ gpuStore: candidateGpuStore,
305
+ dynamicTextureStore: candidateDynamicTextureStore,
306
+ shaderState: {
307
+ device,
308
+ shaderInstance: null,
309
+ sharedShaderModuleAdapter: null,
310
+ sharedImmediateShaderModuleAdapter: null,
311
+ },
312
+ pipelineCacheState: createRendererPipelineCacheState(),
313
+ materialShaderUvSetCounts: new Map(getMaterialShaderUvSetCounts()),
314
+ pipelineState: null,
315
+ emptyPostProcessBgl: null,
316
+ growMeshSsbo: undefined,
317
+ meshSsboState: undefined,
318
+ };
319
+ setCandidateBuildState(candidateState);
320
+ setCandidateShaderState(candidateState.shaderState);
321
+ setCandidatePipelineCacheState(candidateState.pipelineCacheState);
322
+ setCandidateMaterialShaderUvSetCounts(candidateState.materialShaderUvSetCounts);
323
+ setCandidateEmptyPostProcessBgl(candidateState.emptyPostProcessBgl);
324
+ // Copy only the active catalog's CPU declarations into the candidate.
325
+ // Its manifest/module cache remains candidate-owned, so no lost-device
326
+ // ShaderModule can cross the recovery boundary while late-installed
327
+ // material shaders remain available to the normal builder.
328
+ const activeShaderCatalog = getActiveShaderState().shaderInstance;
329
+ if (activeShaderCatalog !== null) {
330
+ const candidateShaderCatalog = getShader();
331
+ for (const identifier of activeShaderCatalog.materialShaderIdentifiers()) {
332
+ const entry = activeShaderCatalog.findMaterialArtifact(identifier);
333
+ if (entry.ok && !candidateShaderCatalog.findMaterialArtifact(identifier).ok) {
334
+ candidateShaderCatalog.installMaterialArtifact(identifier, {
335
+ source: entry.value.source,
336
+ paramSchema: entry.value.paramSchema,
337
+ });
338
+ }
339
+ }
340
+ }
341
+ let candidatePostProcessResources: RecoveryPostProcessResources | undefined;
342
+ let recoveryGraphCandidate: RecoveryGraphCandidate | undefined;
343
+ let recoveryRootBundle: RecoveryRootBundle | undefined;
344
+ // This owner is intentionally optional before candidate construction. The
345
+ // continuation may invoke cleanup while an earlier async recovery step is
346
+ // still pending; cleanup must therefore never close over a TDZ binding.
347
+ let candidateFeatureHost: RenderFeatureHost | undefined;
348
+ let candidateReleased = false;
349
+ const releaseCandidate = (): void => {
350
+ if (candidateReleased) return;
351
+ setRecoveryPhase('cleanup');
352
+ candidateReleased = true;
353
+ if (candidatePostProcessResources !== undefined) {
354
+ renderSystem.discardRecoveryPostProcessResources(candidatePostProcessResources);
355
+ candidatePostProcessResources = undefined;
356
+ }
357
+ const featureHost = candidateFeatureHost;
358
+ candidateFeatureHost = undefined;
359
+ const rootBundle = recoveryRootBundle;
360
+ recoveryRootBundle = undefined;
361
+ const candidateGraphOwned = recoveryGraphCandidate !== undefined;
362
+ if (rootBundle !== undefined) {
363
+ rootBundle.discard();
364
+ } else if (recoveryGraphCandidate !== undefined) {
365
+ renderSystem.discardRecoveryGraphCandidate(recoveryGraphCandidate);
366
+ } else if (featureHost !== undefined) {
367
+ const disposedFeatureHost = featureHost.dispose();
368
+ if (!disposedFeatureHost.ok) internals.errorRegistry.fire(disposedFeatureHost.error);
369
+ }
370
+ recoveryGraphCandidate = undefined;
371
+ candidateState.pipelineState?.perPassResources.drainBloomResources?.();
372
+ if (!candidateGraphOwned) {
373
+ candidateGpuStore.destroyAll();
374
+ }
375
+ candidateDynamicTextureStore.destroyAll();
376
+ recoveryScope.abandon();
377
+ if (getCandidateBuildState() === candidateState) setCandidateBuildState(undefined);
378
+ if (getCandidateShaderState() === candidateState.shaderState)
379
+ setCandidateShaderState(undefined);
380
+ if (getCandidatePipelineCacheState() === candidateState.pipelineCacheState) {
381
+ setCandidatePipelineCacheState(undefined);
382
+ }
383
+ if (getCandidateMaterialShaderUvSetCounts() === candidateState.materialShaderUvSetCounts) {
384
+ setCandidateMaterialShaderUvSetCounts(undefined);
385
+ }
386
+ setCandidateEmptyPostProcessBgl(undefined);
387
+ };
388
+ setCandidateCleanup(releaseCandidate);
389
+ // Re-attach the device.lost fan-out to the new device's lost Promise,
390
+ // reusing the SAME registries the host already subscribed to (SSOT
391
+ // helper). When this device is lost again, health() flips to
392
+ // `device-lost` and the host can call recover() once more.
393
+ let candidatePublished = false;
394
+ let candidateLost = false;
395
+ attachDeviceLostFanout(device, internals.pack, {
396
+ lostRegistry: internals.lostRegistry,
397
+ errorRegistry: internals.errorRegistry,
398
+ healthRegistry: internals.healthRegistry,
399
+ generation: recoveryScope.generation,
400
+ currentGeneration: () => internals.generationState.current,
401
+ onStaleLoss: () => internals.generationState.onStaleLoss?.(),
402
+ onDeviceLost: (detail) => {
403
+ if (candidatePublished) internals.lossObserver.current?.(detail);
404
+ else candidateLost = true;
405
+ },
406
+ });
407
+
408
+ // Step (e): rebuild GPU-bound state against the new device. Every cache,
409
+ // shader adapter, hook, and store used by this build belongs to
410
+ // candidateState; the active aggregate remains untouched on every await.
411
+ candidateGpuStore.configureGpuDevice(
412
+ device,
413
+ adaptMipmapShaderModuleFactory(internals.pack.createShaderModule),
414
+ (world, pod) => {
415
+ const handle: Handle<'EquirectAsset', 'shared'> = world.allocSharedRef(
416
+ 'EquirectAsset',
417
+ pod,
418
+ );
419
+ return ok(handle);
420
+ },
421
+ device.caps,
422
+ );
423
+ candidateGpuStore.configureIblDevice(
424
+ device,
425
+ internals.pack.createShaderModule === undefined
426
+ ? undefined
427
+ : (
428
+ (createShaderModule) => (device, descriptor) =>
429
+ createShaderModule(device, descriptor)
430
+ )(internals.pack.createShaderModule),
431
+ );
432
+ // A device loss invalidates the transient video textures too. Rebind
433
+ // through the store owner so its cached textures are destroyed before
434
+ // the first post-recovery upload; unlike CPU asset payloads, these
435
+ // handles cannot survive a device replacement.
436
+ candidateDynamicTextureStore.configureGpuDevice(adaptDynamicTextureDevice(device));
437
+ const abortDisposedRecovery = (
438
+ phase: RecoveryPhase,
439
+ ): Result<void, RecoverFailure> | undefined => {
440
+ if (!isRendererDisposed() && !candidateLost) return undefined;
441
+ const cause = new RecoverError(
442
+ isRendererDisposed() ? 'recover-not-needed' : 'recover-device-unavailable',
443
+ );
444
+ recordFailure(phase, cause, {
445
+ retryable: !isRendererDisposed(),
446
+ guidance: isRendererDisposed() ? 'rebuild-renderer' : 'retry',
447
+ owner: isRendererDisposed() ? 'renderer' : 'backend',
448
+ resourceKind: 'surface',
449
+ });
450
+ releaseCandidate();
451
+ return err(cause);
452
+ };
453
+ try {
454
+ setRecoveryPhase('rehydrate');
455
+ const preparation = await runRecoveryStep(
456
+ async (attemptToken) => {
457
+ await prepareMaterialShaders(
458
+ device,
459
+ getShader,
460
+ assets,
461
+ candidateState.materialShaderUvSetCounts,
462
+ );
463
+ if (attemptToken !== undefined && !attemptToken.isValid('rehydrate', Date.now())) {
464
+ return err(
465
+ new RhiError({
466
+ code: 'webgpu-runtime-error',
467
+ expected: 'recovery shader preparation belongs to the live attempt',
468
+ hint: 'discard the late candidate completion',
469
+ detail: {
470
+ error: { code: 'recovery-attempt-invalid', message: 'late shader completion' },
471
+ },
472
+ }),
473
+ );
474
+ }
475
+ return ok(undefined);
476
+ },
477
+ 'rehydrate',
478
+ deadline,
479
+ () => Date.now(),
480
+ continuation,
481
+ );
482
+ if (preparation.kind === 'timeout') {
483
+ recordFailure('rehydrate', preparation.timeout.cause);
484
+ releaseCandidate();
485
+ return err(new RecoverError('recover-device-unavailable'));
486
+ }
487
+ if (preparation.kind === 'error') throw preparation.error;
488
+ const abortedAfterShaders = abortDisposedRecovery('rehydrate');
489
+ if (abortedAfterShaders !== undefined) return abortedAfterShaders;
490
+ setRecoveryPhase('compile-graph');
491
+ const pipeline = await runRecoveryStep(
492
+ async (attemptToken) => {
493
+ const value = await buildPipeline(recoveryScope, device, candidateGpuStore);
494
+ if (attemptToken !== undefined && !attemptToken.isValid('compile-graph', Date.now())) {
495
+ return err(
496
+ new RhiError({
497
+ code: 'webgpu-runtime-error',
498
+ expected: 'recovery pipeline preparation belongs to the live attempt',
499
+ hint: 'discard the late candidate completion',
500
+ detail: {
501
+ error: { code: 'recovery-attempt-invalid', message: 'late pipeline completion' },
502
+ },
503
+ }),
504
+ );
505
+ }
506
+ return ok(value);
507
+ },
508
+ 'compile-graph',
509
+ deadline,
510
+ () => Date.now(),
511
+ continuation,
512
+ );
513
+ if (pipeline.kind === 'timeout') {
514
+ recordFailure('compile-graph', pipeline.timeout.cause);
515
+ releaseCandidate();
516
+ return err(new RecoverError('recover-device-unavailable'));
517
+ }
518
+ if (pipeline.kind === 'error') throw pipeline.error;
519
+ candidateState.pipelineState = pipeline.value;
520
+ const abortedAfterPipeline = abortDisposedRecovery('compile-graph');
521
+ if (abortedAfterPipeline !== undefined) return abortedAfterPipeline;
522
+ const postProcessResources = renderSystem.prepareRecoveryPostProcessResources(device);
523
+ if (!postProcessResources.ok) throw postProcessResources.error;
524
+ candidatePostProcessResources = postProcessResources.value;
525
+ const abortedAfterPostProcessPreparation = abortDisposedRecovery('compile-graph');
526
+ if (abortedAfterPostProcessPreparation !== undefined) {
527
+ return abortedAfterPostProcessPreparation;
528
+ }
529
+ } catch (cause) {
530
+ // Pipeline rebuild failed against the new device. Treat as a device
531
+ // unavailability (the device was acquired but is not usable); health
532
+ // stays `device-lost` so the host can retry.
533
+ recordFailure('compile-graph', cause);
534
+ if (cause instanceof RhiError) internals.errorRegistry.fire(cause);
535
+ releaseCandidate();
536
+ const aborted = abortDisposedRecovery('compile-graph');
537
+ if (aborted !== undefined) return aborted;
538
+ return err(new RecoverError('recover-device-unavailable'));
539
+ }
540
+ // Promote only after the complete replacement build is ready. The old
541
+ // scope is then retired exactly once; a failed attempt abandons its
542
+ // candidate in the catch path and can be retried with a new generation.
543
+ const candidatePipelineState = candidateState.pipelineState;
544
+ if (candidatePipelineState === null || candidatePostProcessResources === undefined) {
545
+ const cause = new RecoverError('recover-device-unavailable');
546
+ recordFailure('compile-graph', cause);
547
+ releaseCandidate();
548
+ return err(cause);
549
+ }
550
+ const candidatePostProcess = candidatePostProcessResources;
551
+ // Prepare the replacement context and graph through detached candidate
552
+ // bindings. The active device, stores, scope, generation and feature
553
+ // owners remain untouched until publishAndRetireRendererGeneration.
554
+ const candidatePostProcessPipelineCache = new Map<string, RenderPipeline>();
555
+ const candidateFeatureHostResult = createRenderFeatureHost(
556
+ internals.featureHost?.features ?? [],
557
+ );
558
+ if (!candidateFeatureHostResult.ok) {
559
+ recordFailure('compile-graph', candidateFeatureHostResult.error, {
560
+ owner: 'feature',
561
+ resourceKind: 'feature',
562
+ });
563
+ releaseCandidate();
564
+ return err(new RecoverError('recover-device-unavailable'));
565
+ }
566
+ candidateFeatureHost = candidateFeatureHostResult.value;
567
+ const createdCandidateFeatureHost = candidateFeatureHost;
568
+ const candidateInternals = Object.create(internals) as WebGPURendererInternals &
569
+ RenderSystemInternals;
570
+ Object.defineProperties(candidateInternals, {
571
+ device: { value: candidateState.device },
572
+ context: { value: candidateState.context },
573
+ deviceScope: { value: candidateState.scope },
574
+ gpuStore: { value: candidateState.gpuStore },
575
+ dynamicTextureStore: { value: candidateState.dynamicTextureStore },
576
+ recoveryColdWorkGuard: { value: undefined },
577
+ getMaterialShaderPipeline: { value: getMaterialShaderPipeline },
578
+ getMaterialShaderPipelineEntry: { value: getMaterialShaderPipelineEntry },
579
+ getMaterialShaderBindingContract: { value: getCachedMaterialShaderBindingContract },
580
+ getParamSchema: { value: getParamSchema },
581
+ getMaterialBindGroupLayout: { value: getMaterialBindGroupLayout },
582
+ metrics: { value: metrics },
583
+ buildPostProcessPipeline: { value: buildPostProcessPipeline },
584
+ getPipelineState: { value: () => candidatePipelineState },
585
+ featureHost: { value: createdCandidateFeatureHost },
586
+ lookupPostProcess: { value: renderSystem.lookupPostProcess },
587
+ getRenderTargetPhysical: { value: () => undefined },
588
+ resolveRenderTargetTextureSource: { value: () => undefined },
589
+ encodeRenderTargetReadbacks: { value: () => undefined },
590
+ getPostProcessParamsBuffer: {
591
+ value: (id: string) => candidatePostProcess.paramsBuffers.get(id),
592
+ },
593
+ getPostProcessPipeline: {
594
+ value: (
595
+ id: string,
596
+ bgl: BindGroupLayout,
597
+ colorFormat: GPUTextureFormat,
598
+ entryOverride?: PostProcessShaderEntry,
599
+ ) => {
600
+ const entry = entryOverride ?? candidateInternals.lookupPostProcess?.(id);
601
+ if (entry === undefined || candidateInternals.buildPostProcessPipeline === undefined) {
602
+ return null;
603
+ }
604
+ const key = `${id}|${colorFormat}|${postProcessShaderEntrySignature(entry)}`;
605
+ const cached = candidatePostProcessPipelineCache.get(key);
606
+ if (cached !== undefined) return cached;
607
+ const built = candidateInternals.buildPostProcessPipeline(
608
+ entry,
609
+ bgl,
610
+ colorFormat,
611
+ postProcessShaderPipelineLabel(id, entry.source),
612
+ );
613
+ if (built === null) return null;
614
+ candidatePostProcessPipelineCache.set(key, built);
615
+ return built;
616
+ },
617
+ },
618
+ getRecoveryPostProcessPipelines: {
619
+ value: () => candidatePostProcessPipelineCache,
620
+ },
621
+ });
622
+ const configured = ensureContextConfigured(
623
+ candidateInternals,
624
+ candidatePipelineState,
625
+ internals.errorRegistry,
626
+ );
627
+ if (!configured.ok) {
628
+ recordFailure('compile-graph', configured.error, {
629
+ owner: 'surface',
630
+ resourceKind: 'surface',
631
+ });
632
+ releaseCandidate();
633
+ return err(new RecoverError('recover-device-unavailable'));
634
+ }
635
+ const graphPreparation = renderSystem.prepareRecoveryGraphCandidate({
636
+ internals: candidateInternals,
637
+ pipelineState: candidatePipelineState,
638
+ });
639
+ if (graphPreparation.kind === 'failed') {
640
+ // prepareRecoveryGraphCandidate owns and releases the detached feature
641
+ // host before returning a failed result. Do not let the outer recovery
642
+ // cleanup call the same host's dispose method a second time.
643
+ candidateFeatureHost = undefined;
644
+ recordFailure(
645
+ 'compile-graph',
646
+ structuredRendererCause(
647
+ graphPreparation.cause ?? new Error(graphPreparation.reason),
648
+ 'recovery graph candidate',
649
+ ),
650
+ {
651
+ owner: 'render-graph',
652
+ resourceKind: 'pipeline',
653
+ },
654
+ );
655
+ releaseCandidate();
656
+ return err(new RecoverError('recover-device-unavailable'));
657
+ }
658
+ if (graphPreparation.kind === 'ready') {
659
+ recoveryGraphCandidate = graphPreparation.candidate;
660
+ const candidateGraph = recoveryGraphCandidate.frameState.perFrameGraph;
661
+ const candidateGraphTexture = candidateGraph?.getColorTargetTexture('scene-color');
662
+ const candidateGraphView = candidateGraph?.getColorTargetView('scene-color');
663
+ const candidateGraphDescriptor = candidateGraph?.getColorTargetDescriptor('scene-color');
664
+ const candidateGraphFormat = candidateGraphDescriptor?.format;
665
+ if (
666
+ candidateGraphTexture !== undefined &&
667
+ candidateGraphView !== undefined &&
668
+ (candidateGraphFormat === 'rgba16float' || candidateGraphFormat === 'rgba8unorm')
669
+ ) {
670
+ const graphProbe = await runRecoveryStep(
671
+ () =>
672
+ probeCandidateGraphExecution(device, {
673
+ texture: candidateGraphTexture,
674
+ view: candidateGraphView,
675
+ format: candidateGraphFormat,
676
+ }),
677
+ 'compile-graph',
678
+ deadline,
679
+ () => Date.now(),
680
+ continuation,
681
+ );
682
+ if (graphProbe.kind === 'timeout' || graphProbe.kind === 'error') {
683
+ const cause = graphProbe.kind === 'timeout' ? graphProbe.timeout.cause : graphProbe.error;
684
+ recordFailure('compile-graph', cause, {
685
+ retryable: true,
686
+ guidance: 'retry',
687
+ owner: 'render-graph',
688
+ resourceKind: 'texture',
689
+ });
690
+ releaseCandidate();
691
+ return err(new RecoverError('recover-device-unavailable'));
692
+ }
693
+ }
694
+ const candidateIsValid = (): boolean =>
695
+ !isRendererDisposed() &&
696
+ !candidateLost &&
697
+ recoveryScope.isAlive() &&
698
+ !deadlineExpired('compile-graph');
699
+ const setupSubmission = renderSystem.submitCandidateSetup(
700
+ recoveryGraphCandidate,
701
+ candidateIsValid,
702
+ );
703
+ if (!setupSubmission.ok) {
704
+ recordFailure('compile-graph', setupSubmission.error, {
705
+ owner: 'gpu-residency',
706
+ resourceKind: 'texture',
707
+ });
708
+ releaseCandidate();
709
+ return err(new RecoverError('recover-device-unavailable'));
710
+ }
711
+ const completion = setupSubmission.value.completion;
712
+ if (completion !== undefined) {
713
+ const setupCompletion = await runRecoveryStep(
714
+ async (attemptToken) => {
715
+ try {
716
+ await completion;
717
+ } catch (cause) {
718
+ if (cause instanceof RhiError) return err(cause);
719
+ return err(
720
+ new RhiError({
721
+ code: 'webgpu-runtime-error',
722
+ expected: 'recovery mip setup completion resolves after candidate submission',
723
+ hint: `discard the candidate after setup completion raised: ${String(cause)}`,
724
+ }),
725
+ );
726
+ }
727
+ if (attemptToken !== undefined && !attemptToken.isValid('compile-graph', Date.now())) {
728
+ return err(
729
+ new RhiError({
730
+ code: 'webgpu-runtime-error',
731
+ expected: 'recovery mip setup completion belongs to the live attempt',
732
+ hint: 'discard the late candidate completion',
733
+ }),
734
+ );
735
+ }
736
+ return candidateIsValid()
737
+ ? ok(undefined)
738
+ : err(
739
+ new RhiError({
740
+ code: 'webgpu-runtime-error',
741
+ expected: 'recovery mip setup completes before candidate publication',
742
+ hint: 'discard the stale recovery candidate and retry after device loss',
743
+ }),
744
+ );
745
+ },
746
+ 'compile-graph',
747
+ deadline,
748
+ () => Date.now(),
749
+ continuation,
750
+ );
751
+ if (setupCompletion.kind === 'timeout' || setupCompletion.kind === 'error') {
752
+ recordFailure(
753
+ 'compile-graph',
754
+ setupCompletion.kind === 'timeout'
755
+ ? setupCompletion.timeout.cause
756
+ : setupCompletion.error,
757
+ { owner: 'gpu-residency', resourceKind: 'texture' },
758
+ );
759
+ releaseCandidate();
760
+ return err(new RecoverError('recover-device-unavailable'));
761
+ }
762
+ }
763
+ }
764
+ const abortedAfterPrewarm = abortDisposedRecovery('compile-graph');
765
+ if (abortedAfterPrewarm !== undefined) return abortedAfterPrewarm;
766
+ const preparedRecoveryRoots = renderSystem.prepareRecoveryRoots({
767
+ scope: recoveryScope,
768
+ device,
769
+ gpuStore: candidateGpuStore,
770
+ graphCandidate: recoveryGraphCandidate,
771
+ });
772
+ recoveryRootBundle = preparedRecoveryRoots;
773
+ const recoveryRootCount = preparedRecoveryRoots.roots.length;
774
+ const candidateGeneration = await runRecoveryStep<RendererGeneration, RhiError>(
775
+ async (attemptToken) => {
776
+ const value = await buildGenerationAggregate<RendererGeneration>({
777
+ scope: recoveryScope,
778
+ device,
779
+ context,
780
+ pipeline: candidatePipelineState,
781
+ producerBindings: {
782
+ gpuStore: candidateGpuStore,
783
+ dynamicTextureStore: candidateDynamicTextureStore,
784
+ shaderState: candidateState.shaderState,
785
+ pipelineCacheState: candidateState.pipelineCacheState,
786
+ materialShaderUvSetCounts: candidateState.materialShaderUvSetCounts,
787
+ emptyPostProcessBgl: candidateState.emptyPostProcessBgl,
788
+ growMeshSsbo: candidateState.growMeshSsbo,
789
+ meshSsboState: candidateState.meshSsboState,
790
+ },
791
+ roots: preparedRecoveryRoots.roots,
792
+ });
793
+ if (attemptToken !== undefined && !attemptToken.isValid('compile-graph', Date.now())) {
794
+ if (value.ok) value.value.scope.abandon();
795
+ return err(
796
+ new RhiError({
797
+ code: 'webgpu-runtime-error',
798
+ expected: 'recovery generation aggregate belongs to the live attempt',
799
+ hint: 'discard the late generation aggregate',
800
+ }),
801
+ );
802
+ }
803
+ return value.ok
804
+ ? value
805
+ : err(
806
+ new RhiError({
807
+ code: 'webgpu-runtime-error',
808
+ expected: 'recovery generation aggregate commits every candidate root',
809
+ hint: `discard the candidate after ${value.error.resourceKind} assembly failed`,
810
+ detail: {
811
+ error: {
812
+ code: value.error.code,
813
+ message: `candidate generation failed at ${value.error.resourceKind}`,
814
+ },
815
+ },
816
+ }),
817
+ );
818
+ },
819
+ 'compile-graph',
820
+ deadline,
821
+ () => Date.now(),
822
+ continuation,
823
+ );
824
+ if (candidateGeneration.kind === 'timeout' || candidateGeneration.kind === 'error') {
825
+ recordFailure(
826
+ 'compile-graph',
827
+ candidateGeneration.kind === 'timeout'
828
+ ? candidateGeneration.timeout.cause
829
+ : candidateGeneration.error,
830
+ candidateGeneration.kind === 'error'
831
+ ? {
832
+ owner: candidateGeneration.error instanceof RhiError ? 'renderer' : 'renderer',
833
+ resourceKind: 'pipeline',
834
+ }
835
+ : {},
836
+ );
837
+ releaseCandidate();
838
+ return err(new RecoverError('recover-device-unavailable'));
839
+ }
840
+ const abortedAfterGeneration = abortDisposedRecovery('compile-graph');
841
+ if (abortedAfterGeneration !== undefined) return abortedAfterGeneration;
842
+ // Device creation, resource creation, and queue.submit are not enough to
843
+ // admit a replacement generation. Prove the exact candidate device can
844
+ // execute and expose one render attachment immediately before publication;
845
+ // the probe has no graph, shader, asset, or surface dependency, so a
846
+ // failure stays owned by backend readiness instead of being misreported as
847
+ // a renderer draw failure.
848
+ const deviceProbe = await runRecoveryStep(
849
+ () => probeCandidateDeviceExecution(candidateGeneration.value.device),
850
+ 'compile-graph',
851
+ deadline,
852
+ () => Date.now(),
853
+ continuation,
854
+ );
855
+ if (deviceProbe.kind === 'timeout' || deviceProbe.kind === 'error') {
856
+ const cause = deviceProbe.kind === 'timeout' ? deviceProbe.timeout.cause : deviceProbe.error;
857
+ recordFailure('compile-graph', cause, {
858
+ retryable: true,
859
+ guidance: 'retry',
860
+ owner: 'backend',
861
+ resourceKind: 'texture',
862
+ });
863
+ releaseCandidate();
864
+ return err(new RecoverError('recover-device-unavailable'));
865
+ }
866
+ // Candidate readiness is complete. The owner helper performs the only
867
+ // active swap and retires the previous aggregate synchronously.
868
+ setRecoveryPhase('publish');
869
+ try {
870
+ publishAndRetireRendererGeneration(
871
+ candidateGeneration.value,
872
+ candidatePostProcessResources,
873
+ preparedRecoveryRoots,
874
+ recoveryGraphCandidate,
875
+ recoveryColdWorkGuard,
876
+ );
877
+ if (recoveryGraphCandidate !== undefined) {
878
+ recoveryGraphCandidate = undefined;
879
+ }
880
+ } catch (cause) {
881
+ recordFailure('publish', cause, {
882
+ guidance: 'rebuild-renderer',
883
+ });
884
+ if (cause instanceof RhiError) internals.errorRegistry.fire(cause);
885
+ releaseCandidate();
886
+ return err(new RecoverError('recover-device-unavailable'));
887
+ }
888
+ candidatePostProcessResources = undefined;
889
+ recoveryRootBundle = undefined;
890
+ candidatePublished = true;
891
+ candidateReleased = true;
892
+ onRootsCommitted(recoveryRootCount);
893
+ setCandidateBuildState(undefined);
894
+ setCandidateShaderState(undefined);
895
+ setCandidatePipelineCacheState(undefined);
896
+ setCandidateMaterialShaderUvSetCounts(undefined);
897
+ setCandidateEmptyPostProcessBgl(undefined);
898
+
899
+ // Step (f): the renderer is alive again. The retained candidate frame has
900
+ // already configured the context, compiled the graph, and populated the
901
+ // visible residency set before this publication boundary.
902
+ internals.healthRegistry.fire({ reason: 'alive', recoverable: false });
903
+ return ok(undefined);
904
+ }
905
+ return { recoverOnce };
906
+ }