@forgeax/engine-render 0.1.23 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/README.md +131 -9
  2. package/dist/assembly/bundler-contract.d.ts +9 -0
  3. package/dist/assembly/bundler-contract.d.ts.map +1 -0
  4. package/dist/assembly/device-feature-admission.d.ts +8 -0
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -1
  6. package/dist/assembly/extended-lighting-fallback.d.ts +13 -0
  7. package/dist/assembly/extended-lighting-fallback.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +7 -243
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/material/assembly.d.ts +3 -2
  11. package/dist/assembly/material/assembly.d.ts.map +1 -1
  12. package/dist/assembly/material/standard-layer-projection.d.ts +10 -0
  13. package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -0
  14. package/dist/assembly/material-shader-policy.d.ts +168 -0
  15. package/dist/assembly/material-shader-policy.d.ts.map +1 -0
  16. package/dist/assembly/material-shader-prewarm.d.ts +21 -0
  17. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
  18. package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
  19. package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
  20. package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
  21. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
  22. package/dist/assembly/recovery/generation.d.ts +46 -0
  23. package/dist/assembly/recovery/generation.d.ts.map +1 -0
  24. package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
  25. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
  26. package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
  27. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
  28. package/dist/assembly/renderer-facade.d.ts +10 -0
  29. package/dist/assembly/renderer-facade.d.ts.map +1 -0
  30. package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
  31. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  32. package/dist/assembly/renderer-helpers.d.ts +19 -0
  33. package/dist/assembly/renderer-helpers.d.ts.map +1 -0
  34. package/dist/assembly/renderer-lifecycle.d.ts +34 -0
  35. package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
  36. package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
  37. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
  38. package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
  39. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
  40. package/dist/assembly/webgpu-ready.d.ts +55 -0
  41. package/dist/assembly/webgpu-ready.d.ts.map +1 -0
  42. package/dist/assembly/webgpu-renderer.d.ts +152 -0
  43. package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
  44. package/dist/authoring.mjs +1 -1
  45. package/dist/{chunk-4L3P6DT5.mjs → chunk-GE3SDD72.mjs} +37 -5
  46. package/dist/chunk-GE3SDD72.mjs.map +1 -0
  47. package/dist/{chunk-HZUPSYW7.mjs → chunk-KZFTK6DO.mjs} +102 -15
  48. package/dist/chunk-KZFTK6DO.mjs.map +1 -0
  49. package/dist/{chunk-FF7K3FSO.mjs → chunk-Q752SHI5.mjs} +89 -89
  50. package/dist/{chunk-FF7K3FSO.mjs.map → chunk-Q752SHI5.mjs.map} +1 -1
  51. package/dist/{chunk-QODA5UKU.mjs → chunk-RR5L4SRS.mjs} +2 -2
  52. package/dist/chunk-RR5L4SRS.mjs.map +1 -0
  53. package/dist/{chunk-YALPS44A.mjs → chunk-TFH5I7JJ.mjs} +47 -47
  54. package/dist/chunk-TFH5I7JJ.mjs.map +1 -0
  55. package/dist/{chunk-FBC4R44J.mjs → chunk-UQYS7FRM.mjs} +57 -57
  56. package/dist/chunk-UQYS7FRM.mjs.map +1 -0
  57. package/dist/{chunk-4NPUASZR.mjs → chunk-YKB5DQV7.mjs} +161 -19
  58. package/dist/chunk-YKB5DQV7.mjs.map +1 -0
  59. package/dist/{chunk-ZUWKAZ2I.mjs → chunk-YRUYGA4A.mjs} +615 -237
  60. package/dist/chunk-YRUYGA4A.mjs.map +1 -0
  61. package/dist/{chunk-DIYW3M5D.mjs → chunk-Z3YYH2MB.mjs} +2210 -2213
  62. package/dist/chunk-Z3YYH2MB.mjs.map +1 -0
  63. package/dist/construct-renderer.mjs +19841 -16190
  64. package/dist/construct-renderer.mjs.map +1 -1
  65. package/dist/device/device-scope.d.ts +2 -0
  66. package/dist/device/device-scope.d.ts.map +1 -1
  67. package/dist/device/gpu-residency.d.ts +24 -4
  68. package/dist/device/gpu-residency.d.ts.map +1 -1
  69. package/dist/environment/frame.d.ts +5 -0
  70. package/dist/environment/frame.d.ts.map +1 -1
  71. package/dist/environment/inspection.d.ts +7 -0
  72. package/dist/environment/inspection.d.ts.map +1 -1
  73. package/dist/environment/lifecycle.d.ts +20 -2
  74. package/dist/environment/lifecycle.d.ts.map +1 -1
  75. package/dist/errors/recover.d.ts +58 -2
  76. package/dist/errors/recover.d.ts.map +1 -1
  77. package/dist/errors/render.d.ts +13 -0
  78. package/dist/errors/render.d.ts.map +1 -1
  79. package/dist/extract/contracts.d.ts +899 -0
  80. package/dist/extract/contracts.d.ts.map +1 -0
  81. package/dist/features/host.d.ts +2 -0
  82. package/dist/features/host.d.ts.map +1 -1
  83. package/dist/features/prepared-gpu-work.d.ts +3 -0
  84. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  85. package/dist/features/prepared-graphics-store.d.ts +2 -0
  86. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  87. package/dist/gpu-driven/production-raster.d.ts +28 -0
  88. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  89. package/dist/gpu-scene.d.ts +1 -0
  90. package/dist/gpu-scene.d.ts.map +1 -1
  91. package/dist/ibl/IblPipelineCache.d.ts +7 -6
  92. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  93. package/dist/ibl/skylight-bind-group.d.ts +7 -1
  94. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  95. package/dist/index.d.ts +1 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.mjs +6 -6
  98. package/dist/internal.mjs +6 -6
  99. package/dist/light-buffer-layout.d.ts +3 -0
  100. package/dist/light-buffer-layout.d.ts.map +1 -1
  101. package/dist/materials.d.ts +34 -10
  102. package/dist/materials.d.ts.map +1 -1
  103. package/dist/mesh-material-bindings.d.ts +56 -0
  104. package/dist/mesh-material-bindings.d.ts.map +1 -1
  105. package/dist/occlusion-proxy.d.ts +4 -0
  106. package/dist/occlusion-proxy.d.ts.map +1 -0
  107. package/dist/pbr-pipeline.d.ts +32 -9
  108. package/dist/pbr-pipeline.d.ts.map +1 -1
  109. package/dist/points-lines/prepare.d.ts +3 -0
  110. package/dist/points-lines/prepare.d.ts.map +1 -1
  111. package/dist/points-lines/standard-owner.d.ts +33 -0
  112. package/dist/points-lines/standard-owner.d.ts.map +1 -0
  113. package/dist/record/frame-snapshot.d.ts +5 -0
  114. package/dist/record/frame-snapshot.d.ts.map +1 -1
  115. package/dist/record/frame-targets.d.ts.map +1 -1
  116. package/dist/record/frame.d.ts +14 -2
  117. package/dist/record/frame.d.ts.map +1 -1
  118. package/dist/record/main-pass-geometry.d.ts +14 -1
  119. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  120. package/dist/record/main-pass-material.d.ts +7 -17
  121. package/dist/record/main-pass-material.d.ts.map +1 -1
  122. package/dist/record/main-pass.d.ts.map +1 -1
  123. package/dist/record/mesh-ssbo.d.ts +11 -3
  124. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  125. package/dist/record/recovery-pipeline.d.ts +30 -0
  126. package/dist/record/recovery-pipeline.d.ts.map +1 -0
  127. package/dist/record/render-context.d.ts +26 -4
  128. package/dist/record/render-context.d.ts.map +1 -1
  129. package/dist/record/typed-frame-graph.d.ts +1 -1
  130. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  131. package/dist/recovery/render-system-candidate.d.ts +65 -0
  132. package/dist/recovery/render-system-candidate.d.ts.map +1 -0
  133. package/dist/render-contract.d.ts +64 -7
  134. package/dist/render-contract.d.ts.map +1 -1
  135. package/dist/render-system-extract-tail.d.ts +12 -0
  136. package/dist/render-system-extract-tail.d.ts.map +1 -0
  137. package/dist/render-system-extract.d.ts +156 -22
  138. package/dist/render-system-extract.d.ts.map +1 -1
  139. package/dist/render-system.d.ts +128 -4
  140. package/dist/render-system.d.ts.map +1 -1
  141. package/dist/scene/render-scene.d.ts +19 -0
  142. package/dist/scene/render-scene.d.ts.map +1 -1
  143. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  144. package/dist/temporal/index.mjs +6 -6
  145. package/dist/temporal/taa-history-store.d.ts +15 -1
  146. package/dist/temporal/taa-history-store.d.ts.map +1 -1
  147. package/dist/temporal/transaction.d.ts +5 -0
  148. package/dist/temporal/transaction.d.ts.map +1 -1
  149. package/package.json +20 -20
  150. package/src/__tests__/device-feature-admission.unit.test.ts +6 -0
  151. package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
  152. package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
  153. package/src/__tests__/environment-generation.integration.test.ts +23 -0
  154. package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
  155. package/src/__tests__/factory-contract.integration.test.ts +122 -7
  156. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
  157. package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
  158. package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
  159. package/src/__tests__/material-contract-inventory.unit.test.ts +24 -9
  160. package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
  161. package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
  162. package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
  163. package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
  164. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
  165. package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
  166. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +1 -1
  167. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
  168. package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
  169. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
  170. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +85 -18
  171. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
  172. package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
  173. package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
  174. package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
  175. package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
  176. package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
  177. package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
  178. package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
  179. package/src/__tests__/shader-manifest-fixture.ts +13 -16
  180. package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
  181. package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
  182. package/src/__tests__/transmission-prewarm.unit.test.ts +95 -85
  183. package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
  184. package/src/assembly/bundler-contract.ts +9 -0
  185. package/src/assembly/device-feature-admission.ts +15 -5
  186. package/src/assembly/extended-lighting-fallback.ts +216 -0
  187. package/src/assembly/factory.ts +81 -10088
  188. package/src/assembly/material/assembly.ts +18 -2
  189. package/src/assembly/material/standard-layer-projection.ts +16 -0
  190. package/src/assembly/material-shader-policy.ts +854 -0
  191. package/src/assembly/material-shader-prewarm.ts +88 -0
  192. package/src/assembly/mesh-ssbo-grow.ts +270 -0
  193. package/src/assembly/recovery/device-loss-fanout.ts +89 -0
  194. package/src/assembly/recovery/generation.ts +141 -0
  195. package/src/assembly/recovery/recovery-attempt.ts +800 -0
  196. package/src/assembly/recovery/renderer-recover.ts +906 -0
  197. package/src/assembly/renderer-facade.ts +342 -0
  198. package/src/assembly/renderer-frame-transaction.ts +45 -0
  199. package/src/assembly/renderer-helpers.ts +99 -0
  200. package/src/assembly/renderer-lifecycle.ts +159 -0
  201. package/src/assembly/shader-prewarm-policy.ts +157 -0
  202. package/src/assembly/webgpu-ready-contract.ts +14 -0
  203. package/src/assembly/webgpu-ready.ts +4079 -0
  204. package/src/assembly/webgpu-renderer.ts +4095 -0
  205. package/src/components/camera.ts +1 -1
  206. package/src/debug-draw-glue.ts +1 -1
  207. package/src/device/device-scope.ts +35 -6
  208. package/src/device/gpu-residency.ts +194 -26
  209. package/src/environment/frame.ts +6 -0
  210. package/src/environment/inspection.ts +10 -0
  211. package/src/environment/lifecycle.ts +114 -8
  212. package/src/errors/recover.ts +76 -5
  213. package/src/errors/render.ts +17 -0
  214. package/src/extract/contracts.ts +974 -0
  215. package/src/features/host.ts +16 -0
  216. package/src/features/prepared-gpu-work.ts +20 -2
  217. package/src/features/prepared-graphics-store.ts +17 -0
  218. package/src/gpu-driven/production-raster.ts +97 -2
  219. package/src/gpu-scene.ts +3 -2
  220. package/src/ibl/IblPipelineCache.ts +134 -50
  221. package/src/ibl/skylight-bind-group.ts +26 -0
  222. package/src/index.ts +9 -1
  223. package/src/light-buffer-layout.ts +3 -0
  224. package/src/materials.ts +262 -45
  225. package/src/mesh-material-bindings.ts +59 -0
  226. package/src/occlusion-proxy.ts +64 -0
  227. package/src/pbr-pipeline.ts +139 -15
  228. package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
  229. package/src/points-lines/prepare.ts +9 -0
  230. package/src/points-lines/standard-owner.ts +403 -0
  231. package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
  232. package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
  233. package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
  234. package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
  235. package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
  236. package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
  237. package/src/record/__tests__/temporal-payload.unit.test.ts +22 -43
  238. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -1
  239. package/src/record/frame-snapshot.ts +15 -0
  240. package/src/record/frame-targets.ts +11 -1
  241. package/src/record/frame.ts +6 -2
  242. package/src/record/helpers.ts +1 -1
  243. package/src/record/main-pass-geometry.ts +230 -5
  244. package/src/record/main-pass-material.ts +316 -209
  245. package/src/record/main-pass-sprite-draws.ts +6 -7
  246. package/src/record/main-pass.ts +94 -6
  247. package/src/record/mesh-ssbo.ts +32 -10
  248. package/src/record/recovery-pipeline.ts +695 -0
  249. package/src/record/render-context.ts +35 -4
  250. package/src/record/typed-frame-graph.ts +45 -1
  251. package/src/record/view-ubo.ts +3 -3
  252. package/src/recovery/render-system-candidate.ts +839 -0
  253. package/src/render-contract.ts +65 -7
  254. package/src/render-system-extract-tail.ts +2983 -0
  255. package/src/render-system-extract.ts +124 -2977
  256. package/src/render-system.ts +868 -659
  257. package/src/scene/render-scene.ts +124 -1
  258. package/src/temporal/frame-coordinator.ts +4 -1
  259. package/src/temporal/taa-history-store.ts +46 -2
  260. package/src/temporal/temporal-view.ts +1 -1
  261. package/src/temporal/transaction.ts +9 -0
  262. package/src/tilemap-chunk-extract-system.ts +1 -1
  263. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
  264. package/dist/chunk-4L3P6DT5.mjs.map +0 -1
  265. package/dist/chunk-4NPUASZR.mjs.map +0 -1
  266. package/dist/chunk-DIYW3M5D.mjs.map +0 -1
  267. package/dist/chunk-FBC4R44J.mjs.map +0 -1
  268. package/dist/chunk-HZUPSYW7.mjs.map +0 -1
  269. package/dist/chunk-QODA5UKU.mjs.map +0 -1
  270. package/dist/chunk-YALPS44A.mjs.map +0 -1
  271. package/dist/chunk-ZUWKAZ2I.mjs.map +0 -1
@@ -1,10 +1,12 @@
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,
9
11
  resolveMaterialShaderBindingContract,
10
12
  resolveMaterialShaderVariantSet,
@@ -18,6 +20,7 @@ import {
18
20
  type PipelineBuilderContext,
19
21
  validateTemporalPipelineContract,
20
22
  } from '../pipeline-builder';
23
+ import { effectiveMaterialLayoutIdentity } from '../record/main-pass-geometry';
21
24
  import { userRegionTextureFieldOrder } from '../record/main-pass-material';
22
25
 
23
26
  const hdrpVariantKey =
@@ -25,7 +28,10 @@ const hdrpVariantKey =
25
28
  const urpVariantKey =
26
29
  'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=true+VERTEX_COLOR_AVAILABLE=false';
27
30
 
28
- function noColorVariant(definesKey: string, clusterForward: boolean) {
31
+ function noColorVariant(
32
+ definesKey: string,
33
+ clusterForward: boolean,
34
+ ): MaterialShaderManifestVariant {
29
35
  return {
30
36
  definesKey,
31
37
  defines: {
@@ -57,7 +63,7 @@ describe('material shader variant identity', () => {
57
63
  } as never),
58
64
  ).toEqual({
59
65
  requiredFeatures: [],
60
- requiredLimits: { maxSampledTexturesPerShaderStage: 20 },
66
+ requiredLimits: { maxSampledTexturesPerShaderStage: 21 },
61
67
  });
62
68
  expect(
63
69
  deviceOptionsForAdapter({
@@ -80,6 +86,17 @@ describe('material shader variant identity', () => {
80
86
  ).toBe(urpVariantKey);
81
87
  });
82
88
 
89
+ it('keeps physical root declarations out of capability variant selection', () => {
90
+ const manifest = noColorManifest();
91
+ expect(
92
+ selectNoColorPbrVariant(
93
+ manifest,
94
+ true,
95
+ `${hdrpVariantKey}+CLEARCOAT_TEXTURE_AVAILABLE=true+TRANSMISSION_AVAILABLE=true`,
96
+ )?.definesKey,
97
+ ).toBe(hdrpVariantKey);
98
+ });
99
+
83
100
  it('closes the WebGL2 colored PBR axes on the URP variant and layout', () => {
84
101
  const variants = [
85
102
  {
@@ -497,13 +514,12 @@ describe('temporal material PSO creation contract', () => {
497
514
  });
498
515
 
499
516
  describe('material shader capability variant resolution', () => {
500
- it('keeps transmission disabled unless the record path opts in', () => {
517
+ it('rewrites only backend capability axes', () => {
501
518
  const variants = [
502
519
  {
503
520
  defines: {
504
521
  CLUSTER_FORWARD_AVAILABLE: false,
505
522
  STORAGE_BUFFER_AVAILABLE: false,
506
- TRANSMISSION_AVAILABLE: false,
507
523
  VERTEX_COLOR_AVAILABLE: false,
508
524
  },
509
525
  },
@@ -511,7 +527,6 @@ describe('material shader capability variant resolution', () => {
511
527
  defines: {
512
528
  CLUSTER_FORWARD_AVAILABLE: false,
513
529
  STORAGE_BUFFER_AVAILABLE: false,
514
- TRANSMISSION_AVAILABLE: true,
515
530
  VERTEX_COLOR_AVAILABLE: false,
516
531
  },
517
532
  },
@@ -524,17 +539,7 @@ describe('material shader capability variant resolution', () => {
524
539
  false,
525
540
  ),
526
541
  ).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',
542
+ 'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=false+VERTEX_COLOR_AVAILABLE=false',
538
543
  );
539
544
  });
540
545
 
@@ -560,6 +565,16 @@ describe('material shader capability variant resolution', () => {
560
565
  });
561
566
 
562
567
  describe('material shader binding contract', () => {
568
+ it('does not reuse the shared PBR layout when compact texture names shift semantics', () => {
569
+ expect(
570
+ isSharedMaterialUserRegionCompatible([
571
+ { name: 'baseColor', type: 'color' },
572
+ { name: 'baseColorTexture', type: 'texture2d' },
573
+ { name: 'normalTexture', type: 'texture2d' },
574
+ ]),
575
+ ).toBe(false);
576
+ });
577
+
563
578
  it('preserves compact authored texture order for custom material schemas', () => {
564
579
  expect(
565
580
  userRegionTextureFieldOrder([
@@ -573,7 +588,6 @@ describe('material shader binding contract', () => {
573
588
  'baseColorTexture',
574
589
  'metallicRoughnessTexture',
575
590
  'normalTexture',
576
- 'specularTintTexture',
577
591
  'emissiveTexture',
578
592
  'occlusionTexture',
579
593
  'transmissionTexture',
@@ -650,8 +664,29 @@ describe('material shader binding contract', () => {
650
664
  });
651
665
 
652
666
  describe('material shader color-domain contract', () => {
667
+ it('keeps shared sprite layouts stable while splitting standard clearcoat maps', () => {
668
+ const sharedSpriteSchema = [{ name: 'tint', type: 'color' as const }];
669
+ const standardClearcoatSchema = [
670
+ { name: 'clearcoat', type: 'f32' as const },
671
+ { name: 'clearcoatTexture', type: 'texture2d' as const },
672
+ ];
673
+
674
+ expect(effectiveMaterialLayoutIdentity('forgeax::sprite', sharedSpriteSchema)).toBeUndefined();
675
+ expect(
676
+ effectiveMaterialLayoutIdentity('forgeax::default-standard-pbr', standardClearcoatSchema),
677
+ ).toBeTypeOf('string');
678
+ expect(
679
+ effectiveMaterialLayoutIdentity('forgeax::default-standard-pbr', [
680
+ { name: 'clearcoat', type: 'f32' as const },
681
+ ]),
682
+ ).toBeUndefined();
683
+ });
684
+
653
685
  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');
686
+ const source = [
687
+ readFileSync(resolve(import.meta.dirname, '../assembly/webgpu-renderer.ts'), 'utf8'),
688
+ readFileSync(resolve(import.meta.dirname, '../assembly/webgpu-ready.ts'), 'utf8'),
689
+ ].join('\n');
655
690
  expect(source).toContain(
656
691
  "isHdr &&\n (materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit')",
657
692
  );
@@ -659,6 +694,38 @@ describe('material shader color-domain contract', () => {
659
694
  expect(source).toContain('colorFormat: isHdr ? HDR_COLOR_ATTACHMENT_FORMAT : ldrColorFormat');
660
695
  expect(source).toContain("entryPoint = 'fs_main_hdr'");
661
696
  });
697
+
698
+ it('keeps physical map layout derivation in the shared pipeline owner', () => {
699
+ const source = readFileSync(
700
+ resolve(import.meta.dirname, '../assembly/webgpu-renderer.ts'),
701
+ 'utf8',
702
+ );
703
+ expect(source).toContain('isCanonicalStandardPbrMaterialShader(materialShaderId)');
704
+ expect(source).toContain('requiresStandardMapLayout');
705
+ });
706
+
707
+ it('does not reuse the scalar shared BGL for authored clearcoat maps', () => {
708
+ const source = readFileSync(
709
+ resolve(import.meta.dirname, '../assembly/webgpu-renderer.ts'),
710
+ 'utf8',
711
+ );
712
+ expect(source).toMatch(
713
+ /if \(\s*!isStandardMapLayout\s*&&\s*isSharedMaterialUserRegionCompatible\(/u,
714
+ );
715
+ });
716
+
717
+ it('keeps the fixed standard texture ABI before authored clearcoat slots', () => {
718
+ const pipelineSource = readFileSync(resolve(import.meta.dirname, '../pbr-pipeline.ts'), 'utf8');
719
+ const recordSource = readFileSync(
720
+ resolve(import.meta.dirname, '../record/main-pass-material.ts'),
721
+ 'utf8',
722
+ );
723
+ expect(pipelineSource).toContain(
724
+ 'const physicalFields = physicalTextureFields(effectiveSchema)',
725
+ );
726
+ expect(pipelineSource).toContain('appendTextureInjection(afterTransmission, physicalFields)');
727
+ expect(recordSource).toContain('BUILTIN_USER_REGION_TEXTURE_FIELDS');
728
+ });
662
729
  });
663
730
 
664
731
  describe('material shader vertex input contract', () => {
@@ -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
+ });
@@ -0,0 +1,59 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ classifyRecoveryCapability,
4
+ type RecoveryCapabilityFacts,
5
+ } from '../gpu-driven/production-raster';
6
+
7
+ const allCapabilities: RecoveryCapabilityFacts = {
8
+ generation: 7,
9
+ compute: true,
10
+ storageBuffer: true,
11
+ indirectDrawing: true,
12
+ multisample: true,
13
+ shaderCompilation: true,
14
+ };
15
+
16
+ describe('renderer recovery capability matrix', () => {
17
+ it('publishes the standard fallback when optional capability is absent', () => {
18
+ const result = classifyRecoveryCapability({
19
+ ...allCapabilities,
20
+ multisample: false,
21
+ });
22
+
23
+ expect(result).toEqual({
24
+ status: 'fallback',
25
+ generation: 7,
26
+ disabled: [],
27
+ });
28
+ });
29
+
30
+ it('reports optional GPU-driven work as disabled without allocation', () => {
31
+ const result = classifyRecoveryCapability({
32
+ ...allCapabilities,
33
+ compute: false,
34
+ storageBuffer: false,
35
+ indirectDrawing: false,
36
+ });
37
+
38
+ expect(result).toEqual({
39
+ status: 'disabled',
40
+ generation: 7,
41
+ disabled: ['gpu-driven'],
42
+ allocations: 0,
43
+ });
44
+ });
45
+
46
+ it('refuses a generation that lacks a core shader capability', () => {
47
+ const result = classifyRecoveryCapability({
48
+ ...allCapabilities,
49
+ shaderCompilation: false,
50
+ });
51
+
52
+ expect(result).toEqual({
53
+ status: 'refused',
54
+ generation: 7,
55
+ failedOwner: 'shader-material-pipeline',
56
+ resourceKind: 'pipeline',
57
+ });
58
+ });
59
+ });
@@ -0,0 +1,73 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ createRecoveryFailedError,
4
+ RECOVERY_PHASES,
5
+ type RecoveryFailureDetail,
6
+ } from '../errors/recover';
7
+
8
+ const receipt = Object.freeze({ owner: 'renderer', generation: 4, resourceCount: 3 });
9
+
10
+ function detail(overrides: Partial<RecoveryFailureDetail> = {}): RecoveryFailureDetail {
11
+ return {
12
+ phase: 'compile-graph',
13
+ oldGeneration: 4,
14
+ candidateGeneration: 5,
15
+ attempt: 2,
16
+ elapsedMs: 120,
17
+ retryable: true,
18
+ guidance: 'retry',
19
+ owner: 'standard-pipeline',
20
+ resourceKind: 'pipeline',
21
+ lastOutcome: 'failed',
22
+ rehydratedRoots: 3,
23
+ staleLossEvents: 1,
24
+ cause: new Error('candidate failed'),
25
+ cleanupFailures: [],
26
+ receipt,
27
+ ...overrides,
28
+ };
29
+ }
30
+
31
+ describe('renderer recovery error contract', () => {
32
+ it('keeps recovery phases and guidance closed and machine-readable', () => {
33
+ expect(RECOVERY_PHASES).toEqual([
34
+ 'quiesce',
35
+ 'acquire-adapter',
36
+ 'acquire-device',
37
+ 'rehydrate',
38
+ 'compile-graph',
39
+ 'publish',
40
+ 'cleanup',
41
+ ]);
42
+
43
+ const failure = createRecoveryFailedError(detail());
44
+ expect(failure.code).toBe('recovery-failed');
45
+ expect(failure.expected).toContain('complete replacement');
46
+ expect(failure.hint).toContain('detail');
47
+ expect(failure.detail.phase).toBe('compile-graph');
48
+ expect(failure.detail.oldGeneration).toBe(4);
49
+ expect(failure.detail.candidateGeneration).toBe(5);
50
+ expect(failure.detail.attempt).toBe(2);
51
+ expect(failure.detail.elapsedMs).toBe(120);
52
+ expect(failure.detail.retryable).toBe(true);
53
+ expect(failure.detail.guidance).toBe('retry');
54
+ expect(failure.detail.owner).toBe('standard-pipeline');
55
+ expect(failure.detail.resourceKind).toBe('pipeline');
56
+ expect(failure.detail.lastOutcome).toBe('failed');
57
+ expect(failure.detail.rehydratedRoots).toBe(3);
58
+ expect(failure.detail.staleLossEvents).toBe(1);
59
+ expect(failure.detail.cleanupFailures).toEqual([]);
60
+ expect(Object.isFrozen(failure.detail)).toBe(true);
61
+ });
62
+
63
+ it.each([
64
+ ['retry', true],
65
+ ['repair-owner', true],
66
+ ['rebuild-renderer', false],
67
+ ] as const)('preserves the %s guidance branch', (guidance, retryable) => {
68
+ const failure = createRecoveryFailedError(detail({ guidance, retryable }));
69
+ expect(failure.detail.guidance).toBe(guidance);
70
+ expect(failure.detail.retryable).toBe(retryable);
71
+ expect(failure.message).not.toContain('retry');
72
+ });
73
+ });