@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
@@ -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', () => {
@@ -53,19 +68,11 @@ describe('vertex color does not change the material contract', () => {
53
68
  expect(material.passes?.[0]?.renderState).toMatchObject({ depthWriteEnabled: false });
54
69
  });
55
70
 
56
- it.each([
57
- [
58
- 'unlit',
59
- Materials.unlit([1, 1, 1, 1], { renderState: { cullMode: 'none', frontFace: 'cw' } }),
60
- ],
61
- [
62
- 'standard',
63
- Materials.standard({
64
- baseColor: [1, 1, 1, 1],
65
- renderState: { cullMode: 'none', frontFace: 'cw' },
66
- }),
67
- ],
68
- ] as const)('projects the %s material raster orientation into its ShadowCaster pass', (_name, material) => {
71
+ it('projects the standard material raster orientation into its ShadowCaster pass', () => {
72
+ const material = Materials.standard({
73
+ baseColor: [1, 1, 1, 1],
74
+ renderState: { cullMode: 'none', frontFace: 'cw' },
75
+ });
69
76
  const shadowCaster = material.passes?.find((entry) => entry.name === 'shadow-caster');
70
77
  expect(shadowCaster?.renderState).toMatchObject({ cullMode: 'none', frontFace: 'cw' });
71
78
  });
@@ -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
  });
@@ -0,0 +1,69 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { Materials } from '../materials';
3
+
4
+ const surface = {
5
+ surfaceModule: 'game_3d::rusted_iron_surface',
6
+ parameters: [
7
+ { name: 'ironColor', type: 'color' as const },
8
+ { name: 'alphaClipThreshold', type: 'f32' as const, optional: true },
9
+ ],
10
+ values: { ironColor: [0.4, 0.45, 0.47, 1], alphaClipThreshold: 0.5 },
11
+ };
12
+
13
+ describe('custom Standard Surface pass policy', () => {
14
+ it('keeps opaque and alpha-clip custom surfaces on Forward, Deferred, and ShadowCaster', () => {
15
+ const material = Materials.standard(surface as never);
16
+ expect(material.passes?.map((pass) => pass.name)).toEqual([
17
+ 'forward',
18
+ 'deferred',
19
+ 'shadow-caster',
20
+ ]);
21
+ });
22
+
23
+ it('does not create Deferred for an explicitly blended custom surface', () => {
24
+ const material = Materials.standard({
25
+ ...surface,
26
+ renderState: { blend: { color: {}, alpha: {} } },
27
+ } as never);
28
+ expect(material.passes?.map((pass) => pass.name)).not.toContain('deferred');
29
+ });
30
+
31
+ it('does not create Deferred when the effective root declares a physical layer', () => {
32
+ const material = Materials.standard({
33
+ ...surface,
34
+ parameters: [
35
+ { name: 'clearcoat', type: 'f32' },
36
+ { name: 'clearcoatRoughness', type: 'f32' },
37
+ ],
38
+ values: { clearcoat: 0, clearcoatRoughness: 0.5 },
39
+ } as never);
40
+ expect(material.passes?.map((pass) => pass.name)).toEqual(['forward', 'shadow-caster']);
41
+ });
42
+
43
+ it('keeps optional physical fields inactive until the root makes the fragment effective', () => {
44
+ const material = Materials.standard({
45
+ ...surface,
46
+ parameters: [
47
+ { name: 'clearcoat', type: 'f32', optional: true },
48
+ { name: 'clearcoatRoughness', type: 'f32', optional: true },
49
+ ],
50
+ values: { clearcoat: 0, clearcoatRoughness: 0.5 },
51
+ } as never);
52
+ expect(material.passes?.map((pass) => pass.name)).toEqual([
53
+ 'forward',
54
+ 'deferred',
55
+ 'shadow-caster',
56
+ ]);
57
+ });
58
+
59
+ it('projects an explicitly declared default clearcoat root as Forward-only', () => {
60
+ const material = Materials.standard({
61
+ baseColor: [0.28, 0.035, 0.075, 1],
62
+ metallic: 0.12,
63
+ roughness: 0.18,
64
+ clearcoat: 0.82,
65
+ clearcoatRoughness: 0.08,
66
+ });
67
+ expect(material.passes?.map((pass) => pass.name)).toEqual(['forward', 'shadow-caster']);
68
+ });
69
+ });
@@ -3,7 +3,9 @@ import { World } from '@forgeax/engine-ecs';
3
3
  import { Transform } from '@forgeax/engine-scene';
4
4
  import type { ShaderRegistry } from '@forgeax/engine-shader';
5
5
  import type { MaterialAsset } from '@forgeax/engine-types';
6
+ import { deriveStandardLayerPlan } from '@forgeax/engine-types';
6
7
  import { describe, expect, it } from 'vitest';
8
+ import { projectStandardSurfacePasses } from '../assembly/material/surface-projection';
7
9
  import { MeshFilter } from '../components/mesh-filter';
8
10
  import { MeshRenderer } from '../components/mesh-renderer';
9
11
  import { extractFrames, type MaterialSnapshotCachesByWorld } from '../render-system-extract';
@@ -50,4 +52,71 @@ describe('material snapshot mutation', () => {
50
52
  expect(second?.baseColor).toEqual(new Float32Array([0, 1, 0]));
51
53
  expect(second).not.toBe(first);
52
54
  });
55
+
56
+ it('keeps the pipeline identity stable for a value-only child update', () => {
57
+ const world = new World();
58
+ const assets = new AssetRegistry({
59
+ findMaterialArtifact: () => ({ ok: false, error: new Error('not registered') }),
60
+ } as unknown as ShaderRegistry);
61
+ const values: Record<string, unknown> = { baseColor: [1, 0, 0, 1] };
62
+ const material: MaterialAsset = {
63
+ kind: 'material',
64
+ passes: [
65
+ {
66
+ name: 'Forward',
67
+ program: { module: 'forgeax_material::standard' },
68
+ renderState: { tags: { LightMode: 'Forward' }, queue: 2000 },
69
+ },
70
+ ],
71
+ values: values as NonNullable<MaterialAsset['values']>,
72
+ };
73
+ const materialHandle = world.allocSharedRef('MaterialAsset', material);
74
+ world.spawn(
75
+ { component: Transform, data: {} },
76
+ { component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
77
+ { component: MeshRenderer, data: { materials: [materialHandle] } },
78
+ );
79
+ const materialCaches: MaterialSnapshotCachesByWorld = new WeakMap();
80
+ const first = extractFrames([world], 0, assets, undefined, materialCaches).renderables[0];
81
+ values.baseColor = [0, 1, 0, 1];
82
+ const second = extractFrames([world], 0, assets, undefined, materialCaches).renderables[0];
83
+
84
+ expect(first?.gpuDrivenDraws?.[0]?.pipelineClass).toBe(
85
+ second?.gpuDrivenDraws?.[0]?.pipelineClass,
86
+ );
87
+ expect(first?.material?.materialShaderId).toBe(second?.material?.materialShaderId);
88
+ });
89
+
90
+ it('falsifies legacy surface, physical deferred, and forward fallback projections', () => {
91
+ const basePlan = deriveStandardLayerPlan([]);
92
+ const physicalPlan = deriveStandardLayerPlan([
93
+ { name: 'clearcoat', type: 'f32' },
94
+ { name: 'clearcoatRoughness', type: 'f32' },
95
+ ]);
96
+
97
+ const base = projectStandardSurfacePasses({
98
+ surfaceModule: 'game_3d::rusted_iron_surface',
99
+ values: {},
100
+ layerPlan: basePlan,
101
+ geometryVariant: 'rigid',
102
+ lightingLane: 'direct',
103
+ });
104
+ const physical = projectStandardSurfacePasses({
105
+ surfaceModule: 'game_3d::rusted_iron_surface',
106
+ values: {},
107
+ layerPlan: physicalPlan,
108
+ geometryVariant: 'rigid',
109
+ lightingLane: 'direct',
110
+ });
111
+ const legacy = base.map((entry) => ({
112
+ ...entry,
113
+ program: { ...entry.program, moduleSlots: { surface: 'legacy::surface_data' } },
114
+ }));
115
+ const physicalDeferred = physical.some((entry) => entry.name === 'deferred');
116
+ const forwardFallback = base.some((entry) => entry.name === 'forward');
117
+
118
+ expect(legacy[0]?.program.moduleSlots?.surface).not.toBe('game_3d::rusted_iron_surface');
119
+ expect(physicalDeferred).toBe(false);
120
+ expect(forwardFallback).toBe(true);
121
+ });
53
122
  });
@@ -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,62 @@
1
+ import type { MaterialAsset } from '@forgeax/engine-types';
2
+ import { describe, expectTypeOf, it } from 'vitest';
3
+ import {
4
+ type CustomStandardSurfaceOptions,
5
+ type DefaultStandardOptions,
6
+ Materials,
7
+ type StandardOptions,
8
+ } from '../materials';
9
+
10
+ const parameters = [{ name: 'ironColor', type: 'color' as const }];
11
+ const values = { ironColor: [0.4, 0.45, 0.47, 1] as const };
12
+
13
+ describe('Standard Surface authoring union', () => {
14
+ it('exposes one callable Parameters union for default and custom options', () => {
15
+ const defaultOptions = { baseColor: [1, 1, 1, 1] as const } satisfies DefaultStandardOptions;
16
+ const customOptions = {
17
+ surfaceModule: 'game_3d::rusted_iron_surface',
18
+ parameters,
19
+ values,
20
+ } satisfies CustomStandardSurfaceOptions;
21
+ expectTypeOf<Parameters<typeof Materials.standard>[0]>().toEqualTypeOf<StandardOptions>();
22
+
23
+ function acceptStandard<T extends StandardOptions>(options: T): T {
24
+ return options;
25
+ }
26
+
27
+ acceptStandard(defaultOptions);
28
+ acceptStandard(customOptions);
29
+ });
30
+
31
+ it('accepts a custom Surface without convenience fields', () => {
32
+ const options = {
33
+ surfaceModule: 'game_3d::rusted_iron_surface',
34
+ parameters,
35
+ values,
36
+ castShadow: true,
37
+ } satisfies CustomStandardSurfaceOptions;
38
+ const material = Materials.standard(options);
39
+ expectTypeOf(material.passes?.[0]?.program.moduleSlots?.surface).toBeString;
40
+ });
41
+
42
+ it('keeps default and custom options mutually exclusive', () => {
43
+ expectTypeOf<DefaultStandardOptions>().toHaveProperty('baseColor');
44
+ Materials.standard({
45
+ surfaceModule: 'game_3d::rusted_iron_surface',
46
+ parameters,
47
+ values,
48
+ // @ts-expect-error Custom Surface options cannot add Standard convenience fields.
49
+ baseColor: [1, 1, 1, 1],
50
+ });
51
+ });
52
+
53
+ it('keeps custom Surface parameters sparse and root-owned', () => {
54
+ const custom = Materials.standard({
55
+ surfaceModule: 'game_3d::rusted_iron_surface',
56
+ parameters,
57
+ values,
58
+ });
59
+ expectTypeOf(custom.parameters).toEqualTypeOf<MaterialAsset['parameters']>();
60
+ expectTypeOf(custom.values).toEqualTypeOf<MaterialAsset['values']>();
61
+ });
62
+ });
@@ -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
+ });
@@ -33,4 +33,28 @@ describe('Materials.standard transmission validation', () => {
33
33
  }),
34
34
  ).toThrow(MaterialTransmissionContractError);
35
35
  });
36
+
37
+ it('preserves the typed physical contract error for incomplete custom layers', () => {
38
+ const options = {
39
+ surfaceModule: 'game_3d::custom_surface',
40
+ parameters: [{ name: 'clearcoat', type: 'f32' as const }],
41
+ values: { clearcoat: 0.5 },
42
+ };
43
+
44
+ expect(() => Materials.standard(options as never)).toThrow(Error);
45
+ try {
46
+ Materials.standard(options as never);
47
+ } catch (error) {
48
+ expect(error).toMatchObject({
49
+ code: 'material-physical-contract-invalid',
50
+ detail: {
51
+ code: 'material-physical-contract-invalid',
52
+ material: 'Standard',
53
+ layer: 'clearcoat',
54
+ missing: ['clearcoatRoughness'],
55
+ reason: 'incomplete-layer',
56
+ },
57
+ });
58
+ }
59
+ });
36
60
  });
@@ -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 = [
@@ -47,10 +47,6 @@ const featureBufferSources = [
47
47
  source: readFileSync(new URL('../record/main-pass-sprite-draws.ts', import.meta.url), 'utf8'),
48
48
  names: ['GPU_BUFFER_USAGE_STORAGE', 'GPU_BUFFER_USAGE_UNIFORM', 'GPU_BUFFER_USAGE_COPY_DST'],
49
49
  },
50
- {
51
- source: readFileSync(new URL('../record/shadow-pass.ts', import.meta.url), 'utf8'),
52
- names: ['GPU_BUFFER_USAGE_STORAGE', 'GPU_BUFFER_USAGE_UNIFORM', 'GPU_BUFFER_USAGE_COPY_DST'],
53
- },
54
50
  {
55
51
  source: readFileSync(new URL('../systems/skin-palette-allocator.ts', import.meta.url), 'utf8'),
56
52
  names: ['GPU_BUFFER_USAGE_STORAGE', 'GPU_BUFFER_USAGE_UNIFORM', 'GPU_BUFFER_USAGE_COPY_DST'],
@@ -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', () => {