@forgeax/engine-render 0.1.29 → 0.1.31

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 (350) hide show
  1. package/README.md +248 -21
  2. package/dist/assembly/atmosphere-shader-prewarm.d.ts +1 -1
  3. package/dist/assembly/atmosphere-shader-prewarm.d.ts.map +1 -1
  4. package/dist/assembly/device-feature-admission.d.ts +4 -3
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -1
  6. package/dist/assembly/dynamic-geometry-host.d.ts +6 -0
  7. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  8. package/dist/assembly/host-contract.d.ts +6 -0
  9. package/dist/assembly/host-contract.d.ts.map +1 -1
  10. package/dist/assembly/material/assembly.d.ts +26 -3
  11. package/dist/assembly/material/assembly.d.ts.map +1 -1
  12. package/dist/assembly/material/pipeline-helpers.d.ts +24 -0
  13. package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -0
  14. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  15. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
  16. package/dist/assembly/recovery/recovery-attempt.d.ts +1 -6
  17. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -1
  18. package/dist/assembly/recovery/renderer-recover.d.ts +1 -0
  19. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  20. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  21. package/dist/assembly/shader-prewarm-policy.d.ts +1 -1
  22. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -1
  23. package/dist/assembly/webgpu-ready.d.ts +1 -1
  24. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  25. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  26. package/dist/capture/scene-projection.d.ts +41 -0
  27. package/dist/capture/scene-projection.d.ts.map +1 -0
  28. package/dist/{chunk-BQVQOQJH.mjs → chunk-7MQ3XBSP.mjs} +20 -14
  29. package/dist/chunk-7MQ3XBSP.mjs.map +1 -0
  30. package/dist/{chunk-S4DNKQYQ.mjs → chunk-C3DDSXYK.mjs} +86 -33
  31. package/dist/chunk-C3DDSXYK.mjs.map +1 -0
  32. package/dist/{chunk-BTWH7BP4.mjs → chunk-FUUO4UND.mjs} +73 -29
  33. package/dist/chunk-FUUO4UND.mjs.map +1 -0
  34. package/dist/{chunk-NRNXBLIJ.mjs → chunk-JRBDO2KB.mjs} +46 -6
  35. package/dist/chunk-JRBDO2KB.mjs.map +1 -0
  36. package/dist/{chunk-NICXSHAT.mjs → chunk-MDQJPY3T.mjs} +8 -6
  37. package/dist/chunk-MDQJPY3T.mjs.map +1 -0
  38. package/dist/{chunk-JQTSEI3Z.mjs → chunk-NO4WZCAH.mjs} +3 -3
  39. package/dist/chunk-NO4WZCAH.mjs.map +1 -0
  40. package/dist/{chunk-FUODTTIA.mjs → chunk-PSEQ6QPH.mjs} +621 -108
  41. package/dist/chunk-PSEQ6QPH.mjs.map +1 -0
  42. package/dist/{chunk-33XZFTQU.mjs → chunk-S5UE2T5P.mjs} +48 -6
  43. package/dist/chunk-S5UE2T5P.mjs.map +1 -0
  44. package/dist/components/directional-light.d.ts +2 -2
  45. package/dist/components/dynamic-resolution.d.ts +15 -0
  46. package/dist/components/dynamic-resolution.d.ts.map +1 -0
  47. package/dist/components/index.d.ts +1 -0
  48. package/dist/components/index.d.ts.map +1 -1
  49. package/dist/components/light-helpers.d.ts.map +1 -1
  50. package/dist/components/reflection-probe.d.ts +1 -0
  51. package/dist/components/reflection-probe.d.ts.map +1 -1
  52. package/dist/components/skylight.d.ts +4 -1
  53. package/dist/components/skylight.d.ts.map +1 -1
  54. package/dist/construct-renderer.mjs +2172 -796
  55. package/dist/construct-renderer.mjs.map +1 -1
  56. package/dist/device/device-scope.d.ts +2 -0
  57. package/dist/device/device-scope.d.ts.map +1 -1
  58. package/dist/device/gpu-residency.d.ts +2 -2
  59. package/dist/device/gpu-residency.d.ts.map +1 -1
  60. package/dist/dynamic-geometry.d.ts.map +1 -1
  61. package/dist/environment/background.d.ts +3 -2
  62. package/dist/environment/background.d.ts.map +1 -1
  63. package/dist/environment/ibl.d.ts +11 -0
  64. package/dist/environment/ibl.d.ts.map +1 -0
  65. package/dist/environment/storage.d.ts +14 -0
  66. package/dist/environment/storage.d.ts.map +1 -0
  67. package/dist/errors/gpu-driven.d.ts +2 -2
  68. package/dist/errors/gpu-driven.d.ts.map +1 -1
  69. package/dist/errors/render.d.ts +35 -1
  70. package/dist/errors/render.d.ts.map +1 -1
  71. package/dist/extract/camera.d.ts.map +1 -1
  72. package/dist/extract/contracts.d.ts +7 -0
  73. package/dist/extract/contracts.d.ts.map +1 -1
  74. package/dist/extract/gpu-driven.d.ts +18 -1
  75. package/dist/extract/gpu-driven.d.ts.map +1 -1
  76. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  77. package/dist/gpu-driven/material-artifacts.d.ts +24 -0
  78. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -0
  79. package/dist/gpu-driven/material-bindings.d.ts +7 -0
  80. package/dist/gpu-driven/material-bindings.d.ts.map +1 -0
  81. package/dist/gpu-driven/prepared-draw.d.ts +2 -0
  82. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  83. package/dist/gpu-driven/production-raster.d.ts +27 -6
  84. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  85. package/dist/gpu-resource.d.ts +5 -2
  86. package/dist/gpu-resource.d.ts.map +1 -1
  87. package/dist/gpu-scene-schema.d.ts.map +1 -1
  88. package/dist/gpu-scene.d.ts.map +1 -1
  89. package/dist/hdrp-buffers.d.ts +3 -0
  90. package/dist/hdrp-buffers.d.ts.map +1 -1
  91. package/dist/ibl/IblPipelineCache.d.ts +2 -0
  92. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  93. package/dist/ibl/skylight-bind-group.d.ts +3 -1
  94. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  95. package/dist/index.d.ts +83 -81
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.mjs +7 -5
  98. package/dist/index.mjs.map +1 -1
  99. package/dist/inspection-types.d.ts +16 -1
  100. package/dist/inspection-types.d.ts.map +1 -1
  101. package/dist/internal.mjs +6 -6
  102. package/dist/material-row.d.ts +10 -0
  103. package/dist/material-row.d.ts.map +1 -1
  104. package/dist/pbr-pipeline.d.ts +3 -1
  105. package/dist/pbr-pipeline.d.ts.map +1 -1
  106. package/dist/pipeline/render-extent.d.ts +51 -0
  107. package/dist/pipeline/render-extent.d.ts.map +1 -0
  108. package/dist/pipeline/standard-build-context.d.ts +7 -0
  109. package/dist/pipeline/standard-build-context.d.ts.map +1 -0
  110. package/dist/pipeline/standard-forward-lane.d.ts +3 -2
  111. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  112. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  113. package/dist/pipeline/standard-post.d.ts +1 -1
  114. package/dist/pipeline/standard-post.d.ts.map +1 -1
  115. package/dist/pipeline/standard-profile.d.ts +2 -1
  116. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  117. package/dist/pipeline-builder.d.ts +3 -1
  118. package/dist/pipeline-builder.d.ts.map +1 -1
  119. package/dist/plugin.d.ts.map +1 -1
  120. package/dist/post-process-errors.d.ts +9 -2
  121. package/dist/post-process-errors.d.ts.map +1 -1
  122. package/dist/prepare/extended-lighting/resources.d.ts +3 -3
  123. package/dist/record/frame-lighting.d.ts.map +1 -1
  124. package/dist/record/frame-snapshot.d.ts +3 -8
  125. package/dist/record/frame-snapshot.d.ts.map +1 -1
  126. package/dist/record/frame.d.ts +3 -2
  127. package/dist/record/frame.d.ts.map +1 -1
  128. package/dist/record/main-pass-geometry.d.ts +7 -2
  129. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  130. package/dist/record/main-pass-material.d.ts +6 -3
  131. package/dist/record/main-pass-material.d.ts.map +1 -1
  132. package/dist/record/main-pass.d.ts +2 -0
  133. package/dist/record/main-pass.d.ts.map +1 -1
  134. package/dist/record/prepared-material-bindings.d.ts.map +1 -1
  135. package/dist/record/recovery-pipeline.d.ts +9 -1
  136. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  137. package/dist/record/render-context.d.ts +34 -3
  138. package/dist/record/render-context.d.ts.map +1 -1
  139. package/dist/record/shadow-pass.d.ts +2 -2
  140. package/dist/record/shadow-pass.d.ts.map +1 -1
  141. package/dist/record/typed-frame-graph.d.ts +5 -2
  142. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  143. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  144. package/dist/reflection/background.d.ts +14 -0
  145. package/dist/reflection/background.d.ts.map +1 -0
  146. package/dist/reflection/filter.d.ts.map +1 -1
  147. package/dist/reflection/projection.d.ts +1 -0
  148. package/dist/reflection/projection.d.ts.map +1 -1
  149. package/dist/reflection/record-owner.d.ts +4 -2
  150. package/dist/reflection/record-owner.d.ts.map +1 -1
  151. package/dist/render-contract.d.ts +47 -32
  152. package/dist/render-contract.d.ts.map +1 -1
  153. package/dist/render-graph-primitives.d.ts.map +1 -1
  154. package/dist/render-pipeline.d.ts +17 -1
  155. package/dist/render-pipeline.d.ts.map +1 -1
  156. package/dist/render-system-extract-tail.d.ts +9 -2
  157. package/dist/render-system-extract-tail.d.ts.map +1 -1
  158. package/dist/render-system-extract.d.ts +29 -2
  159. package/dist/render-system-extract.d.ts.map +1 -1
  160. package/dist/render-system.d.ts.map +1 -1
  161. package/dist/scene/render-scene.d.ts +2 -0
  162. package/dist/scene/render-scene.d.ts.map +1 -1
  163. package/dist/ssao-config.d.ts +8 -6
  164. package/dist/ssao-config.d.ts.map +1 -1
  165. package/dist/ssr/graph.d.ts.map +1 -1
  166. package/dist/temporal/gpu.d.ts.map +1 -1
  167. package/dist/temporal/index.mjs +5 -5
  168. package/dist/temporal/standard-scene-data.d.ts +2 -1
  169. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  170. package/dist/temporal/target-coverage-attachment.d.ts +15 -0
  171. package/dist/temporal/target-coverage-attachment.d.ts.map +1 -0
  172. package/dist/temporal/temporal-view.d.ts +6 -2
  173. package/dist/temporal/temporal-view.d.ts.map +1 -1
  174. package/dist/temporal/view.d.ts +6 -0
  175. package/dist/temporal/view.d.ts.map +1 -1
  176. package/dist/typed-render-graph-primitives.d.ts +6 -0
  177. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  178. package/dist/volume/component.d.ts +1 -0
  179. package/dist/volume/component.d.ts.map +1 -1
  180. package/package.json +21 -21
  181. package/src/__tests__/capture-scene-projection.unit.test.ts +24 -0
  182. package/src/__tests__/device-feature-admission.unit.test.ts +15 -24
  183. package/src/__tests__/device-loss-fanout.unit.test.ts +43 -0
  184. package/src/__tests__/dynamic-geometry-batch.unit.test.ts +157 -0
  185. package/src/__tests__/dynamic-geometry.unit.test.ts +36 -0
  186. package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +81 -0
  187. package/src/__tests__/factory-contract.integration.test.ts +164 -1
  188. package/src/__tests__/frame-observation.contract.test.ts +21 -0
  189. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +1 -1
  190. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +20 -1
  191. package/src/__tests__/gpu-driven-lod-cache.unit.test.ts +0 -3
  192. package/src/__tests__/gpu-driven-mesh-world-identity.unit.test.ts +0 -3
  193. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +11 -6
  194. package/src/__tests__/gpu-driven-production.integration.test.ts +13 -25
  195. package/src/__tests__/gpu-driven-view.unit.test.ts +1 -0
  196. package/src/__tests__/gpu-pass-timing-browser-runner.ts +15 -0
  197. package/src/__tests__/gpu-pass-timing.browser.test.ts +14 -1
  198. package/src/__tests__/gpu-resource-scope.unit.test.ts +71 -0
  199. package/src/__tests__/gpu-scene.unit.test.ts +55 -1
  200. package/src/__tests__/materials-standard-contract.unit.test.ts +5 -1
  201. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +8 -0
  202. package/src/__tests__/morph-neutral-admission.unit.test.ts +19 -0
  203. package/src/__tests__/physical-clearcoat.integration.test.ts +6 -1
  204. package/src/__tests__/post-process-error-policy-owner.unit.test.ts +6 -5
  205. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +216 -5
  206. package/src/__tests__/reflection-fallback-async-publication.unit.test.ts +68 -1
  207. package/src/__tests__/reflection-probe-authoring.unit.test.ts +12 -10
  208. package/src/__tests__/reflection-probe-box.dawn.test.ts +71 -0
  209. package/src/__tests__/reflection-probe-filter.unit.test.ts +9 -0
  210. package/src/__tests__/reflection-probe-orientation.dawn.test.ts +294 -0
  211. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +5 -15
  212. package/src/__tests__/reflection-probe-updates.unit.test.ts +129 -0
  213. package/src/__tests__/render-error-code-owner.test-d.ts +3 -0
  214. package/src/__tests__/render-error-detail-ssot.test-d.ts +5 -1
  215. package/src/__tests__/render-extent.unit.test.ts +63 -0
  216. package/src/__tests__/render-scene-projection.unit.test.ts +3 -2
  217. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +173 -9
  218. package/src/__tests__/shader-manifest-fixture.ts +6 -0
  219. package/src/__tests__/skinned-shadow-caster.test.ts +8 -1
  220. package/src/__tests__/specular-environment-scale.dawn.test.ts +4 -2
  221. package/src/__tests__/ssao-config.unit.test.ts +7 -0
  222. package/src/__tests__/ssao-surface.dawn.test.ts +252 -0
  223. package/src/__tests__/ssr-cube-silhouette.dawn.test.ts +270 -0
  224. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +51 -43
  225. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +4 -0
  226. package/src/__tests__/ssr-gpu-dispatch.ts +10 -5
  227. package/src/__tests__/ssr-graph-topology.unit.test.ts +2 -2
  228. package/src/__tests__/ssr-hiz.browser.test.ts +1 -1
  229. package/src/__tests__/ssr-hiz.dawn.test.ts +1 -1
  230. package/src/__tests__/ssr-spatial.integration.test.ts +1 -1
  231. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +40 -0
  232. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +238 -1
  233. package/src/__tests__/standard-profile-oracle.unit.test.ts +27 -0
  234. package/src/__tests__/taa-coverage-edge.dawn.test.ts +6 -0
  235. package/src/__tests__/taa-neighborhood-equivalence.dawn.test.ts +5 -1
  236. package/src/__tests__/taa-stability-feedback.ts +1 -0
  237. package/src/__tests__/temporal-reset-matrix.unit.test.ts +2 -2
  238. package/src/__tests__/temporal-view.unit.test.ts +17 -0
  239. package/src/__tests__/transmission-prewarm.unit.test.ts +57 -0
  240. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +62 -13
  241. package/src/__tests__/volumetric-fog-topology.unit.test.ts +7 -7
  242. package/src/assembly/atmosphere-shader-prewarm.ts +3 -2
  243. package/src/assembly/device-feature-admission.ts +9 -4
  244. package/src/assembly/dynamic-geometry-host.ts +236 -163
  245. package/src/assembly/host-contract.ts +9 -0
  246. package/src/assembly/material/assembly.ts +350 -6
  247. package/src/assembly/material/pipeline-helpers.ts +100 -0
  248. package/src/assembly/material-shader-policy.ts +39 -18
  249. package/src/assembly/recovery/device-loss-fanout.ts +5 -0
  250. package/src/assembly/recovery/recovery-attempt.ts +2 -29
  251. package/src/assembly/recovery/renderer-recover.ts +6 -9
  252. package/src/assembly/renderer-facade.ts +22 -1
  253. package/src/assembly/shader-prewarm-policy.ts +14 -1
  254. package/src/assembly/webgpu-ready.ts +28 -19
  255. package/src/assembly/webgpu-renderer.ts +95 -95
  256. package/src/capture/scene-projection.ts +37 -0
  257. package/src/components/__tests__/dynamic-resolution-schema.unit.test.ts +65 -0
  258. package/src/components/__tests__/light-radius.integration.test.ts +59 -0
  259. package/src/components/directional-light.ts +3 -3
  260. package/src/components/dynamic-resolution.ts +67 -0
  261. package/src/components/index.ts +1 -0
  262. package/src/components/light-helpers.ts +6 -1
  263. package/src/components/reflection-probe.ts +1 -0
  264. package/src/components/skylight.ts +4 -1
  265. package/src/device/device-scope.ts +13 -7
  266. package/src/device/gpu-residency.ts +11 -11
  267. package/src/dynamic-geometry.ts +13 -8
  268. package/src/environment/background.ts +30 -12
  269. package/src/environment/ibl.ts +211 -0
  270. package/src/environment/storage.ts +63 -0
  271. package/src/errors/gpu-driven.ts +10 -3
  272. package/src/errors/render.ts +64 -0
  273. package/src/extract/camera.ts +12 -1
  274. package/src/extract/contracts.ts +10 -0
  275. package/src/extract/gpu-driven.ts +72 -17
  276. package/src/gpu-driven/batch-topology.ts +33 -11
  277. package/src/gpu-driven/material-artifacts.ts +179 -0
  278. package/src/gpu-driven/material-bindings.ts +41 -0
  279. package/src/gpu-driven/prepared-draw.ts +13 -2
  280. package/src/gpu-driven/production-raster.ts +681 -164
  281. package/src/gpu-resource.ts +21 -2
  282. package/src/gpu-scene-schema.ts +11 -2
  283. package/src/gpu-scene.ts +14 -3
  284. package/src/hdrp-buffers.ts +69 -0
  285. package/src/ibl/IblPipelineCache.ts +5 -0
  286. package/src/ibl/skylight-bind-group.ts +27 -17
  287. package/src/index.ts +92 -81
  288. package/src/inspection-types.ts +16 -1
  289. package/src/material-row.ts +60 -11
  290. package/src/pbr-pipeline.ts +13 -2
  291. package/src/pipeline/render-extent.ts +111 -0
  292. package/src/pipeline/standard-build-context.ts +7 -0
  293. package/src/pipeline/standard-forward-lane.ts +41 -24
  294. package/src/pipeline/standard-pipeline.ts +47 -18
  295. package/src/pipeline/standard-post.ts +4 -0
  296. package/src/pipeline/standard-profile.ts +2 -1
  297. package/src/pipeline-builder.ts +12 -3
  298. package/src/plugin.ts +4 -0
  299. package/src/post-process-errors.ts +26 -18
  300. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +2 -2
  301. package/src/prepare/extended-lighting/resources.ts +3 -3
  302. package/src/record/__tests__/main-pass-geometry.unit.test.ts +68 -0
  303. package/src/record/__tests__/material-bg-cache.unit.test.ts +45 -0
  304. package/src/record/__tests__/material-slot-plan.unit.test.ts +9 -0
  305. package/src/record/__tests__/recovery-pipeline.unit.test.ts +47 -1
  306. package/src/record/__tests__/sprite-material-abi.unit.test.ts +7 -3
  307. package/src/record/__tests__/temporal-payload.unit.test.ts +2 -0
  308. package/src/record/frame-lighting.ts +3 -1
  309. package/src/record/frame-snapshot.ts +13 -11
  310. package/src/record/frame.ts +150 -74
  311. package/src/record/main-pass-geometry.ts +60 -9
  312. package/src/record/main-pass-material.ts +47 -19
  313. package/src/record/main-pass.ts +63 -4
  314. package/src/record/prepared-material-bindings.ts +4 -0
  315. package/src/record/recovery-pipeline.ts +56 -0
  316. package/src/record/render-context.ts +34 -3
  317. package/src/record/shadow-pass.ts +187 -50
  318. package/src/record/typed-frame-graph.ts +60 -3
  319. package/src/record/view-ubo.ts +1 -1
  320. package/src/recovery/render-system-candidate.ts +54 -11
  321. package/src/reflection/background.ts +151 -0
  322. package/src/reflection/filter.ts +3 -0
  323. package/src/reflection/projection.ts +16 -6
  324. package/src/reflection/record-owner.ts +282 -86
  325. package/src/render-contract.ts +50 -32
  326. package/src/render-graph-primitives.ts +28 -3
  327. package/src/render-pipeline.ts +20 -1
  328. package/src/render-system-extract-tail.ts +23 -3
  329. package/src/render-system-extract.ts +127 -42
  330. package/src/render-system.ts +35 -7
  331. package/src/scene/render-scene.ts +8 -0
  332. package/src/ssao-config.ts +25 -8
  333. package/src/ssr/graph.ts +78 -58
  334. package/src/temporal/gpu.ts +5 -0
  335. package/src/temporal/standard-scene-data.ts +7 -1
  336. package/src/temporal/target-coverage-attachment.ts +72 -0
  337. package/src/temporal/temporal-view.ts +10 -4
  338. package/src/temporal/view.ts +12 -2
  339. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +24 -9
  340. package/src/typed-render-graph-primitives.ts +38 -0
  341. package/src/volume/component.ts +1 -0
  342. package/src/volume/passes.ts +3 -3
  343. package/dist/chunk-33XZFTQU.mjs.map +0 -1
  344. package/dist/chunk-BQVQOQJH.mjs.map +0 -1
  345. package/dist/chunk-BTWH7BP4.mjs.map +0 -1
  346. package/dist/chunk-FUODTTIA.mjs.map +0 -1
  347. package/dist/chunk-JQTSEI3Z.mjs.map +0 -1
  348. package/dist/chunk-NICXSHAT.mjs.map +0 -1
  349. package/dist/chunk-NRNXBLIJ.mjs.map +0 -1
  350. package/dist/chunk-S4DNKQYQ.mjs.map +0 -1
@@ -24,9 +24,11 @@ import type { MeshGpuHandles } from '../device/gpu-residency';
24
24
  import { GpuDrivenPreparationError } from '../errors/gpu-driven';
25
25
  import {
26
26
  gpuDrivenDrawKey,
27
+ gpuDrivenMaterialArtifactKey,
27
28
  gpuDrivenShadowDrawKey,
28
29
  gpuDrivenSourceDrawItemIndex,
29
30
  } from '../extract/gpu-driven';
31
+ import { GPU_SCENE_LAYOUTS } from '../gpu-scene-schema';
30
32
  import {
31
33
  GPU_BUFFER_USAGE_COPY_DST,
32
34
  GPU_BUFFER_USAGE_INDEX,
@@ -40,14 +42,20 @@ import type {
40
42
  GpuDrivenProductionInspection,
41
43
  GpuDrivenStructureMetrics,
42
44
  } from '../inspection-types';
45
+ import { isStandardPbrMaterialShader, resolvePipelineGroup2Contract } from '../pbr-pipeline';
43
46
  import type { PipelineBuilderShaderModuleFactory } from '../pipeline-builder';
44
47
  import type { ValidatedRenderable } from '../record/frame-snapshot';
45
48
  import { worldEntityKey } from '../record/frame-snapshot';
46
49
  import { geometryRenderStateForTopology } from '../record/main-pass-material';
47
50
  import { MESH_SSBO_BYTES, packInstanceStorageBuffer } from '../record/mesh-ssbo';
48
- import type { PipelineState } from '../record/render-context';
51
+ import {
52
+ MATERIAL_PER_ENTITY_STRIDE,
53
+ type PipelineState,
54
+ type RenderSystemInternals,
55
+ } from '../record/render-context';
49
56
  import type { CameraSnapshot } from '../render-contract';
50
57
  import type {
58
+ GpuDrivenSceneMeshBinding,
51
59
  GpuDrivenStandardPbrFrameResources,
52
60
  RenderPipelineFrame,
53
61
  RenderPipelineGpuDrivenProjection,
@@ -67,6 +75,7 @@ import {
67
75
  projectedHeightForCandidate,
68
76
  type SubmissionPlan,
69
77
  } from './batch-topology';
78
+ import { materialBindingKey } from './material-bindings';
70
79
  import { type GpuDrivenPbrProgram, resolveStandardPbrProgram } from './pbr-program';
71
80
  import {
72
81
  type ShadowViewIdentity,
@@ -99,10 +108,17 @@ export interface GpuDrivenShadowBatchProjection {
99
108
  readonly meshBindGroup: BindGroup;
100
109
  readonly visibleBindGroup: BindGroup;
101
110
  readonly deformation: 'rigid' | 'skin';
111
+ /** Actual producer-selected shadow-pass artifact for this batch. */
112
+ readonly shadowArtifact: MaterialShaderArtifact;
113
+ /** Pass entry points selected by the authored ShadowCaster dispatch. */
114
+ readonly shadowVertexEntry?: string;
115
+ readonly shadowFragmentEntry?: string;
102
116
  /** Material snapshot used to assemble the receipt-owned alpha-mask BG. */
103
117
  readonly material: MaterialSnapshot;
104
118
  /** Composite entity identity used only for video-resource routing. */
105
119
  readonly materialEntityKey: number;
120
+ /** Geometry-owned COLOR_0 fact shared by main and shadow ABI variants. */
121
+ readonly vertexColorAvailable: boolean;
106
122
  }
107
123
 
108
124
  /**
@@ -227,6 +243,8 @@ export function adaptStandardPbrFrameResources(input: {
227
243
  interface PreparedBatch {
228
244
  readonly batch: GpuDrivenBatch;
229
245
  readonly mesh: MeshGpuHandles;
246
+ /** Exact producer artifact selected by every draw in this homogeneous batch. */
247
+ readonly artifact: MaterialShaderArtifact;
230
248
  readonly renderState: MaterialRenderState | undefined;
231
249
  /** Stable World attachment key used to join same-submit GPU counters. */
232
250
  readonly worldKey: number;
@@ -298,12 +316,8 @@ function hasFiniteOrderedBounds(bounds: Float32Array | undefined): boolean {
298
316
 
299
317
  function skinLaneReason(
300
318
  snapshot: RenderableSnapshot,
301
- material: MaterialSnapshot,
302
319
  ): 'skin-bounds-missing' | 'skin-address-missing' | undefined {
303
- const isSkinnedPbr =
304
- material.materialShaderId === 'forgeax::pbr-skin' ||
305
- material.materialShaderId === 'forgeax::default-standard-pbr-skin';
306
- if (!isSkinnedPbr || snapshot.skin === undefined) return undefined;
320
+ if (snapshot.skin === undefined) return undefined;
307
321
  if (!hasFiniteOrderedBounds(snapshot.skin.bounds)) return 'skin-bounds-missing';
308
322
  if (
309
323
  snapshot.skin.storageOrUniform !== 'storage' ||
@@ -347,15 +361,19 @@ function preparedPbrCandidateEligible(
347
361
  snapshot: RenderableSnapshot,
348
362
  batch: GpuDrivenBatch,
349
363
  candidate: GpuDrivenBatch['candidates'][number],
350
- skinArtifact?: MaterialShaderArtifact,
364
+ artifact: MaterialShaderArtifact | undefined,
351
365
  ): boolean {
352
366
  const material = snapshot.materials[batch.key.materialSlot] ?? snapshot.material;
353
367
  const prepared = candidate.prepared;
354
368
  if (prepared === undefined) return false;
355
- const standard =
356
- prepared.identity.material === 'forgeax::default-standard-pbr' ||
357
- prepared.identity.material === 'forgeax::pbr-skin' ||
358
- prepared.identity.material === 'forgeax::default-standard-pbr-skin';
369
+ const receipt = artifact?.receipt;
370
+ const publishedAbiMatches =
371
+ receipt !== undefined &&
372
+ artifact?.material === prepared.identity.material &&
373
+ (prepared.receiptIdentity === undefined ||
374
+ (prepared.receiptIdentity === receipt.receiptIdentity &&
375
+ prepared.receiptGeneration === receipt.generation)) &&
376
+ receipt.reflection.layoutIdentity === artifact?.layoutIdentity;
359
377
  const alphaBlend = material.transparent === true || material.renderState?.blend !== undefined;
360
378
  const cpuOnlyMaterialResources =
361
379
  (material.textureSources?.size ?? 0) > 0 || (material.videoTextureFields?.size ?? 0) > 0;
@@ -363,18 +381,19 @@ function preparedPbrCandidateEligible(
363
381
  // GPU-owned candidate set so the residual transmission draw is not swallowed
364
382
  // by the generic Standard PBR lane.
365
383
  const transmission =
384
+ isStandardPbrMaterialShader(prepared.identity.material) &&
366
385
  typeof material.paramSnapshot?.transmission === 'number' &&
367
386
  material.paramSnapshot.transmission > 0;
368
387
  const skinReady =
369
388
  prepared.identity.deformation !== 'skin' ||
370
- (skinArtifact?.receipt?.skinPaletteAddress !== undefined &&
389
+ (artifact?.receipt?.skinPaletteAddress !== undefined &&
371
390
  snapshot.skin?.storageOrUniform === 'storage' &&
372
391
  snapshot.skin.customDataStart >= 0 &&
373
392
  Number.isInteger(snapshot.skin.customDataStart) &&
374
393
  snapshot.skin.buffer !== undefined &&
375
394
  hasFiniteOrderedBounds(snapshot.skin.bounds));
376
395
  return (
377
- standard &&
396
+ publishedAbiMatches &&
378
397
  !alphaBlend &&
379
398
  !cpuOnlyMaterialResources &&
380
399
  !transmission &&
@@ -432,7 +451,13 @@ function gpuDrivenProjectionKey(
432
451
  deformation: 'rigid' | 'skin',
433
452
  artifact: MaterialShaderArtifact,
434
453
  ): string {
435
- return `${batch.batchId}:${batch.generation}:${deformation}:${artifact.layoutIdentity}`;
454
+ const receipt = artifact.receipt;
455
+ return `${batch.batchId}:${batch.generation}:${gpuDrivenMaterialArtifactKey({
456
+ material: artifact.material,
457
+ deformation,
458
+ receiptIdentity: receipt?.receiptIdentity,
459
+ receiptGeneration: receipt?.generation ?? 0,
460
+ })}:${artifact.variantSet ?? ''}:${materialArtifactProgramIdentity(artifact)}:${artifact.vertexEntry ?? receipt?.sceneIndexEntry ?? ''}:${artifact.fragmentEntry ?? ''}`;
436
461
  }
437
462
 
438
463
  function gpuDrivenShadowProjectionKey(
@@ -454,6 +479,26 @@ function sourceDrawForCandidate(
454
479
  );
455
480
  }
456
481
 
482
+ const materialArtifactProgramIdentities = new WeakMap<
483
+ MaterialShaderArtifact,
484
+ { readonly source: string; readonly identity: string }
485
+ >();
486
+
487
+ /** Hash a publication once; replacement source invalidates the same artifact key. */
488
+ function materialArtifactProgramIdentity(artifact: MaterialShaderArtifact): string {
489
+ const source = artifact.specializationKey ?? artifact.wgsl;
490
+ const cached = materialArtifactProgramIdentities.get(artifact);
491
+ if (cached?.source === source) return cached.identity;
492
+ let hash = 2166136261;
493
+ for (let index = 0; index < source.length; index += 1) {
494
+ hash ^= source.charCodeAt(index);
495
+ hash = Math.imul(hash, 16777619);
496
+ }
497
+ const identity = `${source.length}:${hash >>> 0}`;
498
+ materialArtifactProgramIdentities.set(artifact, { source, identity });
499
+ return identity;
500
+ }
501
+
457
502
  function shadowKeysForCandidate(
458
503
  slot: PersistentGpuDrivenState['slots'][number],
459
504
  candidate: GpuDrivenBatch['candidates'][number],
@@ -524,8 +569,10 @@ function shadowMembershipIsGpuCompatible(
524
569
  const membership = matching[0];
525
570
  if (membership?.cpuReason !== undefined) return false;
526
571
  if (membership?.renderState !== undefined) return false;
527
- const shader = membership?.materialShaderId;
528
- return shader === undefined || shader === 'forgeax::default-shadow-caster';
572
+ // Older synthetic membership fixtures omit the producer flag and remain
573
+ // compatible; extracted records always carry it. A false flag is an
574
+ // authored/producer decision and must keep the draw on its CPU lane.
575
+ return membership?.gpuDrivenEligible !== false;
529
576
  }
530
577
 
531
578
  function selectedLodLevel(candidate: GpuDrivenCandidate): number {
@@ -578,6 +625,9 @@ function filteredPlan(
578
625
  camera?: CameraSnapshot,
579
626
  activeEntityKeys?: ReadonlySet<number>,
580
627
  worldKeys?: readonly number[],
628
+ materialArtifact?: MaterialShaderArtifact,
629
+ materialArtifacts?: ReadonlyMap<string, MaterialShaderArtifact>,
630
+ materialBindingClasses?: ReadonlyMap<string, string>,
581
631
  ): {
582
632
  readonly plan: SubmissionPlan;
583
633
  readonly batches: readonly PreparedBatch[];
@@ -601,6 +651,7 @@ function filteredPlan(
601
651
  }
602
652
  }
603
653
  const eligibleMainCandidates = new Set<string>();
654
+ const eligibleArtifacts = new Map<string, MaterialShaderArtifact>();
604
655
  const drawKeys = new Set<string>();
605
656
  const eligibleShadowCandidates = new Set<string>();
606
657
  const shadowDrawKeys = new Set<string>();
@@ -617,13 +668,23 @@ function filteredPlan(
617
668
  for (const candidate of batch.candidates) {
618
669
  if (candidate.primitiveIndex !== slot.slot) continue;
619
670
  const draw = sourceDrawForCandidate(slot, candidate);
671
+ const selectedArtifact = artifactForPreparedCandidate(
672
+ candidate,
673
+ materialArtifacts,
674
+ materialArtifact,
675
+ skinArtifact,
676
+ );
620
677
  const mainEligible =
621
678
  draw !== undefined &&
622
- preparedPbrCandidateEligible(slot.snapshot, batch, candidate, skinArtifact) &&
679
+ preparedPbrCandidateEligible(slot.snapshot, batch, candidate, selectedArtifact) &&
623
680
  mesh !== undefined &&
624
681
  meshSupportsBatch(mesh, batch);
682
+ const candidateKey = shadowCandidateKey(candidate);
683
+ if (mainEligible && selectedArtifact !== undefined) {
684
+ eligibleArtifacts.set(candidateKey, selectedArtifact);
685
+ }
625
686
  if (!shadowSelectionProvided && mainEligible && draw !== undefined) {
626
- eligibleMainCandidates.add(shadowCandidateKey(candidate));
687
+ eligibleMainCandidates.add(candidateKey);
627
688
  const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
628
689
  drawKeys.add(
629
690
  gpuDrivenDrawKey(
@@ -661,6 +722,7 @@ function filteredPlan(
661
722
  {
662
723
  readonly source: GpuDrivenBatch;
663
724
  readonly mesh: MeshGpuHandles;
725
+ readonly artifact: MaterialShaderArtifact;
664
726
  readonly renderState: MaterialRenderState | undefined;
665
727
  readonly candidates: GpuDrivenCandidate[];
666
728
  }
@@ -684,6 +746,8 @@ function filteredPlan(
684
746
  if (slot === undefined) continue;
685
747
  const mesh = meshBySlot.get(slot.slot);
686
748
  if (mesh === undefined) continue;
749
+ const artifact = eligibleArtifacts.get(shadowCandidateKey(candidate));
750
+ if (artifact === undefined) continue;
687
751
  const physicalLodRanges = mesh.lodRanges
688
752
  ?.slice(1)
689
753
  .map((ranges) => ranges[candidate.drawItemIndex]);
@@ -701,7 +765,23 @@ function filteredPlan(
701
765
  const selectedRange = selectedLodRange(batch, projected);
702
766
  const groupKey = JSON.stringify({
703
767
  key: batch.key,
768
+ artifact: [
769
+ artifact.material,
770
+ artifact.layoutIdentity,
771
+ artifact.receipt?.receiptIdentity ?? '',
772
+ artifact.receipt?.generation ?? 0,
773
+ artifact.variantSet ?? '',
774
+ materialArtifactProgramIdentity(artifact),
775
+ ],
704
776
  worldKey: worldKeyFor(slot),
777
+ bindingClass:
778
+ materialBindingClasses?.get(
779
+ materialBindingKey(
780
+ slot.snapshot,
781
+ (slot.snapshot.materials[batch.key.materialSlot] ?? slot.snapshot.material)
782
+ .materialHandle ?? -1,
783
+ ),
784
+ ) ?? '',
705
785
  lodCoverages: projected.lodCoverages ?? null,
706
786
  lodRanges: projected.lodRanges ?? null,
707
787
  selectedRange,
@@ -713,6 +793,7 @@ function filteredPlan(
713
793
  groups.set(groupKey, {
714
794
  source: batch,
715
795
  mesh,
796
+ artifact,
716
797
  renderState: geometryRenderStateForTopology(batch.key.topology, material.renderState),
717
798
  candidates: [projected],
718
799
  });
@@ -765,6 +846,7 @@ function filteredPlan(
765
846
  batches.push({
766
847
  batch: filtered,
767
848
  mesh: group.mesh,
849
+ artifact: group.artifact,
768
850
  renderState: group.renderState,
769
851
  worldKey: worldKeyFor(firstSlot),
770
852
  primitiveSlot: firstSlot.slot,
@@ -833,6 +915,7 @@ function shadowCasterMembershipSignature(
833
915
  entry.materialShaderId ?? '',
834
916
  JSON.stringify(entry.renderState ?? null),
835
917
  entry.cpuReason ?? '',
918
+ entry.gpuDrivenEligible === undefined ? '' : String(entry.gpuDrivenEligible),
836
919
  ].join(':'),
837
920
  )
838
921
  .sort()
@@ -845,11 +928,8 @@ type FilteredProductionPlan = ReturnType<typeof filteredPlan>;
845
928
  function sceneRequiresGpuDrivenArtifact(scene: PersistentGpuDrivenState): boolean {
846
929
  return scene.plan.batches.some((batch) =>
847
930
  batch.candidates.some((candidate) => {
848
- const material = candidate.prepared?.identity.material;
849
931
  return (
850
- (material === 'forgeax::default-standard-pbr' ||
851
- material === 'forgeax::pbr-skin' ||
852
- material === 'forgeax::default-standard-pbr-skin') &&
932
+ candidate.prepared?.receiptIdentity !== undefined &&
853
933
  (candidate.prepared?.identity.deformation === 'rigid' ||
854
934
  candidate.prepared?.identity.deformation === 'skin')
855
935
  );
@@ -857,6 +937,31 @@ function sceneRequiresGpuDrivenArtifact(scene: PersistentGpuDrivenState): boolea
857
937
  );
858
938
  }
859
939
 
940
+ function missingMaterialArtifact(
941
+ scene: PersistentGpuDrivenState,
942
+ artifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined,
943
+ rigidFallback: MaterialShaderArtifact | undefined,
944
+ skinFallback: MaterialShaderArtifact | undefined,
945
+ ): GpuDrivenPreparationError | undefined {
946
+ for (const batch of scene.plan.batches) {
947
+ for (const candidate of batch.candidates) {
948
+ const prepared = candidate.prepared;
949
+ if (prepared?.receiptIdentity === undefined) continue;
950
+ if (
951
+ artifactForPreparedCandidate(candidate, artifacts, rigidFallback, skinFallback) !==
952
+ undefined
953
+ )
954
+ continue;
955
+ return new GpuDrivenPreparationError('missing-material-receipt', {
956
+ reason: 'material-receipt-missing',
957
+ owner: 'material',
958
+ expected: `published artifact for ${prepared.identity.material} (${prepared.receiptIdentity}@${prepared.receiptGeneration})`,
959
+ });
960
+ }
961
+ }
962
+ return undefined;
963
+ }
964
+
860
965
  function nonCpuPreparationFailure(
861
966
  scene: PersistentGpuDrivenState,
862
967
  ): { readonly error: GpuDrivenPreparationError; readonly drawKeys: readonly string[] } | undefined {
@@ -926,8 +1031,110 @@ function sameEntityKeySet(
926
1031
  return true;
927
1032
  }
928
1033
 
1034
+ type MaterialPipelineFactory = NonNullable<RenderSystemInternals['getMaterialShaderPipelineEntry']>;
1035
+
1036
+ function artifactForPreparedCandidate(
1037
+ candidate: GpuDrivenBatch['candidates'][number],
1038
+ artifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined,
1039
+ rigidFallback: MaterialShaderArtifact | undefined,
1040
+ skinFallback: MaterialShaderArtifact | undefined,
1041
+ ): MaterialShaderArtifact | undefined {
1042
+ const prepared = candidate.prepared;
1043
+ if (prepared === undefined) return undefined;
1044
+ const exact = artifacts?.get(
1045
+ gpuDrivenMaterialArtifactKey({
1046
+ material: prepared.identity.material,
1047
+ deformation: prepared.identity.deformation,
1048
+ receiptIdentity: prepared.receiptIdentity,
1049
+ receiptGeneration: prepared.receiptGeneration,
1050
+ }),
1051
+ );
1052
+ if (exact !== undefined) return exact;
1053
+ // A map supplied by the real record path is authoritative. A receipt-backed
1054
+ // draw missing from it is a producer/load failure, not permission to pair
1055
+ // the draw with another material's artifact.
1056
+ if (artifacts !== undefined && prepared.receiptIdentity !== undefined) return undefined;
1057
+ const fallback = prepared.identity.deformation === 'skin' ? skinFallback : rigidFallback;
1058
+ return fallback?.material === prepared.identity.material ? fallback : undefined;
1059
+ }
1060
+
1061
+ function shadowArtifactForPreparedCandidate(
1062
+ candidate: GpuDrivenBatch['candidates'][number],
1063
+ artifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined,
1064
+ fallback: MaterialShaderArtifact | undefined,
1065
+ ): MaterialShaderArtifact | undefined {
1066
+ const prepared = candidate.prepared;
1067
+ if (prepared === undefined) return undefined;
1068
+ const exact = artifacts?.get(
1069
+ gpuDrivenMaterialArtifactKey({
1070
+ material: prepared.identity.material,
1071
+ deformation: prepared.identity.deformation,
1072
+ receiptIdentity: prepared.receiptIdentity,
1073
+ receiptGeneration: prepared.receiptGeneration,
1074
+ }),
1075
+ );
1076
+ if (exact !== undefined) return exact;
1077
+ // Direct production fixtures predate the separate shadow publication map.
1078
+ // They are allowed to reuse the already validated artifact; a real frame
1079
+ // passes the authoritative map and therefore fails closed on a missing
1080
+ // shadow program instead of silently selecting the forward program.
1081
+ return artifacts === undefined ? fallback : undefined;
1082
+ }
1083
+
1084
+ function materialArtifactSignature(
1085
+ artifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined,
1086
+ rigidFallback: MaterialShaderArtifact | undefined,
1087
+ skinFallback: MaterialShaderArtifact | undefined,
1088
+ ): string {
1089
+ const rows =
1090
+ artifacts === undefined
1091
+ ? []
1092
+ : [...artifacts.entries()]
1093
+ .map(([key, artifact]) => [
1094
+ key,
1095
+ artifact.material,
1096
+ artifact.layoutIdentity,
1097
+ artifact.receipt?.receiptIdentity ?? '',
1098
+ artifact.receipt?.generation ?? 0,
1099
+ artifact.variantSet ?? '',
1100
+ materialArtifactProgramIdentity(artifact),
1101
+ artifact.vertexEntry ?? artifact.receipt?.sceneIndexEntry ?? '',
1102
+ artifact.fragmentEntry ?? '',
1103
+ ])
1104
+ .sort((left, right) => String(left[0] ?? '').localeCompare(String(right[0] ?? '')));
1105
+ return JSON.stringify({
1106
+ rows,
1107
+ rigid:
1108
+ rigidFallback === undefined
1109
+ ? undefined
1110
+ : [
1111
+ rigidFallback.material,
1112
+ rigidFallback.layoutIdentity,
1113
+ rigidFallback.receipt?.receiptIdentity ?? '',
1114
+ rigidFallback.receipt?.generation ?? 0,
1115
+ rigidFallback.variantSet ?? '',
1116
+ materialArtifactProgramIdentity(rigidFallback),
1117
+ rigidFallback.vertexEntry ?? rigidFallback.receipt?.sceneIndexEntry ?? '',
1118
+ rigidFallback.fragmentEntry ?? '',
1119
+ ],
1120
+ skin:
1121
+ skinFallback === undefined
1122
+ ? undefined
1123
+ : [
1124
+ skinFallback.material,
1125
+ skinFallback.layoutIdentity,
1126
+ skinFallback.receipt?.receiptIdentity ?? '',
1127
+ skinFallback.receipt?.generation ?? 0,
1128
+ skinFallback.variantSet ?? '',
1129
+ materialArtifactProgramIdentity(skinFallback),
1130
+ skinFallback.vertexEntry ?? skinFallback.receipt?.sceneIndexEntry ?? '',
1131
+ skinFallback.fragmentEntry ?? '',
1132
+ ],
1133
+ });
1134
+ }
1135
+
929
1136
  /** The GPU-driven bridge to the existing four-group Standard PBR pipeline. */
930
- class StandardPbrRasterAdapter {
1137
+ class MaterialAbiRasterAdapter {
931
1138
  private readonly pipelines = new Map<string, RenderPipeline>();
932
1139
  private readonly projectionBuffers = new Set<Buffer>();
933
1140
  private readonly projectionBindings = new Map<
@@ -966,13 +1173,14 @@ class StandardPbrRasterAdapter {
966
1173
  private readonly pipelineLayout: import('@forgeax/engine-rhi').PipelineLayout,
967
1174
  private readonly meshLayout: BindGroupLayout,
968
1175
  private readonly instancesBindGroupLayout: BindGroupLayout,
969
- private readonly programs: ReadonlyMap<string, GpuDrivenPbrProgram>,
1176
+ private readonly programs: ReadonlyMap<string, GpuDrivenPbrProgram> | undefined,
970
1177
  private instancesBindGroup: BindGroup,
971
1178
  private readonly instanceBuffer: Buffer,
972
1179
  private readonly instanceBufferSize: number,
973
1180
  private visibleBuffer: Buffer,
974
1181
  private readonly artifact: MaterialShaderArtifact,
975
1182
  private readonly deformation: 'rigid' | 'skin',
1183
+ private readonly pipelineFactory: MaterialPipelineFactory | undefined,
976
1184
  private paletteBuffer: Buffer | undefined,
977
1185
  private paletteBindingWindowBytes: number | undefined,
978
1186
  ) {}
@@ -985,8 +1193,51 @@ class StandardPbrRasterAdapter {
985
1193
  readonly deformation?: 'rigid' | 'skin';
986
1194
  readonly paletteBuffer?: Buffer;
987
1195
  readonly paletteBindingWindowBytes?: number;
988
- }): Result<StandardPbrRasterAdapter, RhiError> {
1196
+ /** Renderer-owned factory for the selected artifact's actual PSO. */
1197
+ readonly pipelineFactory?: MaterialPipelineFactory;
1198
+ }): Result<MaterialAbiRasterAdapter, RhiError | GpuDrivenPreparationError> {
989
1199
  const deformation = input.deformation ?? 'rigid';
1200
+ const receipt = input.artifact.receipt;
1201
+ if (receipt === undefined) {
1202
+ return err(
1203
+ new RhiError({
1204
+ code: 'rhi-not-available',
1205
+ expected: 'published MaterialProgramAbi receipt for the selected material artifact',
1206
+ hint: 'recook and publish the material before creating a GPU-driven raster owner',
1207
+ }),
1208
+ );
1209
+ }
1210
+ const materialRowMatchesSceneTable =
1211
+ receipt.materialRow.byteLength === GPU_SCENE_LAYOUTS.material.stride &&
1212
+ new Set(receipt.materialRow.fields).size === receipt.materialRow.fields.length;
1213
+ if (!materialRowMatchesSceneTable) {
1214
+ return err(
1215
+ new GpuDrivenPreparationError('material-abi-not-executable', {
1216
+ reason: 'material-abi-not-executable',
1217
+ owner: 'material',
1218
+ expected: `scene-index material row ${GPU_SCENE_LAYOUTS.material.stride} bytes with unique producer fields`,
1219
+ actual: `row ${receipt.materialRow.byteLength} bytes with fields ${receipt.materialRow.fields.join(',')}`,
1220
+ }),
1221
+ );
1222
+ }
1223
+ if (
1224
+ receipt.reflection.layoutIdentity !== input.artifact.layoutIdentity ||
1225
+ receipt.directEntry.length === 0 ||
1226
+ receipt.sceneIndexEntry.length === 0 ||
1227
+ receipt.resourceSlots.some((slot) => slot.group !== 1)
1228
+ ) {
1229
+ return err(
1230
+ new RhiError({
1231
+ code: 'rhi-descriptor-invalid',
1232
+ expected: `complete selected MaterialProgramAbi reflection, scene row stride ${GPU_SCENE_LAYOUTS.material.stride}, and resource slots`,
1233
+ hint: 'repair the producer publication or use the owner for its non-scene material layout',
1234
+ }),
1235
+ );
1236
+ }
1237
+ // Scene-index shaders declare the dedicated slot-3 ABI. Keep the legacy
1238
+ // fields as a compatibility fallback for synthetic fixtures; the real
1239
+ // factory publishes the dedicated handles whenever the capable storage
1240
+ // lane is enabled, so production draws never bind the direct-draw BGL.
990
1241
  const pipelineLayout =
991
1242
  deformation === 'skin'
992
1243
  ? input.pipelineState.gpuDrivenPbrSkinPipelineLayout
@@ -996,25 +1247,26 @@ class StandardPbrRasterAdapter {
996
1247
  return err(
997
1248
  new RhiError({
998
1249
  code: 'rhi-not-available',
999
- expected: `${deformation === 'skin' ? 'skinned ' : ''}Standard PBR pipeline layout`,
1000
- hint: 'initialize the producer Standard PBR pipeline before enabling GPU-driven raster',
1250
+ expected: `${deformation === 'skin' ? 'skinned ' : ''}published material pipeline layout`,
1251
+ hint: 'initialize the producer material pipeline before enabling GPU-driven raster',
1001
1252
  }),
1002
1253
  );
1003
1254
  }
1004
1255
  const programs = input.pipelineState.gpuDrivenPbrPrograms;
1005
1256
  if (
1006
- programs === undefined ||
1007
- resolveStandardPbrProgram(
1008
- programs,
1009
- input.artifact.material,
1010
- input.artifact.receipt?.vertexInputs.some((attribute) => attribute.semantic === 'color'),
1011
- ) === undefined
1257
+ input.pipelineFactory === undefined &&
1258
+ (programs === undefined ||
1259
+ resolveStandardPbrProgram(
1260
+ programs,
1261
+ input.artifact.material,
1262
+ input.artifact.receipt?.vertexInputs.some((attribute) => attribute.semantic === 'color'),
1263
+ ) === undefined)
1012
1264
  ) {
1013
1265
  return err(
1014
1266
  new RhiError({
1015
1267
  code: 'rhi-not-available',
1016
- expected: `producer-owned ${deformation === 'skin' ? 'skinned ' : ''}Standard PBR shader module for the current device generation`,
1017
- hint: 'publish the validated Standard PBR module from buildReadyWebGPU before enabling GPU-driven raster',
1268
+ expected: `producer-owned ${deformation === 'skin' ? 'skinned ' : ''}material shader module for the current device generation`,
1269
+ hint: 'publish the selected material module from buildReadyWebGPU before enabling GPU-driven raster',
1018
1270
  }),
1019
1271
  );
1020
1272
  }
@@ -1026,7 +1278,7 @@ class StandardPbrRasterAdapter {
1026
1278
  return err(
1027
1279
  new RhiError({
1028
1280
  code: 'rhi-not-available',
1029
- expected: `${deformation === 'skin' ? 'skinned ' : ''}Standard PBR mesh bind-group layout`,
1281
+ expected: `${deformation === 'skin' ? 'skinned ' : ''}material mesh bind-group layout`,
1030
1282
  hint: 'publish the exact mesh-array layout selected by the material receipt',
1031
1283
  }),
1032
1284
  );
@@ -1077,7 +1329,7 @@ class StandardPbrRasterAdapter {
1077
1329
  });
1078
1330
  if (!instancesBindGroup.ok) return instancesBindGroup;
1079
1331
  return ok(
1080
- new StandardPbrRasterAdapter(
1332
+ new MaterialAbiRasterAdapter(
1081
1333
  input.device,
1082
1334
  pipelineLayout,
1083
1335
  meshLayout,
@@ -1089,6 +1341,7 @@ class StandardPbrRasterAdapter {
1089
1341
  input.visibleBuffer,
1090
1342
  input.artifact,
1091
1343
  deformation,
1344
+ input.pipelineFactory,
1092
1345
  input.paletteBuffer,
1093
1346
  input.paletteBindingWindowBytes,
1094
1347
  ),
@@ -1190,6 +1443,8 @@ class StandardPbrRasterAdapter {
1190
1443
  topology: GpuDrivenBatch['key']['topology'],
1191
1444
  stripIndexFormat: 'uint16' | 'uint32' | undefined,
1192
1445
  renderState: MaterialRenderState | undefined,
1446
+ selectedArtifact: MaterialShaderArtifact = this.artifact,
1447
+ additionalColorFormats: readonly TextureFormat[] = [],
1193
1448
  ): Result<RenderPipeline, RhiError> {
1194
1449
  const vertexLayout = deriveVertexBufferLayoutFromProjection(layoutProjection, {
1195
1450
  shaderUvSetCount,
@@ -1203,12 +1458,25 @@ class StandardPbrRasterAdapter {
1203
1458
  }),
1204
1459
  );
1205
1460
  }
1206
- const program = resolveStandardPbrProgram(
1207
- this.programs,
1208
- this.artifact.material,
1209
- layoutProjection.attributes.some((attribute) => attribute.key === 'color'),
1210
- );
1211
- if (program === undefined) {
1461
+ const receipt = selectedArtifact.receipt;
1462
+ if (receipt === undefined) {
1463
+ return err(
1464
+ new RhiError({
1465
+ code: 'rhi-not-available',
1466
+ expected: 'selected MaterialProgramAbi receipt for GPU-driven raster',
1467
+ hint: 'keep the draw on the producer recovery path until its ABI is published',
1468
+ }),
1469
+ );
1470
+ }
1471
+ const program =
1472
+ this.programs === undefined
1473
+ ? undefined
1474
+ : resolveStandardPbrProgram(
1475
+ this.programs,
1476
+ this.artifact.material,
1477
+ layoutProjection.attributes.some((attribute) => attribute.key === 'color'),
1478
+ );
1479
+ if (this.pipelineFactory === undefined && program === undefined) {
1212
1480
  return err(
1213
1481
  new RhiError({
1214
1482
  code: 'rhi-not-available',
@@ -1217,15 +1485,78 @@ class StandardPbrRasterAdapter {
1217
1485
  }),
1218
1486
  );
1219
1487
  }
1220
- const key = `${format}|${sampleCount}|${vertexLayout.arrayStride}|${topology}|${stripIndexFormat ?? ''}|${JSON.stringify(renderState ?? null)}|${program.artifact.layoutIdentity}|${layoutProjection.digest}|uv${shaderUvSetCount}`;
1488
+ const key = `${format}:${additionalColorFormats.join(',')}|${sampleCount}|${vertexLayout.arrayStride}|${topology}|${stripIndexFormat ?? ''}|${JSON.stringify(renderState ?? null)}|${selectedArtifact.material}|${selectedArtifact.layoutIdentity}|${receipt.receiptIdentity}|${receipt.generation}|${selectedArtifact.variantSet ?? ''}|${materialArtifactProgramIdentity(selectedArtifact)}|${selectedArtifact.vertexEntry ?? receipt.sceneIndexEntry}|${selectedArtifact.fragmentEntry ?? ''}|${layoutProjection.digest}|uv${shaderUvSetCount}`;
1221
1489
  const cached = this.pipelines.get(key);
1222
1490
  if (cached !== undefined) return ok(cached);
1491
+ if (this.pipelineFactory !== undefined) {
1492
+ const pipelineEntry = this.pipelineFactory(
1493
+ selectedArtifact.specializationKey ?? selectedArtifact.material,
1494
+ false,
1495
+ renderState,
1496
+ topology,
1497
+ stripIndexFormat,
1498
+ selectedArtifact.variantSet,
1499
+ 'forward',
1500
+ undefined,
1501
+ sampleCount,
1502
+ format,
1503
+ selectedArtifact.uvSetCount ?? shaderUvSetCount,
1504
+ 'depth24plus-stencil8',
1505
+ undefined,
1506
+ layoutProjection,
1507
+ // GPU-driven receipts are already producer-validated. Use the
1508
+ // synchronous shader-module adapter for this submission seam so a
1509
+ // newly loaded authored material cannot lose its first draw to the
1510
+ // ordinary async pre-bake warm-up window.
1511
+ 'immediate',
1512
+ this.deformation === 'skin' ? 'gpu-driven-skin' : 'gpu-driven-pbr',
1513
+ receipt.sceneIndexEntry,
1514
+ additionalColorFormats,
1515
+ selectedArtifact.vertexEntry,
1516
+ selectedArtifact.fragmentEntry,
1517
+ );
1518
+ if (pipelineEntry === null) {
1519
+ return err(
1520
+ new RhiError({
1521
+ code: 'rhi-not-available',
1522
+ expected: `producer-owned scene-index pipeline for material '${selectedArtifact.material}'`,
1523
+ hint: 'publish a matching program, entry, layout and resource contract before GPU admission',
1524
+ }),
1525
+ );
1526
+ }
1527
+ const expectedGroup2Contract = resolvePipelineGroup2Contract(selectedArtifact.wgsl);
1528
+ if (pipelineEntry.group2Contract !== expectedGroup2Contract) {
1529
+ return err(
1530
+ new RhiError({
1531
+ code: 'rhi-descriptor-invalid',
1532
+ expected: `GPU-driven ${this.deformation === 'skin' ? 'skinned ' : ''}material pipeline group(2) ${expectedGroup2Contract} contract`,
1533
+ hint: 'custom attachment or incompatible group(2) layouts remain on their specialized lane',
1534
+ }),
1535
+ );
1536
+ }
1537
+ this.pipelines.set(key, pipelineEntry.pipeline);
1538
+ return ok(pipelineEntry.pipeline);
1539
+ }
1540
+ if (
1541
+ program === undefined ||
1542
+ selectedArtifact.material !== this.artifact.material ||
1543
+ selectedArtifact.layoutIdentity !== this.artifact.layoutIdentity ||
1544
+ selectedArtifact.receipt?.receiptIdentity !== this.artifact.receipt?.receiptIdentity
1545
+ ) {
1546
+ return err(
1547
+ new RhiError({
1548
+ code: 'rhi-not-available',
1549
+ expected: 'selected material pipeline factory for the requested ABI artifact',
1550
+ hint: 'do not pair a receipt-backed custom or color variant with the legacy fixed module',
1551
+ }),
1552
+ );
1553
+ }
1223
1554
  const pipeline = this.device.createRenderPipeline({
1224
1555
  label: `standard-pbr-gpu-driven.${key}`,
1225
1556
  layout: this.pipelineLayout,
1226
1557
  vertex: {
1227
1558
  module: program.module,
1228
- entryPoint: program.artifact.receipt?.sceneIndexEntry ?? 'vs_scene_index',
1559
+ entryPoint: selectedArtifact.vertexEntry ?? receipt.sceneIndexEntry,
1229
1560
  buffers: [
1230
1561
  {
1231
1562
  arrayStride: vertexLayout.arrayStride,
@@ -1241,7 +1572,7 @@ class StandardPbrRasterAdapter {
1241
1572
  fragment: {
1242
1573
  module: program.module,
1243
1574
  entryPoint: 'fs_main',
1244
- targets: [{ format }],
1575
+ targets: [format, ...additionalColorFormats].map((format) => ({ format })),
1245
1576
  },
1246
1577
  primitive: {
1247
1578
  topology,
@@ -1261,6 +1592,15 @@ class StandardPbrRasterAdapter {
1261
1592
  return pipeline;
1262
1593
  }
1263
1594
 
1595
+ /**
1596
+ * Drop only program-derived caches when a publication switches. Resource
1597
+ * buffers stay alive until their normal fence retirement, while the next
1598
+ * draw must obtain a PSO for the new artifact rather than reuse the old one.
1599
+ */
1600
+ invalidatePipelineCache(): void {
1601
+ this.pipelines.clear();
1602
+ }
1603
+
1264
1604
  projectionBindGroup(
1265
1605
  rows: Float32Array,
1266
1606
  key?: string,
@@ -1307,6 +1647,23 @@ class StandardPbrRasterAdapter {
1307
1647
  return this.writeProjectionRows(key, retained, rows);
1308
1648
  }
1309
1649
 
1650
+ sceneMeshBinding(key: string): GpuDrivenSceneMeshBinding {
1651
+ const binding = this.stagedProjectionBindings.get(key) ?? this.projectionBindings.get(key);
1652
+ if (binding === undefined)
1653
+ throw new RhiError({
1654
+ code: 'rhi-not-available',
1655
+ expected: 'retained scene mesh projection',
1656
+ hint: 'prepare this batch projection before binding its lighting resources',
1657
+ });
1658
+ return {
1659
+ meshBuffer: binding.buffer,
1660
+ meshBytes: binding.capacityBytes,
1661
+ ...(this.deformation === 'skin' && this.paletteBuffer !== undefined
1662
+ ? { paletteBuffer: this.paletteBuffer }
1663
+ : {}),
1664
+ };
1665
+ }
1666
+
1310
1667
  /** Stage the projection keys required by the frame being prepared. */
1311
1668
  stageProjectionKeys(keys: ReadonlySet<string>): void {
1312
1669
  this.stagedProjectionKeys = new Set(keys);
@@ -1541,6 +1898,7 @@ export interface PreparedGpuDrivenFrame {
1541
1898
  graph: RenderGraphBuilder<RenderPipelineFrame>,
1542
1899
  format: TextureFormat,
1543
1900
  sampleCount: 1 | 4,
1901
+ additionalColorFormats?: readonly TextureFormat[],
1544
1902
  ): Result<RenderPipelineGpuDrivenProjection, RenderGraphError | RhiError>;
1545
1903
  }
1546
1904
 
@@ -1665,8 +2023,8 @@ export function classifyRecoveryCapability(
1665
2023
  export class GpuDrivenProduction {
1666
2024
  private disposed = false;
1667
2025
  private view: GpuDrivenView | undefined;
1668
- private standardRaster: StandardPbrRasterAdapter | undefined;
1669
- private skinRaster: StandardPbrRasterAdapter | undefined;
2026
+ private materialRaster: MaterialAbiRasterAdapter | undefined;
2027
+ private skinRaster: MaterialAbiRasterAdapter | undefined;
1670
2028
  private readonly sceneIdentities = new WeakMap<object, number>();
1671
2029
  private nextSceneIdentity = 1;
1672
2030
  private filteredCache:
@@ -1680,6 +2038,7 @@ export class GpuDrivenProduction {
1680
2038
  readonly activeEntityRevision?: number;
1681
2039
  readonly activeEntityKeys?: ReadonlySet<number>;
1682
2040
  readonly skinArtifact?: MaterialShaderArtifact;
2041
+ readonly materialArtifactSignature: string;
1683
2042
  readonly value: FilteredProductionPlan;
1684
2043
  }
1685
2044
  | undefined;
@@ -1693,6 +2052,7 @@ export class GpuDrivenProduction {
1693
2052
  readonly meshResidencyEpoch: number;
1694
2053
  readonly membership: string;
1695
2054
  readonly skinArtifact?: MaterialShaderArtifact;
2055
+ readonly materialArtifactSignature: string;
1696
2056
  readonly value: FilteredProductionPlan;
1697
2057
  }
1698
2058
  | undefined;
@@ -1743,8 +2103,7 @@ export class GpuDrivenProduction {
1743
2103
  readonly filtered: FilteredProductionPlan;
1744
2104
  readonly scene: PersistentGpuDrivenState['scene'];
1745
2105
  readonly contentRevision: number;
1746
- readonly artifact: MaterialShaderArtifact;
1747
- readonly skinArtifact: MaterialShaderArtifact | undefined;
2106
+ readonly materialArtifactSignature: string;
1748
2107
  }
1749
2108
  | undefined;
1750
2109
  private validatedResidencyCache: GpuDrivenResidencyValidationCache | undefined;
@@ -1860,12 +2219,23 @@ export class GpuDrivenProduction {
1860
2219
  readonly meshBySlot: ReadonlyMap<number, MeshGpuHandles>;
1861
2220
  readonly viewBindGroupLayout: BindGroupLayout;
1862
2221
  readonly meshResidencyEpoch: number;
1863
- readonly hdrp: boolean;
2222
+ readonly materialBindingClasses?: ReadonlyMap<string, string>;
1864
2223
  /** Cooked producer artifact selected from the frame material catalog. */
2224
+ readonly materialArtifact?: MaterialShaderArtifact;
2225
+ /** Compatibility input for callers that have not migrated their name. */
1865
2226
  readonly standardPbrArtifact?: MaterialShaderArtifact;
1866
2227
  /** Cooked producer artifact for the GPU skinning variant. */
2228
+ readonly materialSkinArtifact?: MaterialShaderArtifact;
2229
+ /** Compatibility input for callers that have not migrated their name. */
1867
2230
  readonly standardPbrSkinArtifact?: MaterialShaderArtifact;
2231
+ /** Per-draw producer artifacts keyed by prepared identity and receipt. */
2232
+ readonly materialArtifacts?: ReadonlyMap<string, MaterialShaderArtifact>;
2233
+ /** Per-draw shadow-pass artifacts keyed by the same prepared identity. */
2234
+ readonly shadowMaterialArtifacts?: ReadonlyMap<string, MaterialShaderArtifact>;
2235
+ readonly materialPipelineState?: PipelineState;
1868
2236
  readonly standardPbrPipelineState?: PipelineState;
2237
+ /** Existing renderer owner that builds the selected artifact PSO/BGL. */
2238
+ readonly materialPipelineFactory?: MaterialPipelineFactory;
1869
2239
  /** Extracted ShadowCaster ownership keyed by concrete draw item and pass. */
1870
2240
  readonly shadowCasterDrawKeys?: ReadonlySet<string>;
1871
2241
  /** Structured ShadowCaster ownership facts for exact pass admission. */
@@ -1905,7 +2275,7 @@ export class GpuDrivenProduction {
1905
2275
  paletteUploadBytes: 0,
1906
2276
  };
1907
2277
  this.lane = 'cpu-semantic';
1908
- this.laneReason = input.hdrp ? 'unsupported' : 'capability';
2278
+ this.laneReason = 'capability';
1909
2279
  this.submissionState = 'idle';
1910
2280
  this.shadowExpectedDrawKeys = new Set();
1911
2281
  const capability = classifyRecoveryCapability({
@@ -1918,7 +2288,6 @@ export class GpuDrivenProduction {
1918
2288
  });
1919
2289
  if (
1920
2290
  input.scene === undefined ||
1921
- input.hdrp ||
1922
2291
  selectGpuLodLane(this.device.caps) === 'cpu' ||
1923
2292
  capability.status === 'disabled'
1924
2293
  ) {
@@ -1953,10 +2322,18 @@ export class GpuDrivenProduction {
1953
2322
  this.preparationFailure = preparationFailureInspection(preparationFailure.error);
1954
2323
  return err(preparationFailure.error);
1955
2324
  }
1956
- const standardArtifact = input.standardPbrArtifact;
1957
- const skinArtifact = input.standardPbrSkinArtifact;
1958
- const standardPipelineState = input.standardPbrPipelineState;
1959
- if (standardArtifact === undefined || standardPipelineState === undefined) {
2325
+ const materialArtifact = input.materialArtifact ?? input.standardPbrArtifact;
2326
+ const skinArtifact = input.materialSkinArtifact ?? input.standardPbrSkinArtifact;
2327
+ const materialPipelineState = input.materialPipelineState ?? input.standardPbrPipelineState;
2328
+ const artifactSignature = `${materialArtifactSignature(
2329
+ input.materialArtifacts,
2330
+ materialArtifact,
2331
+ skinArtifact,
2332
+ )}|shadow=${materialArtifactSignature(input.shadowMaterialArtifacts, undefined, undefined)}|bindings=${JSON.stringify([...(input.materialBindingClasses ?? [])])}`;
2333
+ if (
2334
+ (materialArtifact === undefined && input.materialArtifacts === undefined) ||
2335
+ materialPipelineState === undefined
2336
+ ) {
1960
2337
  this.gpuOwnedEntityCount = 0;
1961
2338
  this.laneReason = 'resource-not-ready';
1962
2339
  if (sceneRequiresGpuDrivenArtifact(input.scene)) {
@@ -1967,22 +2344,39 @@ export class GpuDrivenProduction {
1967
2344
  this.mainTotalDrawItems = Math.max(this.mainTotalDrawItems, this.blockedDrawItems);
1968
2345
  this.lane = 'blocked';
1969
2346
  const failure =
1970
- standardArtifact === undefined
2347
+ materialArtifact === undefined && input.materialArtifacts === undefined
1971
2348
  ? new GpuDrivenPreparationError('missing-material-receipt', {
1972
2349
  reason: 'material-receipt-missing',
1973
2350
  owner: 'material',
1974
- expected: 'producer-owned Standard PBR GPU-driven artifact',
2351
+ expected: 'producer-owned MaterialProgramAbi GPU-driven artifact',
1975
2352
  })
1976
2353
  : new GpuDrivenPreparationError('resource-not-ready', {
1977
2354
  reason: 'material-resource-missing',
1978
2355
  owner: 'material',
1979
- expected: 'producer-owned Standard PBR GPU-driven pipeline state',
2356
+ expected: 'producer-owned GPU-driven material pipeline state',
1980
2357
  });
1981
2358
  this.preparationFailure = preparationFailureInspection(failure);
1982
2359
  return err(failure);
1983
2360
  }
1984
2361
  return ok(undefined);
1985
2362
  }
2363
+ const missingArtifact = missingMaterialArtifact(
2364
+ scene,
2365
+ input.materialArtifacts,
2366
+ materialArtifact,
2367
+ skinArtifact,
2368
+ );
2369
+ if (missingArtifact !== undefined) {
2370
+ this.gpuOwnedEntityCount = 0;
2371
+ this.lane = 'blocked';
2372
+ this.laneReason = 'resource-not-ready';
2373
+ this.blockedDrawItems = scene.plan.batches.reduce(
2374
+ (total, batch) => total + batch.candidates.length,
2375
+ 0,
2376
+ );
2377
+ this.preparationFailure = preparationFailureInspection(missingArtifact);
2378
+ return err(missingArtifact);
2379
+ }
1986
2380
  const projectionFingerprint =
1987
2381
  this.filteredCache?.source === scene.plan &&
1988
2382
  this.filteredCache.lodProjectionFingerprint === ''
@@ -2005,7 +2399,8 @@ export class GpuDrivenProduction {
2005
2399
  this.filteredCache.meshResidencyEpoch !== input.meshResidencyEpoch ||
2006
2400
  this.filteredCache.lodProjectionFingerprint !== projectionFingerprint ||
2007
2401
  !cachedVisibilityMatches ||
2008
- this.filteredCache.skinArtifact !== skinArtifact
2402
+ this.filteredCache.skinArtifact !== skinArtifact ||
2403
+ this.filteredCache.materialArtifactSignature !== artifactSignature
2009
2404
  ) {
2010
2405
  filtered = filteredPlan(
2011
2406
  scene.plan,
@@ -2017,6 +2412,9 @@ export class GpuDrivenProduction {
2017
2412
  input.camera,
2018
2413
  input.activeEntityKeys,
2019
2414
  scene.worldKeys,
2415
+ materialArtifact,
2416
+ input.materialArtifacts,
2417
+ input.materialBindingClasses,
2020
2418
  );
2021
2419
  this.filteredCache = {
2022
2420
  source: scene.plan,
@@ -2032,6 +2430,7 @@ export class GpuDrivenProduction {
2032
2430
  ? {}
2033
2431
  : { activeEntityKeys: input.activeEntityKeys }),
2034
2432
  ...(skinArtifact === undefined ? {} : { skinArtifact }),
2433
+ materialArtifactSignature: artifactSignature,
2035
2434
  value: filtered,
2036
2435
  };
2037
2436
  this.gpuOwnedSnapshotsMaterialized = scene.slots.length;
@@ -2060,7 +2459,8 @@ export class GpuDrivenProduction {
2060
2459
  this.shadowFilteredCache.lodProjectionFingerprint !== projectionFingerprint ||
2061
2460
  this.shadowFilteredCache.meshResidencyEpoch !== input.meshResidencyEpoch ||
2062
2461
  this.shadowFilteredCache.membership !== shadowMembership ||
2063
- this.shadowFilteredCache.skinArtifact !== skinArtifact
2462
+ this.shadowFilteredCache.skinArtifact !== skinArtifact ||
2463
+ this.shadowFilteredCache.materialArtifactSignature !== artifactSignature
2064
2464
  ) {
2065
2465
  const projectedShadowFiltered = filteredPlan(
2066
2466
  scene.plan,
@@ -2072,6 +2472,8 @@ export class GpuDrivenProduction {
2072
2472
  input.camera,
2073
2473
  undefined,
2074
2474
  scene.worldKeys,
2475
+ materialArtifact,
2476
+ input.materialArtifacts,
2075
2477
  );
2076
2478
  shadowFiltered = projectedShadowFiltered;
2077
2479
  this.shadowFilteredCache = {
@@ -2083,6 +2485,7 @@ export class GpuDrivenProduction {
2083
2485
  meshResidencyEpoch: input.meshResidencyEpoch,
2084
2486
  membership: shadowMembership,
2085
2487
  ...(skinArtifact === undefined ? {} : { skinArtifact }),
2488
+ materialArtifactSignature: artifactSignature,
2086
2489
  value: shadowFiltered,
2087
2490
  };
2088
2491
  }
@@ -2117,32 +2520,30 @@ export class GpuDrivenProduction {
2117
2520
  filtered,
2118
2521
  scene: scene.scene,
2119
2522
  contentRevision: scene.scene.contentRevision,
2120
- artifact: standardArtifact,
2121
- skinArtifact,
2523
+ materialArtifactSignature: artifactSignature,
2122
2524
  };
2123
2525
  const previousSource = this.committedProjectionSource;
2124
2526
  const reuseProjectionRows =
2125
2527
  previousSource?.filtered === filtered &&
2126
2528
  previousSource.scene === scene.scene &&
2127
2529
  previousSource.contentRevision === scene.scene.contentRevision &&
2128
- previousSource.artifact === standardArtifact &&
2129
- previousSource.skinArtifact === skinArtifact;
2530
+ previousSource.materialArtifactSignature === artifactSignature;
2130
2531
  if (!reuseProjectionRows) {
2131
2532
  const slotsByPrimitive = new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
2533
+ const representative = filtered.batches[0];
2534
+ if (representative === undefined) return ok(undefined);
2132
2535
  const projection = projectStandardPbrScene({
2133
- artifact: standardArtifact,
2134
- candidates: filtered.batches.flatMap(({ batch }) =>
2135
- batch.candidates.flatMap((candidate) => {
2136
- const slot = slotsByPrimitive.get(candidate.primitiveIndex);
2137
- return slot === undefined ? [] : [sceneCandidate(slot, candidate)];
2138
- }),
2139
- ),
2536
+ artifact: representative.artifact,
2537
+ candidates: representative.batch.candidates.flatMap((candidate) => {
2538
+ const slot = slotsByPrimitive.get(candidate.primitiveIndex);
2539
+ return slot === undefined ? [] : [sceneCandidate(slot, candidate)];
2540
+ }),
2140
2541
  });
2141
2542
  if (!projection.ok) {
2142
2543
  return err(
2143
2544
  new RhiError({
2144
2545
  code: 'shader-compile-failed',
2145
- expected: 'receipt-derived Standard PBR scene projection',
2546
+ expected: 'receipt-derived material scene projection',
2146
2547
  hint: 'publish one validated MaterialShaderArtifact receipt before entering the GPU lane',
2147
2548
  }),
2148
2549
  );
@@ -2171,26 +2572,55 @@ export class GpuDrivenProduction {
2171
2572
  }),
2172
2573
  );
2173
2574
  }
2174
- if (this.standardRaster === undefined) {
2175
- const created = StandardPbrRasterAdapter.create({
2575
+ const representativeArtifact = materialArtifact ?? filtered.batches[0]?.artifact;
2576
+ if (representativeArtifact === undefined) {
2577
+ return err(
2578
+ new RhiError({
2579
+ code: 'rhi-not-available',
2580
+ expected: 'one selected MaterialProgramAbi artifact for GPU-driven batches',
2581
+ hint: 'keep receipt-backed draws blocked until Catalog/load publishes their artifact',
2582
+ }),
2583
+ );
2584
+ }
2585
+ const committedArtifactChanged =
2586
+ this.committedProjectionSource !== undefined &&
2587
+ this.committedProjectionSource.materialArtifactSignature !== artifactSignature;
2588
+ if (committedArtifactChanged) {
2589
+ // ShadowViewStatePool's cache token is light/view scoped and cannot see
2590
+ // a material publication replacement. Drop the retained projections at
2591
+ // the same boundary as the main PSO cache so a cache hit cannot replay
2592
+ // the previous shadow program or BGL with the new receipt.
2593
+ this.shadowBatchProjections.clear();
2594
+ this.shadowBatchProjectionSource = undefined;
2595
+ this.shadowViews?.invalidate();
2596
+ }
2597
+ if (this.committedProjectionSource?.materialArtifactSignature !== artifactSignature) {
2598
+ this.materialRaster?.invalidatePipelineCache();
2599
+ this.skinRaster?.invalidatePipelineCache();
2600
+ }
2601
+ if (this.materialRaster === undefined) {
2602
+ const created = MaterialAbiRasterAdapter.create({
2176
2603
  device: this.device,
2177
- pipelineState: standardPipelineState,
2178
- artifact: standardArtifact,
2604
+ pipelineState: materialPipelineState,
2605
+ artifact: representativeArtifact,
2179
2606
  visibleBuffer,
2607
+ ...(input.materialPipelineFactory === undefined
2608
+ ? {}
2609
+ : { pipelineFactory: input.materialPipelineFactory }),
2180
2610
  });
2181
2611
  if (!created.ok) return created;
2182
- this.standardRaster = created.value;
2612
+ this.materialRaster = created.value;
2183
2613
  } else {
2184
- const rebound = this.standardRaster.ensureVisibleBuffer(visibleBuffer);
2614
+ const rebound = this.materialRaster.ensureVisibleBuffer(visibleBuffer);
2185
2615
  if (!rebound.ok) return rebound;
2186
2616
  }
2187
- const standardRaster = this.standardRaster;
2188
- if (standardRaster === undefined) {
2617
+ const materialRaster = this.materialRaster;
2618
+ if (materialRaster === undefined) {
2189
2619
  return err(
2190
2620
  new RhiError({
2191
2621
  code: 'internal-error',
2192
- expected: 'Standard PBR raster adapter after prepared artifact admission',
2193
- hint: 'keep Standard PBR artifact and PipelineState on one frame identity',
2622
+ expected: 'material ABI raster adapter after prepared artifact admission',
2623
+ hint: 'keep the selected material artifact and PipelineState on one frame identity',
2194
2624
  }),
2195
2625
  );
2196
2626
  }
@@ -2201,9 +2631,10 @@ export class GpuDrivenProduction {
2201
2631
  const skinSnapshot = scene.slots.find(
2202
2632
  (slot) => slot.slot === skinBatch.batch.candidates[0]?.primitiveIndex,
2203
2633
  )?.snapshot.skin;
2204
- const skinAllocator = standardPipelineState.skinPaletteAllocator;
2634
+ const skinAllocator = materialPipelineState.skinPaletteAllocator;
2635
+ const selectedSkinArtifact = skinArtifact ?? skinBatch.artifact;
2205
2636
  if (
2206
- skinArtifact === undefined ||
2637
+ selectedSkinArtifact === undefined ||
2207
2638
  skinSnapshot === undefined ||
2208
2639
  skinAllocator === null ||
2209
2640
  skinAllocator === undefined
@@ -2211,20 +2642,23 @@ export class GpuDrivenProduction {
2211
2642
  return err(
2212
2643
  new RhiError({
2213
2644
  code: 'rhi-not-available',
2214
- expected: 'skinned Standard PBR artifact and persistent palette owner',
2645
+ expected: 'skinned material artifact and persistent palette owner',
2215
2646
  hint: 'keep missing skin producer resources on the CPU deformation path',
2216
2647
  }),
2217
2648
  );
2218
2649
  }
2219
2650
  if (this.skinRaster === undefined) {
2220
- const created = StandardPbrRasterAdapter.create({
2651
+ const created = MaterialAbiRasterAdapter.create({
2221
2652
  device: this.device,
2222
- pipelineState: standardPipelineState,
2223
- artifact: skinArtifact,
2653
+ pipelineState: materialPipelineState,
2654
+ artifact: selectedSkinArtifact,
2224
2655
  visibleBuffer,
2225
2656
  deformation: 'skin',
2226
2657
  paletteBuffer: skinSnapshot.buffer,
2227
2658
  paletteBindingWindowBytes: skinAllocator.bindingWindowBytes,
2659
+ ...(input.materialPipelineFactory === undefined
2660
+ ? {}
2661
+ : { pipelineFactory: input.materialPipelineFactory }),
2228
2662
  });
2229
2663
  if (!created.ok) return created;
2230
2664
  this.skinRaster = created.value;
@@ -2257,11 +2691,11 @@ export class GpuDrivenProduction {
2257
2691
  }
2258
2692
  // A pose can change while conservative bounds, allocation and scene rows
2259
2693
  // remain unchanged. Read the palette owner, never optional telemetry.
2260
- const paletteContentRevision = standardPipelineState.skinPaletteAllocator?.contentRevision;
2694
+ const paletteContentRevision = materialPipelineState.skinPaletteAllocator?.contentRevision;
2261
2695
  if (paletteContentRevision !== this.committedPaletteContentRevision)
2262
2696
  shadowViewPool.invalidate();
2263
2697
  const abortProvisionalResources = (): void => {
2264
- standardRaster.abortProjectionBindings();
2698
+ materialRaster.abortProjectionBindings();
2265
2699
  skinRaster?.abortProjectionBindings();
2266
2700
  shadowViewPool._abortResourceReplacement();
2267
2701
  };
@@ -2275,13 +2709,13 @@ export class GpuDrivenProduction {
2275
2709
  };
2276
2710
  // Start the provisional resource set before any retained row is updated;
2277
2711
  // an aborted graph can then restore the active rows from its snapshot.
2278
- standardRaster.stageProjectionKeys(new Set());
2712
+ materialRaster.stageProjectionKeys(new Set());
2279
2713
  skinRaster?.stageProjectionKeys(new Set());
2280
2714
  for (const prepared of filtered.batches) {
2281
2715
  if (reuseProjectionRows) {
2282
2716
  const deformation = prepared.batch.candidates[0]?.prepared?.identity.deformation ?? 'rigid';
2283
- const raster = deformation === 'skin' ? skinRaster : standardRaster;
2284
- const artifact = deformation === 'skin' ? skinArtifact : standardArtifact;
2717
+ const raster = deformation === 'skin' ? skinRaster : materialRaster;
2718
+ const artifact = prepared.artifact;
2285
2719
  if (raster !== undefined && artifact !== undefined)
2286
2720
  raster.noteProjectionKey(gpuDrivenProjectionKey(prepared.batch, deformation, artifact));
2287
2721
  continue;
@@ -2293,9 +2727,9 @@ export class GpuDrivenProduction {
2293
2727
  );
2294
2728
  if (admittedCandidates.length === 0) continue;
2295
2729
  const deformation = admittedCandidates[0]?.prepared?.identity.deformation ?? 'rigid';
2296
- const raster = deformation === 'skin' ? skinRaster : standardRaster;
2297
- const artifact = deformation === 'skin' ? skinArtifact : standardArtifact;
2298
- if (raster === undefined || artifact === undefined) continue;
2730
+ const raster = deformation === 'skin' ? skinRaster : materialRaster;
2731
+ const artifact = prepared.artifact;
2732
+ if (raster === undefined) continue;
2299
2733
  const projectionKey = gpuDrivenProjectionKey(prepared.batch, deformation, artifact);
2300
2734
  raster.noteProjectionKey(projectionKey);
2301
2735
  const candidates = admittedCandidates.flatMap((candidate) => {
@@ -2308,7 +2742,7 @@ export class GpuDrivenProduction {
2308
2742
  return err(
2309
2743
  new RhiError({
2310
2744
  code: 'shader-compile-failed',
2311
- expected: 'receipt-derived Standard PBR scene projection',
2745
+ expected: 'receipt-derived material scene projection',
2312
2746
  hint: 'keep the retained projection rows on the current material receipt',
2313
2747
  }),
2314
2748
  );
@@ -2342,9 +2776,10 @@ export class GpuDrivenProduction {
2342
2776
  sceneIdentity,
2343
2777
  revision: filtered.plan.revision,
2344
2778
  meshResidencyEpoch: input.meshResidencyEpoch,
2779
+ materialArtifactSignature: artifactSignature,
2345
2780
  standardPbrLayoutIdentity: this.standardPbrProjection?.layoutIdentity,
2346
2781
  viewResourceGeneration: viewInspection.resourceGeneration,
2347
- batches: filtered.batches.map(({ batch, mesh }) => [
2782
+ batches: filtered.batches.map(({ batch, mesh, artifact }) => [
2348
2783
  batch.batchId,
2349
2784
  batch.generation,
2350
2785
  batch.visibleBase,
@@ -2369,6 +2804,12 @@ export class GpuDrivenProduction {
2369
2804
  mesh.iboBytes,
2370
2805
  mesh.indexFormat,
2371
2806
  mesh.uvSetCount,
2807
+ artifact.material,
2808
+ artifact.layoutIdentity,
2809
+ artifact.receipt?.receiptIdentity,
2810
+ artifact.receipt?.generation,
2811
+ artifact.variantSet,
2812
+ artifact.wgsl.length,
2372
2813
  ]),
2373
2814
  capacities: [
2374
2815
  viewInspection.candidateCapacity,
@@ -2409,8 +2850,8 @@ export class GpuDrivenProduction {
2409
2850
  standardPbrFrameResources: {
2410
2851
  materialBindGroups: [],
2411
2852
  materialSlotIndicesByEntity: new Map(),
2412
- instancesBindGroup: standardRaster.instancesFrameGroup,
2413
- materialStride: 512,
2853
+ instancesBindGroup: materialRaster.instancesFrameGroup,
2854
+ materialStride: MATERIAL_PER_ENTITY_STRIDE,
2414
2855
  sceneMaterialBuffer: scene.scene.materialBuffer,
2415
2856
  },
2416
2857
  _commitGpuResourceReplacement: () => {
@@ -2419,7 +2860,7 @@ export class GpuDrivenProduction {
2419
2860
  _commitResourceReplacement: () => {
2420
2861
  view._commitResourceReplacement();
2421
2862
  shadowViewPool._commitResourceReplacement();
2422
- standardRaster.commitProjectionBindings();
2863
+ materialRaster.commitProjectionBindings();
2423
2864
  skinRaster?.commitProjectionBindings();
2424
2865
  this.committedProjectionSource = projectionSource;
2425
2866
  this.committedPaletteContentRevision = paletteContentRevision;
@@ -2428,7 +2869,7 @@ export class GpuDrivenProduction {
2428
2869
  },
2429
2870
  _abortResourceReplacement: () => {
2430
2871
  shadowViewPool._abortResourceReplacement();
2431
- standardRaster.abortProjectionBindings();
2872
+ materialRaster.abortProjectionBindings();
2432
2873
  skinRaster?.abortProjectionBindings();
2433
2874
  this.committedProjectionSource = undefined;
2434
2875
  this.submissionState = 'aborted';
@@ -2443,8 +2884,10 @@ export class GpuDrivenProduction {
2443
2884
  shadowFiltered.plan.batches.length > 0
2444
2885
  ? this.updateShadowViews(
2445
2886
  views,
2446
- standardArtifact,
2887
+ materialArtifact,
2447
2888
  skinArtifact,
2889
+ input.materialArtifacts,
2890
+ input.shadowMaterialArtifacts,
2448
2891
  shadowFiltered,
2449
2892
  scene,
2450
2893
  input.shadowCasterDrawKeys,
@@ -2459,21 +2902,25 @@ export class GpuDrivenProduction {
2459
2902
  ),
2460
2903
  );
2461
2904
  },
2462
- project: (graph, format, sampleCount) => {
2905
+ project: (graph, format, sampleCount, additionalColorFormats) => {
2463
2906
  this.indirectDrawCount = admittedRasterBatchCount(filtered.plan);
2464
2907
  const outputs = view.addPasses(graph);
2465
2908
  if (!outputs.ok) return outputs;
2466
- if (standardRaster !== undefined && standardArtifact !== undefined) {
2909
+ if (materialRaster !== undefined && filtered.batches.length > 0) {
2467
2910
  const standardProjected: Array<{
2468
2911
  readonly batch: GpuDrivenBatch;
2469
2912
  readonly mesh: MeshGpuHandles;
2470
2913
  readonly materialEntityKey: number;
2471
2914
  readonly vertex: GraphBuffer;
2472
2915
  readonly index?: GraphBuffer;
2473
- readonly pipeline: RenderPipeline;
2916
+ readonly pipelineFor: (
2917
+ formats: readonly TextureFormat[],
2918
+ ) => Result<RenderPipeline, RhiError>;
2474
2919
  readonly meshBindGroup: BindGroup;
2475
- readonly raster: StandardPbrRasterAdapter;
2920
+ readonly raster: MaterialAbiRasterAdapter;
2476
2921
  readonly deformation: 'rigid' | 'skin';
2922
+ readonly clustered: boolean;
2923
+ readonly projectionKey: string;
2477
2924
  }> = [];
2478
2925
  const standardAccesses: GraphAccess[] = [
2479
2926
  { resource: outputs.value.primitive, usage: 'storage-read' },
@@ -2496,14 +2943,14 @@ export class GpuDrivenProduction {
2496
2943
  );
2497
2944
  if (admittedCandidates.length === 0) continue;
2498
2945
  const deformation = admittedCandidates[0]?.prepared?.identity.deformation ?? 'rigid';
2499
- const raster = deformation === 'skin' ? skinRaster : standardRaster;
2500
- const artifact = deformation === 'skin' ? skinArtifact : standardArtifact;
2501
- if (raster === undefined || artifact === undefined) {
2946
+ const raster = deformation === 'skin' ? skinRaster : materialRaster;
2947
+ const artifact = prepared.artifact;
2948
+ if (raster === undefined) {
2502
2949
  return err(
2503
2950
  new RhiError({
2504
2951
  code: 'rhi-not-available',
2505
- expected: `${deformation === 'skin' ? 'skinned ' : ''}Standard PBR GPU-driven raster owner`,
2506
- hint: 'keep an unsupported deformation on the CPU fallback lane',
2952
+ expected: `${deformation === 'skin' ? 'skinned ' : ''}published material ABI raster owner`,
2953
+ hint: 'publish the selected material artifact before entering the GPU lane',
2507
2954
  }),
2508
2955
  );
2509
2956
  }
@@ -2524,7 +2971,7 @@ export class GpuDrivenProduction {
2524
2971
  return err(
2525
2972
  new RhiError({
2526
2973
  code: 'shader-compile-failed',
2527
- expected: 'receipt-derived Standard PBR scene projection',
2974
+ expected: 'receipt-derived material scene projection',
2528
2975
  hint: 'keep the frame artifact receipt and projected rows on one identity',
2529
2976
  }),
2530
2977
  );
@@ -2577,20 +3024,31 @@ export class GpuDrivenProduction {
2577
3024
  standardAccesses.push({ resource: buffers.index, usage: 'index-read' });
2578
3025
  }
2579
3026
  }
2580
- const pipeline = raster.pipeline(
2581
- format,
2582
- sampleCount,
2583
- prepared.mesh.layoutProjection,
2584
- standardPipelineState.standardPbrShaderUvSetCount,
2585
- prepared.batch.key.topology,
2586
- prepared.batch.key.drawKind === 'indexed' &&
2587
- (prepared.batch.key.topology === 'line-strip' ||
2588
- prepared.batch.key.topology === 'triangle-strip')
2589
- ? prepared.mesh.indexFormat
2590
- : undefined,
2591
- prepared.renderState,
2592
- );
2593
- if (!pipeline.ok) return pipeline;
3027
+ const pipelineFor = (
3028
+ formats: readonly TextureFormat[],
3029
+ ): Result<RenderPipeline, RhiError> => {
3030
+ return raster.pipeline(
3031
+ formats[0] ?? format,
3032
+ sampleCount,
3033
+ prepared.mesh.layoutProjection,
3034
+ // The producer receipt owns the selected shader's UV ABI. The
3035
+ // Standard count is only a fallback for legacy synthetic
3036
+ // fixtures; using it for a custom program would add aliases
3037
+ // the shader never declares (or omit a declared custom set).
3038
+ artifact.uvSetCount ?? materialPipelineState.standardPbrShaderUvSetCount,
3039
+ prepared.batch.key.topology,
3040
+ prepared.batch.key.drawKind === 'indexed' &&
3041
+ (prepared.batch.key.topology === 'line-strip' ||
3042
+ prepared.batch.key.topology === 'triangle-strip')
3043
+ ? prepared.mesh.indexFormat
3044
+ : undefined,
3045
+ prepared.renderState,
3046
+ artifact,
3047
+ formats.slice(1),
3048
+ );
3049
+ };
3050
+ const initialPipeline = pipelineFor([format, ...(additionalColorFormats ?? [])]);
3051
+ if (!initialPipeline.ok) return initialPipeline;
2594
3052
  standardProjected.push({
2595
3053
  batch: prepared.batch,
2596
3054
  mesh: prepared.mesh,
@@ -2599,10 +3057,14 @@ export class GpuDrivenProduction {
2599
3057
  materialSlot.snapshot.entityKey,
2600
3058
  ),
2601
3059
  ...buffers,
2602
- pipeline: pipeline.value,
3060
+ pipelineFor,
2603
3061
  meshBindGroup: meshBindGroup.value,
2604
3062
  raster,
2605
3063
  deformation,
3064
+ clustered: ['cluster', 'skin-cluster'].includes(
3065
+ resolvePipelineGroup2Contract(artifact.wgsl),
3066
+ ),
3067
+ projectionKey: gpuDrivenProjectionKey(prepared.batch, deformation, artifact),
2606
3068
  });
2607
3069
  }
2608
3070
  if (
@@ -2612,7 +3074,7 @@ export class GpuDrivenProduction {
2612
3074
  return err(
2613
3075
  new RhiError({
2614
3076
  code: 'internal-error',
2615
- expected: 'one actual Standard PBR raster dispatch for each eligible batch',
3077
+ expected: 'one actual material ABI raster dispatch for each eligible batch',
2616
3078
  hint: 'repair the receipt-backed batch projection before encoding the frame',
2617
3079
  }),
2618
3080
  );
@@ -2636,10 +3098,6 @@ export class GpuDrivenProduction {
2636
3098
  let currentVertex: GraphBuffer | undefined;
2637
3099
  let currentIndex: GraphBuffer | undefined;
2638
3100
  for (const batch of rasterDispatches) {
2639
- if (currentPipeline !== batch.pipeline) {
2640
- pass.setPipeline(batch.pipeline);
2641
- currentPipeline = batch.pipeline;
2642
- }
2643
3101
  const globalMaterialSlots = frameResources.materialSlotIndicesByEntity.get(
2644
3102
  batch.materialEntityKey,
2645
3103
  );
@@ -2655,6 +3113,20 @@ export class GpuDrivenProduction {
2655
3113
  hint: 'populate materialSlotIndicesByEntity from the main-pass material producer',
2656
3114
  });
2657
3115
  }
3116
+ if (
3117
+ frameResources.selectedMaterialSlots !== undefined &&
3118
+ !frameResources.selectedMaterialSlots.has(globalMaterialSlot)
3119
+ )
3120
+ continue;
3121
+ const selectedPipeline = batch.pipelineFor(
3122
+ frameResources.colorFormats ?? [format, ...(additionalColorFormats ?? [])],
3123
+ );
3124
+ if (!selectedPipeline.ok) throw selectedPipeline.error;
3125
+ const pipeline = selectedPipeline.value;
3126
+ if (currentPipeline !== pipeline) {
3127
+ pass.setPipeline(pipeline);
3128
+ currentPipeline = pipeline;
3129
+ }
2658
3130
  const materialBindGroup = frameResources.materialBindGroups[globalMaterialSlot];
2659
3131
  if (materialBindGroup === undefined) {
2660
3132
  throw new RhiError({
@@ -2666,11 +3138,18 @@ export class GpuDrivenProduction {
2666
3138
  pass.setBindGroup(1, materialBindGroup, [
2667
3139
  globalMaterialSlot * frameResources.materialStride,
2668
3140
  ]);
2669
- pass.setBindGroup(
2670
- 2,
2671
- batch.meshBindGroup,
2672
- batch.deformation === 'skin' ? [0, 0, 0] : [0],
2673
- );
3141
+ const meshBindGroup = batch.clustered
3142
+ ? frameResources.resolveClusteredMeshBindGroup?.(
3143
+ batch.raster.sceneMeshBinding(batch.projectionKey),
3144
+ )
3145
+ : batch.meshBindGroup;
3146
+ if (meshBindGroup === undefined)
3147
+ throw new RhiError({
3148
+ code: 'rhi-not-available',
3149
+ expected: 'current scene-index lighting binding',
3150
+ hint: 'resolve clustered surface resources during the material pass',
3151
+ });
3152
+ pass.setBindGroup(2, meshBindGroup, batch.deformation === 'skin' ? [0, 0, 0] : [0]);
2674
3153
  const visibleWindow = batch.raster.visibleWindowBindGroup(
2675
3154
  batch.batch.visibleBase,
2676
3155
  batch.batch.visibleCapacity,
@@ -2704,8 +3183,8 @@ export class GpuDrivenProduction {
2704
3183
  return err(
2705
3184
  new RhiError({
2706
3185
  code: 'internal-error',
2707
- expected: 'Standard PBR raster projection branch',
2708
- hint: 'preserve the artifact-backed Standard PBR branch as the only raster owner',
3186
+ expected: 'material ABI raster projection branch',
3187
+ hint: 'preserve the selected-artifact branch as the only raster owner',
2709
3188
  }),
2710
3189
  );
2711
3190
  },
@@ -2714,8 +3193,10 @@ export class GpuDrivenProduction {
2714
3193
 
2715
3194
  updateShadowViews(
2716
3195
  views: readonly Omit<ShadowViewUpdateInput, 'sourcePlan' | 'scene'>[],
2717
- artifact: MaterialShaderArtifact,
3196
+ artifact: MaterialShaderArtifact | undefined,
2718
3197
  skinArtifact: MaterialShaderArtifact | undefined,
3198
+ materialArtifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined,
3199
+ shadowMaterialArtifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined,
2719
3200
  filtered: FilteredProductionPlan,
2720
3201
  scene: PersistentGpuDrivenState,
2721
3202
  shadowCasterDrawKeys: ReadonlySet<string> | undefined,
@@ -2730,13 +3211,13 @@ export class GpuDrivenProduction {
2730
3211
  }
2731
3212
  const nextProjections = new Map<string, ReadonlyMap<number, GpuDrivenShadowBatchProjection>>();
2732
3213
  const nextShadowDrawKeysByView = new Map<string, ReadonlySet<string>>();
2733
- const standardRaster = this.standardRaster;
2734
- if (standardRaster === undefined) {
3214
+ const materialRaster = this.materialRaster;
3215
+ if (materialRaster === undefined) {
2735
3216
  return err(
2736
3217
  new RhiError({
2737
3218
  code: 'internal-error',
2738
- expected: 'Standard PBR raster adapter for shadow projection',
2739
- hint: 'prepare the artifact-backed raster owner before updating shadow views',
3219
+ expected: 'material ABI raster adapter for shadow projection',
3220
+ hint: 'prepare the selected-artifact raster owner before updating shadow views',
2740
3221
  }),
2741
3222
  );
2742
3223
  }
@@ -2760,11 +3241,23 @@ export class GpuDrivenProduction {
2760
3241
  nextShadowDrawKeysByView.set(identityKey, previousClaims);
2761
3242
  for (const batch of updated.value.plan.batches) {
2762
3243
  const deformation = batch.candidates[0]?.prepared?.identity.deformation ?? 'rigid';
2763
- const batchArtifact = deformation === 'skin' ? skinArtifact : artifact;
2764
- const raster = deformation === 'skin' ? this.skinRaster : standardRaster;
2765
- if (batchArtifact !== undefined && raster !== undefined) {
3244
+ const batchArtifact =
3245
+ preparedByBatch.get(batch.batchId)?.artifact ??
3246
+ artifactForPreparedCandidate(
3247
+ batch.candidates[0] as GpuDrivenBatch['candidates'][number],
3248
+ materialArtifacts,
3249
+ artifact,
3250
+ skinArtifact,
3251
+ );
3252
+ const shadowArtifact = shadowArtifactForPreparedCandidate(
3253
+ batch.candidates[0] as GpuDrivenBatch['candidates'][number],
3254
+ shadowMaterialArtifacts,
3255
+ batchArtifact,
3256
+ );
3257
+ const raster = deformation === 'skin' ? this.skinRaster : materialRaster;
3258
+ if (shadowArtifact !== undefined && raster !== undefined) {
2766
3259
  raster.noteProjectionKey(
2767
- gpuDrivenShadowProjectionKey(identityKey, batch, deformation, batchArtifact),
3260
+ gpuDrivenShadowProjectionKey(identityKey, batch, deformation, shadowArtifact),
2768
3261
  );
2769
3262
  }
2770
3263
  }
@@ -2822,14 +3315,19 @@ export class GpuDrivenProduction {
2822
3315
  );
2823
3316
  }
2824
3317
  const deformation = batch.candidates[0]?.prepared?.identity.deformation ?? 'rigid';
2825
- const raster = deformation === 'skin' ? this.skinRaster : standardRaster;
2826
- const batchArtifact = deformation === 'skin' ? skinArtifact : artifact;
2827
- if (raster === undefined || batchArtifact === undefined) {
3318
+ const raster = deformation === 'skin' ? this.skinRaster : materialRaster;
3319
+ const batchArtifact = prepared.artifact;
3320
+ const shadowArtifact = shadowArtifactForPreparedCandidate(
3321
+ batch.candidates[0] as GpuDrivenBatch['candidates'][number],
3322
+ shadowMaterialArtifacts,
3323
+ batchArtifact,
3324
+ );
3325
+ if (raster === undefined || batchArtifact === undefined || shadowArtifact === undefined) {
2828
3326
  return err(
2829
3327
  new RhiError({
2830
3328
  code: 'rhi-not-available',
2831
- expected: `${deformation === 'skin' ? 'skinned ' : ''}Standard PBR shadow projection owner`,
2832
- hint: 'keep a shadow batch on the CPU caster path until its material receipt is ready',
3329
+ expected: `${deformation === 'skin' ? 'skinned ' : ''}selected shadow MaterialProgramAbi artifact`,
3330
+ hint: 'publish a matching shadow-pass program before entering the capable shadow lane',
2833
3331
  }),
2834
3332
  );
2835
3333
  }
@@ -2842,13 +3340,13 @@ export class GpuDrivenProduction {
2842
3340
  );
2843
3341
  return [sceneCandidate(slot, candidate, materialIndex)];
2844
3342
  });
2845
- const projected = projectStandardPbrScene({ artifact: batchArtifact, candidates });
3343
+ const projected = projectStandardPbrScene({ artifact: shadowArtifact, candidates });
2846
3344
  if (!projected.ok) {
2847
3345
  return err(
2848
3346
  new RhiError({
2849
3347
  code: 'shader-compile-failed',
2850
3348
  expected: 'receipt-derived shadow scene-index projection',
2851
- hint: 'publish one validated Standard PBR receipt per shadow batch',
3349
+ hint: 'publish one validated material receipt per shadow batch',
2852
3350
  }),
2853
3351
  );
2854
3352
  }
@@ -2856,7 +3354,7 @@ export class GpuDrivenProduction {
2856
3354
  identityKey,
2857
3355
  batch,
2858
3356
  deformation,
2859
- batchArtifact,
3357
+ shadowArtifact,
2860
3358
  );
2861
3359
  raster.noteProjectionKey(projectionKey);
2862
3360
  const meshBindGroup = raster.projectionBindGroup(
@@ -2887,16 +3385,36 @@ export class GpuDrivenProduction {
2887
3385
  }),
2888
3386
  );
2889
3387
  }
3388
+ const firstWorldEntity = worldEntityKey(
3389
+ firstSlot.snapshot.worldId,
3390
+ firstSlot.snapshot.entityKey,
3391
+ );
3392
+ const shadowEntry = shadowCasterMembership?.find(
3393
+ (membership) =>
3394
+ membership.worldEntity === firstWorldEntity &&
3395
+ membership.materialHandle === (material.materialHandle ?? -1) &&
3396
+ membership.drawItemIndex === firstCandidate?.drawItemIndex,
3397
+ );
2890
3398
  projections.set(batch.batchId, {
2891
3399
  mesh: prepared.mesh,
2892
3400
  meshBindGroup: meshBindGroup.value,
2893
3401
  visibleBindGroup: visibleBindGroup.value,
2894
3402
  deformation,
3403
+ shadowArtifact,
3404
+ ...(shadowEntry?.vertexEntry === undefined
3405
+ ? {}
3406
+ : { shadowVertexEntry: shadowEntry.vertexEntry }),
3407
+ ...(shadowEntry?.fragmentEntry === undefined
3408
+ ? {}
3409
+ : { shadowFragmentEntry: shadowEntry.fragmentEntry }),
2895
3410
  material,
2896
3411
  materialEntityKey: worldEntityKey(
2897
3412
  firstSlot.snapshot.worldId,
2898
3413
  firstSlot.snapshot.entityKey,
2899
3414
  ),
3415
+ vertexColorAvailable: prepared.mesh.layoutProjection.attributes.some(
3416
+ (attribute) => attribute.key === 'color',
3417
+ ),
2900
3418
  });
2901
3419
  }
2902
3420
  nextProjections.set(identityKey, projections);
@@ -2919,8 +3437,8 @@ export class GpuDrivenProduction {
2919
3437
  this.disposed = true;
2920
3438
  this.view?.dispose();
2921
3439
  this.view = undefined;
2922
- this.standardRaster?.dispose();
2923
- this.standardRaster = undefined;
3440
+ this.materialRaster?.dispose();
3441
+ this.materialRaster = undefined;
2924
3442
  this.committedProjectionSource = undefined;
2925
3443
  this.skinRaster?.dispose();
2926
3444
  this.skinRaster = undefined;
@@ -3222,8 +3740,7 @@ export class GpuDrivenProduction {
3222
3740
  if (draws.length === 0) {
3223
3741
  totalDrawItems += 1;
3224
3742
  this.cpuFallbackDrawItems += 1;
3225
- const material = row.source.material;
3226
- const skinReason = skinLaneReason(row.source, material);
3743
+ const skinReason = skinLaneReason(row.source);
3227
3744
  if (skinReason !== undefined) {
3228
3745
  this.cpuDeformationFallbackDrawItems += 1;
3229
3746
  addDeformationReason(skinReason);
@@ -3248,7 +3765,7 @@ export class GpuDrivenProduction {
3248
3765
  }
3249
3766
  this.cpuFallbackDrawItems += 1;
3250
3767
  cpuResidual = true;
3251
- const skinReason = skinLaneReason(row.source, material);
3768
+ const skinReason = skinLaneReason(row.source);
3252
3769
  if (skinReason !== undefined) {
3253
3770
  this.cpuDeformationFallbackDrawItems += 1;
3254
3771
  addDeformationReason(skinReason);