@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
@@ -1,7 +1,7 @@
1
1
  import { err, ok, type Result } from '@forgeax/engine-types';
2
- import type { DeviceScope } from '../device/device-scope';
2
+ import type { DeviceScope, LifecycleResourceSpec } from '../device/device-scope';
3
3
  import { EnvironmentGenerationFailedError } from '../errors/render';
4
- import type { EnvironmentFrame } from './frame';
4
+ import { type EnvironmentFrame, SKYLIGHT_RECOVERY_FALLBACK } from './frame';
5
5
  import type { EnvironmentGeneration, EnvironmentGenerationFailure } from './generation';
6
6
  import { sharedEnvironmentFrame } from './graph';
7
7
  import {
@@ -20,8 +20,11 @@ interface RetiredGeneration {
20
20
  readonly generation: EnvironmentGeneration;
21
21
  }
22
22
 
23
+ let nextEnvironmentToken = 1;
24
+
23
25
  /** Owns candidate, active, LKG, and DeviceScope generation transitions. */
24
26
  export class EnvironmentLifecycle {
27
+ private readonly token = nextEnvironmentToken++;
25
28
  private currentScope: DeviceScope;
26
29
  private nextGeneration = 0;
27
30
  private active: EnvironmentGeneration | undefined;
@@ -31,6 +34,13 @@ export class EnvironmentLifecycle {
31
34
  private readonly retired: RetiredGeneration[] = [];
32
35
  private readonly leaseCounts = new Map<number, number>();
33
36
  private lastCandidateFailure: EnvironmentInspectionFailure | undefined;
37
+ private recovery:
38
+ | {
39
+ readonly deviceGeneration: number;
40
+ readonly frame: EnvironmentFrame;
41
+ readonly lane: 'direct' | 'clustered';
42
+ }
43
+ | undefined;
34
44
 
35
45
  constructor(scope: DeviceScope) {
36
46
  this.currentScope = scope;
@@ -41,6 +51,63 @@ export class EnvironmentLifecycle {
41
51
  this.currentScope = scope;
42
52
  }
43
53
 
54
+ resetForRecover(deviceGeneration: number): void {
55
+ if (!Number.isSafeInteger(deviceGeneration) || deviceGeneration < 0) {
56
+ throw new RangeError('deviceGeneration must be a non-negative safe integer.');
57
+ }
58
+ const frame = this.active === undefined ? undefined : this.frames.get(this.active.signature);
59
+ if (frame !== undefined) {
60
+ this.recovery = Object.freeze({
61
+ deviceGeneration,
62
+ frame,
63
+ lane: this.active?.lane ?? 'direct',
64
+ });
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Create an environment owner for a recovery candidate without moving the
70
+ * active owner's scope or generation maps. Only the selected CPU frame and
71
+ * its lane are shared; every candidate generation/frame map starts empty.
72
+ */
73
+ createRecoveryCandidate(scope: DeviceScope): EnvironmentLifecycle {
74
+ const candidate = new EnvironmentLifecycle(scope);
75
+ const activeFrame =
76
+ this.recovery?.frame ??
77
+ (this.active === undefined ? undefined : this.frames.get(this.active.signature));
78
+ if (activeFrame !== undefined) {
79
+ candidate.recovery = Object.freeze({
80
+ deviceGeneration: scope.generation,
81
+ frame: activeFrame,
82
+ lane: this.recovery?.lane ?? this.active?.lane ?? 'direct',
83
+ });
84
+ }
85
+ candidate.lastCandidateFailure = this.lastCandidateFailure;
86
+ return candidate;
87
+ }
88
+
89
+ /** True when a CPU environment frame is available for candidate recovery. */
90
+ hasRecoveryFrame(): boolean {
91
+ return (
92
+ this.recovery?.frame !== undefined ||
93
+ (this.active !== undefined && this.frames.has(this.active.signature))
94
+ );
95
+ }
96
+
97
+ createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown> {
98
+ return {
99
+ kind: 'texture',
100
+ create: async () => {
101
+ const recovered = await this.recover(scope);
102
+ if (!recovered.ok) throw recovered.error;
103
+ return recovered.value;
104
+ },
105
+ cleanup: (value) => {
106
+ this.discard(value as EnvironmentGeneration);
107
+ },
108
+ };
109
+ }
110
+
44
111
  isActive(generation: EnvironmentGeneration): boolean {
45
112
  return (
46
113
  this.active?.generation === generation.generation &&
@@ -56,6 +123,14 @@ export class EnvironmentLifecycle {
56
123
  this.frames.delete(generation.signature);
57
124
  generation.retired = true;
58
125
  generation.scope.retire();
126
+ if (this.recovery?.frame.signature === generation.signature) this.recovery = undefined;
127
+ if (this.candidates.size === 0) this.recovery = undefined;
128
+ }
129
+
130
+ /** Drop every uncommitted generation owned by an abandoned candidate. */
131
+ discardRecoveryCandidates(): void {
132
+ for (const generation of [...this.candidates.values()]) this.discard(generation);
133
+ this.recovery = undefined;
59
134
  }
60
135
 
61
136
  recordCandidateFailure(error: EnvironmentGenerationFailedError): void {
@@ -157,6 +232,28 @@ export class EnvironmentLifecycle {
157
232
  }
158
233
  this.active = generation;
159
234
  this.lkg = generation;
235
+ this.recovery = undefined;
236
+ }
237
+
238
+ /** Close the old active generation before a replacement owner is installed. */
239
+ retireActiveForReplacement(): void {
240
+ const active = this.active;
241
+ if (active === undefined) return;
242
+ this.active = undefined;
243
+ if (this.lkg === active) this.lkg = undefined;
244
+ active.retired = true;
245
+ active.scope.retire();
246
+ this.recovery = undefined;
247
+ }
248
+
249
+ /** Close an already-published generation when its owning scope retires. */
250
+ retirePublishedGeneration(generation: EnvironmentGeneration): void {
251
+ if (this.active !== generation) return;
252
+ this.active = undefined;
253
+ if (this.lkg === generation) this.lkg = undefined;
254
+ generation.retired = true;
255
+ generation.scope.retire();
256
+ this.recovery = undefined;
160
257
  }
161
258
 
162
259
  acquire(): EnvironmentLease {
@@ -237,24 +334,33 @@ export class EnvironmentLifecycle {
237
334
  ...(this.lastCandidateFailure === undefined
238
335
  ? {}
239
336
  : { lastCandidateFailure: this.lastCandidateFailure }),
337
+ ...(this.recovery === undefined
338
+ ? {}
339
+ : {
340
+ recovery: {
341
+ token: this.token,
342
+ deviceGeneration: this.recovery.deviceGeneration,
343
+ status: 'uninitialized' as const,
344
+ fallback: SKYLIGHT_RECOVERY_FALLBACK.kind,
345
+ },
346
+ }),
240
347
  });
241
348
  }
242
349
 
243
350
  async recover(
244
351
  scope: DeviceScope,
245
352
  ): Promise<Result<EnvironmentGeneration, EnvironmentGenerationFailedError>> {
246
- if (this.active === undefined)
247
- return err(new EnvironmentGenerationFailedError('none', 'prepare'));
248
353
  this.currentScope = scope;
249
354
  const previous = this.active;
250
- this.candidates.delete(previous.signature);
251
- const previousFrame = this.frames.get(previous.signature);
355
+ const previousFrame =
356
+ this.recovery?.frame ??
357
+ (previous === undefined ? undefined : this.frames.get(previous.signature));
252
358
  if (previousFrame === undefined) {
253
359
  return err(new EnvironmentGenerationFailedError('none', 'prepare'));
254
360
  }
255
361
  const result = await this.ensure(
256
- sharedEnvironmentFrame(previousFrame, previous.lane),
257
- previous.lane,
362
+ sharedEnvironmentFrame(previousFrame, previous?.lane ?? 'direct'),
363
+ this.recovery?.lane ?? previous?.lane ?? 'direct',
258
364
  );
259
365
  if (!result.ok) return result;
260
366
  return ok(result.value);
@@ -6,10 +6,13 @@
6
6
  // `recover(): Promise<Result<void, RecoverFailure>>`, NOT the onError fanout
7
7
  // channel (see render-contract.ts RendererError composition).
8
8
 
9
- import type { DeviceScopeReceipt } from '../device/resource-types';
9
+ import type { RecoveryGuidance, RecoveryPhase } from '../assembly/renderer-lifecycle';
10
+ import type { DeviceResourceKind, DeviceScopeReceipt } from '../device/resource-types';
10
11
  import type { ReflectionFallbackRecoveryAction } from '../inspection-types';
11
12
  import type { SsrAdmissionError, SsrOwnerRecoveryAction } from '../ssr/errors';
12
13
 
14
+ export type { RecoveryGuidance, RecoveryPhase } from '../assembly/renderer-lifecycle';
15
+ export { RECOVERY_PHASES } from '../assembly/renderer-lifecycle';
13
16
  export type { ReflectionFallbackRecoveryAction } from '../inspection-types';
14
17
 
15
18
  export function reflectionFallbackRecoveryAction(
@@ -40,6 +43,15 @@ export function ssrAdmissionRecoveryAction(error: SsrAdmissionError): SsrOwnerRe
40
43
  }
41
44
  }
42
45
 
46
+ /** Closed outcome vocabulary shared by recovery inspection and errors. */
47
+ export type RecoveryOutcome = 'none' | 'succeeded' | 'failed' | 'disposed';
48
+
49
+ /**
50
+ * `RecoveryPhase` is the stable lower-case hyphenated phase union used by
51
+ * inspect/recovery detail. `RecoveryGuidance` is the closed next-action union:
52
+ * retry the attempt, repair its owner, or rebuild the Renderer.
53
+ */
54
+
43
55
  // ── RecoverError (feat-20260621-renderer-health-recover-skeleton M1) ─────────
44
56
 
45
57
  /**
@@ -73,13 +85,13 @@ const RECOVER_ERROR_POLICY = {
73
85
  'recover-not-needed': {
74
86
  message: 'recover-not-needed: renderer is not in a degraded state',
75
87
  expected:
76
- 'renderer is healthy; call health() first to confirm degraded state before calling recover()',
77
- hint: 'call health() first to confirm degraded state before calling recover()',
88
+ 'renderer is healthy; use state() or inspect() to confirm degraded state before calling recover()',
89
+ hint: 'use state() or inspect().state to confirm degraded state before calling recover()',
78
90
  },
79
91
  'recover-not-implemented': {
80
92
  message: 'recover-not-implemented: self-heal recovery is not yet implemented',
81
93
  expected: 'recovery is not yet implemented; self-heal lands in S5',
82
- hint: 'self-heal recovery lands in S5; health().reason still reflects the degraded state',
94
+ hint: 'self-heal recovery lands in S5; inspect().state still reflects the lifecycle state',
83
95
  },
84
96
  'recover-adapter-unavailable': {
85
97
  message: 'recover-adapter-unavailable: requestAdapter returned no adapter during rebuild',
@@ -127,6 +139,60 @@ export class RecoverError extends Error {
127
139
  }
128
140
  }
129
141
 
142
+ export interface RecoveryFailureDetail {
143
+ /** The bounded phase that produced this failure. */
144
+ readonly phase: RecoveryPhase;
145
+ /** Active generation before the attempt; it changes only on publish. */
146
+ readonly oldGeneration: number;
147
+ /** Candidate generation; it changes only on publish. */
148
+ readonly candidateGeneration: number;
149
+ /** Monotonic explicit recovery attempt number. */
150
+ readonly attempt: number;
151
+ /** Wall-clock duration of this bounded attempt. */
152
+ readonly elapsedMs: number;
153
+ /** Whether the caller may explicitly retry without recreating the Renderer. */
154
+ readonly retryable: boolean;
155
+ /** Next action selected by the recovery owner. */
156
+ readonly guidance: RecoveryGuidance;
157
+ /** Producer or lifecycle owner that must be inspected first. */
158
+ readonly owner: string;
159
+ /** Structured resource category, never a raw GPU handle. */
160
+ readonly resourceKind: DeviceResourceKind;
161
+ /** Terminal outcome of this attempt. */
162
+ readonly lastOutcome: Exclude<RecoveryOutcome, 'none' | 'succeeded'>;
163
+ /** Number of candidate roots committed before publication. */
164
+ readonly rehydratedRoots: number;
165
+ /** Number of late notifications ignored from retired generations. */
166
+ readonly staleLossEvents: number;
167
+ /** Original structured cause retained for diagnostics. */
168
+ readonly cause: unknown;
169
+ /** Cleanup failures that prevented a complete candidate publication. */
170
+ readonly cleanupFailures: readonly unknown[];
171
+ /** Detached lifecycle receipt for the failed candidate. */
172
+ readonly receipt: DeviceScopeReceipt;
173
+ }
174
+
175
+ /** Stable structured failure returned by one bounded recovery attempt. */
176
+ export class RecoveryFailedError extends Error {
177
+ readonly code = 'recovery-failed' as const;
178
+ readonly expected = 'one recovery attempt publishes a complete replacement device generation';
179
+ readonly hint = 'inspect detail and follow its guidance';
180
+ readonly detail: RecoveryFailureDetail;
181
+
182
+ constructor(detail: RecoveryFailureDetail) {
183
+ super('renderer recovery failed');
184
+ this.name = 'RecoveryFailedError';
185
+ this.detail = Object.freeze({
186
+ ...detail,
187
+ cleanupFailures: Object.freeze([...detail.cleanupFailures]),
188
+ });
189
+ }
190
+ }
191
+
192
+ export function createRecoveryFailedError(detail: RecoveryFailureDetail): RecoveryFailedError {
193
+ return new RecoveryFailedError(detail);
194
+ }
195
+
130
196
  const RENDER_RECOVERY_ERROR_POLICY = {
131
197
  'recover-lifecycle-failed': {
132
198
  expected: 'the replacement DeviceScope must publish only after lifecycle cleanup succeeds',
@@ -141,10 +207,15 @@ const RENDER_RECOVERY_ERROR_POLICY = {
141
207
  export type RenderRecoveryErrorCode = keyof typeof RENDER_RECOVERY_ERROR_POLICY;
142
208
 
143
209
  export interface RenderRecoveryFailureDetail {
210
+ /** Producer/lifecycle owner whose recovery operation failed. */
144
211
  readonly owner: string;
212
+ /** Device generation associated with the failed recovery operation. */
145
213
  readonly generation: number;
214
+ /** Closed operation kind; no implicit background retry is represented. */
146
215
  readonly recovery: 'recover' | 'rebuild' | 'stop';
216
+ /** Lifecycle receipt retained as structured evidence, not a live handle. */
147
217
  readonly receipt: DeviceScopeReceipt;
218
+ /** Original structured cause for the failed operation. */
148
219
  readonly cause: unknown;
149
220
  }
150
221
 
@@ -165,7 +236,7 @@ export class RenderRecoveryError extends Error {
165
236
  }
166
237
  }
167
238
 
168
- export type RecoverFailure = RecoverError | RenderRecoveryError;
239
+ export type RecoverFailure = RecoverError | RenderRecoveryError | RecoveryFailedError;
169
240
 
170
241
  export type IblCapabilityLossCode = 'ibl-hdr-capability-loss';
171
242
 
@@ -27,6 +27,11 @@ import type {
27
27
  } from '../features/vocabulary';
28
28
  import type { SceneDataLane, SceneDataSchemaId } from '../temporal/scene-data';
29
29
  import type { TransmissionCapabilityFact } from '../transmission/backdrop';
30
+ import type { RecoveryOutcome, RecoveryPhase } from './recover';
31
+
32
+ // Public render errors keep one machine-readable order: closed `code`,
33
+ // expected state, actionable `hint`, then typed `detail`. Consumers branch on
34
+ // that shape and never use Error.message as a protocol.
30
35
 
31
36
  export interface LifecycleConstructionFailureDetail {
32
37
  readonly owner: string;
@@ -1359,7 +1364,19 @@ export interface RendererOperationDetailByCode {
1359
1364
  };
1360
1365
  readonly 'recovery-failed': {
1361
1366
  readonly operation: 'recover';
1367
+ readonly phase: RecoveryPhase;
1362
1368
  readonly oldGeneration: number;
1369
+ readonly candidateGeneration: number;
1370
+ readonly attempt: number;
1371
+ readonly elapsedMs: number;
1372
+ readonly retryable: boolean;
1373
+ readonly guidance: 'retry' | 'repair-owner' | 'rebuild-renderer';
1374
+ readonly owner: string;
1375
+ readonly resourceKind: string;
1376
+ readonly lastOutcome: RecoveryOutcome;
1377
+ readonly rehydratedRoots: number;
1378
+ readonly staleLossEvents: number;
1379
+ readonly cleanupFailures: readonly RendererOperationCause[];
1363
1380
  readonly cause: RendererOperationCause;
1364
1381
  };
1365
1382
  readonly 'cleanup-failed': {