@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
@@ -20,8 +20,8 @@ import {
20
20
  import {
21
21
  DEFAULT_MSDF_TEXT_PARAM_SCHEMA,
22
22
  DEFAULT_SPRITE_PARAM_SCHEMA,
23
- DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
24
23
  DEFAULT_UNLIT_PARAM_SCHEMA,
24
+ STANDARD_PIPELINE_PARAM_SCHEMA,
25
25
  } from '@forgeax/engine-shader';
26
26
  import type {
27
27
  Handle,
@@ -38,10 +38,16 @@ import { VideoUploadUnsupportedError } from '../errors/render';
38
38
  import {
39
39
  assembleMaterialWithSkylightEntries,
40
40
  type SkylightBindGroupResources,
41
+ type TextureInjectionResource,
41
42
  type TransmissionBindGroupResources,
42
43
  } from '../ibl/skylight-bind-group';
44
+ import type { IblBindingEntryInspection } from '../mesh-material-bindings';
43
45
  import type { PipelineGroup2Contract } from '../pbr-pipeline';
44
- import { isStandardPbrMaterialShader } from '../pbr-pipeline';
46
+ import {
47
+ isCanonicalStandardPbrMaterialShader,
48
+ isStandardPbrMaterialShader,
49
+ physicalTextureFields,
50
+ } from '../pbr-pipeline';
45
51
  import { deriveTextureExtent } from '../render-data';
46
52
  import type { MaterialSnapshot } from '../render-system-extract';
47
53
  import type { RenderTargetTextureSource } from '../targets/contracts';
@@ -50,7 +56,12 @@ import {
50
56
  resolveRenderTargetMaterialSource,
51
57
  } from '../targets/material-source';
52
58
  import type { StandardReflectionProbeBinding } from './frame-lighting';
53
- import type { BindGroupCounts, MaterialBgAssemblyCacheEntry } from './frame-snapshot';
59
+ import type {
60
+ BindGroupCounts,
61
+ MaterialBgAssemblyCacheEntry,
62
+ RenderFrameState,
63
+ } from './frame-snapshot';
64
+ import { getOpaqueResourceIdentity } from './frame-snapshot';
54
65
  import { extractEntryResourceHandle, getOrCreatePerEntity } from './mesh-ssbo';
55
66
  import {
56
67
  type PipelineState,
@@ -232,6 +243,18 @@ function isImageError(error: unknown): error is ImageError {
232
243
  return typeof code === 'string' && code.startsWith('image-');
233
244
  }
234
245
 
246
+ function materialDiagnosticsEnabled(): boolean {
247
+ if (typeof globalThis !== 'object' || globalThis === null || !('process' in globalThis)) {
248
+ return false;
249
+ }
250
+ const processValue = (
251
+ globalThis as {
252
+ readonly process?: { readonly env?: Record<string, string | undefined> };
253
+ }
254
+ ).process;
255
+ return processValue?.env?.FORGEAX_MATERIAL_DIAGNOSTICS === '1';
256
+ }
257
+
235
258
  export function residentTextureView(
236
259
  world: World,
237
260
  store: GpuResidencyCache,
@@ -240,7 +263,15 @@ export function residentTextureView(
240
263
  worldId: World | number = world,
241
264
  ): TextureView | undefined {
242
265
  const podRes = resolveAssetHandle<TextureAsset>(world, handle);
243
- if (!podRes.ok) return undefined;
266
+ const diagnosticsEnabled = materialDiagnosticsEnabled();
267
+ if (!podRes.ok) {
268
+ if (diagnosticsEnabled) {
269
+ console.error(
270
+ `[render-material] texture resolve failed: ${JSON.stringify({ handle: handleSlot(handle), error: podRes.error })}`,
271
+ );
272
+ }
273
+ return undefined;
274
+ }
244
275
  const residentRes = store.ensureResident(handle, podRes.value, worldId);
245
276
  if (!residentRes.ok) {
246
277
  // Preserve the concrete asset-capability boundary on the renderer error
@@ -249,9 +280,41 @@ export function residentTextureView(
249
280
  if (residentRes.error instanceof RhiError || isImageError(residentRes.error)) {
250
281
  runtime.errorRegistry.fire(residentRes.error);
251
282
  }
283
+ if (diagnosticsEnabled) {
284
+ console.error(
285
+ `[render-material] texture residency failed: ${JSON.stringify({
286
+ handle: handleSlot(handle),
287
+ pod: {
288
+ kind: podRes.value.kind,
289
+ shape: podRes.value.shape,
290
+ format: podRes.value.format,
291
+ dataByteLength: podRes.value.data.byteLength,
292
+ mips: podRes.value.mips,
293
+ },
294
+ error: residentRes.error,
295
+ })}`,
296
+ );
297
+ }
252
298
  return undefined;
253
299
  }
254
- return store.getTextureGpuView(handle, worldId);
300
+ const view = store.getTextureGpuView(handle, worldId);
301
+ if (diagnosticsEnabled) {
302
+ console.error(
303
+ `[render-material] texture residency ready: ${JSON.stringify({
304
+ handle: handleSlot(handle),
305
+ pod: {
306
+ kind: podRes.value.kind,
307
+ shape: podRes.value.shape,
308
+ format: podRes.value.format,
309
+ dataByteLength: podRes.value.data.byteLength,
310
+ mips: podRes.value.mips,
311
+ },
312
+ receipt: 'receipt' in residentRes.value ? residentRes.value.receipt : undefined,
313
+ viewReady: view !== undefined,
314
+ })}`,
315
+ );
316
+ }
317
+ return view;
255
318
  }
256
319
 
257
320
  function residentSampler(
@@ -398,7 +461,6 @@ export const BUILTIN_USER_REGION_TEXTURE_FIELDS: readonly string[] = [
398
461
  'baseColorTexture',
399
462
  'metallicRoughnessTexture',
400
463
  'normalTexture',
401
- 'specularTintTexture',
402
464
  'emissiveTexture',
403
465
  'occlusionTexture',
404
466
  'transmissionTexture',
@@ -406,21 +468,6 @@ export const BUILTIN_USER_REGION_TEXTURE_FIELDS: readonly string[] = [
406
468
  ];
407
469
 
408
470
  const LEGACY_MATERIAL_TEXTURE_SCALE_OFFSET = 80;
409
- // The Standard schema owns five transmission numeric fields between
410
- // normalScale and the engine-owned coordinate records. Keep this fallback
411
- // aligned with derive(DEFAULT_STANDARD_PBR_PARAM_SCHEMA): the first record is
412
- // at byte 128, not the pre-transmission byte 96.
413
- const STANDARD_PBR_TEXTURE_SCALE_OFFSET = 128;
414
- const MATERIAL_TEXTURE_SCALE_FIELDS = [
415
- 'baseColorTexture',
416
- 'metallicRoughnessTexture',
417
- 'normalTexture',
418
- 'specularTintTexture',
419
- 'emissiveTexture',
420
- 'occlusionTexture',
421
- 'transmissionTexture',
422
- 'thicknessTexture',
423
- ] as const;
424
471
  const LEGACY_MATERIAL_TEXTURE_SCALE_FIELDS = [
425
472
  'baseColorTexture',
426
473
  'metallicRoughnessTexture',
@@ -428,13 +475,7 @@ const LEGACY_MATERIAL_TEXTURE_SCALE_FIELDS = [
428
475
  'emissiveTexture',
429
476
  'occlusionTexture',
430
477
  ] as const;
431
- const STANDARD_PBR_TEXTURE_COORDINATE_OFFSET = 128;
432
- const STANDARD_PBR_TEXTURE_COORDINATE_STRIDE = 8;
433
478
  const DEFAULT_LEGACY_TEXTURE_SCALES = new Float32Array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1]);
434
- const DEFAULT_STANDARD_TEXTURE_COORDINATES = new Float32Array([
435
- 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0,
436
- 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 1, 1,
437
- ]);
438
479
 
439
480
  export interface StandardReflectionProbeMaterialProjection {
440
481
  readonly probeIndex: number | undefined;
@@ -536,13 +577,10 @@ export function applyMaterialTextureUvScales(
536
577
  }
537
578
  return;
538
579
  }
539
- // Standard PBR has a schema-derived 128-byte authored region before the
540
- // engine-owned UV records. Sprite, sprite-lit, unlit, and text keep their
541
- // own legacy tail positions because their WGSL layouts differ.
542
- const textureScaleOffset = isStandardPbrMaterialShader(material.materialShaderId)
543
- ? STANDARD_PBR_TEXTURE_SCALE_OFFSET
544
- : LEGACY_MATERIAL_TEXTURE_SCALE_OFFSET;
545
- const isStandardPbr = textureScaleOffset === STANDARD_PBR_TEXTURE_SCALE_OFFSET;
580
+ // Unknown legacy material paths retain their historical packed scale tail.
581
+ // Every registered Standard/material-schema path returns above and is
582
+ // therefore fully schema-driven (including physical texture coordinates).
583
+ const textureScaleOffset = LEGACY_MATERIAL_TEXTURE_SCALE_OFFSET;
546
584
  const hasTextureMetadata =
547
585
  material.textureCoordinates !== undefined ||
548
586
  material.textureHandles !== undefined ||
@@ -553,15 +591,10 @@ export function applyMaterialTextureUvScales(
553
591
  material.emissiveTexture !== undefined ||
554
592
  material.occlusionTexture !== undefined;
555
593
  if (!hasTextureMetadata) {
556
- f32.set(
557
- isStandardPbr ? DEFAULT_STANDARD_TEXTURE_COORDINATES : DEFAULT_LEGACY_TEXTURE_SCALES,
558
- textureScaleOffset / 4,
559
- );
594
+ f32.set(DEFAULT_LEGACY_TEXTURE_SCALES, textureScaleOffset / 4);
560
595
  return;
561
596
  }
562
- const fields = isStandardPbr
563
- ? MATERIAL_TEXTURE_SCALE_FIELDS
564
- : LEGACY_MATERIAL_TEXTURE_SCALE_FIELDS;
597
+ const fields = LEGACY_MATERIAL_TEXTURE_SCALE_FIELDS;
565
598
  for (let index = 0; index < fields.length; index++) {
566
599
  const field = fields[index];
567
600
  if (field === undefined) continue;
@@ -570,39 +603,19 @@ export function applyMaterialTextureUvScales(
570
603
  handle === undefined ? undefined : resolveAssetHandle<TextureAsset>(world, handle);
571
604
  const texture = resolved?.ok === true ? resolved.value : undefined;
572
605
  const [u, v] = materialTextureUvScale(texture);
573
- if (isStandardPbr) {
574
- const coordinates = material.textureCoordinates?.get(field);
575
- const transform = coordinates?.transform;
576
- const offset =
577
- STANDARD_PBR_TEXTURE_COORDINATE_OFFSET / 4 + index * STANDARD_PBR_TEXTURE_COORDINATE_STRIDE;
578
- f32[offset] = transform?.offset?.[0] ?? 0;
579
- f32[offset + 1] = transform?.offset?.[1] ?? 0;
580
- f32[offset + 2] = transform?.scale?.[0] ?? 1;
581
- f32[offset + 3] = transform?.scale?.[1] ?? 1;
582
- f32[offset + 4] = coordinates?.set ?? 0;
583
- f32[offset + 5] = transform?.rotation ?? 0;
584
- f32[offset + 6] = u;
585
- f32[offset + 7] = v;
586
- } else {
587
- const offset = textureScaleOffset / 4 + index * 2;
588
- f32[offset] = u;
589
- f32[offset + 1] = v;
590
- }
606
+ const offset = textureScaleOffset / 4 + index * 2;
607
+ f32[offset] = u;
608
+ f32[offset + 1] = v;
591
609
  }
592
610
  }
593
611
 
594
612
  function materialCoordinateSchema(
595
613
  material: MaterialSnapshot,
596
614
  ): readonly ParamSchemaEntry[] | undefined {
597
- if (
598
- isStandardPbrMaterialShader(material.materialShaderId) &&
599
- material.paramSnapshot !== undefined
600
- ) {
601
- return DEFAULT_STANDARD_PBR_PARAM_SCHEMA;
602
- }
603
615
  if (material.materialParamSchema !== undefined && material.materialParamSchema.length > 0) {
604
616
  return material.materialParamSchema;
605
617
  }
618
+ if (isStandardPbrMaterialShader(material.materialShaderId)) return STANDARD_PIPELINE_PARAM_SCHEMA;
606
619
  switch (material.materialShaderId) {
607
620
  case 'forgeax::default-unlit':
608
621
  return DEFAULT_UNLIT_PARAM_SCHEMA;
@@ -652,6 +665,13 @@ export function defaultViewForUserRegionField(
652
665
  }
653
666
  if (field === 'normalTexture') return pipelineState.defaultNormalTextureView;
654
667
  if (field === 'baseColorTexture') return pipelineState.fallbackTextureView;
668
+ if (field === 'anisotropyTexture') {
669
+ // The flat-normal fallback encodes a zero tangent direction and unit B
670
+ // strength; the shader's zero-vector guard selects its neutral (1, 0)
671
+ // axis. This keeps an absent anisotropy map neutral even on hosts that do
672
+ // not allocate the optional dedicated identity texture.
673
+ return pipelineState.defaultAnisotropyTextureView ?? pipelineState.defaultNormalTextureView;
674
+ }
655
675
  return pipelineState.defaultWhiteTextureView;
656
676
  }
657
677
 
@@ -698,17 +718,7 @@ export function detectNineSliceScaleTooSmall(
698
718
  }
699
719
  }
700
720
 
701
- /**
702
- * Build the 384-byte Material UBO payload for a PBR / unlit material entry
703
- * (feat-20260527-sprite-nineslice M2 / w11; D-7 regression-net helper).
704
- *
705
- * Byte-for-byte equivalent to the legacy hard-coded PBR write path; each
706
- * deviation is caught by `render-system-record-pbr-ubo-stable.test.ts`.
707
- * The schema-driven paramSnapshot overlay (feat-20260523 M9-T05; AC-14)
708
- * positions vec4 / f32 slots onto std140 [slot0 vec4, slot1 f32 metallic,
709
- * slot1 f32 roughness] mirroring the engine-shipped default-standard-pbr
710
- * Material struct.
711
- */
721
+ /** Build one max-sized, schema-driven Standard material UBO payload. */
712
722
  export function buildPbrMaterialUboPayload(material: MaterialSnapshot): Uint8Array {
713
723
  const buf = new Uint8Array(STANDARD_PBR_UBO_SIZE);
714
724
  writePbrMaterialUboPayload(buf, material);
@@ -718,11 +728,10 @@ export function buildPbrMaterialUboPayload(material: MaterialSnapshot): Uint8Arr
718
728
  /**
719
729
  * Write the standard material payload into caller-owned storage.
720
730
  *
721
- * The allocating wrapper above remains the byte-stable test/helper surface,
722
- * while the frame recorder reuses one scratch slot for all materials. A
723
- * scene with thousands of submeshes otherwise allocates one 384-byte
724
- * Uint8Array per material per frame, creating avoidable GC pressure without
725
- * changing GPU-visible bytes.
731
+ * The allocating wrapper above remains the helper/test surface, while the
732
+ * frame recorder reuses one scratch slot for all materials. Field offsets are
733
+ * read from the material's admitted schema; no Standard field offset table is
734
+ * maintained in the record stage.
726
735
  */
727
736
  export function writePbrMaterialUboPayload(
728
737
  buf: Uint8Array | Float32Array,
@@ -735,114 +744,52 @@ export function writePbrMaterialUboPayload(
735
744
  }
736
745
  buf.fill(0);
737
746
  const f32 = materialUboFloatView(buf);
738
- // Layout (issue-1: channelMap split per D-8) -- byte-equivalent to the
739
- // post-split sidecar paramSchema for default-standard-pbr (12 numeric
740
- // entries packed std140 into one merged UBO at binding(0)).
741
- // f32[0..3] baseColor (offset 0, vec4)
742
- // f32[4] metallic (offset 16)
743
- // f32[5] roughness (offset 20)
744
- // f32[6] metallicChannel (offset 24) -- glTF 2.0 default = B = 2
745
- // f32[7] roughnessChannel (offset 28) -- glTF 2.0 default = G = 1
746
- // f32[8] aoChannel (offset 32) -- glTF 2.0 default = R = 0
747
- // f32[9] extraChannel (offset 36) -- reserved = 0
748
- // (offset 40..47 implicit pad: vec3 align=16)
749
- // f32[12..14] emissive (offset 48, vec3)
750
- // f32[15] emissiveIntensity (offset 60)
751
- // f32[16] occlusionStrength (offset 64)
752
- // f32[17] alphaCutoff (offset 68)
753
- // f32[18] clearcoat (offset 72)
754
- // f32[19] clearcoatRoughness (offset 76)
755
- // f32[20..22] specularTint (offset 80, vec3)
756
- // f32[23] normalScale (offset 92)
757
- // f32[24] transmission (offset 96)
758
- // f32[25] ior (offset 100)
759
- // f32[26] thickness (offset 104)
760
- // f32[28..30] attenuationColor (offset 112, vec3)
761
- // f32[31] attenuationDistance(offset 124)
762
- // f32[32..95] engine-owned texture coordinate records (offset 128)
763
- f32[0] = material.baseColor[0] ?? 0;
764
- f32[1] = material.baseColor[1] ?? 0;
765
- f32[2] = material.baseColor[2] ?? 0;
766
- f32[3] = 1;
767
- f32[4] = material.metallic;
768
- f32[5] = material.roughness;
769
- // channelMap default = (B, G, R, _) per AGENTS.md + glTF 2.0
770
- // KHR_materials_pbrSpecularGlossiness ARM packing -- now 4 independent
771
- // f32 selectors; fragment casts each to u32 at the pick site.
772
- f32[6] = 2; // metallicChannel <- B
773
- f32[7] = 1; // roughnessChannel <- G
774
- f32[8] = 0; // aoChannel <- R
775
- f32[9] = 0; // extraChannel <- reserved
776
- // emissive vec3 + emissiveIntensity (offset 48..63)
777
- const em = material.emissive;
778
- f32[12] = em?.[0] ?? 0;
779
- f32[13] = em?.[1] ?? 0;
780
- f32[14] = em?.[2] ?? 0;
781
- f32[15] = material.emissiveIntensity ?? 0;
782
- // occlusionStrength (offset 64)
783
- f32[16] = material.occlusionStrength ?? 1;
784
- // alphaCutoff (offset 68).
785
- const alphaCutoff = material.paramSnapshot?.alphaCutoff;
786
- f32[17] = typeof alphaCutoff === 'number' ? alphaCutoff : 0;
787
- f32[18] = material.clearcoat ?? 0;
788
- f32[19] = material.clearcoatRoughness ?? 0.5;
789
- const specularTint = material.specularTint;
790
- f32[20] = specularTint?.[0] ?? 1;
791
- f32[21] = specularTint?.[1] ?? 1;
792
- f32[22] = specularTint?.[2] ?? 1;
793
- const snapshotNumber = (name: string, fallback: number): number => {
794
- const value = material.paramSnapshot?.[name];
795
- return typeof value === 'number' ? value : fallback;
796
- };
797
- const snapshotVec3 = (
798
- name: string,
799
- fallback: readonly [number, number, number],
800
- ): readonly [number, number, number] => {
801
- const value = material.paramSnapshot?.[name];
802
- return Array.isArray(value)
803
- ? [value[0] ?? fallback[0], value[1] ?? fallback[1], value[2] ?? fallback[2]]
804
- : fallback;
747
+ const schema =
748
+ material.materialParamSchema === undefined || material.materialParamSchema.length === 0
749
+ ? STANDARD_PIPELINE_PARAM_SCHEMA
750
+ : material.materialParamSchema;
751
+ const snapshot = material.paramSnapshot;
752
+ const fallback = (name: string, entry: (typeof schema)[number]): unknown => {
753
+ switch (name) {
754
+ case 'baseColor':
755
+ return [
756
+ material.baseColor[0] ?? 1,
757
+ material.baseColor[1] ?? 1,
758
+ material.baseColor[2] ?? 1,
759
+ 1,
760
+ ];
761
+ case 'metallic':
762
+ return material.metallic;
763
+ case 'roughness':
764
+ return material.roughness;
765
+ case 'specularColor':
766
+ return material.specularColor ?? [1, 1, 1];
767
+ case 'normalScale':
768
+ return material.normalScale ?? 1;
769
+ case 'emissive':
770
+ return material.emissive ?? [0, 0, 0];
771
+ case 'emissiveIntensity':
772
+ return material.emissiveIntensity ?? 0;
773
+ case 'occlusionStrength':
774
+ return material.occlusionStrength ?? 1;
775
+ default:
776
+ return entry.default;
777
+ }
805
778
  };
806
- f32[23] = material.normalScale ?? snapshotNumber('normalScale', 1);
807
- f32[24] = snapshotNumber('transmission', 0);
808
- f32[25] = snapshotNumber('ior', 1.5);
809
- f32[26] = snapshotNumber('thickness', 0);
810
- const attenuationColor = snapshotVec3('attenuationColor', [1, 1, 1]);
811
- f32[28] = attenuationColor[0];
812
- f32[29] = attenuationColor[1];
813
- f32[30] = attenuationColor[2];
814
- f32[31] = snapshotNumber('attenuationDistance', 0);
815
- // Schema-driven paramSnapshot overlay (feat-20260523 M9-T05, AC-14):
816
- // for user-shaders with a paramSnapshot, project the first vec4/color
817
- // entry onto slot 0 and the first two f32 entries onto slot 1's first
818
- // two floats. The default-standard-pbr path lands on the same offsets
819
- // via the explicit fields above, so the overlay is a no-op for the
820
- // engine's stock PBR shader (charter P4 + R-H regression fence).
821
- const paramSnap = material.paramSnapshot;
822
- if (paramSnap !== undefined) {
823
- const f32Snap = (name: string): number | undefined => {
824
- const v = paramSnap[name];
825
- return typeof v === 'number' ? v : undefined;
826
- };
827
- const colorSnap = (name: string): readonly number[] | undefined => {
828
- const v = paramSnap[name];
829
- return Array.isArray(v) && v.every((x) => typeof x === 'number')
830
- ? (v as readonly number[])
831
- : undefined;
832
- };
833
- // Walk schema in declared order, fill positional UBO slots.
834
- const materialShaderId = material.materialShaderId;
835
- // Note: schema lookup is the caller's job (it owns the runtime ref);
836
- // when the helper has no access to runtime, paramSnap is consumed
837
- // positionally without a schema walk. The inline caller (recordFrame)
838
- // still performs the schema-driven walk and writes the overlay before
839
- // queueing this payload, so this helper is only the byte-stable
840
- // baseline. To preserve byte-for-byte equivalence in the helper-only
841
- // unit-test path, the overlay pass is skipped when the helper is
842
- // called without an external schema reference.
843
- void materialShaderId;
844
- void f32Snap;
845
- void colorSnap;
779
+ for (const entry of derivedParamSchema(schema).uboLayout.entries) {
780
+ const value =
781
+ snapshot?.[entry.name] ??
782
+ fallback(entry.name, schema.find((item) => item.name === entry.name) ?? entry);
783
+ const offset = entry.offset / 4;
784
+ const width = entry.size / 4;
785
+ if (typeof value === 'number') {
786
+ f32[offset] = value;
787
+ } else if (Array.isArray(value)) {
788
+ for (let index = 0; index < Math.min(width, value.length); index += 1) {
789
+ const component = value[index];
790
+ if (typeof component === 'number') f32[offset + index] = component;
791
+ }
792
+ }
846
793
  }
847
794
  }
848
795
 
@@ -891,13 +838,7 @@ export function applyParamSnapshotToUbo(
891
838
  if (paramSchema === undefined) return;
892
839
  if (paramSnapshot === undefined) return;
893
840
  const f32 = materialUboFloatView(payload);
894
- const effectiveSchema =
895
- Object.hasOwn(paramSnapshot, 'transmission') &&
896
- Object.hasOwn(paramSnapshot, 'ior') &&
897
- Object.hasOwn(paramSnapshot, 'thickness')
898
- ? DEFAULT_STANDARD_PBR_PARAM_SCHEMA
899
- : paramSchema;
900
- const { uboLayout } = derivedParamSchema(effectiveSchema);
841
+ const { uboLayout } = derivedParamSchema(paramSchema);
901
842
  for (const entry of uboLayout.entries) {
902
843
  const value = paramSnapshot[entry.name];
903
844
  if (value === undefined) continue;
@@ -962,6 +903,46 @@ export interface PerSubmeshMaterialBgDeps {
962
903
  /** Active transmission backdrop; undefined/null means layout placeholder. */
963
904
  readonly transmissionBackdropView?: TextureView | null;
964
905
  readonly bindGroupCounts: BindGroupCounts;
906
+ readonly frameState: RenderFrameState;
907
+ }
908
+
909
+ function recordIblMaterialBinding(
910
+ frameState: RenderFrameState,
911
+ materialBgl: BindGroupLayout,
912
+ bindGroup: BindGroup,
913
+ entries: readonly BindGroupEntry[],
914
+ iblStart: number,
915
+ cache: 'hit' | 'miss',
916
+ ): void {
917
+ const receipt = frameState.iblBindingInspection;
918
+ if (receipt === undefined) return;
919
+ const skylightBindingStart = entries.findIndex((entry) => entry.binding === iblStart);
920
+ if (skylightBindingStart < 0) return;
921
+ const iblEntries = entries.slice(skylightBindingStart, skylightBindingStart + 7);
922
+ const projected: IblBindingEntryInspection[] = [];
923
+ for (const entry of iblEntries) {
924
+ if (entry.resource.kind === 'externalTexture') continue;
925
+ const value =
926
+ entry.resource.kind === 'buffer' ? entry.resource.value.buffer : entry.resource.value;
927
+ if (typeof value !== 'object' || value === null) continue;
928
+ projected.push({
929
+ binding: entry.binding,
930
+ kind: entry.resource.kind,
931
+ resourceIdentity: getOpaqueResourceIdentity(value as object),
932
+ });
933
+ }
934
+ frameState.iblBindingInspection = {
935
+ ...receipt,
936
+ material: {
937
+ bindGroupIdentity: getOpaqueResourceIdentity(bindGroup as object),
938
+ materialBglIdentity: getOpaqueResourceIdentity(materialBgl as object),
939
+ cache,
940
+ skylightBindingStart: iblEntries[0]?.binding ?? 0,
941
+ entries: projected,
942
+ reflectionBindings: projected.map((entry) => entry.binding),
943
+ },
944
+ errors: [],
945
+ };
965
946
  }
966
947
 
967
948
  /** @internal Exported only so buffer-generation invalidation stays regression-tested. */
@@ -1009,6 +990,7 @@ export function buildPerSubmeshMaterialBg(
1009
990
  materialBgAssemblyCache,
1010
991
  transmissionBackdropView,
1011
992
  bindGroupCounts,
993
+ frameState,
1012
994
  } = deps;
1013
995
  const effectiveSkylightResources =
1014
996
  resolveReflectionProbeResources?.(materialWorld, entityKey) ?? skylightResources;
@@ -1017,10 +999,31 @@ export function buildPerSubmeshMaterialBg(
1017
999
  smMaterialHandle === undefined ? undefined : `${materialWorld.identity}:${smMaterialHandle}`;
1018
1000
  const smHasVideoFields = (submeshMaterial.videoTextureFields?.size ?? 0) > 0;
1019
1001
  const smShaderId = submeshMaterial.materialShaderId;
1002
+ // The cooked shader schema owns the resource ABI. A material snapshot may
1003
+ // carry only the authored values (for example a clearcoat-only root omits
1004
+ // the standard emissive/occlusion maps), while the Standard template still
1005
+ // declares those slots and expects neutral fallbacks to be bound.
1006
+ const smSchema =
1007
+ (smShaderId !== undefined ? runtime.getParamSchema?.(smShaderId) : undefined) ??
1008
+ submeshMaterial.materialParamSchema;
1020
1009
  const smPerShaderBgl =
1021
- smShaderId !== undefined ? runtime.getMaterialBindGroupLayout?.(smShaderId) : undefined;
1010
+ smShaderId !== undefined
1011
+ ? runtime.getMaterialBindGroupLayout?.(smShaderId, smSchema)
1012
+ : undefined;
1022
1013
  const smMaterialBgl = smPerShaderBgl ?? pipelineState.materialBindGroupLayout;
1014
+ const standardMaterial = isStandardPbrMaterialShader(smShaderId);
1015
+ const physicalFields =
1016
+ standardMaterial && smSchema !== undefined ? physicalTextureFields(smSchema) : [];
1017
+ // Standard's built-in shader owns the canonical seven-pair user region even
1018
+ // when the authored root only declares a subset. Physical maps are appended
1019
+ // after the reserved IBL/transmission tail, so compacting this list would
1020
+ // shift the shader's IBL bindings and invalidate the render pipeline.
1021
+ const smUserRegionFields =
1022
+ smPerShaderBgl === undefined || isCanonicalStandardPbrMaterialShader(smShaderId)
1023
+ ? BUILTIN_USER_REGION_TEXTURE_FIELDS
1024
+ : userRegionTextureFieldOrder(smSchema).filter((field) => !physicalFields.includes(field));
1023
1025
  const hasActiveTransmissionBackdrop = transmissionBackdropView != null;
1026
+ const diagnosticsEnabled = materialDiagnosticsEnabled();
1024
1027
  if (smMaterialHandle !== undefined && !smHasVideoFields && !hasActiveTransmissionBackdrop) {
1025
1028
  const cached =
1026
1029
  materialCacheKey === undefined ? undefined : materialBgAssemblyCache.get(materialCacheKey);
@@ -1035,21 +1038,48 @@ export function buildPerSubmeshMaterialBg(
1035
1038
  transmissionBackdropView,
1036
1039
  )
1037
1040
  ) {
1041
+ if (diagnosticsEnabled) {
1042
+ recordIblMaterialBinding(
1043
+ frameState,
1044
+ smMaterialBgl,
1045
+ cached.bindGroup,
1046
+ [
1047
+ {
1048
+ binding: 1 + smUserRegionFields.length * 2,
1049
+ resource: { kind: 'textureView', value: skylightResources.irradianceView },
1050
+ },
1051
+ {
1052
+ binding: 2 + smUserRegionFields.length * 2,
1053
+ resource: { kind: 'sampler', value: skylightResources.irradianceSampler },
1054
+ },
1055
+ {
1056
+ binding: 3 + smUserRegionFields.length * 2,
1057
+ resource: { kind: 'textureView', value: skylightResources.prefilterView },
1058
+ },
1059
+ {
1060
+ binding: 4 + smUserRegionFields.length * 2,
1061
+ resource: { kind: 'sampler', value: skylightResources.prefilterSampler },
1062
+ },
1063
+ {
1064
+ binding: 5 + smUserRegionFields.length * 2,
1065
+ resource: { kind: 'textureView', value: skylightResources.brdfLutView },
1066
+ },
1067
+ {
1068
+ binding: 6 + smUserRegionFields.length * 2,
1069
+ resource: { kind: 'sampler', value: skylightResources.brdfLutSampler },
1070
+ },
1071
+ {
1072
+ binding: 7 + smUserRegionFields.length * 2,
1073
+ resource: { kind: 'buffer', value: { buffer: skylightResources.intensityBuffer } },
1074
+ },
1075
+ ],
1076
+ 1 + smUserRegionFields.length * 2,
1077
+ 'hit',
1078
+ );
1079
+ }
1038
1080
  return cached.bindGroup;
1039
1081
  }
1040
1082
  }
1041
- const smSchema =
1042
- submeshMaterial.materialParamSchema ??
1043
- (smShaderId !== undefined ? runtime.getParamSchema?.(smShaderId) : undefined);
1044
- // The shipped PBR shaders always own the eight texture pairs declared by the
1045
- // standard shader artifact. The runtime PBR BGL exposes those pairs directly;
1046
- // emissive and occlusion are not duplicated in an engine injection region.
1047
- // Keep the record projection on the shader-owned layout so IBL starts at
1048
- // binding 17 rather than colliding with transmissionSampler.
1049
- const smUserRegionFields =
1050
- smPerShaderBgl === undefined || isStandardPbrMaterialShader(smShaderId)
1051
- ? BUILTIN_USER_REGION_TEXTURE_FIELDS
1052
- : userRegionTextureFieldOrder(smSchema);
1053
1083
  let materialResourcesResident = true;
1054
1084
  const smSamplerForField = (field: string | undefined): Sampler => {
1055
1085
  const handle = field === undefined ? undefined : submeshMaterial.samplerHandles?.get(field);
@@ -1132,6 +1162,38 @@ export function buildPerSubmeshMaterialBg(
1132
1162
  },
1133
1163
  );
1134
1164
  }
1165
+ // Physical Standard maps are deliberately appended after the stable IBL /
1166
+ // transmission tail. Keep their resource projection separate from the
1167
+ // user-region loop so the slot order is the same as
1168
+ // `appendTextureInjection(afterTransmission, physicalFields)` in the
1169
+ // layout owner.
1170
+ const physicalTextureInjections: TextureInjectionResource[] = [];
1171
+ for (let slot = 0; slot < physicalFields.length; slot += 1) {
1172
+ const field = physicalFields[slot];
1173
+ if (field === undefined) continue;
1174
+ let view = defaultViewForUserRegionField(field, pipelineState, smSchema);
1175
+ const targetSource = materialRenderTargetSourceForField(
1176
+ submeshMaterial,
1177
+ field,
1178
+ resolveRenderTargetTextureSource,
1179
+ );
1180
+ if (targetSource !== undefined) {
1181
+ if (targetSource.textureView !== undefined) view = targetSource.textureView;
1182
+ else materialResourcesResident = false;
1183
+ } else {
1184
+ const handle = submeshMaterial.textureHandles?.get(field);
1185
+ if (handle !== undefined) {
1186
+ const resident = residentTextureView(materialWorld, store, runtime, handle);
1187
+ if (resident !== undefined) view = resident;
1188
+ else materialResourcesResident = false;
1189
+ }
1190
+ }
1191
+ physicalTextureInjections.push({
1192
+ slot,
1193
+ sampler: smSamplerForField(field),
1194
+ view,
1195
+ });
1196
+ }
1135
1197
  // The backdrop is a full mip-chain view owned by the pipeline. Reuse the
1136
1198
  // pipeline's existing filtering/clamp sampler for screen-space sampling;
1137
1199
  // the material default sampler intentionally keeps repeat addressing for
@@ -1147,6 +1209,7 @@ export function buildPerSubmeshMaterialBg(
1147
1209
  sampler: transmissionSampler,
1148
1210
  ...(transmissionBackdropView === undefined ? {} : { backdropView: transmissionBackdropView }),
1149
1211
  } satisfies TransmissionBindGroupResources,
1212
+ physicalTextureInjections,
1150
1213
  );
1151
1214
  const createBindGroup = (): BindGroup => {
1152
1215
  const result = runtime.device.createBindGroup({
@@ -1157,16 +1220,60 @@ export function buildPerSubmeshMaterialBg(
1157
1220
  if (!result.ok) throw result.error;
1158
1221
  return result.value;
1159
1222
  };
1223
+ const materialCacheHandles = smMergedEntries.map((entry) => extractEntryResourceHandle(entry));
1224
+ if (
1225
+ diagnosticsEnabled &&
1226
+ submeshMaterial.textureHandles === undefined &&
1227
+ smMaterialHandle !== undefined
1228
+ ) {
1229
+ console.error(
1230
+ `[render-material] scalar material cache handles: ${JSON.stringify({
1231
+ entityKey,
1232
+ materialHandle: smMaterialHandle,
1233
+ shader: smShaderId,
1234
+ paramSnapshot: submeshMaterial.paramSnapshot,
1235
+ handleTypes: materialCacheHandles.map((handle) => typeof handle),
1236
+ })}`,
1237
+ );
1238
+ }
1239
+ if (
1240
+ diagnosticsEnabled &&
1241
+ materialCacheHandles.some((handle) => typeof handle !== 'object' || handle === null)
1242
+ ) {
1243
+ console.error(
1244
+ `[render-material] invalid material cache handle: ${JSON.stringify({
1245
+ entityKey,
1246
+ materialHandle: smMaterialHandle,
1247
+ shader: smShaderId,
1248
+ entries: smMergedEntries.map((entry, index) => ({
1249
+ index,
1250
+ binding: entry.binding,
1251
+ kind: entry.resource.kind,
1252
+ handleType: typeof materialCacheHandles[index],
1253
+ })),
1254
+ })}`,
1255
+ );
1256
+ }
1160
1257
  const smBindGroup = hasActiveTransmissionBackdrop
1161
1258
  ? createBindGroup()
1162
1259
  : getOrCreatePerEntity(
1163
1260
  materialBgShared,
1164
1261
  smShaderId ?? '',
1165
- smMergedEntries.map((e) => extractEntryResourceHandle(e)),
1262
+ materialCacheHandles,
1166
1263
  'material-shared',
1167
1264
  createBindGroup,
1168
1265
  bindGroupCounts,
1169
1266
  );
1267
+ if (diagnosticsEnabled) {
1268
+ recordIblMaterialBinding(
1269
+ frameState,
1270
+ smMaterialBgl,
1271
+ smBindGroup,
1272
+ smMergedEntries,
1273
+ smBaseEntries.length,
1274
+ 'miss',
1275
+ );
1276
+ }
1170
1277
  if (
1171
1278
  smMaterialHandle !== undefined &&
1172
1279
  !smHasVideoFields &&