@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
@@ -14,7 +14,7 @@
14
14
  // outside M3 scope — the short-name mat4.perspective
15
15
  // already writes [0, 1] NDC, matching the ortho branch.
16
16
  // projection === 1 → orthographic path; RenderSystem builds
17
- // mat4.orthographic(out, left, right, bottom, top,
17
+ // mat4.orthographic(out, left, right, top, bottom,
18
18
  // near, far) (WebGPU [0, 1] NDC; same z convention).
19
19
  //
20
20
  // Naming convention: forgeax uses the bare `Camera` name (no `Component`
@@ -23,8 +23,8 @@ export function addTypedDebugOverlayPass(
23
23
  projection,
24
24
  frame.camera.orthoLeft,
25
25
  frame.camera.orthoRight,
26
- frame.camera.orthoBottom,
27
26
  frame.camera.orthoTop,
27
+ frame.camera.orthoBottom,
28
28
  frame.camera.near,
29
29
  frame.camera.far,
30
30
  );
@@ -46,6 +46,17 @@ interface OwnedResource {
46
46
  readonly cleanup: (value: unknown) => void | Promise<void>;
47
47
  }
48
48
 
49
+ function idempotentCleanup<T>(
50
+ cleanup: (value: T) => void | Promise<void>,
51
+ ): (value: T) => void | Promise<void> {
52
+ let cleaned = false;
53
+ return (value: T) => {
54
+ if (cleaned) return;
55
+ cleaned = true;
56
+ return cleanup(value);
57
+ };
58
+ }
59
+
49
60
  export class DeviceScope {
50
61
  readonly generation: number;
51
62
  readonly owner: string;
@@ -117,7 +128,7 @@ export class DeviceScope {
117
128
  const ref = new DeviceResourceRef(kind, this.owner, this.generation, value);
118
129
  this.resources.push({
119
130
  ref: ref as DeviceResourceRef<unknown>,
120
- cleanup: cleanup as (resource: unknown) => void | Promise<void>,
131
+ cleanup: idempotentCleanup(cleanup) as (resource: unknown) => void | Promise<void>,
121
132
  });
122
133
  return ref;
123
134
  }
@@ -142,9 +153,21 @@ export class DeviceScope {
142
153
  if (this.lifecycleState === 'active') this.lifecycleState = 'retiring';
143
154
  }
144
155
 
156
+ private clearChildren(): void {
157
+ for (const child of [...this.children].reverse()) {
158
+ child.retire();
159
+ }
160
+ }
161
+
162
+ private detachChild(child: DeviceScope): void {
163
+ const index = this.children.indexOf(child);
164
+ if (index >= 0) this.children.splice(index, 1);
165
+ }
166
+
145
167
  retire(): void {
146
168
  if (this.lifecycleState !== 'active' && this.lifecycleState !== 'retiring') return;
147
169
  this.lifecycleState = 'retiring';
170
+ this.clearChildren();
148
171
  this._clearResources();
149
172
  this.lifecycleState = 'retired';
150
173
  }
@@ -152,12 +175,14 @@ export class DeviceScope {
152
175
  abandon(): void {
153
176
  if (this.lifecycleState !== 'active') return;
154
177
  this.lifecycleState = 'abandoned';
178
+ this.clearChildren();
155
179
  this._clearResources();
156
180
  }
157
181
 
158
182
  dispose(): void {
159
183
  if (this.lifecycleState === 'disposed') return;
160
184
  this.lifecycleState = 'disposed';
185
+ this.clearChildren();
161
186
  this._clearResources();
162
187
  }
163
188
 
@@ -177,6 +202,9 @@ export class DeviceScope {
177
202
  replacement.dispose();
178
203
  return transaction.failure('dispose');
179
204
  }
205
+ // The replacement is now the new owner. Detach it before retiring the
206
+ // old parent so parent cleanup cannot retire the just-published child.
207
+ this.detachChild(replacement);
180
208
  this.retire();
181
209
  return ok(replacement);
182
210
  }
@@ -217,6 +245,7 @@ export interface LifecycleTransactionOptions {
217
245
  interface CreatedResource {
218
246
  readonly spec: LifecycleResourceSpec<unknown>;
219
247
  readonly value: unknown;
248
+ readonly cleanup: (value: unknown) => void | Promise<void>;
220
249
  }
221
250
 
222
251
  export class LifecycleTransaction {
@@ -250,10 +279,10 @@ export class LifecycleTransaction {
250
279
  }
251
280
  const value = await spec.create();
252
281
  if (!this.scope.isAlive()) {
253
- await this.cleanupOne(spec, value, created);
282
+ await this.cleanupOne(value, created, idempotentCleanup(spec.cleanup));
254
283
  return this.failure('dispose');
255
284
  }
256
- created.push({ spec, value });
285
+ created.push({ spec, value, cleanup: idempotentCleanup(spec.cleanup) });
257
286
  } catch (cause) {
258
287
  const cleanupFailures = await this.cleanupCreated(created);
259
288
  return this.failure('create', spec.kind, cleanupFailures, cause);
@@ -296,7 +325,7 @@ export class LifecycleTransaction {
296
325
  const item = created[index];
297
326
  if (item === undefined) continue;
298
327
  try {
299
- await item.spec.cleanup(item.value);
328
+ await item.cleanup(item.value);
300
329
  } catch (cause) {
301
330
  failures.push({ resourceKind: item.spec.kind, cause });
302
331
  }
@@ -305,13 +334,13 @@ export class LifecycleTransaction {
305
334
  }
306
335
 
307
336
  private async cleanupOne(
308
- spec: LifecycleResourceSpec<unknown>,
309
337
  value: unknown,
310
338
  created: readonly CreatedResource[],
339
+ cleanup: (value: unknown) => void | Promise<void>,
311
340
  ): Promise<void> {
312
341
  await this.cleanupCreated(created);
313
342
  try {
314
- await spec.cleanup(value);
343
+ await cleanup(value);
315
344
  } catch {
316
345
  // The terminal dispose result is intentionally bounded; prior cleanup
317
346
  // evidence remains in the transaction failure shape.
@@ -32,7 +32,9 @@
32
32
  import {
33
33
  blitMipmapsSync,
34
34
  getOrCreateMipmapPipeline,
35
+ type MipmapEncoderWork,
35
36
  type MipmapShaderModuleFactory,
37
+ prepareMipmaps,
36
38
  } from '@forgeax/engine-assets-runtime';
37
39
  import type { World } from '@forgeax/engine-ecs';
38
40
  import {
@@ -98,6 +100,7 @@ import {
98
100
  runIblPrecompute,
99
101
  } from '../ibl/IblPipelineCache';
100
102
  import type { RhiErrorListenerRegistry } from '../lifecycle';
103
+ import type { RecoveryColdWorkGuard } from '../record/render-context';
101
104
  import {
102
105
  type CubeRenderData,
103
106
  deriveRenderDataCubemap,
@@ -106,7 +109,7 @@ import {
106
109
  type MeshRenderData,
107
110
  type TextureRenderData,
108
111
  } from '../render-data';
109
- import type { DeviceScope } from './device-scope';
112
+ import type { DeviceScope, LifecycleResourceSpec } from './device-scope';
110
113
 
111
114
  const DYNAMIC_OFFSET_STRIDE = 256;
112
115
  const FACE_COUNT = 6;
@@ -182,6 +185,7 @@ export function writePrefilterUniforms(
182
185
  subPassIdx: number,
183
186
  roughness: number,
184
187
  mipFaceSize: number,
188
+ sourceMipLevelCount = 1,
185
189
  ): Result<void, RhiError> {
186
190
  if (subPassIdx < 0 || subPassIdx >= PREFILTER_SUBPASS_COUNT) {
187
191
  return invalidUniformInput(
@@ -189,13 +193,20 @@ export function writePrefilterUniforms(
189
193
  `writePrefilterUniforms received subPassIdx=${subPassIdx}`,
190
194
  );
191
195
  }
192
- const payload = new Float32Array([roughness, mipFaceSize, 0, 0]);
196
+ if (!Number.isInteger(sourceMipLevelCount) || sourceMipLevelCount < 1) {
197
+ return invalidUniformInput(
198
+ 'sourceMipLevelCount is a positive integer matching the bound source cube mip levels',
199
+ `writePrefilterUniforms received sourceMipLevelCount=${sourceMipLevelCount}`,
200
+ );
201
+ }
202
+ const payload = new Float32Array([roughness, mipFaceSize, sourceMipLevelCount, 0]);
193
203
  return device.queue.writeBuffer(buffer, subPassIdx * DYNAMIC_OFFSET_STRIDE, payload);
194
204
  }
195
205
 
196
206
  export function writeAllPrefilterUniforms(
197
207
  device: FaceUniformsDevice,
198
208
  buffer: Buffer,
209
+ sourceMipLevelCount = 1,
199
210
  ): Result<void, RhiError> {
200
211
  for (let mip = 0; mip < PREFILTER_MIP_LEVELS; mip++) {
201
212
  const roughness = mip / (PREFILTER_MIP_LEVELS - 1);
@@ -207,6 +218,7 @@ export function writeAllPrefilterUniforms(
207
218
  mip * FACE_COUNT + face,
208
219
  roughness,
209
220
  mipFaceSize,
221
+ sourceMipLevelCount,
210
222
  );
211
223
  if (!result.ok) return result;
212
224
  }
@@ -303,6 +315,11 @@ export interface TextureGpuEntry {
303
315
  readonly receipt: TextureResidencyReceipt;
304
316
  }
305
317
 
318
+ /** Candidate-only residency work; no queue submission is hidden here. */
319
+ export interface RecoveryResidencyPreparation {
320
+ readonly mipmapWork?: MipmapEncoderWork;
321
+ }
322
+
306
323
  // feat-20260630-equirect-kind-internalized-ibl-declarative-skyligh M2 / w11
307
324
  // (D-3): the projection-status truth lives here (the store's cubemap map is the
308
325
  // single authority — the SkylightSnapshot carries no status, D-3). `status`:
@@ -529,6 +546,7 @@ export class GpuResidencyCache {
529
546
  // Hardware-probe caps injected at `configureGpuDevice`; guards the HDR cubemap
530
547
  // path (_uploadCubemapFromEquirect) when `rgba16floatRenderable` is false.
531
548
  private caps: RhiCaps | undefined = undefined;
549
+ private recoveryColdWorkGuard: RecoveryColdWorkGuard | undefined;
532
550
  private deviceEpoch = 0;
533
551
  private textureGeneration = 0;
534
552
 
@@ -607,6 +625,11 @@ export class GpuResidencyCache {
607
625
  this.deviceScope = scope;
608
626
  }
609
627
 
628
+ /** Arm the first-published-frame assertion on this generation's store. */
629
+ setRecoveryColdWorkGuard(guard: RecoveryColdWorkGuard | undefined): void {
630
+ this.recoveryColdWorkGuard = guard;
631
+ }
632
+
610
633
  /**
611
634
  * Wrap a raw RHI Buffer handle into a GpuBuffer. The wrapper holds a
612
635
  * reference to the device so `.destroy()` forwards to
@@ -695,6 +718,39 @@ export class GpuResidencyCache {
695
718
  this.meshGpuHandles.clear();
696
719
  }
697
720
 
721
+ /** Number of candidate-owned GPU resources currently held by this cache. */
722
+ recoveryResourceCount(): number {
723
+ let cubemapResources = 0;
724
+ const seen = new Set<GpuTexture>();
725
+ for (const entry of this.cubemapGpuHandles.values()) {
726
+ if (entry.texture !== null && !seen.has(entry.texture)) {
727
+ seen.add(entry.texture);
728
+ cubemapResources += 1;
729
+ }
730
+ }
731
+ return (
732
+ this.meshGpuHandles.size +
733
+ this.textureGpuHandles.size +
734
+ this.samplerGpuHandles.size +
735
+ cubemapResources
736
+ );
737
+ }
738
+
739
+ /** Candidate root for the cache owner, not a scalar readiness marker. */
740
+ createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown> {
741
+ return {
742
+ kind: 'texture',
743
+ create: () => {
744
+ if (!scope.isAlive()) throw new Error('GPU residency candidate scope is not active.');
745
+ if (this.recoveryResourceCount() === 0) {
746
+ throw new Error('GPU residency candidate has no prepared resources.');
747
+ }
748
+ return this;
749
+ },
750
+ cleanup: () => undefined,
751
+ };
752
+ }
753
+
698
754
  /**
699
755
  * Set the error registry channel (wired by createRenderer). evict /
700
756
  * releaseUnreferenced fire structured errors here instead of throwing -
@@ -970,6 +1026,7 @@ export class GpuResidencyCache {
970
1026
  const key = this.worldKey(handleSlot(handle), worldId);
971
1027
  const existing = this.samplerGpuHandles.get(key);
972
1028
  if (existing !== undefined) return ok(existing);
1029
+ this.recoveryColdWorkGuard?.noteUploadColdWork();
973
1030
  const device = this.gpuDevice;
974
1031
  if (device === undefined) {
975
1032
  return err(
@@ -1102,6 +1159,7 @@ export class GpuResidencyCache {
1102
1159
  const key = this.worldKey(id, worldId);
1103
1160
  const existing = this.meshGpuHandles.get(key);
1104
1161
  if (existing !== undefined) return ok(existing);
1162
+ this.recoveryColdWorkGuard?.noteUploadColdWork();
1105
1163
  const projected = deriveRenderDataMesh(pod);
1106
1164
  if (!projected.ok) return projected;
1107
1165
  return this.uploadMeshById(key, pod, projected.value, lodMeshes);
@@ -1110,6 +1168,7 @@ export class GpuResidencyCache {
1110
1168
  const texKey = this.worldKey(id, worldId);
1111
1169
  const existing = this.textureGpuHandles.get(texKey);
1112
1170
  if (existing !== undefined) return ok(existing);
1171
+ this.recoveryColdWorkGuard?.noteUploadColdWork();
1113
1172
  const projected = deriveRenderDataTexture(pod);
1114
1173
  if (!projected.ok) return projected;
1115
1174
  const decoded: DecodedImage = decodedFromTexture(pod);
@@ -1124,6 +1183,101 @@ export class GpuResidencyCache {
1124
1183
  }
1125
1184
  }
1126
1185
 
1186
+ /**
1187
+ * Prepare one candidate-visible resource without the normal draw-time mip
1188
+ * submission. Base writes and resource creation stay owned by this cache;
1189
+ * generated mip passes are returned to the recovery transaction for one
1190
+ * explicit finish/submit before publication.
1191
+ */
1192
+ prepareResidentForRecovery(
1193
+ handle: Handle<'MeshAsset', 'shared'>,
1194
+ pod: TypesMeshAsset,
1195
+ worldId?: World | number,
1196
+ ): Result<RecoveryResidencyPreparation, RhiError | AssetError | ImageError>;
1197
+ prepareResidentForRecovery(
1198
+ handle: Handle<'TextureAsset', 'shared'>,
1199
+ pod: TextureAsset,
1200
+ worldId?: World | number,
1201
+ ): Result<RecoveryResidencyPreparation, RhiError | AssetError | ImageError>;
1202
+ prepareResidentForRecovery(
1203
+ handle: Handle<'MeshAsset', 'shared'> | Handle<'TextureAsset', 'shared'>,
1204
+ pod: TypesMeshAsset | TextureAsset,
1205
+ worldId: World | number = 0,
1206
+ ): Result<RecoveryResidencyPreparation, RhiError | AssetError | ImageError> {
1207
+ const id = handleSlot(handle);
1208
+ if (pod.kind === 'mesh') {
1209
+ const key = this.worldKey(id, worldId);
1210
+ if (this.meshGpuHandles.has(key)) return ok({});
1211
+ const projected = deriveRenderDataMesh(pod);
1212
+ if (!projected.ok) return projected;
1213
+ const uploaded = this.uploadMeshById(key, pod, projected.value);
1214
+ return uploaded.ok ? ok({}) : uploaded;
1215
+ }
1216
+
1217
+ const cacheKey = this.worldKey(id, worldId);
1218
+ if (this.textureGpuHandles.has(cacheKey)) return ok({});
1219
+ const projected = deriveRenderDataTexture(pod);
1220
+ if (!projected.ok) return projected;
1221
+ const decoded = decodedFromTexture(pod);
1222
+ const prepared = this.prepareTextureUpload(
1223
+ handle as Handle<'TextureAsset', 'shared'>,
1224
+ pod,
1225
+ decoded,
1226
+ projected.value,
1227
+ );
1228
+ if (!prepared.ok) return prepared;
1229
+ const device = this.gpuDevice;
1230
+ const gpuTexture = prepared.value.gpuTexture;
1231
+ if (device === undefined || gpuTexture === undefined) {
1232
+ return err(
1233
+ new RhiError({
1234
+ code: 'rhi-not-available',
1235
+ expected: 'GpuResidencyCache.configureGpuDevice wired before recovery residency',
1236
+ hint: 'configure the candidate store before preparing recovery resources',
1237
+ }),
1238
+ );
1239
+ }
1240
+
1241
+ let mipmapWork: MipmapEncoderWork | undefined;
1242
+ if (!projected.value.compressed && pod.mips.kind === 'generate' && prepared.value.levels > 1) {
1243
+ const preparedMipmaps = prepareMipmaps(device, gpuTexture, {
1244
+ format: projected.value.format,
1245
+ width: pod.shape.extent.width,
1246
+ height: pod.shape.extent.height,
1247
+ levels: prepared.value.levels,
1248
+ });
1249
+ if (!preparedMipmaps.ok) {
1250
+ new GpuTexture(device, gpuTexture).destroy();
1251
+ return preparedMipmaps;
1252
+ }
1253
+ mipmapWork = preparedMipmaps.value;
1254
+ }
1255
+ const viewRes = device.createTextureView(gpuTexture, {
1256
+ label: `texture-view-${cacheKey}`,
1257
+ dimension: projected.value.shape.viewDimension,
1258
+ });
1259
+ if (!viewRes.ok) {
1260
+ new GpuTexture(device, gpuTexture).destroy();
1261
+ return viewRes;
1262
+ }
1263
+ const previous = this.textureGpuHandles.get(cacheKey);
1264
+ const entry: TextureGpuEntry = {
1265
+ texture: this.wrapTex(gpuTexture),
1266
+ view: viewRes.value,
1267
+ receipt: this.textureReceipt(pod, projected.value),
1268
+ };
1269
+ this.textureGpuHandles.set(cacheKey, entry);
1270
+ this.materialResourceEpoch += 1;
1271
+ if (
1272
+ previous !== undefined &&
1273
+ previous.texture !== entry.texture &&
1274
+ !previous.texture.isDestroyed
1275
+ ) {
1276
+ previous.texture.destroy();
1277
+ }
1278
+ return ok(mipmapWork === undefined ? {} : { mipmapWork });
1279
+ }
1280
+
1127
1281
  /**
1128
1282
  * Eager public surface: upload decoded image bytes into a GPU texture and
1129
1283
  * cache (texture + view) under the handle. The TextureAsset POD supplies the
@@ -1618,6 +1772,7 @@ export class GpuResidencyCache {
1618
1772
  // universally renderable rgba8 path; the same format is threaded through
1619
1773
  // the cubemap and all three IBL side textures by IblPipelineCache.
1620
1774
  const outputFormat = this.caps?.rgba16floatRenderable === false ? 'rgba8unorm' : tex.format;
1775
+ const sourceMipLevelCount = 1;
1621
1776
 
1622
1777
  // Lazy projection runs from the record arm, where the RHI device is always
1623
1778
  // wired (research R-5). The IBL path is deliberately independent from the
@@ -1643,7 +1798,7 @@ export class GpuResidencyCache {
1643
1798
  const gpuTextureResult = device.createTexture({
1644
1799
  label: `cubemap-${sourceId}`,
1645
1800
  size: { width: cubeFaceSize, height: cubeFaceSize, depthOrArrayLayers: 6 },
1646
- mipLevelCount: 1,
1801
+ mipLevelCount: sourceMipLevelCount,
1647
1802
  sampleCount: 1,
1648
1803
  dimension: '2d',
1649
1804
  format: outputFormat,
@@ -1682,13 +1837,6 @@ export class GpuResidencyCache {
1682
1837
  // surfacing exactly one destroy call per physical resource.
1683
1838
  const gpuTextureWrapper = this.wrapTex(gpuTexture);
1684
1839
 
1685
- this.cubemapGpuHandles.set(sourceId, {
1686
- status: 'ready',
1687
- texture: gpuTextureWrapper,
1688
- view: cubeView,
1689
- faceViews,
1690
- });
1691
-
1692
1840
  // The minted cubemap handle is a synthetic shared ref (identity token for
1693
1841
  // the GPU cube residency); its POD is an EquirectAsset placeholder matching
1694
1842
  // the projected dimensions/format (D-3: the retired cube-texture asset kind
@@ -1701,20 +1849,6 @@ export class GpuResidencyCache {
1701
1849
  data: tex.data,
1702
1850
  colorSpace: 'linear',
1703
1851
  };
1704
- const regResult = registerCube(world, cubeAsset);
1705
- if (!regResult.ok) return recordFailed(regResult.error);
1706
- const cubeHandle = regResult.value;
1707
- const cubeId = handleSlot(cubeHandle);
1708
-
1709
- this.cubemapGpuHandles.set(cubeId, {
1710
- status: 'ready',
1711
- texture: gpuTextureWrapper,
1712
- view: cubeView,
1713
- faceViews,
1714
- });
1715
-
1716
- this.cubemapIdempotentMap.set(sourceId, cubeHandle);
1717
-
1718
1852
  const pipelinesResult = await createIblPipelines(
1719
1853
  scope,
1720
1854
  device,
@@ -1748,6 +1882,9 @@ export class GpuResidencyCache {
1748
1882
  });
1749
1883
  if (!equirectTextureResult.ok) return recordFailed(equirectTextureResult.error);
1750
1884
  const equirectGpuTex = equirectTextureResult.value;
1885
+ scope._adopt('texture', equirectGpuTex, (texture) => {
1886
+ device.destroyTexture(texture);
1887
+ });
1751
1888
 
1752
1889
  const equirectViewResult = device.createTextureView(equirectGpuTex, {
1753
1890
  label: `equirect-view-${sourceId}`,
@@ -1775,9 +1912,15 @@ export class GpuResidencyCache {
1775
1912
  const faceBufferResult = createFaceUniformsBuffer(device);
1776
1913
  if (!faceBufferResult.ok) return recordFailed(faceBufferResult.error);
1777
1914
  const faceBuf = faceBufferResult.value;
1915
+ scope._adopt('buffer', faceBuf, (buffer) => {
1916
+ device.destroyBuffer(buffer);
1917
+ });
1778
1918
  const prefilterBufferResult = createPrefilterUniformsBuffer(device);
1779
1919
  if (!prefilterBufferResult.ok) return recordFailed(prefilterBufferResult.error);
1780
1920
  const prefBuf = prefilterBufferResult.value;
1921
+ scope._adopt('buffer', prefBuf, (buffer) => {
1922
+ device.destroyBuffer(buffer);
1923
+ });
1781
1924
 
1782
1925
  const cubeVertexResult = device.createBuffer({
1783
1926
  label: 'ibl-cube-verts',
@@ -1787,15 +1930,18 @@ export class GpuResidencyCache {
1787
1930
  });
1788
1931
  if (!cubeVertexResult.ok) return recordFailed(cubeVertexResult.error);
1789
1932
  const cubeVertex = cubeVertexResult.value;
1933
+ scope._adopt('buffer', cubeVertex, (buffer) => {
1934
+ device.destroyBuffer(buffer);
1935
+ });
1790
1936
 
1791
1937
  const cubeVertexWriteResult = device.queue.writeBuffer(cubeVertex, 0, CUBEMAP_FACE_VERTICES);
1792
1938
  if (!cubeVertexWriteResult.ok) return recordFailed(cubeVertexWriteResult.error);
1793
1939
  const faceUniformsResult = writeAllFaceUniforms(device, faceBuf);
1794
1940
  if (!faceUniformsResult.ok) return recordFailed(faceUniformsResult.error);
1795
- const prefilterUniformsResult = writeAllPrefilterUniforms(device, prefBuf);
1941
+ const prefilterUniformsResult = writeAllPrefilterUniforms(device, prefBuf, sourceMipLevelCount);
1796
1942
  if (!prefilterUniformsResult.ok) return recordFailed(prefilterUniformsResult.error);
1797
1943
 
1798
- const runResult = runIblPrecompute({
1944
+ const runResult = await runIblPrecompute({
1799
1945
  scope,
1800
1946
  device,
1801
1947
  equirectGpuTex,
@@ -1817,6 +1963,28 @@ export class GpuResidencyCache {
1817
1963
  );
1818
1964
  }
1819
1965
 
1966
+ // A source becomes visible to the record arm only after all four passes
1967
+ // completed their queue fence. Registration and the idempotent map are
1968
+ // publication, not allocation, so a failed precompute cannot expose a
1969
+ // ready cube or a loadable handle.
1970
+ this.cubemapGpuHandles.set(sourceId, {
1971
+ status: 'ready',
1972
+ texture: gpuTextureWrapper,
1973
+ view: cubeView,
1974
+ faceViews,
1975
+ });
1976
+ const regResult = registerCube(world, cubeAsset);
1977
+ if (!regResult.ok) return recordFailed(regResult.error);
1978
+ const cubeHandle = regResult.value;
1979
+ const cubeId = handleSlot(cubeHandle);
1980
+ this.cubemapGpuHandles.set(cubeId, {
1981
+ status: 'ready',
1982
+ texture: gpuTextureWrapper,
1983
+ view: cubeView,
1984
+ faceViews,
1985
+ });
1986
+ this.cubemapIdempotentMap.set(sourceId, cubeHandle);
1987
+
1820
1988
  return ok(cubeHandle);
1821
1989
  }
1822
1990
 
@@ -12,6 +12,12 @@ import { environmentFactSignatures } from './signature';
12
12
 
13
13
  export type { EnvironmentCandidate, EnvironmentFrame, EnvironmentSource, FogCandidate, FogFrame };
14
14
 
15
+ /** Producer-local fallback while replacement environment storage is uninitialized. */
16
+ export const SKYLIGHT_RECOVERY_FALLBACK = Object.freeze({
17
+ kind: 'skylight',
18
+ active: false,
19
+ } as const);
20
+
15
21
  export interface SunCandidate {
16
22
  readonly entityKey: number;
17
23
  readonly direction: readonly [number, number, number];
@@ -38,6 +38,14 @@ export interface EnvironmentInspection {
38
38
  readonly residentBytes: number;
39
39
  readonly retiringBytes: number;
40
40
  readonly lastCandidateFailure: EnvironmentInspectionFailure | undefined;
41
+ readonly recovery:
42
+ | {
43
+ readonly token: number;
44
+ readonly deviceGeneration: number;
45
+ readonly status: 'uninitialized';
46
+ readonly fallback: 'skylight';
47
+ }
48
+ | undefined;
41
49
  }
42
50
 
43
51
  export function inspectEnvironment(
@@ -50,6 +58,7 @@ export function inspectEnvironment(
50
58
  readonly lkg?: EnvironmentInspectionGeneration;
51
59
  readonly candidate?: EnvironmentInspectionGeneration;
52
60
  readonly lastCandidateFailure?: EnvironmentInspectionFailure;
61
+ readonly recovery?: EnvironmentInspection['recovery'];
53
62
  readonly retiringBytes?: number;
54
63
  } = {},
55
64
  ): EnvironmentInspection {
@@ -83,5 +92,6 @@ export function inspectEnvironment(
83
92
  residentBytes: active?.residentBytes ?? 0,
84
93
  retiringBytes: details.retiringBytes ?? active?.retiringBytes ?? 0,
85
94
  lastCandidateFailure: details.lastCandidateFailure,
95
+ recovery: details.recovery,
86
96
  });
87
97
  }