@forgeax/engine-render 0.1.23 → 0.1.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (322) hide show
  1. package/README.md +231 -10
  2. package/dist/assembly/bundler-contract.d.ts +9 -0
  3. package/dist/assembly/bundler-contract.d.ts.map +1 -0
  4. package/dist/assembly/device-feature-admission.d.ts +8 -0
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -1
  6. package/dist/assembly/extended-lighting-fallback.d.ts +13 -0
  7. package/dist/assembly/extended-lighting-fallback.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +7 -243
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/material/assembly.d.ts +3 -2
  11. package/dist/assembly/material/assembly.d.ts.map +1 -1
  12. package/dist/assembly/material/standard-layer-projection.d.ts +10 -0
  13. package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -0
  14. package/dist/assembly/material/surface-projection.d.ts +24 -0
  15. package/dist/assembly/material/surface-projection.d.ts.map +1 -0
  16. package/dist/assembly/material-shader-policy.d.ts +188 -0
  17. package/dist/assembly/material-shader-policy.d.ts.map +1 -0
  18. package/dist/assembly/material-shader-prewarm.d.ts +21 -0
  19. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
  20. package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
  21. package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
  22. package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
  23. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
  24. package/dist/assembly/recovery/generation.d.ts +46 -0
  25. package/dist/assembly/recovery/generation.d.ts.map +1 -0
  26. package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
  27. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
  28. package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
  29. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
  30. package/dist/assembly/renderer-facade.d.ts +10 -0
  31. package/dist/assembly/renderer-facade.d.ts.map +1 -0
  32. package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
  33. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  34. package/dist/assembly/renderer-helpers.d.ts +19 -0
  35. package/dist/assembly/renderer-helpers.d.ts.map +1 -0
  36. package/dist/assembly/renderer-lifecycle.d.ts +34 -0
  37. package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
  38. package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
  39. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
  40. package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
  41. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
  42. package/dist/assembly/webgpu-ready.d.ts +55 -0
  43. package/dist/assembly/webgpu-ready.d.ts.map +1 -0
  44. package/dist/assembly/webgpu-renderer.d.ts +152 -0
  45. package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
  46. package/dist/assembly/webgpu-vertex-layouts.d.ts +89 -0
  47. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -0
  48. package/dist/assets/asset-decoders.d.ts.map +1 -1
  49. package/dist/authoring.mjs +1 -1
  50. package/dist/{chunk-FF7K3FSO.mjs → chunk-ADHHVYLW.mjs} +89 -89
  51. package/dist/chunk-ADHHVYLW.mjs.map +1 -0
  52. package/dist/{chunk-YALPS44A.mjs → chunk-E34VL5VI.mjs} +47 -47
  53. package/dist/chunk-E34VL5VI.mjs.map +1 -0
  54. package/dist/{chunk-4L3P6DT5.mjs → chunk-GE3SDD72.mjs} +37 -5
  55. package/dist/chunk-GE3SDD72.mjs.map +1 -0
  56. package/dist/{chunk-HZUPSYW7.mjs → chunk-HKXTW355.mjs} +105 -15
  57. package/dist/chunk-HKXTW355.mjs.map +1 -0
  58. package/dist/chunk-JSEDTZYS.mjs +413 -0
  59. package/dist/chunk-JSEDTZYS.mjs.map +1 -0
  60. package/dist/{chunk-FBC4R44J.mjs → chunk-KM2NOX2I.mjs} +57 -57
  61. package/dist/chunk-KM2NOX2I.mjs.map +1 -0
  62. package/dist/{chunk-DIYW3M5D.mjs → chunk-N3RT2EUR.mjs} +2288 -2238
  63. package/dist/chunk-N3RT2EUR.mjs.map +1 -0
  64. package/dist/{chunk-ZUWKAZ2I.mjs → chunk-TCLDX7SZ.mjs} +1167 -368
  65. package/dist/chunk-TCLDX7SZ.mjs.map +1 -0
  66. package/dist/{chunk-QODA5UKU.mjs → chunk-ZZ4YQ474.mjs} +2 -2
  67. package/dist/chunk-ZZ4YQ474.mjs.map +1 -0
  68. package/dist/components/instances.d.ts +13 -31
  69. package/dist/components/instances.d.ts.map +1 -1
  70. package/dist/construct-renderer.mjs +20688 -16314
  71. package/dist/construct-renderer.mjs.map +1 -1
  72. package/dist/device/device-scope.d.ts +2 -0
  73. package/dist/device/device-scope.d.ts.map +1 -1
  74. package/dist/device/gpu-residency.d.ts +24 -4
  75. package/dist/device/gpu-residency.d.ts.map +1 -1
  76. package/dist/environment/frame.d.ts +5 -0
  77. package/dist/environment/frame.d.ts.map +1 -1
  78. package/dist/environment/inspection.d.ts +7 -0
  79. package/dist/environment/inspection.d.ts.map +1 -1
  80. package/dist/environment/lifecycle.d.ts +20 -2
  81. package/dist/environment/lifecycle.d.ts.map +1 -1
  82. package/dist/errors/recover.d.ts +58 -2
  83. package/dist/errors/recover.d.ts.map +1 -1
  84. package/dist/errors/render.d.ts +13 -0
  85. package/dist/errors/render.d.ts.map +1 -1
  86. package/dist/extract/contracts.d.ts +899 -0
  87. package/dist/extract/contracts.d.ts.map +1 -0
  88. package/dist/features/host.d.ts +2 -0
  89. package/dist/features/host.d.ts.map +1 -1
  90. package/dist/features/prepared-gpu-work.d.ts +3 -0
  91. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  92. package/dist/features/prepared-graphics-store.d.ts +2 -0
  93. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  94. package/dist/gpu-driven/production-raster.d.ts +34 -1
  95. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  96. package/dist/gpu-scene.d.ts +5 -0
  97. package/dist/gpu-scene.d.ts.map +1 -1
  98. package/dist/ibl/IblPipelineCache.d.ts +7 -6
  99. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  100. package/dist/ibl/skylight-bind-group.d.ts +7 -1
  101. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  102. package/dist/index.d.ts +2 -1
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.mjs +27 -9
  105. package/dist/index.mjs.map +1 -1
  106. package/dist/instance-buffer-cache.d.ts +7 -1
  107. package/dist/instance-buffer-cache.d.ts.map +1 -1
  108. package/dist/instances.d.ts +101 -0
  109. package/dist/instances.d.ts.map +1 -0
  110. package/dist/internal.mjs +6 -6
  111. package/dist/light-buffer-layout.d.ts +3 -0
  112. package/dist/light-buffer-layout.d.ts.map +1 -1
  113. package/dist/materials.d.ts +96 -11
  114. package/dist/materials.d.ts.map +1 -1
  115. package/dist/mesh-material-bindings.d.ts +56 -0
  116. package/dist/mesh-material-bindings.d.ts.map +1 -1
  117. package/dist/occlusion-proxy.d.ts +4 -0
  118. package/dist/occlusion-proxy.d.ts.map +1 -0
  119. package/dist/pbr-pipeline.d.ts +32 -9
  120. package/dist/pbr-pipeline.d.ts.map +1 -1
  121. package/dist/pipeline-builder.d.ts.map +1 -1
  122. package/dist/pipeline-spec.d.ts.map +1 -1
  123. package/dist/points-lines/prepare.d.ts +3 -0
  124. package/dist/points-lines/prepare.d.ts.map +1 -1
  125. package/dist/points-lines/standard-owner.d.ts +33 -0
  126. package/dist/points-lines/standard-owner.d.ts.map +1 -0
  127. package/dist/record/frame-snapshot.d.ts +18 -2
  128. package/dist/record/frame-snapshot.d.ts.map +1 -1
  129. package/dist/record/frame-targets.d.ts.map +1 -1
  130. package/dist/record/frame.d.ts +14 -2
  131. package/dist/record/frame.d.ts.map +1 -1
  132. package/dist/record/main-pass-geometry.d.ts +36 -4
  133. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  134. package/dist/record/main-pass-material.d.ts +7 -17
  135. package/dist/record/main-pass-material.d.ts.map +1 -1
  136. package/dist/record/main-pass.d.ts.map +1 -1
  137. package/dist/record/mesh-ssbo.d.ts +11 -3
  138. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  139. package/dist/record/recovery-pipeline.d.ts +30 -0
  140. package/dist/record/recovery-pipeline.d.ts.map +1 -0
  141. package/dist/record/render-context.d.ts +28 -5
  142. package/dist/record/render-context.d.ts.map +1 -1
  143. package/dist/record/shadow-pass.d.ts +11 -1
  144. package/dist/record/shadow-pass.d.ts.map +1 -1
  145. package/dist/record/typed-frame-graph.d.ts +1 -1
  146. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  147. package/dist/recovery/render-system-candidate.d.ts +65 -0
  148. package/dist/recovery/render-system-candidate.d.ts.map +1 -0
  149. package/dist/render-contract.d.ts +68 -7
  150. package/dist/render-contract.d.ts.map +1 -1
  151. package/dist/render-system-extract-tail.d.ts +16 -0
  152. package/dist/render-system-extract-tail.d.ts.map +1 -0
  153. package/dist/render-system-extract.d.ts +180 -22
  154. package/dist/render-system-extract.d.ts.map +1 -1
  155. package/dist/render-system.d.ts +131 -4
  156. package/dist/render-system.d.ts.map +1 -1
  157. package/dist/scene/render-scene-types.d.ts +2 -0
  158. package/dist/scene/render-scene-types.d.ts.map +1 -1
  159. package/dist/scene/render-scene.d.ts +31 -1
  160. package/dist/scene/render-scene.d.ts.map +1 -1
  161. package/dist/systems/skin-palette-allocator.d.ts +6 -0
  162. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  163. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  164. package/dist/temporal/index.mjs +6 -6
  165. package/dist/temporal/taa-history-store.d.ts +15 -1
  166. package/dist/temporal/taa-history-store.d.ts.map +1 -1
  167. package/dist/temporal/transaction.d.ts +5 -0
  168. package/dist/temporal/transaction.d.ts.map +1 -1
  169. package/package.json +20 -20
  170. package/src/__tests__/device-feature-admission.unit.test.ts +6 -0
  171. package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
  172. package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
  173. package/src/__tests__/environment-generation.integration.test.ts +23 -0
  174. package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
  175. package/src/__tests__/factory-contract.integration.test.ts +122 -7
  176. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
  177. package/src/__tests__/gpu-scene.unit.test.ts +38 -2
  178. package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
  179. package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
  180. package/src/__tests__/instances-store.unit.test.ts +46 -0
  181. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  182. package/src/__tests__/material-contract-inventory.unit.test.ts +29 -22
  183. package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
  184. package/src/__tests__/material-pass-policy.unit.test.ts +69 -0
  185. package/src/__tests__/material-snapshot-mutation.integration.test.ts +69 -0
  186. package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
  187. package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
  188. package/src/__tests__/material-surface-authoring.test-d.ts +62 -0
  189. package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
  190. package/src/__tests__/materials-transmission-validation.unit.test.ts +24 -0
  191. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -5
  192. package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
  193. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +1 -1
  194. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
  195. package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
  196. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
  197. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +127 -18
  198. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
  199. package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
  200. package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
  201. package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
  202. package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
  203. package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
  204. package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
  205. package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
  206. package/src/__tests__/shader-manifest-fixture.ts +13 -16
  207. package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
  208. package/src/__tests__/skin-motion-regression.unit.test.ts +33 -0
  209. package/src/__tests__/skinned-shadow-caster.test.ts +30 -2
  210. package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
  211. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +60 -0
  212. package/src/__tests__/surface-standard-pipeline.browser.test.ts +20 -0
  213. package/src/__tests__/surface-standard-pipeline.dawn.test.ts +20 -0
  214. package/src/__tests__/surface-standard-pipeline.fixture.ts +132 -0
  215. package/src/__tests__/surface-variant-provenance.integration.test.ts +33 -0
  216. package/src/__tests__/transmission-prewarm.unit.test.ts +95 -85
  217. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +1 -1
  218. package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
  219. package/src/assembly/bundler-contract.ts +9 -0
  220. package/src/assembly/device-feature-admission.ts +15 -5
  221. package/src/assembly/extended-lighting-fallback.ts +216 -0
  222. package/src/assembly/factory.ts +83 -10088
  223. package/src/assembly/material/__tests__/standard-layer-plan-projection.integration.test.ts +18 -0
  224. package/src/assembly/material/__tests__/surface-pass-projection.integration.test.ts +62 -0
  225. package/src/assembly/material/assembly.ts +18 -2
  226. package/src/assembly/material/standard-layer-projection.ts +16 -0
  227. package/src/assembly/material/surface-projection.ts +90 -0
  228. package/src/assembly/material-shader-policy.ts +932 -0
  229. package/src/assembly/material-shader-prewarm.ts +88 -0
  230. package/src/assembly/mesh-ssbo-grow.ts +270 -0
  231. package/src/assembly/recovery/device-loss-fanout.ts +89 -0
  232. package/src/assembly/recovery/generation.ts +141 -0
  233. package/src/assembly/recovery/recovery-attempt.ts +800 -0
  234. package/src/assembly/recovery/renderer-recover.ts +906 -0
  235. package/src/assembly/renderer-facade.ts +342 -0
  236. package/src/assembly/renderer-frame-transaction.ts +45 -0
  237. package/src/assembly/renderer-helpers.ts +99 -0
  238. package/src/assembly/renderer-lifecycle.ts +159 -0
  239. package/src/assembly/shader-prewarm-policy.ts +157 -0
  240. package/src/assembly/webgpu-ready-contract.ts +14 -0
  241. package/src/assembly/webgpu-ready.ts +4066 -0
  242. package/src/assembly/webgpu-renderer.ts +4027 -0
  243. package/src/assembly/webgpu-vertex-layouts.ts +132 -0
  244. package/src/assets/asset-decoders.ts +27 -4
  245. package/src/components/camera.ts +1 -1
  246. package/src/components/instances.ts +14 -197
  247. package/src/debug-draw-glue.ts +1 -1
  248. package/src/device/device-scope.ts +35 -6
  249. package/src/device/gpu-residency.ts +194 -26
  250. package/src/environment/frame.ts +6 -0
  251. package/src/environment/inspection.ts +10 -0
  252. package/src/environment/lifecycle.ts +114 -8
  253. package/src/errors/recover.ts +76 -5
  254. package/src/errors/render.ts +17 -0
  255. package/src/extract/contracts.ts +974 -0
  256. package/src/features/host.ts +16 -0
  257. package/src/features/prepared-gpu-work.ts +20 -2
  258. package/src/features/prepared-graphics-store.ts +17 -0
  259. package/src/gpu-driven/production-raster.ts +131 -3
  260. package/src/gpu-scene.ts +87 -31
  261. package/src/ibl/IblPipelineCache.ts +134 -50
  262. package/src/ibl/skylight-bind-group.ts +26 -0
  263. package/src/index.ts +10 -1
  264. package/src/instance-buffer-cache.ts +14 -1
  265. package/src/instances.ts +288 -0
  266. package/src/light-buffer-layout.ts +3 -0
  267. package/src/materials.ts +365 -53
  268. package/src/mesh-material-bindings.ts +59 -0
  269. package/src/occlusion-proxy.ts +64 -0
  270. package/src/pbr-pipeline.ts +139 -15
  271. package/src/pipeline-builder.ts +14 -10
  272. package/src/pipeline-spec.ts +6 -14
  273. package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
  274. package/src/points-lines/prepare.ts +9 -0
  275. package/src/points-lines/standard-owner.ts +403 -0
  276. package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
  277. package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
  278. package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
  279. package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
  280. package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
  281. package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
  282. package/src/record/__tests__/temporal-payload.unit.test.ts +22 -43
  283. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -1
  284. package/src/record/frame-snapshot.ts +34 -2
  285. package/src/record/frame-targets.ts +11 -1
  286. package/src/record/frame.ts +37 -4
  287. package/src/record/helpers.ts +1 -1
  288. package/src/record/main-pass-geometry.ts +727 -63
  289. package/src/record/main-pass-material.ts +316 -209
  290. package/src/record/main-pass-sprite-draws.ts +7 -8
  291. package/src/record/main-pass.ts +94 -6
  292. package/src/record/mesh-ssbo.ts +32 -10
  293. package/src/record/recovery-pipeline.ts +695 -0
  294. package/src/record/render-context.ts +39 -4
  295. package/src/record/shadow-pass.ts +46 -99
  296. package/src/record/typed-frame-graph.ts +45 -1
  297. package/src/record/view-ubo.ts +3 -3
  298. package/src/recovery/render-system-candidate.ts +876 -0
  299. package/src/render-contract.ts +69 -7
  300. package/src/render-system-extract-tail.ts +3039 -0
  301. package/src/render-system-extract.ts +177 -3003
  302. package/src/render-system.ts +906 -662
  303. package/src/scene/render-scene-types.ts +2 -0
  304. package/src/scene/render-scene.ts +703 -37
  305. package/src/systems/skin-palette-allocator.ts +129 -7
  306. package/src/temporal/frame-coordinator.ts +4 -1
  307. package/src/temporal/taa-history-store.ts +46 -2
  308. package/src/temporal/temporal-view.ts +1 -1
  309. package/src/temporal/transaction.ts +9 -0
  310. package/src/tilemap-chunk-extract-system.ts +1 -1
  311. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
  312. package/src/transmission/__tests__/surface-compatibility.integration.test.ts +32 -0
  313. package/dist/chunk-4L3P6DT5.mjs.map +0 -1
  314. package/dist/chunk-4NPUASZR.mjs +0 -203
  315. package/dist/chunk-4NPUASZR.mjs.map +0 -1
  316. package/dist/chunk-DIYW3M5D.mjs.map +0 -1
  317. package/dist/chunk-FBC4R44J.mjs.map +0 -1
  318. package/dist/chunk-FF7K3FSO.mjs.map +0 -1
  319. package/dist/chunk-HZUPSYW7.mjs.map +0 -1
  320. package/dist/chunk-QODA5UKU.mjs.map +0 -1
  321. package/dist/chunk-YALPS44A.mjs.map +0 -1
  322. package/dist/chunk-ZUWKAZ2I.mjs.map +0 -1
@@ -0,0 +1,403 @@
1
+ import { resolveAssetHandle } from '@forgeax/engine-assets-runtime';
2
+ import { deriveVertexLayoutProjection } from '@forgeax/engine-geometry';
3
+ import { err, ok } from '@forgeax/engine-rhi';
4
+ import type { MaterialAsset, MeshAsset } from '@forgeax/engine-types';
5
+ import { toShared } from '@forgeax/engine-types';
6
+ import { PointsLinesMaterialUnsupportedError } from '../errors/render';
7
+ import {
8
+ GPU_BUFFER_USAGE_COPY_DST,
9
+ GPU_BUFFER_USAGE_INDEX,
10
+ GPU_BUFFER_USAGE_VERTEX,
11
+ } from '../gpu-usage';
12
+ import type { ValidatedRenderable } from '../record/frame-snapshot';
13
+ import type {
14
+ PointsLinesRecordOwner,
15
+ PointsLinesRecordSubmission,
16
+ RenderSystemInternals,
17
+ } from '../record/render-context';
18
+ import type { RecoveryPointsLinesCandidate } from '../render-system';
19
+ import { admitPointsLines } from './admission';
20
+ import { PointsLinesExpansionCache } from './expansion-cache';
21
+ import type { PointsLinesInspection, PointsLinesSourceError } from './inspection';
22
+ import { inspectPointsLines } from './inspection';
23
+ import {
24
+ createPointsLinesLanePreparationAdapter,
25
+ type PointsLinesLanePreparationAdapter,
26
+ } from './prepare';
27
+ import {
28
+ createPointsLinesLaneAdapter,
29
+ type PointsLinesBackend,
30
+ type PointsLinesLane,
31
+ } from './record';
32
+ import type { PointsLinesRetainedSnapshot } from './snapshot';
33
+
34
+ type PointsLinesGpuResources = {
35
+ readonly vertexBuffer: import('@forgeax/engine-rhi').Buffer;
36
+ readonly indexBuffer: import('@forgeax/engine-rhi').Buffer;
37
+ };
38
+ type PointsLinesPreparationAdapter = PointsLinesLanePreparationAdapter<PointsLinesGpuResources>;
39
+
40
+ /**
41
+ * The Standard renderer's single Points/Lines prepare owner.
42
+ *
43
+ * The retained snapshot is the identity boundary; this owner only resolves
44
+ * the source assets, runs admission, and publishes the existing preparation
45
+ * and record contracts to the main geometry loop. The expansion cache is
46
+ * shared across lanes and this owner keeps the one prepared vertex/index
47
+ * resource pair for each retained identity.
48
+ */
49
+ export class StandardPointsLinesOwner implements PointsLinesRecordOwner {
50
+ private readonly cache = new PointsLinesExpansionCache();
51
+ private readonly preparations = new Map<string, PointsLinesPreparationAdapter>();
52
+ private readonly active = new Map<string, PointsLinesInspection>();
53
+ private readonly layoutProjection = deriveVertexLayoutProjection({
54
+ position: new Float32Array(0),
55
+ normal: new Float32Array(0),
56
+ uv: new Float32Array(0),
57
+ tangent: new Float32Array(0),
58
+ });
59
+
60
+ constructor(private readonly internals: RenderSystemInternals) {}
61
+
62
+ beginFrame(): void {
63
+ this.active.clear();
64
+ }
65
+
66
+ prepare(entry: ValidatedRenderable, clustered: boolean): PointsLinesRecordSubmission | undefined {
67
+ return this.prepareWithMaps(entry, clustered, this.internals, this.preparations, this.active);
68
+ }
69
+
70
+ prepareRecoveryCandidate(
71
+ entries: readonly ValidatedRenderable[],
72
+ clustered: boolean,
73
+ runtime: RenderSystemInternals,
74
+ ): RecoveryPointsLinesCandidate | undefined {
75
+ const stagedPreparations = new Map<string, PointsLinesPreparationAdapter>();
76
+ const stagedInspections = new Map<string, PointsLinesInspection>();
77
+ let pointEntryCount = 0;
78
+ try {
79
+ for (const entry of entries) {
80
+ const snapshot = entry.source.pointsLines;
81
+ if (snapshot?.component === undefined) continue;
82
+ pointEntryCount += 1;
83
+ const prepared = this.prepareWithMaps(
84
+ entry,
85
+ clustered,
86
+ runtime,
87
+ stagedPreparations,
88
+ stagedInspections,
89
+ );
90
+ if (prepared === undefined) {
91
+ throw new Error(
92
+ `recovery Points/Lines preparation failed for ${snapshot.worldId}:${snapshot.entityKey}`,
93
+ );
94
+ }
95
+ }
96
+ } catch (cause) {
97
+ for (const preparation of stagedPreparations.values()) {
98
+ preparation.resetForDeviceLoss();
99
+ }
100
+ throw cause;
101
+ }
102
+ if (pointEntryCount === 0) return undefined;
103
+
104
+ let released = false;
105
+ let published = false;
106
+ const candidate: RecoveryPointsLinesCandidate = {
107
+ createRecoveryRoot: (scope) => ({
108
+ kind: 'buffer',
109
+ create: () => {
110
+ if (!scope.isAlive() || released) {
111
+ throw new Error('Points/Lines recovery candidate scope is not active.');
112
+ }
113
+ return candidate;
114
+ },
115
+ cleanup: () => candidate.release(),
116
+ }),
117
+ publish: () => {
118
+ if (released || published) return;
119
+ this.preparations.clear();
120
+ for (const [key, preparation] of stagedPreparations) {
121
+ this.preparations.set(key, preparation);
122
+ }
123
+ published = true;
124
+ },
125
+ release: () => {
126
+ if (released || published) return;
127
+ released = true;
128
+ for (const preparation of stagedPreparations.values()) {
129
+ preparation.resetForDeviceLoss();
130
+ }
131
+ stagedPreparations.clear();
132
+ stagedInspections.clear();
133
+ },
134
+ };
135
+ return candidate;
136
+ }
137
+
138
+ private prepareWithMaps(
139
+ entry: ValidatedRenderable,
140
+ clustered: boolean,
141
+ runtime: RenderSystemInternals,
142
+ preparations: Map<string, PointsLinesPreparationAdapter>,
143
+ inspections: Map<string, PointsLinesInspection>,
144
+ ): PointsLinesRecordSubmission | undefined {
145
+ const snapshot = entry.source.pointsLines;
146
+ if (snapshot === undefined || snapshot.component === undefined || entry.world === undefined) {
147
+ return undefined;
148
+ }
149
+ const key = `${snapshot.worldId}:${snapshot.entityKey}`;
150
+ const topology = snapshot.component === 'Points' ? 'point-list' : 'line-list';
151
+ const backend = this.backend(runtime);
152
+ const lane = backend === 'wgpu-webgl2' ? 'cpu-webgl2' : clustered ? 'clustered' : 'direct';
153
+ const meshResult = resolveAssetHandle<MeshAsset>(
154
+ entry.world,
155
+ toShared<'MeshAsset'>(entry.source.assetHandle),
156
+ );
157
+ const materialResult = resolveAssetHandle<MaterialAsset>(
158
+ entry.world,
159
+ toShared<'MaterialAsset'>(snapshot.materialHandle),
160
+ );
161
+ if (!meshResult.ok || !materialResult.ok) {
162
+ this.publishRefusal(
163
+ inspections,
164
+ snapshot,
165
+ new PointsLinesMaterialUnsupportedError({
166
+ entity: snapshot.entityKey,
167
+ material: 'unresolved',
168
+ pass: 'forward',
169
+ module: 'asset-resolution',
170
+ reason: 'source MeshAsset or MaterialAsset could not be resolved',
171
+ }),
172
+ );
173
+ return undefined;
174
+ }
175
+ const admission = admitPointsLines({
176
+ entity: snapshot.entityKey,
177
+ mesh: meshResult.value,
178
+ material: materialResult.value,
179
+ ...(snapshot.style?.kind === 'points'
180
+ ? {
181
+ points: {
182
+ sizePx: snapshot.style.sizePx,
183
+ shape: snapshot.style.shape === 'circle' ? 1 : 0,
184
+ },
185
+ }
186
+ : { lines: { widthPx: snapshot.style?.widthPx ?? 1 } }),
187
+ });
188
+ if (!admission.ok) {
189
+ this.publishRefusal(inspections, snapshot, admission.error);
190
+ return undefined;
191
+ }
192
+ const geometryKey = this.cache.getOrCreate(snapshot, meshResult.value).key;
193
+ const prepKey = `${geometryKey}:${lane}:${backend}`;
194
+ let preparation = preparations.get(prepKey);
195
+ if (preparation === undefined) {
196
+ // A new expansion is visible cold upload work. Candidate preparation
197
+ // uses an unarmed runtime and stages the result; the published runtime
198
+ // is armed, so a missing staged resource fails before any RHI create.
199
+ runtime.recoveryColdWorkGuard?.noteUploadColdWork();
200
+ preparation = this.createPreparation(runtime, geometryKey, lane, backend);
201
+ preparations.set(prepKey, preparation);
202
+ } else if (preparation.lastKnownGood()?.geometry.key !== geometryKey) {
203
+ runtime.recoveryColdWorkGuard?.noteUploadColdWork();
204
+ }
205
+ const prepared = preparation.prepare(snapshot, meshResult.value);
206
+ if (!prepared.ok) {
207
+ const lkg = preparation.lastKnownGood();
208
+ if (lkg !== undefined) {
209
+ const lkgPlan = createPointsLinesLaneAdapter(lane, backend).createRecordPlan(
210
+ lkg.snapshot,
211
+ lkg.geometry,
212
+ );
213
+ const state = preparation.inspect();
214
+ inspections.set(
215
+ key,
216
+ inspectPointsLines({
217
+ snapshot,
218
+ topology,
219
+ lane,
220
+ pointCount: lkg.geometry.pointCount,
221
+ segmentCount: lkg.geometry.segmentCount,
222
+ sourceBytes: lkg.geometry.sourceBytes,
223
+ derivedBytes: lkg.geometry.derivedBytes,
224
+ cache: { hit: true, rebuilds: state.rebuilds, evictions: 0 },
225
+ drawCount: lkgPlan.drawCount,
226
+ uploadBytes: 0,
227
+ lastKnownGood: true,
228
+ refusal: {
229
+ code: prepared.error.code,
230
+ expected: prepared.error.expected,
231
+ hint: prepared.error.hint,
232
+ detail: prepared.error.detail,
233
+ generation: prepared.error.detail.generation,
234
+ lastKnownGood: true,
235
+ },
236
+ }),
237
+ );
238
+ return {
239
+ plan: lkgPlan,
240
+ vertexBuffer: lkg.resource.vertexBuffer,
241
+ indexBuffer: lkg.resource.indexBuffer,
242
+ layoutProjection: this.layoutProjection,
243
+ };
244
+ }
245
+ this.publishRefusal(inspections, snapshot, prepared.error);
246
+ return undefined;
247
+ }
248
+ const plan = createPointsLinesLaneAdapter(lane, backend).createRecordPlan(
249
+ snapshot,
250
+ prepared.value.geometry,
251
+ );
252
+ const state = preparation.inspect();
253
+ inspections.set(
254
+ key,
255
+ inspectPointsLines({
256
+ snapshot,
257
+ topology,
258
+ lane,
259
+ pointCount: prepared.value.geometry.pointCount,
260
+ segmentCount: prepared.value.geometry.segmentCount,
261
+ sourceBytes: prepared.value.geometry.sourceBytes,
262
+ derivedBytes: prepared.value.geometry.derivedBytes,
263
+ cache: {
264
+ hit: prepared.value.uploadedBytes === 0,
265
+ rebuilds: state.rebuilds,
266
+ evictions: 0,
267
+ },
268
+ drawCount: plan.drawCount,
269
+ uploadBytes: prepared.value.uploadedBytes,
270
+ lastKnownGood: prepared.value.lastKnownGood,
271
+ }),
272
+ );
273
+ return {
274
+ plan,
275
+ vertexBuffer: prepared.value.resource.vertexBuffer,
276
+ indexBuffer: prepared.value.resource.indexBuffer,
277
+ layoutProjection: this.layoutProjection,
278
+ };
279
+ }
280
+
281
+ resetForDeviceLoss(): void {
282
+ for (const preparation of this.preparations.values()) preparation.resetForDeviceLoss();
283
+ this.active.clear();
284
+ }
285
+
286
+ abandonForDeviceLoss(): void {
287
+ for (const preparation of this.preparations.values()) {
288
+ preparation.abandonForDeviceLoss();
289
+ }
290
+ this.active.clear();
291
+ }
292
+
293
+ dispose(): void {
294
+ this.resetForDeviceLoss();
295
+ this.preparations.clear();
296
+ }
297
+
298
+ inspections(): readonly PointsLinesInspection[] {
299
+ return [...this.active.values()];
300
+ }
301
+
302
+ private backend(runtime: RenderSystemInternals): PointsLinesBackend {
303
+ switch (runtime.device.caps.backendKind) {
304
+ case 'wgpu-webgl2':
305
+ return 'wgpu-webgl2';
306
+ case 'null':
307
+ return 'null';
308
+ default:
309
+ return 'webgpu';
310
+ }
311
+ }
312
+
313
+ private publishRefusal(
314
+ inspections: Map<string, PointsLinesInspection>,
315
+ snapshot: PointsLinesRetainedSnapshot,
316
+ error: PointsLinesSourceError,
317
+ ): void {
318
+ inspections.set(
319
+ `${snapshot.worldId}:${snapshot.entityKey}`,
320
+ inspectPointsLines({
321
+ snapshot,
322
+ topology: snapshot.component === 'Points' ? 'point-list' : 'line-list',
323
+ lane: 'refused',
324
+ pointCount: 0,
325
+ segmentCount: 0,
326
+ sourceBytes: 0,
327
+ derivedBytes: 0,
328
+ cache: { hit: false, rebuilds: 0, evictions: 0 },
329
+ drawCount: 0,
330
+ uploadBytes: 0,
331
+ lastKnownGood: false,
332
+ refusal: {
333
+ code: error.code,
334
+ expected: error.expected,
335
+ hint: error.hint,
336
+ detail: error.detail,
337
+ generation: snapshot.meshGeneration,
338
+ lastKnownGood: false,
339
+ },
340
+ }),
341
+ );
342
+ }
343
+
344
+ private createPreparation(
345
+ runtime: RenderSystemInternals,
346
+ geometryKey: string,
347
+ lane: PointsLinesLane,
348
+ backend: PointsLinesBackend,
349
+ ): PointsLinesPreparationAdapter {
350
+ return createPointsLinesLanePreparationAdapter(lane, backend, {
351
+ cache: this.cache,
352
+ adapter: {
353
+ create: (geometry) => {
354
+ const vertex = runtime.device.createBuffer({
355
+ label: `points-lines-vertices:${geometryKey}`,
356
+ size: Math.max(4, geometry.vertices.byteLength),
357
+ usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
358
+ mappedAtCreation: false,
359
+ });
360
+ if (!vertex.ok) return err(vertex.error);
361
+ const index = runtime.device.createBuffer({
362
+ label: `points-lines-indices:${geometryKey}`,
363
+ size: Math.max(4, geometry.indices.byteLength),
364
+ usage: GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST,
365
+ mappedAtCreation: false,
366
+ });
367
+ if (!index.ok) {
368
+ runtime.device.destroyBuffer(vertex.value);
369
+ return err(index.error);
370
+ }
371
+ return ok({ vertexBuffer: vertex.value, indexBuffer: index.value });
372
+ },
373
+ upload: (resource, geometry) => {
374
+ if (geometry.vertices.byteLength > 0) {
375
+ const vertexWrite = runtime.device.queue.writeBuffer(
376
+ resource.vertexBuffer,
377
+ 0,
378
+ geometry.vertices,
379
+ );
380
+ if (!vertexWrite.ok) return err(vertexWrite.error);
381
+ }
382
+ if (geometry.indices.byteLength > 0) {
383
+ const indexWrite = runtime.device.queue.writeBuffer(
384
+ resource.indexBuffer,
385
+ 0,
386
+ geometry.indices,
387
+ );
388
+ if (!indexWrite.ok) return err(indexWrite.error);
389
+ }
390
+ return ok(geometry.derivedBytes);
391
+ },
392
+ validate: (_resource, geometry) =>
393
+ geometry.expandedVertexCount > 0 && geometry.expandedIndexCount > 0
394
+ ? ok(undefined)
395
+ : err(new Error('Points/Lines expansion contains no drawable triangles')),
396
+ destroy: (resource) => {
397
+ runtime.device.destroyBuffer(resource.vertexBuffer);
398
+ runtime.device.destroyBuffer(resource.indexBuffer);
399
+ },
400
+ },
401
+ });
402
+ }
403
+ }
@@ -0,0 +1,60 @@
1
+ import type { RhiCanvasContext, RhiDevice, Texture, TextureView } from '@forgeax/engine-rhi';
2
+ import { err, ok, RhiError } from '@forgeax/engine-rhi';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { acquireSwapChainTarget } from '../frame-targets';
5
+ import type { PipelineState, RenderSystemInternals } from '../render-context';
6
+
7
+ function device(): RhiDevice {
8
+ return {
9
+ caps: { backendKind: 'webgpu', storageBuffer: true },
10
+ limits: {},
11
+ createTextureView: vi.fn(() => ok({} as TextureView)),
12
+ } as unknown as RhiDevice;
13
+ }
14
+
15
+ function unavailableSurfaceError(): RhiError {
16
+ return new RhiError({
17
+ code: 'webgpu-runtime-error',
18
+ expected: 'the surface has a current texture',
19
+ hint: 'configure the surface and retry',
20
+ });
21
+ }
22
+
23
+ describe('acquireSwapChainTarget recovery', () => {
24
+ it('resolves the recorder device before reconfiguring a recovered surface', () => {
25
+ const recorderDevice = device();
26
+ const configuredDevice = device();
27
+ const texture = {} as Texture;
28
+ const getCurrentTexture = vi
29
+ .fn()
30
+ .mockReturnValueOnce(err(unavailableSurfaceError()))
31
+ .mockReturnValueOnce(ok(texture));
32
+ const configure = vi.fn(() => ok(undefined));
33
+ const context = { configure, getCurrentTexture } as unknown as RhiCanvasContext;
34
+ const resolveSurfaceDevice = vi.fn(() => ok(configuredDevice));
35
+ const internals = {
36
+ context,
37
+ device: recorderDevice,
38
+ canvas: { width: 800, height: 600 },
39
+ resolveSurfaceDevice,
40
+ errorRegistry: { fire: vi.fn() },
41
+ healthRegistry: { fire: vi.fn() },
42
+ } as unknown as RenderSystemInternals;
43
+ const pipelineState = {
44
+ device: recorderDevice,
45
+ format: 'bgra8unorm',
46
+ colorAttachmentFormat: 'bgra8unorm',
47
+ perPassResources: { configured: false },
48
+ } as unknown as PipelineState;
49
+
50
+ const result = acquireSwapChainTarget(internals, pipelineState);
51
+
52
+ expect(result).toMatchObject({ currentTexture: texture, targetW: 800, targetH: 600 });
53
+ expect(resolveSurfaceDevice).toHaveBeenCalledWith(recorderDevice);
54
+ expect(configure).toHaveBeenCalledWith(
55
+ expect.objectContaining({ device: configuredDevice, format: 'bgra8unorm' }),
56
+ );
57
+ expect(getCurrentTexture).toHaveBeenCalledTimes(2);
58
+ expect(pipelineState.perPassResources.configured).toBe(true);
59
+ });
60
+ });
@@ -1,5 +1,8 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { geometryRenderStateForPass } from '../main-pass-geometry';
2
+ import {
3
+ geometryRenderStateForPass,
4
+ requestsStandardTransmissionVariant,
5
+ } from '../main-pass-geometry';
3
6
 
4
7
  describe('geometry pass render state', () => {
5
8
  it('keeps the main depth authority while admitting equal temporal depth', () => {
@@ -45,3 +48,25 @@ describe('geometry pass render state', () => {
45
48
  expect(accepted).toEqual([visibleDepth]);
46
49
  });
47
50
  });
51
+
52
+ describe('Standard transmission variant selection', () => {
53
+ it('uses authored transmission presence instead of the canonical reserved schema', () => {
54
+ const baseOnly = {
55
+ materialShaderId: 'forgeax::default-standard-pbr',
56
+ paramSnapshot: { alphaCutoff: 0.25 },
57
+ } as const;
58
+ const transmission = {
59
+ materialShaderId: 'forgeax::default-standard-pbr',
60
+ paramSnapshot: { transmission: 0 },
61
+ } as const;
62
+
63
+ expect(requestsStandardTransmissionVariant(baseOnly)).toBe(false);
64
+ expect(requestsStandardTransmissionVariant(transmission)).toBe(true);
65
+ expect(
66
+ requestsStandardTransmissionVariant({
67
+ materialShaderId: 'custom::standard',
68
+ paramSnapshot: { transmission: 1 },
69
+ }),
70
+ ).toBe(false);
71
+ });
72
+ });
@@ -0,0 +1,38 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { getOrCreateFromChain } from '../mesh-ssbo';
3
+
4
+ describe('mesh bind-group cache chains', () => {
5
+ it('keeps variable-depth material resource chains isolated', () => {
6
+ const root = new WeakMap<object, unknown>();
7
+ const shared = { shared: true };
8
+ const coatSampler = { coatSampler: true };
9
+ const coatTexture = { coatTexture: true };
10
+ const counts = { createBindGroup: 0, keys: [] as string[] };
11
+ const long = getOrCreateFromChain(
12
+ root,
13
+ [shared, coatSampler, coatTexture],
14
+ 'material-shared',
15
+ () => ({ id: 'long' }) as never,
16
+ counts,
17
+ );
18
+ const short = getOrCreateFromChain(
19
+ root,
20
+ [shared, coatSampler],
21
+ 'material-shared',
22
+ () => ({ id: 'short' }) as never,
23
+ counts,
24
+ );
25
+
26
+ expect(
27
+ getOrCreateFromChain(
28
+ root,
29
+ [shared, coatSampler],
30
+ 'material-shared',
31
+ () => ({ id: 'unexpected' }) as never,
32
+ counts,
33
+ ),
34
+ ).toBe(short);
35
+ expect(long).not.toBe(short);
36
+ expect(counts.createBindGroup).toBe(2);
37
+ });
38
+ });
@@ -0,0 +1,124 @@
1
+ import { deriveVertexLayoutProjection } from '@forgeax/engine-geometry';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { makeZeroCameraFallbackSnapshot, type ValidatedRenderable } from '../frame-snapshot';
4
+ import {
5
+ createRecoveryColdWorkGuard,
6
+ prepareRecoveryPipelineReadiness,
7
+ } from '../recovery-pipeline';
8
+ import type { PipelineState, RenderSystemInternals } from '../render-context';
9
+
10
+ function probeRenderable(probe: boolean): ValidatedRenderable {
11
+ const material = {
12
+ baseColor: [1, 1, 1],
13
+ metallic: 0,
14
+ roughness: 1,
15
+ materialShaderId: 'forgeax::default-standard-pbr',
16
+ } as never;
17
+ return {
18
+ source: {
19
+ material,
20
+ materials: [material],
21
+ ...(probe ? { probeBlendRecord: {} } : {}),
22
+ },
23
+ mesh: {
24
+ layoutProjection: deriveVertexLayoutProjection({
25
+ position: new Float32Array(0),
26
+ normal: new Float32Array(0),
27
+ uv: new Float32Array(0),
28
+ tangent: new Float32Array(0),
29
+ }),
30
+ indexFormat: 'uint32',
31
+ submeshes: [
32
+ {
33
+ indexOffset: 0,
34
+ indexCount: 3,
35
+ vertexCount: 3,
36
+ topology: 'triangle-list',
37
+ materialSlot: 0,
38
+ },
39
+ ],
40
+ },
41
+ } as never;
42
+ }
43
+
44
+ function recoveryReadinessFor(
45
+ renderable: ValidatedRenderable,
46
+ reflectionFallbackAvailable = false,
47
+ ) {
48
+ const pipeline = {} as never;
49
+ const internals = {
50
+ device: { caps: { backendKind: 'webgpu', storageBuffer: true } },
51
+ getMaterialShaderPipelineEntry: (..._args: unknown[]) => ({
52
+ pipeline,
53
+ group2Contract: 'cluster' as const,
54
+ }),
55
+ } as unknown as RenderSystemInternals;
56
+ const pipelineState = {
57
+ colorAttachmentFormat: 'bgra8unorm',
58
+ } as unknown as PipelineState;
59
+ return prepareRecoveryPipelineReadiness({
60
+ internals,
61
+ pipelineState,
62
+ camera: makeZeroCameraFallbackSnapshot(),
63
+ standardLighting: { kind: 'no-local-lights' } as never,
64
+ validated: [renderable],
65
+ dispatch: [],
66
+ shadowCastersActive: false,
67
+ splitLdrSprite: false,
68
+ reflectionFallbackAvailable,
69
+ });
70
+ }
71
+
72
+ describe('recovery first-frame cold-work guard', () => {
73
+ it('prepares the probe-enabled Standard PBR variant used by the live record path', () => {
74
+ const readiness = recoveryReadinessFor(probeRenderable(true));
75
+ const standardPbr = readiness.pipelineSpecs.find(
76
+ (spec) => spec.shader.id === 'forgeax::default-standard-pbr',
77
+ );
78
+
79
+ expect(standardPbr?.shader.variantSet).toContain('PROBE_BLEND_AVAILABLE=true');
80
+ });
81
+
82
+ it('does not add the probe ABI to a Standard PBR renderable without a record', () => {
83
+ const readiness = recoveryReadinessFor(probeRenderable(false));
84
+ const standardPbr = readiness.pipelineSpecs.find(
85
+ (spec) => spec.shader.id === 'forgeax::default-standard-pbr',
86
+ );
87
+
88
+ expect(standardPbr?.shader.variantSet).not.toContain('PROBE_BLEND_AVAILABLE=true');
89
+ });
90
+
91
+ it('prepares the Standard PBR reflection fallback MRT variant', () => {
92
+ const readiness = recoveryReadinessFor(probeRenderable(false), true);
93
+ const standardPbr = readiness.pipelineSpecs.find(
94
+ (spec) => spec.shader.id === 'forgeax::default-standard-pbr',
95
+ );
96
+
97
+ expect(standardPbr?.shader.variantSet).toContain('REFLECTION_FALLBACK_AVAILABLE=true');
98
+ expect(standardPbr?.attachments.colorFormats).toEqual(['rgba16float', 'rgba16float']);
99
+ });
100
+
101
+ it('accepts a prepared candidate with no first-frame cold work', () => {
102
+ const guard = createRecoveryColdWorkGuard();
103
+
104
+ guard.arm();
105
+ expect(() => guard.finish()).not.toThrow();
106
+ expect(() => guard.finish()).not.toThrow();
107
+ });
108
+
109
+ it.each([
110
+ [
111
+ 'pipeline',
112
+ (guard: ReturnType<typeof createRecoveryColdWorkGuard>) => guard.notePipelineColdWork(),
113
+ ],
114
+ [
115
+ 'upload',
116
+ (guard: ReturnType<typeof createRecoveryColdWorkGuard>) => guard.noteUploadColdWork(),
117
+ ],
118
+ ] as const)('hard-fails a first-frame %s miss before submission', (_kind, note) => {
119
+ const guard = createRecoveryColdWorkGuard();
120
+
121
+ guard.arm();
122
+ expect(() => note(guard)).toThrow(/recovery first frame performed cold/);
123
+ });
124
+ });