@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,140 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { describe, expect, it } from 'vitest';
4
+
5
+ const renderSystemSource = readFileSync(
6
+ resolve(import.meta.dirname, '../render-system.ts'),
7
+ 'utf8',
8
+ );
9
+ const recoveryCandidateSource = readFileSync(
10
+ resolve(import.meta.dirname, '../recovery/render-system-candidate.ts'),
11
+ 'utf8',
12
+ );
13
+ const rendererSource = readFileSync(
14
+ resolve(import.meta.dirname, '../assembly/webgpu-renderer.ts'),
15
+ 'utf8',
16
+ );
17
+ const rendererRecoverySource = readFileSync(
18
+ resolve(import.meta.dirname, '../assembly/recovery/renderer-recover.ts'),
19
+ 'utf8',
20
+ );
21
+
22
+ describe('recovery candidate preparation contract', () => {
23
+ it('keeps graph preparation outside draw, record, execute, and submit', () => {
24
+ const start = recoveryCandidateSource.indexOf('const prepareRecoveryGraphCandidate');
25
+ const end = recoveryCandidateSource.indexOf('const submitCandidateSetup', start);
26
+ const prepare = recoveryCandidateSource.slice(start, end);
27
+
28
+ expect(prepare).toContain('prepareFrameLighting');
29
+ expect(prepare).toContain('ensureCompiledFrameGraph');
30
+ expect(prepare).toContain("kind: 'no-seed'");
31
+ expect(prepare).toContain("kind: 'failed'");
32
+ expect(prepare).toContain("kind: 'ready'");
33
+ expect(prepare).not.toMatch(
34
+ /\.draw\(|recordFrame\(|executeCompiledFrameGraph|queue\.submit|\.finish\(/,
35
+ );
36
+ });
37
+
38
+ it('keeps active generation bindings unchanged until publication', () => {
39
+ const start = rendererRecoverySource.indexOf('const candidateInternals =');
40
+ const end = rendererRecoverySource.indexOf('const preparedRecoveryRoots =', start);
41
+ const candidate = rendererRecoverySource.slice(start, end);
42
+
43
+ expect(candidate).toContain('prepareRecoveryGraphCandidate');
44
+ expect(candidate).toContain("graphPreparation.kind === 'failed'");
45
+ expect(candidate).toContain("graphPreparation.kind === 'ready'");
46
+ expect(candidate).toContain('submitCandidateSetup');
47
+ expect(candidate).not.toMatch(/internals\.device\s*=/);
48
+ expect(candidate).not.toMatch(/internals\.generationState\.current\s*=/);
49
+ expect(candidate).not.toContain('renderSystem.resetForRecover');
50
+ expect(candidate).not.toContain('featureHost?.recover');
51
+ expect(candidate).not.toContain('renderTargetHost.beginFrame');
52
+ expect(rendererRecoverySource.indexOf('submitCandidateSetup', start)).toBeLessThan(
53
+ rendererRecoverySource.indexOf('const preparedRecoveryRoots', start),
54
+ );
55
+ expect(rendererRecoverySource).toContain('recoveryRootBundle');
56
+ expect(rendererSource).toContain('publishAndRetireRendererGeneration');
57
+ });
58
+
59
+ it('prepares roots from candidate state and switches environment owner only at publish', () => {
60
+ const start = renderSystemSource.indexOf('prepareRecoveryRoots(runtime: RecoveryRootRuntime)');
61
+ const end = renderSystemSource.indexOf('\n },\n };', start);
62
+ const roots = renderSystemSource.slice(start, end);
63
+
64
+ expect(roots).toContain('runtime.scope');
65
+ expect(roots).toContain('createRecoveryCandidate');
66
+ expect(roots).toContain('candidateEnvironmentGeneration');
67
+ expect(roots).toContain('candidateEnvironmentLifecycle.publish');
68
+ expect(roots).toContain('candidateEnvironmentLifecycle.discard');
69
+ expect(roots).toContain('graphCandidate.featureHost.createRecoveryRoot');
70
+ expect(roots).toContain('graphCandidate.featureGpuWork.createRecoveryRoot');
71
+ expect(roots).toContain('graphCandidate.gpuDrivenProduction.createRecoveryRoot');
72
+ expect(roots).toContain('graphCandidate.gpuDrivenScene.createRecoveryRoot');
73
+ expect(roots).toContain('graphCandidate.pointsLines.createRecoveryRoot');
74
+ expect(roots).toContain('runtime.gpuStore.createRecoveryRoot');
75
+ expect(roots).toContain('return compiledGraph');
76
+ expect(roots).not.toContain('environmentLifecycle.createRecoveryRoot');
77
+ expect(roots).not.toContain('featureCount');
78
+ expect(roots).not.toContain("owner: 'gpu-driven-production'");
79
+ expect(rendererSource).toContain('recoveryRootBundle.publish()');
80
+ expect(rendererRecoverySource).toContain('rootBundle.discard()');
81
+ expect(roots).toContain('cleanup: releaseGraphCandidate');
82
+ });
83
+
84
+ it('submits only explicit mip setup work without creating frame evidence', () => {
85
+ const start = recoveryCandidateSource.indexOf('const submitCandidateSetup');
86
+ const end = recoveryCandidateSource.indexOf('\n return {', start);
87
+ const setup = recoveryCandidateSource.slice(start, end);
88
+
89
+ expect(setup).toContain('work.finish()');
90
+ expect(setup).toContain('candidate.device.queue.submit');
91
+ expect(setup).toContain('candidate.device.queue.onSubmittedWorkDone');
92
+ expect(setup.match(/candidate\.device\.queue\.submit/g)).toHaveLength(1);
93
+ expect(setup).not.toContain('submittedFrameCount');
94
+ expect(setup).not.toContain('directFrameId');
95
+ expect(setup).not.toContain('frameState');
96
+ expect(setup).not.toContain('recordFrame');
97
+ });
98
+
99
+ it('arms the first published frame only after candidate pipeline preparation', () => {
100
+ expect(recoveryCandidateSource).toContain(
101
+ 'candidate.recoveryReadiness.assertFirstRecoveryFrame()',
102
+ );
103
+ expect(recoveryCandidateSource).toContain('pointsLinesOwner.prepareRecoveryCandidate');
104
+ expect(renderSystemSource).toContain('pointsLinesOwner.abandonForDeviceLoss');
105
+ expect(rendererSource).toContain(
106
+ 'candidate.producerBindings.gpuStore.setRecoveryColdWorkGuard',
107
+ );
108
+ expect(rendererSource).toContain('recoveryColdWorkGuard.arm()');
109
+ expect(renderSystemSource).toContain('internals.recoveryColdWorkGuard?.notePipelineColdWork()');
110
+ expect(recoveryCandidateSource).toContain('const previousFeatureHost = internals.featureHost');
111
+ expect(recoveryCandidateSource).toContain('const disposed = previousFeatureHost.dispose()');
112
+ expect(recoveryCandidateSource).toContain('previousFeatureHost !== candidate.featureHost');
113
+ });
114
+
115
+ it('keeps material pipelines scoped to the active recovery generation', () => {
116
+ expect(rendererSource).toContain('currentPipelineCacheState().materialShaderPipelineCache');
117
+ expect(rendererSource).not.toMatch(/\n\s*materialShaderPipelineCache\.(get|set|has)\(/);
118
+ });
119
+
120
+ it('resolves recovery variants from the candidate device capabilities', () => {
121
+ const start = rendererSource.indexOf('const resolveCachedMaterialShaderVariantSet');
122
+ const end = rendererSource.indexOf('const findMaterialShaderManifestEntry', start);
123
+ const resolver = rendererSource.slice(start, end);
124
+
125
+ expect(resolver).toContain('const device = currentBuildDevice()');
126
+ expect(resolver).not.toContain('internals.device.caps');
127
+ expect(resolver).not.toContain('internals.device.limits');
128
+ });
129
+
130
+ it('keeps render-system residency bindings live across publication', () => {
131
+ const start = rendererSource.indexOf('const renderSystem: RenderSystem = createRenderSystem({');
132
+ const end = rendererSource.indexOf('errorRegistry: internals.errorRegistry', start);
133
+ const assembly = rendererSource.slice(start, end);
134
+
135
+ expect(assembly).toContain('get gpuStore()');
136
+ expect(assembly).toContain('get dynamicTextureStore()');
137
+ expect(assembly).not.toMatch(/\n\s*gpuStore,\n/);
138
+ expect(assembly).not.toMatch(/\n\s*dynamicTextureStore,\n/);
139
+ });
140
+ });
@@ -1,12 +1,16 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
3
  import { RhiError } from '@forgeax/engine-rhi';
4
+ import type { MaterialShaderManifestVariant } from '@forgeax/engine-shader';
4
5
  import { describe, expect, it, vi } from 'vitest';
5
6
  import { deviceOptionsForAdapter } from '../assembly/device-feature-admission';
6
7
  import {
7
8
  allowsUnlitPreparedFallback,
9
+ isSharedMaterialUserRegionCompatible,
8
10
  normalizeMaterialShaderVariantSet,
11
+ resolveMaterialShaderBackendArtifactKey,
9
12
  resolveMaterialShaderBindingContract,
13
+ resolveMaterialShaderUvSetCount,
10
14
  resolveMaterialShaderVariantSet,
11
15
  resolveMaterialShaderVertexInputContract,
12
16
  selectNoColorPbrVariant,
@@ -18,6 +22,7 @@ import {
18
22
  type PipelineBuilderContext,
19
23
  validateTemporalPipelineContract,
20
24
  } from '../pipeline-builder';
25
+ import { effectiveMaterialLayoutIdentity } from '../record/main-pass-geometry';
21
26
  import { userRegionTextureFieldOrder } from '../record/main-pass-material';
22
27
 
23
28
  const hdrpVariantKey =
@@ -25,7 +30,10 @@ const hdrpVariantKey =
25
30
  const urpVariantKey =
26
31
  'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=true+VERTEX_COLOR_AVAILABLE=false';
27
32
 
28
- function noColorVariant(definesKey: string, clusterForward: boolean) {
33
+ function noColorVariant(
34
+ definesKey: string,
35
+ clusterForward: boolean,
36
+ ): MaterialShaderManifestVariant {
29
37
  return {
30
38
  definesKey,
31
39
  defines: {
@@ -57,7 +65,7 @@ describe('material shader variant identity', () => {
57
65
  } as never),
58
66
  ).toEqual({
59
67
  requiredFeatures: [],
60
- requiredLimits: { maxSampledTexturesPerShaderStage: 20 },
68
+ requiredLimits: { maxSampledTexturesPerShaderStage: 21 },
61
69
  });
62
70
  expect(
63
71
  deviceOptionsForAdapter({
@@ -80,6 +88,17 @@ describe('material shader variant identity', () => {
80
88
  ).toBe(urpVariantKey);
81
89
  });
82
90
 
91
+ it('keeps physical root declarations out of capability variant selection', () => {
92
+ const manifest = noColorManifest();
93
+ expect(
94
+ selectNoColorPbrVariant(
95
+ manifest,
96
+ true,
97
+ `${hdrpVariantKey}+CLEARCOAT_TEXTURE_AVAILABLE=true+TRANSMISSION_AVAILABLE=true`,
98
+ )?.definesKey,
99
+ ).toBe(hdrpVariantKey);
100
+ });
101
+
83
102
  it('closes the WebGL2 colored PBR axes on the URP variant and layout', () => {
84
103
  const variants = [
85
104
  {
@@ -373,6 +392,31 @@ describe('material shader variant identity', () => {
373
392
  ).toBe('STORAGE_BUFFER_AVAILABLE=true');
374
393
  expect(normalizeMaterialShaderVariantSet('CUSTOM=true', undefined)).toBe('CUSTOM=true');
375
394
  });
395
+
396
+ it('projects a published Surface artifact to its WebGL2 capability alias', () => {
397
+ const base = 'surface-specialization';
398
+ const webgl2 = 'forgeax::surface-variant::surface-specialization::webgl2::uniform-fallback';
399
+ const artifact = {
400
+ metadata: {
401
+ variants: [
402
+ {
403
+ backend: 'webgpu',
404
+ capability: 'storage-buffer',
405
+ specializationKey: base,
406
+ },
407
+ {
408
+ backend: 'webgl2',
409
+ capability: 'uniform-fallback',
410
+ specializationKey: webgl2,
411
+ },
412
+ ],
413
+ },
414
+ };
415
+
416
+ expect(resolveMaterialShaderBackendArtifactKey(base, 'wgpu-webgl2', artifact)).toBe(webgl2);
417
+ expect(resolveMaterialShaderBackendArtifactKey(base, 'webgpu', artifact)).toBe(base);
418
+ expect(resolveMaterialShaderBackendArtifactKey(base, 'wgpu-webgl2', undefined)).toBe(base);
419
+ });
376
420
  });
377
421
 
378
422
  describe('temporal material PSO creation contract', () => {
@@ -497,13 +541,12 @@ describe('temporal material PSO creation contract', () => {
497
541
  });
498
542
 
499
543
  describe('material shader capability variant resolution', () => {
500
- it('keeps transmission disabled unless the record path opts in', () => {
544
+ it('rewrites only backend capability axes', () => {
501
545
  const variants = [
502
546
  {
503
547
  defines: {
504
548
  CLUSTER_FORWARD_AVAILABLE: false,
505
549
  STORAGE_BUFFER_AVAILABLE: false,
506
- TRANSMISSION_AVAILABLE: false,
507
550
  VERTEX_COLOR_AVAILABLE: false,
508
551
  },
509
552
  },
@@ -511,7 +554,6 @@ describe('material shader capability variant resolution', () => {
511
554
  defines: {
512
555
  CLUSTER_FORWARD_AVAILABLE: false,
513
556
  STORAGE_BUFFER_AVAILABLE: false,
514
- TRANSMISSION_AVAILABLE: true,
515
557
  VERTEX_COLOR_AVAILABLE: false,
516
558
  },
517
559
  },
@@ -524,17 +566,7 @@ describe('material shader capability variant resolution', () => {
524
566
  false,
525
567
  ),
526
568
  ).toBe(
527
- 'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=false+TRANSMISSION_AVAILABLE=false+VERTEX_COLOR_AVAILABLE=false',
528
- );
529
- expect(
530
- resolveMaterialShaderVariantSet(
531
- 'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=true+TRANSMISSION_AVAILABLE=true+VERTEX_COLOR_AVAILABLE=false',
532
- variants,
533
- 'wgpu-webgl2',
534
- false,
535
- ),
536
- ).toBe(
537
- 'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=false+TRANSMISSION_AVAILABLE=true+VERTEX_COLOR_AVAILABLE=false',
569
+ 'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=false+VERTEX_COLOR_AVAILABLE=false',
538
570
  );
539
571
  });
540
572
 
@@ -560,6 +592,16 @@ describe('material shader capability variant resolution', () => {
560
592
  });
561
593
 
562
594
  describe('material shader binding contract', () => {
595
+ it('does not reuse the shared PBR layout when compact texture names shift semantics', () => {
596
+ expect(
597
+ isSharedMaterialUserRegionCompatible([
598
+ { name: 'baseColor', type: 'color' },
599
+ { name: 'baseColorTexture', type: 'texture2d' },
600
+ { name: 'normalTexture', type: 'texture2d' },
601
+ ]),
602
+ ).toBe(false);
603
+ });
604
+
563
605
  it('preserves compact authored texture order for custom material schemas', () => {
564
606
  expect(
565
607
  userRegionTextureFieldOrder([
@@ -573,7 +615,6 @@ describe('material shader binding contract', () => {
573
615
  'baseColorTexture',
574
616
  'metallicRoughnessTexture',
575
617
  'normalTexture',
576
- 'specularTintTexture',
577
618
  'emissiveTexture',
578
619
  'occlusionTexture',
579
620
  'transmissionTexture',
@@ -650,8 +691,29 @@ describe('material shader binding contract', () => {
650
691
  });
651
692
 
652
693
  describe('material shader color-domain contract', () => {
694
+ it('keeps shared sprite layouts stable while splitting standard clearcoat maps', () => {
695
+ const sharedSpriteSchema = [{ name: 'tint', type: 'color' as const }];
696
+ const standardClearcoatSchema = [
697
+ { name: 'clearcoat', type: 'f32' as const },
698
+ { name: 'clearcoatTexture', type: 'texture2d' as const },
699
+ ];
700
+
701
+ expect(effectiveMaterialLayoutIdentity('forgeax::sprite', sharedSpriteSchema)).toBeUndefined();
702
+ expect(
703
+ effectiveMaterialLayoutIdentity('forgeax::default-standard-pbr', standardClearcoatSchema),
704
+ ).toBeTypeOf('string');
705
+ expect(
706
+ effectiveMaterialLayoutIdentity('forgeax::default-standard-pbr', [
707
+ { name: 'clearcoat', type: 'f32' as const },
708
+ ]),
709
+ ).toBeUndefined();
710
+ });
711
+
653
712
  it('routes HDR sprite targets to fs_main_hdr while unorm targets use fs_main', () => {
654
- const source = readFileSync(resolve(import.meta.dirname, '../assembly/factory.ts'), 'utf8');
713
+ const source = [
714
+ readFileSync(resolve(import.meta.dirname, '../assembly/webgpu-renderer.ts'), 'utf8'),
715
+ readFileSync(resolve(import.meta.dirname, '../assembly/webgpu-ready.ts'), 'utf8'),
716
+ ].join('\n');
655
717
  expect(source).toContain(
656
718
  "isHdr &&\n (materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit')",
657
719
  );
@@ -659,6 +721,38 @@ describe('material shader color-domain contract', () => {
659
721
  expect(source).toContain('colorFormat: isHdr ? HDR_COLOR_ATTACHMENT_FORMAT : ldrColorFormat');
660
722
  expect(source).toContain("entryPoint = 'fs_main_hdr'");
661
723
  });
724
+
725
+ it('keeps physical map layout derivation in the shared pipeline owner', () => {
726
+ const source = readFileSync(
727
+ resolve(import.meta.dirname, '../assembly/webgpu-renderer.ts'),
728
+ 'utf8',
729
+ );
730
+ expect(source).toContain('isCanonicalStandardPbrMaterialShader(materialShaderId)');
731
+ expect(source).toContain('requiresStandardMapLayout');
732
+ });
733
+
734
+ it('does not reuse the scalar shared BGL for authored clearcoat maps', () => {
735
+ const source = readFileSync(
736
+ resolve(import.meta.dirname, '../assembly/webgpu-renderer.ts'),
737
+ 'utf8',
738
+ );
739
+ expect(source).toMatch(
740
+ /if \(\s*!isStandardMapLayout\s*&&\s*isSharedMaterialUserRegionCompatible\(/u,
741
+ );
742
+ });
743
+
744
+ it('keeps the fixed standard texture ABI before authored clearcoat slots', () => {
745
+ const pipelineSource = readFileSync(resolve(import.meta.dirname, '../pbr-pipeline.ts'), 'utf8');
746
+ const recordSource = readFileSync(
747
+ resolve(import.meta.dirname, '../record/main-pass-material.ts'),
748
+ 'utf8',
749
+ );
750
+ expect(pipelineSource).toContain(
751
+ 'const physicalFields = physicalTextureFields(effectiveSchema)',
752
+ );
753
+ expect(pipelineSource).toContain('appendTextureInjection(afterTransmission, physicalFields)');
754
+ expect(recordSource).toContain('BUILTIN_USER_REGION_TEXTURE_FIELDS');
755
+ });
662
756
  });
663
757
 
664
758
  describe('material shader vertex input contract', () => {
@@ -687,6 +781,21 @@ describe('material shader vertex input contract', () => {
687
781
  expect(resolveMaterialShaderVertexInputContract(source)).toBe('none');
688
782
  });
689
783
 
784
+ it('derives authored Standard UV aliases from the vertex input struct', () => {
785
+ const source = `
786
+ struct VsIn {
787
+ @location(0) position: vec3<f32>,
788
+ @location(2) uv: vec2<f32>,
789
+ @location(6) uv1_: vec2<f32>,
790
+ @location(12) uv7_: vec2<f32>,
791
+ }
792
+ struct VsOut { @builtin(position) position: vec4<f32> }
793
+ @vertex fn vs_main(input: VsIn) -> VsOut { var out: VsOut; return out }
794
+ `;
795
+
796
+ expect(resolveMaterialShaderUvSetCount(source)).toBe(8);
797
+ });
798
+
690
799
  it('keeps an explicit prepared vertex layout on a missing shader', () => {
691
800
  expect(allowsUnlitPreparedFallback(null, undefined)).toBe(true);
692
801
  expect(allowsUnlitPreparedFallback(null, 'position-size-color-instance', 'forward')).toBe(true);
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ createContinuationTerminator,
4
+ executeRendererFrameTransaction,
5
+ } from '../assembly/renderer-frame-transaction';
6
+
7
+ describe('renderer frame generation fence', () => {
8
+ it('rejects a generation that changes between finish and submit', () => {
9
+ const activeOwner = { scope: { generation: 3 } };
10
+ const submitted: string[] = [];
11
+ const result = executeRendererFrameTransaction({
12
+ build: () => ({ ok: true, value: 'command' }),
13
+ execute: () => ({ ok: true, value: undefined }),
14
+ finish: () => {
15
+ activeOwner.scope = { generation: 4 };
16
+ return { ok: true, value: undefined };
17
+ },
18
+ submit: () => {
19
+ submitted.push('stale-command');
20
+ return { ok: true, value: undefined };
21
+ },
22
+ commit: () => undefined,
23
+ generationFence: {
24
+ capturedGeneration: 3,
25
+ currentGeneration: () => activeOwner.scope.generation,
26
+ },
27
+ });
28
+
29
+ expect(result).toEqual({ ok: false, error: { stage: 'submit' } });
30
+ expect(submitted).toEqual([]);
31
+ });
32
+
33
+ it('terminates completion and all async continuations exactly once', async () => {
34
+ const terminator = createContinuationTerminator();
35
+ const completion = terminator.promise();
36
+
37
+ expect(terminator.terminate({ code: 'device-lost' })).toBe(true);
38
+ expect(terminator.terminate({ code: 'disposed' })).toBe(false);
39
+ expect(terminator.isTerminated()).toBe(true);
40
+ await expect(completion).resolves.toEqual({ code: 'device-lost' });
41
+
42
+ expect(terminator.guard('readback')).toBe(false);
43
+ expect(terminator.guard('query')).toBe(false);
44
+ expect(terminator.guard('mapAsync')).toBe(false);
45
+ expect(terminator.guard('queue-completion')).toBe(false);
46
+ });
47
+ });
@@ -0,0 +1,77 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ buildGenerationAggregate,
4
+ createGenerationAllocator,
5
+ type GenerationAggregate,
6
+ type GenerationPublication,
7
+ publishGeneration,
8
+ } from '../assembly/recovery/generation';
9
+ import { DeviceScope, type LifecycleResourceSpec } from '../device/device-scope';
10
+
11
+ type TestAggregate = GenerationAggregate<
12
+ { readonly id: string },
13
+ { readonly id: string },
14
+ { readonly id: string },
15
+ { readonly id: string },
16
+ { readonly id: string }
17
+ >;
18
+
19
+ function root(
20
+ kind: LifecycleResourceSpec<unknown>['kind'],
21
+ order: string[],
22
+ ): LifecycleResourceSpec<unknown> {
23
+ return {
24
+ kind,
25
+ create: () => {
26
+ order.push(`create:${kind}`);
27
+ return { kind };
28
+ },
29
+ cleanup: () => {
30
+ order.push(`cleanup:${kind}`);
31
+ },
32
+ };
33
+ }
34
+
35
+ describe('generation assembly', () => {
36
+ it('keeps the candidate detached and publishes one immutable aggregate', async () => {
37
+ const order: string[] = [];
38
+ const scope = DeviceScope.create(4, 'renderer');
39
+ const candidate = await buildGenerationAggregate<TestAggregate>({
40
+ scope,
41
+ device: { id: 'device-4' },
42
+ context: { id: 'context-4' },
43
+ pipeline: { id: 'pipeline-4' },
44
+ producerBindings: { id: 'producers-4' },
45
+ graph: { id: 'graph-4' },
46
+ roots: [root('surface', order), root('pipeline', order)],
47
+ });
48
+
49
+ expect(candidate.ok).toBe(true);
50
+ if (!candidate.ok) return;
51
+
52
+ const publication: GenerationPublication<TestAggregate | undefined> = {
53
+ current: undefined,
54
+ };
55
+ expect(publication.current).toBeUndefined();
56
+ expect(Object.isFrozen(candidate.value)).toBe(true);
57
+ expect(candidate.value.generation).toBe(4);
58
+ expect(candidate.value.scope).toBe(scope);
59
+ expect(order).toEqual(['create:surface', 'create:pipeline']);
60
+
61
+ publishGeneration(publication, candidate.value, (value) => value.scope.isAlive());
62
+
63
+ expect(publication.current).toBe(candidate.value);
64
+ expect(publication.current?.device.id).toBe('device-4');
65
+ expect(publication.current?.context.id).toBe('context-4');
66
+ expect(publication.current?.pipeline.id).toBe('pipeline-4');
67
+ });
68
+
69
+ it('allocates each candidate generation exactly once without moving the active generation', () => {
70
+ const allocator = createGenerationAllocator(9);
71
+
72
+ expect(allocator.next()).toBe(10);
73
+ expect(allocator.next()).toBe(11);
74
+ expect(allocator.next()).toBe(12);
75
+ expect(allocator.activeGeneration).toBe(9);
76
+ });
77
+ });
@@ -0,0 +1,108 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ buildGenerationAggregate,
4
+ type GenerationAggregate,
5
+ } from '../assembly/recovery/generation';
6
+ import { DeviceScope, type LifecycleResourceSpec } from '../device/device-scope';
7
+
8
+ type FailureAggregate = GenerationAggregate<
9
+ { readonly id: string },
10
+ { readonly id: string },
11
+ { readonly id: string },
12
+ { readonly id: string },
13
+ { readonly id: string }
14
+ >;
15
+
16
+ const kinds: LifecycleResourceSpec<unknown>['kind'][] = [
17
+ 'surface',
18
+ 'pipeline',
19
+ 'buffer',
20
+ 'texture',
21
+ 'feature',
22
+ ];
23
+
24
+ function root(
25
+ kind: LifecycleResourceSpec<unknown>['kind'],
26
+ events: string[],
27
+ failure: boolean,
28
+ ): LifecycleResourceSpec<unknown> {
29
+ return {
30
+ kind,
31
+ create: () => {
32
+ events.push(`create:${kind}`);
33
+ if (failure) throw new Error(`failure:${kind}`);
34
+ return { kind };
35
+ },
36
+ cleanup: () => {
37
+ events.push(`cleanup:${kind}`);
38
+ },
39
+ };
40
+ }
41
+
42
+ function aggregate(scope: DeviceScope): FailureAggregate {
43
+ return Object.freeze({
44
+ generation: scope.generation,
45
+ scope,
46
+ device: { id: `device-${scope.generation}` },
47
+ context: { id: `context-${scope.generation}` },
48
+ pipeline: { id: `pipeline-${scope.generation}` },
49
+ producerBindings: { id: `producers-${scope.generation}` },
50
+ graph: { id: `graph-${scope.generation}` },
51
+ });
52
+ }
53
+
54
+ describe('generation assembly failure matrix', () => {
55
+ it.each(kinds)('abandons a candidate when the %s root fails', async (failedKind) => {
56
+ const events: string[] = [];
57
+ const activeScope = DeviceScope.create(9, 'renderer');
58
+ const active = aggregate(activeScope);
59
+ const candidateScope = DeviceScope.create(11, 'renderer');
60
+ const result = await buildGenerationAggregate<FailureAggregate>({
61
+ scope: candidateScope,
62
+ device: { id: 'device-11' },
63
+ context: { id: 'context-11' },
64
+ pipeline: { id: 'pipeline-11' },
65
+ producerBindings: { id: 'producers-11' },
66
+ graph: { id: 'graph-11' },
67
+ roots: kinds.map((kind) => root(kind, events, kind === failedKind)),
68
+ });
69
+
70
+ expect(result.ok).toBe(false);
71
+ if (result.ok) return;
72
+ expect(result.error.code).toBe('generation-assembly-failed');
73
+ expect(result.error.generation).toBe(11);
74
+ expect(result.error.resourceKind).toBe(failedKind);
75
+ expect(result.error.cleanupFailures).toEqual([]);
76
+ expect(candidateScope.state).toBe('abandoned');
77
+ expect(events.filter((event) => event.startsWith('cleanup:'))).toEqual(
78
+ events
79
+ .filter((event) => event.startsWith('create:') && !event.endsWith(failedKind))
80
+ .reverse()
81
+ .map((event) => event.replace('create:', 'cleanup:')),
82
+ );
83
+ expect(active.scope.state).toBe('active');
84
+ expect(active.generation).toBe(9);
85
+ });
86
+
87
+ it('keeps the active aggregate unchanged when candidate publication is skipped', async () => {
88
+ const events: string[] = [];
89
+ const activeScope = DeviceScope.create(20, 'renderer');
90
+ const active = aggregate(activeScope);
91
+ const candidateScope = DeviceScope.create(22, 'renderer');
92
+ const result = await buildGenerationAggregate<FailureAggregate>({
93
+ scope: candidateScope,
94
+ device: { id: 'device-22' },
95
+ context: { id: 'context-22' },
96
+ pipeline: { id: 'pipeline-22' },
97
+ producerBindings: { id: 'producers-22' },
98
+ graph: { id: 'graph-22' },
99
+ roots: [root('surface', events, false), root('pipeline', events, true)],
100
+ });
101
+
102
+ expect(result.ok).toBe(false);
103
+ expect(active).toEqual(aggregate(activeScope));
104
+ expect(active.scope.state).toBe('active');
105
+ expect(candidateScope.generation).toBe(22);
106
+ expect(events).toEqual(['create:surface', 'create:pipeline', 'cleanup:surface']);
107
+ });
108
+ });
@@ -0,0 +1,33 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createRendererProducerRootMatrix } from '../render-system';
3
+
4
+ describe('renderer producer root matrix', () => {
5
+ it('enumerates the current baseline resource families with one owner each', () => {
6
+ const matrix = createRendererProducerRootMatrix();
7
+
8
+ expect(matrix.map((root) => root.kind)).toEqual([
9
+ 'backend-surface',
10
+ 'shader-material-pipeline',
11
+ 'mesh-texture-sampler',
12
+ 'render-scene',
13
+ 'gpu-driven',
14
+ 'feature',
15
+ 'external-source',
16
+ 'render-graph',
17
+ 'target-history',
18
+ 'observation-lease',
19
+ ]);
20
+ expect(new Set(matrix.map((root) => root.owner)).size).toBe(matrix.length);
21
+ expect(matrix.every((root) => root.candidateScope === 'device-scope')).toBe(true);
22
+ });
23
+
24
+ it('separates visible roots from lazy non-visible work', () => {
25
+ const matrix = createRendererProducerRootMatrix();
26
+ const visible = matrix.filter((root) => root.visibility === 'visible-workset');
27
+ const lazy = matrix.filter((root) => root.visibility === 'non-visible-lazy');
28
+
29
+ expect(visible.length).toBeGreaterThan(0);
30
+ expect(lazy.length).toBeGreaterThan(0);
31
+ expect(lazy.every((root) => root.disabledWork === 'zero')).toBe(true);
32
+ });
33
+ });