@forgeax/engine-render 0.1.23 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/README.md +131 -9
  2. package/dist/assembly/bundler-contract.d.ts +9 -0
  3. package/dist/assembly/bundler-contract.d.ts.map +1 -0
  4. package/dist/assembly/device-feature-admission.d.ts +8 -0
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -1
  6. package/dist/assembly/extended-lighting-fallback.d.ts +13 -0
  7. package/dist/assembly/extended-lighting-fallback.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +7 -243
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/material/assembly.d.ts +3 -2
  11. package/dist/assembly/material/assembly.d.ts.map +1 -1
  12. package/dist/assembly/material/standard-layer-projection.d.ts +10 -0
  13. package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -0
  14. package/dist/assembly/material-shader-policy.d.ts +168 -0
  15. package/dist/assembly/material-shader-policy.d.ts.map +1 -0
  16. package/dist/assembly/material-shader-prewarm.d.ts +21 -0
  17. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
  18. package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
  19. package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
  20. package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
  21. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
  22. package/dist/assembly/recovery/generation.d.ts +46 -0
  23. package/dist/assembly/recovery/generation.d.ts.map +1 -0
  24. package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
  25. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
  26. package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
  27. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
  28. package/dist/assembly/renderer-facade.d.ts +10 -0
  29. package/dist/assembly/renderer-facade.d.ts.map +1 -0
  30. package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
  31. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  32. package/dist/assembly/renderer-helpers.d.ts +19 -0
  33. package/dist/assembly/renderer-helpers.d.ts.map +1 -0
  34. package/dist/assembly/renderer-lifecycle.d.ts +34 -0
  35. package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
  36. package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
  37. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
  38. package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
  39. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
  40. package/dist/assembly/webgpu-ready.d.ts +55 -0
  41. package/dist/assembly/webgpu-ready.d.ts.map +1 -0
  42. package/dist/assembly/webgpu-renderer.d.ts +152 -0
  43. package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
  44. package/dist/authoring.mjs +1 -1
  45. package/dist/{chunk-4L3P6DT5.mjs → chunk-GE3SDD72.mjs} +37 -5
  46. package/dist/chunk-GE3SDD72.mjs.map +1 -0
  47. package/dist/{chunk-HZUPSYW7.mjs → chunk-KZFTK6DO.mjs} +102 -15
  48. package/dist/chunk-KZFTK6DO.mjs.map +1 -0
  49. package/dist/{chunk-FF7K3FSO.mjs → chunk-Q752SHI5.mjs} +89 -89
  50. package/dist/{chunk-FF7K3FSO.mjs.map → chunk-Q752SHI5.mjs.map} +1 -1
  51. package/dist/{chunk-QODA5UKU.mjs → chunk-RR5L4SRS.mjs} +2 -2
  52. package/dist/chunk-RR5L4SRS.mjs.map +1 -0
  53. package/dist/{chunk-YALPS44A.mjs → chunk-TFH5I7JJ.mjs} +47 -47
  54. package/dist/chunk-TFH5I7JJ.mjs.map +1 -0
  55. package/dist/{chunk-FBC4R44J.mjs → chunk-UQYS7FRM.mjs} +57 -57
  56. package/dist/chunk-UQYS7FRM.mjs.map +1 -0
  57. package/dist/{chunk-4NPUASZR.mjs → chunk-YKB5DQV7.mjs} +161 -19
  58. package/dist/chunk-YKB5DQV7.mjs.map +1 -0
  59. package/dist/{chunk-ZUWKAZ2I.mjs → chunk-YRUYGA4A.mjs} +615 -237
  60. package/dist/chunk-YRUYGA4A.mjs.map +1 -0
  61. package/dist/{chunk-DIYW3M5D.mjs → chunk-Z3YYH2MB.mjs} +2210 -2213
  62. package/dist/chunk-Z3YYH2MB.mjs.map +1 -0
  63. package/dist/construct-renderer.mjs +19841 -16190
  64. package/dist/construct-renderer.mjs.map +1 -1
  65. package/dist/device/device-scope.d.ts +2 -0
  66. package/dist/device/device-scope.d.ts.map +1 -1
  67. package/dist/device/gpu-residency.d.ts +24 -4
  68. package/dist/device/gpu-residency.d.ts.map +1 -1
  69. package/dist/environment/frame.d.ts +5 -0
  70. package/dist/environment/frame.d.ts.map +1 -1
  71. package/dist/environment/inspection.d.ts +7 -0
  72. package/dist/environment/inspection.d.ts.map +1 -1
  73. package/dist/environment/lifecycle.d.ts +20 -2
  74. package/dist/environment/lifecycle.d.ts.map +1 -1
  75. package/dist/errors/recover.d.ts +58 -2
  76. package/dist/errors/recover.d.ts.map +1 -1
  77. package/dist/errors/render.d.ts +13 -0
  78. package/dist/errors/render.d.ts.map +1 -1
  79. package/dist/extract/contracts.d.ts +899 -0
  80. package/dist/extract/contracts.d.ts.map +1 -0
  81. package/dist/features/host.d.ts +2 -0
  82. package/dist/features/host.d.ts.map +1 -1
  83. package/dist/features/prepared-gpu-work.d.ts +3 -0
  84. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  85. package/dist/features/prepared-graphics-store.d.ts +2 -0
  86. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  87. package/dist/gpu-driven/production-raster.d.ts +28 -0
  88. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  89. package/dist/gpu-scene.d.ts +1 -0
  90. package/dist/gpu-scene.d.ts.map +1 -1
  91. package/dist/ibl/IblPipelineCache.d.ts +7 -6
  92. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  93. package/dist/ibl/skylight-bind-group.d.ts +7 -1
  94. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  95. package/dist/index.d.ts +1 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.mjs +6 -6
  98. package/dist/internal.mjs +6 -6
  99. package/dist/light-buffer-layout.d.ts +3 -0
  100. package/dist/light-buffer-layout.d.ts.map +1 -1
  101. package/dist/materials.d.ts +34 -10
  102. package/dist/materials.d.ts.map +1 -1
  103. package/dist/mesh-material-bindings.d.ts +56 -0
  104. package/dist/mesh-material-bindings.d.ts.map +1 -1
  105. package/dist/occlusion-proxy.d.ts +4 -0
  106. package/dist/occlusion-proxy.d.ts.map +1 -0
  107. package/dist/pbr-pipeline.d.ts +32 -9
  108. package/dist/pbr-pipeline.d.ts.map +1 -1
  109. package/dist/points-lines/prepare.d.ts +3 -0
  110. package/dist/points-lines/prepare.d.ts.map +1 -1
  111. package/dist/points-lines/standard-owner.d.ts +33 -0
  112. package/dist/points-lines/standard-owner.d.ts.map +1 -0
  113. package/dist/record/frame-snapshot.d.ts +5 -0
  114. package/dist/record/frame-snapshot.d.ts.map +1 -1
  115. package/dist/record/frame-targets.d.ts.map +1 -1
  116. package/dist/record/frame.d.ts +14 -2
  117. package/dist/record/frame.d.ts.map +1 -1
  118. package/dist/record/main-pass-geometry.d.ts +14 -1
  119. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  120. package/dist/record/main-pass-material.d.ts +7 -17
  121. package/dist/record/main-pass-material.d.ts.map +1 -1
  122. package/dist/record/main-pass.d.ts.map +1 -1
  123. package/dist/record/mesh-ssbo.d.ts +11 -3
  124. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  125. package/dist/record/recovery-pipeline.d.ts +30 -0
  126. package/dist/record/recovery-pipeline.d.ts.map +1 -0
  127. package/dist/record/render-context.d.ts +26 -4
  128. package/dist/record/render-context.d.ts.map +1 -1
  129. package/dist/record/typed-frame-graph.d.ts +1 -1
  130. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  131. package/dist/recovery/render-system-candidate.d.ts +65 -0
  132. package/dist/recovery/render-system-candidate.d.ts.map +1 -0
  133. package/dist/render-contract.d.ts +64 -7
  134. package/dist/render-contract.d.ts.map +1 -1
  135. package/dist/render-system-extract-tail.d.ts +12 -0
  136. package/dist/render-system-extract-tail.d.ts.map +1 -0
  137. package/dist/render-system-extract.d.ts +156 -22
  138. package/dist/render-system-extract.d.ts.map +1 -1
  139. package/dist/render-system.d.ts +128 -4
  140. package/dist/render-system.d.ts.map +1 -1
  141. package/dist/scene/render-scene.d.ts +19 -0
  142. package/dist/scene/render-scene.d.ts.map +1 -1
  143. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  144. package/dist/temporal/index.mjs +6 -6
  145. package/dist/temporal/taa-history-store.d.ts +15 -1
  146. package/dist/temporal/taa-history-store.d.ts.map +1 -1
  147. package/dist/temporal/transaction.d.ts +5 -0
  148. package/dist/temporal/transaction.d.ts.map +1 -1
  149. package/package.json +20 -20
  150. package/src/__tests__/device-feature-admission.unit.test.ts +6 -0
  151. package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
  152. package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
  153. package/src/__tests__/environment-generation.integration.test.ts +23 -0
  154. package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
  155. package/src/__tests__/factory-contract.integration.test.ts +122 -7
  156. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
  157. package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
  158. package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
  159. package/src/__tests__/material-contract-inventory.unit.test.ts +24 -9
  160. package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
  161. package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
  162. package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
  163. package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
  164. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
  165. package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
  166. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +1 -1
  167. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
  168. package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
  169. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
  170. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +85 -18
  171. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
  172. package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
  173. package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
  174. package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
  175. package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
  176. package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
  177. package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
  178. package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
  179. package/src/__tests__/shader-manifest-fixture.ts +13 -16
  180. package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
  181. package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
  182. package/src/__tests__/transmission-prewarm.unit.test.ts +95 -85
  183. package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
  184. package/src/assembly/bundler-contract.ts +9 -0
  185. package/src/assembly/device-feature-admission.ts +15 -5
  186. package/src/assembly/extended-lighting-fallback.ts +216 -0
  187. package/src/assembly/factory.ts +81 -10088
  188. package/src/assembly/material/assembly.ts +18 -2
  189. package/src/assembly/material/standard-layer-projection.ts +16 -0
  190. package/src/assembly/material-shader-policy.ts +854 -0
  191. package/src/assembly/material-shader-prewarm.ts +88 -0
  192. package/src/assembly/mesh-ssbo-grow.ts +270 -0
  193. package/src/assembly/recovery/device-loss-fanout.ts +89 -0
  194. package/src/assembly/recovery/generation.ts +141 -0
  195. package/src/assembly/recovery/recovery-attempt.ts +800 -0
  196. package/src/assembly/recovery/renderer-recover.ts +906 -0
  197. package/src/assembly/renderer-facade.ts +342 -0
  198. package/src/assembly/renderer-frame-transaction.ts +45 -0
  199. package/src/assembly/renderer-helpers.ts +99 -0
  200. package/src/assembly/renderer-lifecycle.ts +159 -0
  201. package/src/assembly/shader-prewarm-policy.ts +157 -0
  202. package/src/assembly/webgpu-ready-contract.ts +14 -0
  203. package/src/assembly/webgpu-ready.ts +4079 -0
  204. package/src/assembly/webgpu-renderer.ts +4095 -0
  205. package/src/components/camera.ts +1 -1
  206. package/src/debug-draw-glue.ts +1 -1
  207. package/src/device/device-scope.ts +35 -6
  208. package/src/device/gpu-residency.ts +194 -26
  209. package/src/environment/frame.ts +6 -0
  210. package/src/environment/inspection.ts +10 -0
  211. package/src/environment/lifecycle.ts +114 -8
  212. package/src/errors/recover.ts +76 -5
  213. package/src/errors/render.ts +17 -0
  214. package/src/extract/contracts.ts +974 -0
  215. package/src/features/host.ts +16 -0
  216. package/src/features/prepared-gpu-work.ts +20 -2
  217. package/src/features/prepared-graphics-store.ts +17 -0
  218. package/src/gpu-driven/production-raster.ts +97 -2
  219. package/src/gpu-scene.ts +3 -2
  220. package/src/ibl/IblPipelineCache.ts +134 -50
  221. package/src/ibl/skylight-bind-group.ts +26 -0
  222. package/src/index.ts +9 -1
  223. package/src/light-buffer-layout.ts +3 -0
  224. package/src/materials.ts +262 -45
  225. package/src/mesh-material-bindings.ts +59 -0
  226. package/src/occlusion-proxy.ts +64 -0
  227. package/src/pbr-pipeline.ts +139 -15
  228. package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
  229. package/src/points-lines/prepare.ts +9 -0
  230. package/src/points-lines/standard-owner.ts +403 -0
  231. package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
  232. package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
  233. package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
  234. package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
  235. package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
  236. package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
  237. package/src/record/__tests__/temporal-payload.unit.test.ts +22 -43
  238. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -1
  239. package/src/record/frame-snapshot.ts +15 -0
  240. package/src/record/frame-targets.ts +11 -1
  241. package/src/record/frame.ts +6 -2
  242. package/src/record/helpers.ts +1 -1
  243. package/src/record/main-pass-geometry.ts +230 -5
  244. package/src/record/main-pass-material.ts +316 -209
  245. package/src/record/main-pass-sprite-draws.ts +6 -7
  246. package/src/record/main-pass.ts +94 -6
  247. package/src/record/mesh-ssbo.ts +32 -10
  248. package/src/record/recovery-pipeline.ts +695 -0
  249. package/src/record/render-context.ts +35 -4
  250. package/src/record/typed-frame-graph.ts +45 -1
  251. package/src/record/view-ubo.ts +3 -3
  252. package/src/recovery/render-system-candidate.ts +839 -0
  253. package/src/render-contract.ts +65 -7
  254. package/src/render-system-extract-tail.ts +2983 -0
  255. package/src/render-system-extract.ts +124 -2977
  256. package/src/render-system.ts +868 -659
  257. package/src/scene/render-scene.ts +124 -1
  258. package/src/temporal/frame-coordinator.ts +4 -1
  259. package/src/temporal/taa-history-store.ts +46 -2
  260. package/src/temporal/temporal-view.ts +1 -1
  261. package/src/temporal/transaction.ts +9 -0
  262. package/src/tilemap-chunk-extract-system.ts +1 -1
  263. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
  264. package/dist/chunk-4L3P6DT5.mjs.map +0 -1
  265. package/dist/chunk-4NPUASZR.mjs.map +0 -1
  266. package/dist/chunk-DIYW3M5D.mjs.map +0 -1
  267. package/dist/chunk-FBC4R44J.mjs.map +0 -1
  268. package/dist/chunk-HZUPSYW7.mjs.map +0 -1
  269. package/dist/chunk-QODA5UKU.mjs.map +0 -1
  270. package/dist/chunk-YALPS44A.mjs.map +0 -1
  271. package/dist/chunk-ZUWKAZ2I.mjs.map +0 -1
@@ -0,0 +1,113 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ const source = readFileSync(new URL('../device/gpu-residency.ts', import.meta.url), 'utf8');
5
+ const iblSource = readFileSync(new URL('../ibl/IblPipelineCache.ts', import.meta.url), 'utf8');
6
+
7
+ describe('IBL residency publication', () => {
8
+ it('publishes ready and the idempotent cube only after submission succeeds', () => {
9
+ const precomputeCall = source.indexOf('const runResult = await runIblPrecompute({');
10
+ const precomputeSuccess = source.indexOf('if (!runResult.ok)', precomputeCall);
11
+ const sourceReady = source.indexOf("status: 'ready'", precomputeCall);
12
+ const cubeRegistration = source.indexOf('const cubeHandle = regResult.value;', precomputeCall);
13
+ const mapPublication = source.indexOf(
14
+ 'this.cubemapIdempotentMap.set(sourceId, cubeHandle);',
15
+ precomputeCall,
16
+ );
17
+ expect(precomputeCall).toBeGreaterThan(-1);
18
+ expect(precomputeSuccess).toBeGreaterThan(precomputeCall);
19
+ expect(sourceReady).toBeGreaterThan(precomputeSuccess);
20
+ expect(cubeRegistration).toBeGreaterThan(precomputeSuccess);
21
+ expect(mapPublication).toBeGreaterThan(precomputeSuccess);
22
+ });
23
+
24
+ it('keeps pending publication before asynchronous pipeline work', () => {
25
+ const pending = source.indexOf("status: 'pending'");
26
+ const precompute = source.indexOf('const runResult = await runIblPrecompute({');
27
+ expect(pending).toBeGreaterThan(-1);
28
+ expect(pending).toBeLessThan(precompute);
29
+ });
30
+
31
+ it('promotes the IBL candidate only after the completion fence', () => {
32
+ const runStart = iblSource.indexOf('export async function runIblPrecompute(');
33
+ const fence = iblSource.indexOf('await device.queue.onSubmittedWorkDone()', runStart);
34
+ const promotion = iblSource.indexOf(
35
+ 'cache.irradianceTexture = promoted.irradianceTexture',
36
+ runStart,
37
+ );
38
+ const ready = source.indexOf("status: 'ready'", source.indexOf('_uploadCubemapFromEquirect'));
39
+ expect(runStart).toBeGreaterThan(-1);
40
+ expect(fence).toBeGreaterThan(runStart);
41
+ expect(promotion).toBeGreaterThan(fence);
42
+ expect(ready).toBeGreaterThan(fence);
43
+ expect(iblSource.slice(runStart, fence)).not.toContain('cache.irradianceTexture =');
44
+ expect(iblSource.slice(runStart, fence)).not.toContain('cache.prefilterTexture =');
45
+ expect(iblSource.slice(runStart, fence)).not.toContain('cache.brdfLutTexture =');
46
+ });
47
+
48
+ it('awaits precompute before residency publication', () => {
49
+ const call = source.indexOf('const runResult = await runIblPrecompute({');
50
+ const success = source.indexOf('if (!runResult.ok)', call);
51
+ const ready = source.indexOf("status: 'ready'", call);
52
+ const map = source.indexOf('this.cubemapIdempotentMap.set(sourceId, cubeHandle);', call);
53
+ expect(call).toBeGreaterThan(-1);
54
+ expect(success).toBeGreaterThan(call);
55
+ expect(ready).toBeGreaterThan(success);
56
+ expect(map).toBeGreaterThan(success);
57
+ });
58
+
59
+ it('keeps candidate and transient resources under the DeviceScope owner', () => {
60
+ expect(iblSource).toContain("scope._adopt('texture', texture");
61
+ expect(source).toContain("scope._adopt('texture', equirectGpuTex");
62
+ expect(source).toContain("scope._adopt('buffer', faceBuf");
63
+ expect(source).toContain("scope._adopt('buffer', prefBuf");
64
+ expect(source).toContain("scope._adopt('buffer', cubeVertex");
65
+ const fence = iblSource.indexOf('await device.queue.onSubmittedWorkDone()');
66
+ const candidateWrites = iblSource.indexOf('candidate.irradianceTexture = adoptIblTexture');
67
+ expect(candidateWrites).toBeLessThan(fence);
68
+ });
69
+
70
+ it('fences each IBL stage before recording the next stage', () => {
71
+ const runStart = iblSource.indexOf('export async function runIblPrecompute(');
72
+ const stages = ['equirect-to-cube', 'prefilter', 'brdf-lut'];
73
+ let previous = runStart;
74
+ for (const stage of stages) {
75
+ const marker = iblSource.indexOf(`await submitStage('${stage}'`, previous);
76
+ expect(marker).toBeGreaterThan(previous);
77
+ previous = marker;
78
+ }
79
+ expect(iblSource.match(/await submitStage\('/g)).toHaveLength(stages.length);
80
+ expect(iblSource).toContain('await device.queue.onSubmittedWorkDone()');
81
+ expect(
82
+ iblSource.indexOf('cache.irradianceTexture = promoted.irradianceTexture'),
83
+ ).toBeGreaterThan(previous);
84
+ });
85
+
86
+ it('does not promote or increment counters after a failed stage', () => {
87
+ const runStart = iblSource.indexOf('export async function runIblPrecompute(');
88
+ const promotion = iblSource.indexOf(
89
+ 'cache.irradianceTexture = promoted.irradianceTexture',
90
+ runStart,
91
+ );
92
+ const counters = iblSource.indexOf('cache.irradianceBakeCount += 1', runStart);
93
+ const stageFailure = iblSource.indexOf('return err(badAlloc', runStart);
94
+ expect(stageFailure).toBeGreaterThan(runStart);
95
+ expect(stageFailure).toBeLessThan(promotion);
96
+ expect(promotion).toBeLessThan(counters);
97
+ expect(iblSource).toContain('stage}-device-scope-generation');
98
+ });
99
+
100
+ it('fences each irradiance face before the next face', () => {
101
+ const runStart = iblSource.indexOf('export async function runIblPrecompute(');
102
+ const irradianceStart = iblSource.indexOf('// (b) irradiance convolve', runStart);
103
+ const prefilterStart = iblSource.indexOf('// (c) prefilter env', irradianceStart);
104
+ const faceMarkers = iblSource.match(/submitStage\(`irradiance-face-\$\{face\}`/g) ?? [];
105
+ expect(faceMarkers).toHaveLength(1);
106
+ expect(iblSource.slice(irradianceStart, prefilterStart)).toMatch(
107
+ /await submitStage\(`irradiance-face-\$\{face\}`, true\)/,
108
+ );
109
+ expect(iblSource.slice(irradianceStart, prefilterStart)).not.toContain(
110
+ "await submitStage('irradiance', true)",
111
+ );
112
+ });
113
+ });
@@ -36,15 +36,30 @@ describe('vertex color does not change the material contract', () => {
36
36
 
37
37
  it('keeps the standard authoring parameters derived from the shared schema', () => {
38
38
  const material = Materials.standard({ baseColor: [1, 1, 1, 1] });
39
- expect(material.parameters?.map((parameter) => parameter.name)).toEqual(
40
- DEFAULT_STANDARD_PBR_PARAM_SCHEMA.map((entry) => entry.name),
41
- );
42
- expect(material.values).toMatchObject({
43
- transmission: 0,
44
- ior: 1.5,
45
- thickness: 0,
46
- attenuationColor: [1, 1, 1],
47
- });
39
+ expect(material.parameters?.map((parameter) => parameter.name)).toEqual([
40
+ 'baseColor',
41
+ 'metallic',
42
+ 'roughness',
43
+ 'metallicChannel',
44
+ 'roughnessChannel',
45
+ 'aoChannel',
46
+ 'extraChannel',
47
+ 'emissive',
48
+ 'emissiveIntensity',
49
+ 'occlusionStrength',
50
+ 'alphaCutoff',
51
+ 'specular',
52
+ 'specularColor',
53
+ 'normalScale',
54
+ 'ior',
55
+ 'baseColorTexture',
56
+ 'metallicRoughnessTexture',
57
+ 'normalTexture',
58
+ 'emissiveTexture',
59
+ 'occlusionTexture',
60
+ ]);
61
+ expect(material.values).not.toHaveProperty('transmission');
62
+ expect(material.values).toHaveProperty('ior', 1.5);
48
63
  });
49
64
 
50
65
  it('routes active transmission to Forward and ShadowCaster only', () => {
@@ -1,8 +1,10 @@
1
1
  import { DEFAULT_STANDARD_PBR_PARAM_SCHEMA } from '@forgeax/engine-shader';
2
+ import type { MaterialParameter, MaterialPass } from '@forgeax/engine-types';
2
3
  import { derive } from '@forgeax/engine-types';
3
4
  import { describe, expect, it } from 'vitest';
5
+ import { projectStandardLayerPlan } from '../assembly/material/standard-layer-projection.js';
4
6
 
5
- describe('standard material cooked projection', () => {
7
+ describe('cooked material render projection', () => {
6
8
  it('uses one schema for reflection, uniform layout, and resource bindings', () => {
7
9
  const derived = derive(DEFAULT_STANDARD_PBR_PARAM_SCHEMA);
8
10
  const names = derived.numericMembers.map((member) => member.name);
@@ -19,4 +21,20 @@ describe('standard material cooked projection', () => {
19
21
  ]),
20
22
  );
21
23
  });
24
+
25
+ it('projects the prepared contract without reading authored values', () => {
26
+ const parameters: readonly MaterialParameter[] = [
27
+ { name: 'baseColor', type: 'color' },
28
+ { name: 'metallic', type: 'f32' },
29
+ { name: 'roughness', type: 'f32' },
30
+ ];
31
+ const passes: readonly MaterialPass[] = [
32
+ { name: 'forward', program: { module: 'forgeax_material::standard' } },
33
+ { name: 'deferred', program: { module: 'forgeax_material::standard' } },
34
+ { name: 'shadow-caster', program: { module: 'forgeax_material::standard' } },
35
+ ];
36
+ const plan = projectStandardLayerPlan(parameters, passes);
37
+ expect(plan.mode).toBe('base-only');
38
+ expect(plan.passFamily).toEqual(['forward', 'deferred', 'shadow']);
39
+ });
22
40
  });
@@ -1,4 +1,5 @@
1
1
  import type { CookedMaterialRecord } from '@forgeax/engine-pack';
2
+ import { deriveStandardLayerPlan } from '@forgeax/engine-types';
2
3
  import { describe, expect, it, vi } from 'vitest';
3
4
  import { assembleMaterialProjection } from '../assembly/material/assembly.js';
4
5
  import {
@@ -59,6 +60,34 @@ const record: CookedMaterialRecord = {
59
60
  };
60
61
 
61
62
  describe('cooked material static and dynamic routing', () => {
63
+ it('rejects a stale Standard layer plan before render projection', () => {
64
+ const standardPass = { name: 'forward', program: { module: 'forgeax::default-standard-pbr' } };
65
+ const standardResolved = { ...record.resolved, passes: [standardPass] };
66
+ const expected = deriveStandardLayerPlan(
67
+ standardResolved.parameters,
68
+ standardResolved.passes,
69
+ ).identity;
70
+ const standardRecord = {
71
+ ...record,
72
+ resolved: standardResolved,
73
+ receipt: {
74
+ ...record.receipt,
75
+ derivedInterface: { layoutIdentity: 'sha256:layout', layerPlanIdentity: expected },
76
+ },
77
+ } as CookedMaterialRecord;
78
+
79
+ expect(assembleMaterialProjection(standardRecord).layerPlan.identity).toBe(expected);
80
+ expect(() =>
81
+ assembleMaterialProjection({
82
+ ...standardRecord,
83
+ receipt: {
84
+ ...standardRecord.receipt,
85
+ derivedInterface: { layoutIdentity: 'sha256:layout', layerPlanIdentity: 'stale' },
86
+ },
87
+ }),
88
+ ).toThrow('Standard material layer-plan identity mismatch');
89
+ });
90
+
62
91
  it('updates runtime values without requesting a compile or changing the artifact key', () => {
63
92
  const projection = assembleMaterialProjection(record);
64
93
  const updated = updateMaterialRuntimeValues(projection, {
@@ -4,7 +4,7 @@ import { MATERIAL_PER_ENTITY_STRIDE } from '../render-system';
4
4
 
5
5
  const ownerSource = readFileSync(new URL('../record/render-context.ts', import.meta.url), 'utf8');
6
6
  const rendererFactorySource = readFileSync(
7
- new URL('../assembly/factory.ts', import.meta.url),
7
+ new URL('../assembly/webgpu-ready.ts', import.meta.url),
8
8
  'utf8',
9
9
  );
10
10
  const recordSources = [
@@ -16,8 +16,12 @@ const recordSources = [
16
16
 
17
17
  describe('material dynamic-offset stride surface', () => {
18
18
  it('keeps one internal owner and routes every record path through it', () => {
19
- expect(MATERIAL_PER_ENTITY_STRIDE).toBe(512);
20
- expect(ownerSource.match(/export const MATERIAL_PER_ENTITY_STRIDE\s*=\s*512/g)).toHaveLength(1);
19
+ expect(MATERIAL_PER_ENTITY_STRIDE).toBe(768);
20
+ expect(
21
+ ownerSource.match(
22
+ /export const MATERIAL_PER_ENTITY_STRIDE\s*=\s*Math\.ceil\(STANDARD_PBR_UBO_SIZE \/ 256\) \* 256/g,
23
+ ),
24
+ ).toHaveLength(1);
21
25
 
22
26
  for (const source of recordSources) {
23
27
  expect(source).toContain('MATERIAL_PER_ENTITY_STRIDE');
@@ -0,0 +1,104 @@
1
+ import type { Buffer, Sampler, TextureView } from '@forgeax/engine-rhi';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { assembleMaterialWithSkylightEntries } from '../ibl/skylight-bind-group.js';
4
+ import { Materials } from '../materials.js';
5
+
6
+ describe('Materials.standard clearcoat public contract', () => {
7
+ it('declares clearcoat only when the physical root is authored', () => {
8
+ const base = Materials.standard({ baseColor: [1, 1, 1, 1] });
9
+ const zero = Materials.standard({ baseColor: [1, 1, 1, 1], clearcoat: 0 });
10
+ expect(base.parameters?.map((parameter) => parameter.name)).not.toContain('clearcoat');
11
+ expect(zero.parameters?.map((parameter) => parameter.name)).toContain('clearcoat');
12
+ expect(zero.passes?.map((pass) => pass.name)).toEqual(['forward', 'shadow-caster']);
13
+ });
14
+
15
+ it('uses the R, G, and RG channel matrix without a guessed UV set', () => {
16
+ const options = {
17
+ baseColor: [1, 1, 1, 1] as const,
18
+ clearcoat: 0.5,
19
+ clearcoatRoughness: 0.25,
20
+ clearcoatTexture: 1,
21
+ clearcoatRoughnessTexture: 2,
22
+ clearcoatNormalTexture: 3,
23
+ clearcoatNormalScale: 0.75,
24
+ } as Parameters<typeof Materials.standard>[0] & Record<string, unknown>;
25
+ const material = Materials.standard(options);
26
+ expect(material.values).toMatchObject(options);
27
+ expect(material.values).not.toHaveProperty('clearcoatTexCoord');
28
+ });
29
+
30
+ it('keeps scalar clearcoat identity without charging unauthored map slots', () => {
31
+ const scalar = Materials.standard({
32
+ baseColor: [1, 1, 1, 1],
33
+ clearcoat: 0,
34
+ clearcoatRoughness: 0.5,
35
+ clearcoatNormalScale: 1,
36
+ });
37
+ const names = scalar.parameters?.map((parameter) => parameter.name) ?? [];
38
+ expect(names).toEqual(expect.arrayContaining(['clearcoat', 'clearcoatRoughness']));
39
+ expect(names).not.toEqual(
40
+ expect.arrayContaining([
41
+ 'clearcoatTexture',
42
+ 'clearcoatRoughnessTexture',
43
+ 'clearcoatNormalTexture',
44
+ ]),
45
+ );
46
+ });
47
+
48
+ it('declares each clearcoat map slot only when its root value is authored', () => {
49
+ const material = Materials.standard({
50
+ baseColor: [1, 1, 1, 1],
51
+ clearcoat: 1,
52
+ clearcoatTexture: 1,
53
+ clearcoatNormalTexture: 2,
54
+ });
55
+ const names = material.parameters?.map((parameter) => parameter.name) ?? [];
56
+ expect(names).toEqual(expect.arrayContaining(['clearcoatTexture', 'clearcoatNormalTexture']));
57
+ expect(names).not.toContain('clearcoatRoughnessTexture');
58
+ });
59
+
60
+ it('keeps physical scalar fields before texture coordinate records', () => {
61
+ const material = Materials.standard({
62
+ baseColor: [1, 1, 1, 1],
63
+ clearcoat: 0.5,
64
+ clearcoatRoughness: 0.25,
65
+ clearcoatTexture: 1,
66
+ });
67
+ const names = material.parameters?.map((parameter) => parameter.name) ?? [];
68
+ expect(names.indexOf('clearcoat')).toBeLessThan(names.indexOf('baseColorTexture'));
69
+ expect(names.indexOf('clearcoatRoughness')).toBeLessThan(names.indexOf('baseColorTexture'));
70
+ });
71
+
72
+ it('keeps authored clearcoat map pairs contiguous after the material ABI', () => {
73
+ const sampler = {} as Sampler;
74
+ const view = {} as TextureView;
75
+ const entries = Array.from({ length: 17 }, (_, binding) => ({
76
+ binding,
77
+ resource:
78
+ binding === 0
79
+ ? { kind: 'buffer' as const, value: { buffer: {} as Buffer } }
80
+ : binding % 2 === 1
81
+ ? { kind: 'sampler' as const, value: sampler }
82
+ : { kind: 'textureView' as const, value: view },
83
+ }));
84
+ const merged = assembleMaterialWithSkylightEntries(
85
+ entries,
86
+ {
87
+ irradianceView: view,
88
+ irradianceSampler: sampler,
89
+ prefilterView: view,
90
+ prefilterSampler: sampler,
91
+ brdfLutView: view,
92
+ brdfLutSampler: sampler,
93
+ intensityBuffer: {} as Buffer,
94
+ },
95
+ undefined,
96
+ [
97
+ { slot: 0, sampler, view },
98
+ { slot: 1, sampler, view },
99
+ { slot: 2, sampler, view },
100
+ ],
101
+ );
102
+ expect(merged.slice(-6).map((entry) => entry.binding)).toEqual([26, 27, 28, 29, 30, 31]);
103
+ });
104
+ });
@@ -14,7 +14,7 @@ import {
14
14
  const ownerSource = readFileSync(new URL('../gpu-usage.ts', import.meta.url), 'utf8');
15
15
  const meshSsboSource = readFileSync(new URL('../record/mesh-ssbo.ts', import.meta.url), 'utf8');
16
16
  const rendererFactorySource = readFileSync(
17
- new URL('../assembly/factory.ts', import.meta.url),
17
+ new URL('../assembly/webgpu-ready.ts', import.meta.url),
18
18
  'utf8',
19
19
  );
20
20
  const consumerSources = [
@@ -0,0 +1,89 @@
1
+ import { STANDARD_PIPELINE_PARAM_SCHEMA } from '@forgeax/engine-shader';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { Materials } from '../materials.js';
4
+ import {
5
+ buildBindGroupLayoutDescriptor,
6
+ materialBindGroupLayoutIdentity,
7
+ } from '../pbr-pipeline.js';
8
+
9
+ describe('Standard clearcoat root contract', () => {
10
+ it('keeps a declared factor-zero clearcoat physical and forward-only', () => {
11
+ const material = Materials.standard({ baseColor: [1, 1, 1, 1], clearcoat: 0 });
12
+ expect(material.passes?.map((pass) => pass.name)).toEqual(['forward', 'shadow-caster']);
13
+ expect(material.parameters?.map((parameter) => parameter.name)).toContain('clearcoat');
14
+ expect(material.values).toMatchObject({ clearcoat: 0 });
15
+ });
16
+
17
+ it('retains a single root identity when coat maps are present', () => {
18
+ const options = {
19
+ baseColor: [1, 1, 1, 1] as const,
20
+ clearcoat: 0.8,
21
+ clearcoatRoughness: 0.2,
22
+ clearcoatTexture: 11,
23
+ clearcoatRoughnessTexture: 12,
24
+ clearcoatNormalTexture: 13,
25
+ clearcoatNormalScale: 0.5,
26
+ } as Parameters<typeof Materials.standard>[0] & Record<string, unknown>;
27
+ const material = Materials.standard(options);
28
+ expect(material.values).toMatchObject({
29
+ clearcoat: 0.8,
30
+ clearcoatRoughness: 0.2,
31
+ clearcoatTexture: 11,
32
+ clearcoatRoughnessTexture: 12,
33
+ clearcoatNormalTexture: 13,
34
+ clearcoatNormalScale: 0.5,
35
+ });
36
+ });
37
+
38
+ it('keys equivalent schema instances by the same merged layout identity', () => {
39
+ const withTexture = (name: string) => [
40
+ ...STANDARD_PIPELINE_PARAM_SCHEMA,
41
+ { name, type: 'texture2d' as const },
42
+ ];
43
+ const rigidR = withTexture('clearcoatTexture');
44
+ const rigidRClone = [...rigidR];
45
+ const rigidG = withTexture('clearcoatRoughnessTexture');
46
+ const rigidRg = [...rigidR, { name: 'clearcoatNormalTexture', type: 'texture2d' as const }];
47
+
48
+ expect(materialBindGroupLayoutIdentity('forgeax::default-standard-pbr', rigidR)).toBe(
49
+ materialBindGroupLayoutIdentity('forgeax::default-standard-pbr', rigidRClone),
50
+ );
51
+ expect(materialBindGroupLayoutIdentity('forgeax::default-standard-pbr', rigidR)).not.toBe(
52
+ materialBindGroupLayoutIdentity('forgeax::default-standard-pbr', rigidG),
53
+ );
54
+ expect(materialBindGroupLayoutIdentity('forgeax::default-standard-pbr', rigidR)).not.toBe(
55
+ materialBindGroupLayoutIdentity('forgeax::default-standard-pbr', rigidRg),
56
+ );
57
+ });
58
+
59
+ it('keeps the built-in Standard IBL ABI stable when physical maps are appended', () => {
60
+ const schema = [
61
+ ...STANDARD_PIPELINE_PARAM_SCHEMA,
62
+ { name: 'clearcoatTexture', type: 'texture2d' as const },
63
+ { name: 'clearcoatNormalTexture', type: 'texture2d' as const },
64
+ ];
65
+ const descriptor = buildBindGroupLayoutDescriptor(
66
+ {
67
+ shader: { id: 'forgeax::default-standard-pbr', passKind: 'forward', variantSet: undefined },
68
+ attachments: { colorFormats: [], depthFormat: undefined, sampleCount: 1 },
69
+ geometry: { topology: 'triangle-list', vertexLayout: {} },
70
+ renderState: undefined,
71
+ },
72
+ { kind: 'pbr-material-merged', materialParamSchema: schema },
73
+ );
74
+
75
+ // The canonical seven texture pairs occupy bindings 1..14. IBL remains
76
+ // at 15..21 and transmission at 22..23; authored physical maps append at
77
+ // 24..27. Compacting the authored root here shifts WGSL IBL bindings and
78
+ // causes Dawn to reject the pipeline before any pixels are submitted.
79
+ expect(descriptor.entries).toHaveLength(28);
80
+ expect(descriptor.entries.slice(0, 15).map((entry) => entry.binding)).toEqual(
81
+ Array.from({ length: 15 }, (_, binding) => binding),
82
+ );
83
+ expect(descriptor.entries[15]?.texture).toMatchObject({ viewDimension: 'cube' });
84
+ expect(descriptor.entries[17]?.texture).toMatchObject({ viewDimension: 'cube' });
85
+ expect(descriptor.entries[22]?.sampler).toMatchObject({ type: 'filtering' });
86
+ expect(descriptor.entries[23]?.texture).toMatchObject({ viewDimension: '2d' });
87
+ expect(descriptor.entries.slice(24).map((entry) => entry.binding)).toEqual([24, 25, 26, 27]);
88
+ });
89
+ });
@@ -8,7 +8,7 @@ const textureNames = [
8
8
  'baseColorTexture',
9
9
  'metallicRoughnessTexture',
10
10
  'normalTexture',
11
- 'specularTintTexture',
11
+ 'specularColorTexture',
12
12
  'emissiveTexture',
13
13
  'occlusionTexture',
14
14
  'transmissionTexture',
@@ -15,7 +15,7 @@ describe('prepared graphics material pipeline warmup', () => {
15
15
  });
16
16
 
17
17
  it('keeps view-only material bindings on the canonical view group with depth', () => {
18
- const declaration = ownerSource.indexOf('const preparedViewOnlyPipelines');
18
+ const declaration = ownerSource.indexOf('preparedViewOnlyPipelines: new WeakSet()');
19
19
  const classification = ownerSource.indexOf('preparedViewOnlyPipelines.add', declaration);
20
20
  const bindingResolution = ownerSource.indexOf(
21
21
  'preparedViewOnlyPipelines.has(pipeline as object)',
@@ -14,14 +14,14 @@ const evidence = [
14
14
  {
15
15
  code: 'recover-not-needed',
16
16
  expected:
17
- 'renderer is healthy; call health() first to confirm degraded state before calling recover()',
18
- hint: 'call health() first to confirm degraded state before calling recover()',
17
+ 'renderer is healthy; use state() or inspect() to confirm degraded state before calling recover()',
18
+ hint: 'use state() or inspect().state to confirm degraded state before calling recover()',
19
19
  message: 'recover-not-needed: renderer is not in a degraded state',
20
20
  },
21
21
  {
22
22
  code: 'recover-not-implemented',
23
23
  expected: 'recovery is not yet implemented; self-heal lands in S5',
24
- hint: 'self-heal recovery lands in S5; health().reason still reflects the degraded state',
24
+ hint: 'self-heal recovery lands in S5; inspect().state still reflects the lifecycle state',
25
25
  message: 'recover-not-implemented: self-heal recovery is not yet implemented',
26
26
  },
27
27
  {
@@ -47,6 +47,9 @@ describe('RecoverError policy ownership', () => {
47
47
 
48
48
  expect(source).toContain('export type RecoverErrorCode = keyof typeof RECOVER_ERROR_POLICY;');
49
49
  expect(source).not.toMatch(/export type RecoverErrorCode = 'recover-/);
50
+ expect(source).not.toContain('health()');
51
+ expect(source).toContain('state()');
52
+ expect(source).toContain('inspect()');
50
53
  });
51
54
 
52
55
  it('preserves the exact four-code vocabulary and order', () => {
@@ -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
+ });