@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
@@ -2,6 +2,7 @@ import type { MaterialShaderArtifactReceipt } from '@forgeax/engine-shader';
2
2
 
3
3
  export type GpuDrivenPreparationErrorCode =
4
4
  | 'missing-material-receipt'
5
+ | 'material-abi-not-executable'
5
6
  | 'missing-uv'
6
7
  | 'reflection-mismatch'
7
8
  | 'vertex-input-mismatch'
@@ -13,6 +14,7 @@ export type GpuDrivenPreparationErrorCode =
13
14
 
14
15
  export type GpuDrivenPreparationReason =
15
16
  | 'material-receipt-missing'
17
+ | 'material-abi-not-executable'
16
18
  | 'uv-set-missing'
17
19
  | 'reflection-receipt-mismatch'
18
20
  | 'vertex-semantic-mismatch'
@@ -51,10 +53,15 @@ const ERROR_POLICY: Readonly<
51
53
  >
52
54
  > = {
53
55
  'missing-material-receipt': {
54
- expected: 'a producer-owned Standard PBR material receipt is present',
55
- hint: 'cook and load the Standard PBR artifact, then retry preparation',
56
+ expected: 'a producer-owned material program receipt is present',
57
+ hint: 'cook and load the selected material program artifact, then retry preparation',
56
58
  recovery: 'rebuild-producer',
57
59
  },
60
+ 'material-abi-not-executable': {
61
+ expected: 'the published material ABI fits the GPU Scene submission layout',
62
+ hint: 'recook the material with a scene-index ABI supported by the GPU-driven owner or keep it on its authored lane',
63
+ recovery: 'route-cpu-lane',
64
+ },
58
65
  'missing-uv': {
59
66
  expected: 'the geometry exposes every UV set named by the material receipt',
60
67
  hint: 'repair the geometry UV semantic or route the draw to the CPU semantic lane',
@@ -72,7 +79,7 @@ const ERROR_POLICY: Readonly<
72
79
  },
73
80
  'alpha-mask-mismatch': {
74
81
  expected: 'the Alpha Mask cutoff is present in the material receipt',
75
- hint: 'declare alphaCutoff in the Standard PBR material schema and recook it',
82
+ hint: 'declare the pass coverage cutoff in the material schema and recook it',
76
83
  recovery: 'rebuild-producer',
77
84
  },
78
85
  'resource-not-ready': {
@@ -1320,6 +1320,67 @@ export class AtmosphereInvalidParameterError extends Error {
1320
1320
  }
1321
1321
  }
1322
1322
 
1323
+ export interface DynamicResolutionInvalidParameterDetail {
1324
+ readonly field: 'targetGpuMs' | 'minScale' | 'maxScale';
1325
+ readonly value: number;
1326
+ readonly expected: string;
1327
+ }
1328
+
1329
+ export class DynamicResolutionInvalidParameterError extends Error {
1330
+ readonly code = 'dynamic-resolution-invalid-parameter' as const;
1331
+ readonly expected: string;
1332
+ readonly hint: string;
1333
+ readonly detail: DynamicResolutionInvalidParameterDetail;
1334
+
1335
+ constructor(detail: DynamicResolutionInvalidParameterDetail) {
1336
+ super(`DynamicResolution.${detail.field} is invalid`);
1337
+ this.name = 'DynamicResolutionInvalidParameterError';
1338
+ this.expected = `DynamicResolution.${detail.field} must be ${detail.expected}`;
1339
+ this.hint = `set DynamicResolution.${detail.field} to a value that is ${detail.expected}`;
1340
+ this.detail = detail;
1341
+ }
1342
+ }
1343
+
1344
+ export interface DynamicResolutionRequiresTaaDetail {
1345
+ readonly antialias: 'none' | 'fxaa' | 'msaa';
1346
+ }
1347
+
1348
+ export class DynamicResolutionRequiresTaaError extends Error {
1349
+ readonly code = 'dynamic-resolution-requires-taa' as const;
1350
+ readonly expected = "DynamicResolution requires Camera.antialias to be 'taa'";
1351
+ readonly hint = "set Camera.antialias to 'taa' or remove DynamicResolution before retrying";
1352
+ readonly detail: DynamicResolutionRequiresTaaDetail;
1353
+
1354
+ constructor(detail: DynamicResolutionRequiresTaaDetail) {
1355
+ super('DynamicResolution requires TAA');
1356
+ this.name = 'DynamicResolutionRequiresTaaError';
1357
+ this.detail = detail;
1358
+ }
1359
+ }
1360
+
1361
+ export interface DynamicResolutionTimingUnavailableDetail {
1362
+ readonly generation: number;
1363
+ readonly operation: 'timestamp-query';
1364
+ }
1365
+
1366
+ export class DynamicResolutionTimingUnavailableError extends Error {
1367
+ readonly code = 'dynamic-resolution-timing-unavailable' as const;
1368
+ readonly expected = 'adaptive resolution receives a completed GPU timing sample';
1369
+ readonly hint = 'use fixed minScale=maxScale or retry after timestamp support is available';
1370
+ readonly detail: DynamicResolutionTimingUnavailableDetail;
1371
+
1372
+ constructor(detail: DynamicResolutionTimingUnavailableDetail) {
1373
+ super(`DynamicResolution timing is unavailable for generation ${detail.generation}`);
1374
+ this.name = 'DynamicResolutionTimingUnavailableError';
1375
+ this.detail = detail;
1376
+ }
1377
+ }
1378
+
1379
+ export type DynamicResolutionError =
1380
+ | DynamicResolutionInvalidParameterError
1381
+ | DynamicResolutionRequiresTaaError
1382
+ | DynamicResolutionTimingUnavailableError;
1383
+
1323
1384
  export interface OwnerStageFailedDetail {
1324
1385
  readonly owner: 'environment' | 'temporal' | 'fog' | 'renderer';
1325
1386
  readonly stage: 'extract' | 'prepare' | 'record' | 'finish' | 'submit';
@@ -1908,6 +1969,9 @@ export type RenderError =
1908
1969
  | TaaCapsInsufficientError
1909
1970
  | EnvironmentGenerationFailedError
1910
1971
  | AtmosphereInvalidParameterError
1972
+ | DynamicResolutionInvalidParameterError
1973
+ | DynamicResolutionRequiresTaaError
1974
+ | DynamicResolutionTimingUnavailableError
1911
1975
  | OwnerStageFailedError
1912
1976
  | PointsLinesInvalidStyleError
1913
1977
  | PointsLinesTopologyMismatchError
@@ -11,6 +11,10 @@ import {
11
11
  tonemapFromF32,
12
12
  validateCameraColorGrading,
13
13
  } from '../components/camera';
14
+ import {
15
+ DynamicResolution,
16
+ validateDynamicResolutionCamera,
17
+ } from '../components/dynamic-resolution';
14
18
  import { MotionBlur } from '../components/motion-blur';
15
19
  import { ScreenSpaceReflection } from '../components/screen-space-reflection';
16
20
  import type { CameraSnapshot } from '../render-contract';
@@ -28,7 +32,7 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
28
32
  const cameraQuery = world
29
33
  .query({
30
34
  read: [Camera],
31
- optional: [MotionBlur, ScreenSpaceReflection],
35
+ optional: [MotionBlur, DynamicResolution, ScreenSpaceReflection],
32
36
  with: [GlobalTransform],
33
37
  })
34
38
  .unwrap();
@@ -36,10 +40,14 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
36
40
  const cam = row.get(Camera);
37
41
  const motionBlur = row.get(MotionBlur);
38
42
  validateCameraColorGrading(cam.temperature, cam.tint, cam.colorLutStrength);
43
+ const dynamicResolution = row.get(DynamicResolution);
39
44
  const screenSpaceReflection = row.get(ScreenSpaceReflection);
40
45
  const entity = row.entity as EntityHandle;
41
46
  const worldMat = readWorldMatrix(world, entity);
42
47
  if (worldMat === undefined) continue;
48
+ const antialias = antialiasFromF32(cam.antialias);
49
+ const dynamicResolutionResult = validateDynamicResolutionCamera(dynamicResolution, antialias);
50
+ if (!dynamicResolutionResult.ok) throw dynamicResolutionResult.error;
43
51
  cameras.push({
44
52
  entityKey: entity as number,
45
53
  historyVersion: cam.historyVersion,
@@ -65,6 +73,9 @@ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
65
73
  colorLutStrength: cam.colorLutStrength,
66
74
  },
67
75
  antialias: antialiasFromF32(cam.antialias),
76
+ ...(dynamicResolutionResult.value === undefined
77
+ ? {}
78
+ : { dynamicResolution: dynamicResolutionResult.value }),
68
79
  bloom: bloomEnabledFromF32(cam.bloom),
69
80
  bloomThreshold: cam.bloomThreshold,
70
81
  bloomIntensity: cam.bloomIntensity,
@@ -660,6 +660,10 @@ export interface MaterialSnapshot {
660
660
  */
661
661
  readonly materialShaderId?: string | undefined;
662
662
  readonly materialProgramKeys?: Readonly<Record<string, string>> | undefined;
663
+ /** Producer-selected scene-index program keys, when a cooked publication carries them. */
664
+ readonly materialSceneIndexProgramKeys?:
665
+ | Readonly<Record<string, MaterialSceneIndexProgramKey>>
666
+ | undefined;
663
667
  /**
664
668
  * Stable source handle for the material asset. Record uses this only for
665
669
  * same-frame assembly reuse; GPU bind-group validity still comes from the
@@ -776,6 +780,12 @@ export interface MaterialSnapshot {
776
780
  readonly transparent?: boolean | undefined;
777
781
  }
778
782
 
783
+ /** Producer-selected scene-index key plus the Pass kind that owns it. */
784
+ export interface MaterialSceneIndexProgramKey {
785
+ readonly specializationKey: string;
786
+ readonly pass: 'forward' | 'shadow' | 'depth';
787
+ }
788
+
779
789
  export interface MaterialCacheChainIdentity {
780
790
  readonly asset: MaterialAsset;
781
791
  readonly parentGuid: string | undefined;
@@ -10,7 +10,7 @@ import {
10
10
  type PrepareGpuDrivenDrawInput,
11
11
  prepareGpuDrivenDraw,
12
12
  } from '../gpu-driven/prepared-draw';
13
- import { isStandardPbrMaterialShader } from '../pbr-pipeline';
13
+ import { isCanonicalStandardPbrMaterialShader } from '../pbr-pipeline';
14
14
  import type { MaterialSnapshot, RenderableSnapshot } from '../render-system-extract';
15
15
 
16
16
  export {
@@ -72,6 +72,26 @@ export function gpuDrivenShadowDrawKey(
72
72
  return `${gpuDrivenDrawKey(worldEntity, materialHandle, drawItemIndex)}:${passIndex}`;
73
73
  }
74
74
 
75
+ /**
76
+ * Stable lookup key for the producer-published material artifact selected by
77
+ * one prepared draw. The receipt identity is intentionally part of the key:
78
+ * a COLOR_0 variant (or any later ABI variant) must never share a pipeline or
79
+ * projection cache entry with its no-color sibling.
80
+ */
81
+ export function gpuDrivenMaterialArtifactKey(input: {
82
+ readonly material: string;
83
+ readonly deformation: 'rigid' | 'skin';
84
+ readonly receiptIdentity: string | undefined;
85
+ readonly receiptGeneration: number;
86
+ }): string {
87
+ return [
88
+ input.material,
89
+ input.deformation,
90
+ input.receiptIdentity ?? '<legacy-receipt>',
91
+ input.receiptGeneration,
92
+ ].join('|');
93
+ }
94
+
75
95
  /** Resolve the producer-owned source identity for one compact draw snapshot. */
76
96
  export function gpuDrivenSourceDrawItemIndex(
77
97
  draw: GpuDrivenDrawSnapshot,
@@ -136,7 +156,12 @@ interface BuildGpuDrivenDrawsInput {
136
156
  readonly baseSnapshot: RenderableSnapshot;
137
157
  readonly getMaterialShaderArtifact?: (
138
158
  materialShaderId: string,
139
- vertexColorAvailable?: boolean,
159
+ request?: {
160
+ readonly vertexColorAvailable?: boolean;
161
+ readonly deformation?: 'rigid' | 'skin';
162
+ readonly pass?: 'forward' | 'shadow' | 'depth';
163
+ readonly address?: 'direct' | 'scene-index';
164
+ },
140
165
  ) => MaterialShaderArtifact | undefined;
141
166
  /** Resolved lower-detail meshes, in the same order as baseSnapshot.lods. */
142
167
  readonly lodMeshes?: readonly (MeshAsset | undefined)[];
@@ -224,16 +249,38 @@ export function buildGpuDrivenDraws(
224
249
  ? undefined
225
250
  : (() => {
226
251
  const materialShaderId = drawMaterial.materialShaderId;
252
+ const vertexColorAvailable = geometry.vertexInputs.some(
253
+ (input) => input.semantic === 'color',
254
+ );
255
+ // The snapshot keeps the direct program key as its logical
256
+ // material identity. A published scene-index program has a
257
+ // separate specialization key; resolve that concrete artifact
258
+ // before preparing the draw so the indirect pipeline receives the
259
+ // scene entry rather than silently reusing the direct shader.
260
+ const sceneIndexProgramKey = Object.entries(
261
+ drawMaterial.materialSceneIndexProgramKeys ?? {},
262
+ ).find(
263
+ ([passName, program]) =>
264
+ program.pass === 'forward' &&
265
+ drawMaterial.materialProgramKeys?.[passName] === materialShaderId,
266
+ )?.[1]?.specializationKey;
227
267
  const artifact =
228
268
  materialShaderId === undefined
229
269
  ? undefined
230
- : input.getMaterialShaderArtifact?.(
231
- materialShaderId,
232
- geometry.vertexInputs.some((attribute) => attribute.semantic === 'color'),
233
- );
234
- const standardPbr = isStandardPbrMaterialShader(materialShaderId);
270
+ : input.getMaterialShaderArtifact?.(sceneIndexProgramKey ?? materialShaderId, {
271
+ vertexColorAvailable,
272
+ deformation: input.baseSnapshot.skin === undefined ? 'rigid' : 'skin',
273
+ pass: 'forward',
274
+ address: 'scene-index',
275
+ });
276
+ // Canonical roots and explicitly published scene-index programs
277
+ // require their producer receipt. Legacy direct-only aliases do not.
278
+ const requiresReceipt =
279
+ isCanonicalStandardPbrMaterialShader(materialShaderId) ||
280
+ sceneIndexProgramKey !== undefined;
281
+
235
282
  if (artifact === undefined) {
236
- return input.getMaterialShaderArtifact !== undefined && standardPbr
283
+ return input.getMaterialShaderArtifact !== undefined && requiresReceipt
237
284
  ? new GpuDrivenPreparationError('missing-material-receipt', {
238
285
  reason: 'material-receipt-missing',
239
286
  owner: 'material',
@@ -243,13 +290,11 @@ export function buildGpuDrivenDraws(
243
290
  }
244
291
  const receipt = artifact.receipt;
245
292
  if (receipt === undefined) {
246
- return standardPbr
247
- ? new GpuDrivenPreparationError('missing-material-receipt', {
248
- reason: 'material-receipt-missing',
249
- owner: 'material',
250
- expected: `receipt for ${materialShaderId}`,
251
- })
252
- : undefined;
293
+ return new GpuDrivenPreparationError('missing-material-receipt', {
294
+ reason: 'material-receipt-missing',
295
+ owner: 'material',
296
+ expected: `receipt for ${materialShaderId}`,
297
+ });
253
298
  }
254
299
  const skinReceipt: GpuDrivenSkinReceipt | undefined =
255
300
  input.baseSnapshot.skin !== undefined && receipt.skinPaletteAddress !== undefined
@@ -261,15 +306,25 @@ export function buildGpuDrivenDraws(
261
306
  byteOffset: input.baseSnapshot.skin.byteOffset,
262
307
  }
263
308
  : undefined;
309
+ const selectedArtifact =
310
+ artifact === undefined ||
311
+ sceneIndexProgramKey === undefined ||
312
+ materialShaderId === undefined
313
+ ? artifact
314
+ : {
315
+ ...artifact,
316
+ material: materialShaderId,
317
+ specializationKey: sceneIndexProgramKey,
318
+ };
264
319
  const result = resolvePreparedGpuDrivenDraw({
265
320
  snapshot: { ...input.baseSnapshot, material: drawMaterial },
266
- artifact,
321
+ artifact: selectedArtifact ?? artifact,
267
322
  geometry: { ...geometry, topology: draw.topology, indexed: draw.kind === 'indexed' },
268
323
  generation: receipt.generation,
269
324
  draw,
270
325
  ...(skinReceipt === undefined ? {} : { skinReceipt }),
271
326
  });
272
- return result.ok ? result.value : standardPbr ? result.error : undefined;
327
+ return result.ok ? result.value : result.error;
273
328
  })();
274
329
  if (prepared instanceof GpuDrivenPreparationError) {
275
330
  return [{ ...draw, preparationError: prepared, ...withLodRanges }];
@@ -211,6 +211,26 @@ interface MembershipProjection {
211
211
  readonly draws: readonly MembershipDraw[];
212
212
  }
213
213
 
214
+ function hasActiveAlphaMask(
215
+ material: RenderSceneSlot['snapshot']['material'],
216
+ prepared: PreparedGpuDrivenDraw,
217
+ ): boolean {
218
+ const { cutoff, source } = prepared.alphaMask;
219
+ if (cutoff.length === 0 || source.length === 0) return false;
220
+ // Standard's shared receipt describes the alpha-clip interface; the actual
221
+ // mode is selected by its authored cutoff. Custom producers own coverage in
222
+ // their receipt, so a declared clip remains masked without guessing a field.
223
+ if (
224
+ prepared.identity.material === 'forgeax::default-standard-pbr' ||
225
+ prepared.identity.material === 'forgeax::pbr-skin' ||
226
+ prepared.identity.material === 'forgeax::default-standard-pbr-skin'
227
+ ) {
228
+ const value = material.paramSnapshot?.[cutoff];
229
+ return typeof value === 'number' && value > 0;
230
+ }
231
+ return true;
232
+ }
233
+
214
234
  function eligibleDraws(slot: RenderSceneSlot): readonly EligibleGpuDrivenDraw[] {
215
235
  const snapshot = slot.snapshot;
216
236
  const draws = snapshot.gpuDrivenDraws;
@@ -232,13 +252,11 @@ function eligibleDraws(slot: RenderSceneSlot): readonly EligibleGpuDrivenDraw[]
232
252
  const prepared = draw.prepared;
233
253
  const material = snapshot.materials[draw.materialSlot] ?? snapshot.material;
234
254
  if (prepared !== undefined) {
235
- const isStandardPbr =
236
- (material.materialShaderId === 'forgeax::default-standard-pbr' &&
237
- prepared.identity.material === 'forgeax::default-standard-pbr') ||
238
- ((material.materialShaderId === 'forgeax::pbr-skin' ||
239
- material.materialShaderId === 'forgeax::default-standard-pbr-skin') &&
240
- (prepared.identity.material === 'forgeax::pbr-skin' ||
241
- prepared.identity.material === 'forgeax::default-standard-pbr-skin'));
255
+ // Prepared producer identity is the admission proof; the material
256
+ // identifier is only a consistency check, never a built-in allow-list.
257
+ const materialIdentityMatches =
258
+ material.materialShaderId === undefined ||
259
+ prepared.identity.material === material.materialShaderId;
242
260
  const isAlphaBlend =
243
261
  material.transparent === true || material.renderState?.blend !== undefined;
244
262
  const cpuOnlyMaterialResources =
@@ -251,16 +269,20 @@ function eligibleDraws(slot: RenderSceneSlot): readonly EligibleGpuDrivenDraw[]
251
269
  hasFiniteOrderedBounds(snapshot.skin.bounds) &&
252
270
  prepared.skinPaletteAddress !== undefined);
253
271
  if (
254
- !isStandardPbr ||
272
+ !materialIdentityMatches ||
255
273
  isAlphaBlend ||
256
274
  cpuOnlyMaterialResources ||
257
275
  (prepared.identity.deformation !== 'rigid' && prepared.identity.deformation !== 'skin') ||
258
276
  !skinReady
259
277
  )
260
278
  continue;
261
- const preparedIdentity = `${prepared.identity.material}|${prepared.identity.geometry}|${prepared.identity.deformation}`;
262
- const admission =
263
- typeof material.paramSnapshot?.alphaCutoff === 'number' ? 'alpha-mask' : 'opaque';
279
+ const preparedIdentity = [
280
+ prepared.identity.material,
281
+ prepared.identity.geometry,
282
+ prepared.identity.deformation,
283
+ ...(prepared.receiptIdentity === undefined ? [] : [prepared.receiptIdentity]),
284
+ ].join('|');
285
+ const admission = hasActiveAlphaMask(material, prepared) ? 'alpha-mask' : 'opaque';
264
286
  result.push({
265
287
  sourceDrawIndex: drawItemIndex,
266
288
  key: {
@@ -0,0 +1,179 @@
1
+ import type { MaterialShaderArtifact } from '@forgeax/engine-shader';
2
+ import { gpuDrivenMaterialArtifactKey } from '../extract/gpu-driven';
3
+ import type { RenderSystemInternals } from '../record/render-context';
4
+ import type { DispatchEntry, RenderableSnapshot } from '../render-system-extract';
5
+
6
+ type ArtifactRow = { readonly source: RenderableSnapshot; readonly renderableIndex: number };
7
+
8
+ /** Resolve the same producer programs for normal frames and replacement devices. */
9
+ export function collectGpuDrivenMaterialArtifacts({
10
+ rows,
11
+ shadowRows,
12
+ dispatch,
13
+ shadowDispatch,
14
+ resolve,
15
+ clustered,
16
+ reflectionFallback,
17
+ }: {
18
+ readonly rows: readonly ArtifactRow[];
19
+ readonly shadowRows: readonly ArtifactRow[];
20
+ readonly dispatch: readonly DispatchEntry[];
21
+ readonly shadowDispatch: readonly DispatchEntry[];
22
+ readonly resolve: RenderSystemInternals['getMaterialShaderArtifact'];
23
+ readonly clustered: boolean;
24
+ readonly reflectionFallback: boolean;
25
+ }) {
26
+ const materialArtifacts = new Map<string, MaterialShaderArtifact>();
27
+ const shadowMaterialArtifacts = new Map<string, MaterialShaderArtifact>();
28
+ const shadowProgramIds = new Map<string, string>();
29
+ const forwardProgramEntries = new Map<
30
+ string,
31
+ { readonly vertexEntry?: string; readonly fragmentEntry?: string }
32
+ >();
33
+ for (const entry of dispatch) {
34
+ if (entry.tags.LightMode !== 'Forward' || entry.materialShaderId === undefined) continue;
35
+ const key = `${entry.renderableIndex}:${entry.materialHandle}`;
36
+ if (forwardProgramEntries.has(key)) continue;
37
+ forwardProgramEntries.set(key, {
38
+ ...(entry.vertexEntry === undefined ? {} : { vertexEntry: entry.vertexEntry }),
39
+ ...(entry.fragmentEntry === undefined ? {} : { fragmentEntry: entry.fragmentEntry }),
40
+ });
41
+ }
42
+ for (const entry of shadowDispatch) {
43
+ if (entry.tags.LightMode !== 'ShadowCaster' || entry.materialShaderId === undefined) continue;
44
+ shadowProgramIds.set(
45
+ `${entry.renderableIndex}:${entry.materialHandle}`,
46
+ entry.materialShaderId,
47
+ );
48
+ }
49
+ const collectArtifacts = (
50
+ rows: readonly ArtifactRow[],
51
+ pass: 'forward' | 'shadow',
52
+ target: Map<string, MaterialShaderArtifact>,
53
+ ): void => {
54
+ if (resolve === undefined) return;
55
+ for (const row of rows) {
56
+ for (const draw of row.source.gpuDrivenDraws ?? []) {
57
+ const prepared = draw.prepared;
58
+ if (prepared === undefined) continue;
59
+ const material = row.source.materials[draw.materialSlot] ?? row.source.material;
60
+ const logicalMaterialShaderId = material.materialShaderId ?? prepared.identity.material;
61
+ const sceneIndexProgramKeys = material.materialSceneIndexProgramKeys;
62
+ const directMaterialShaderId =
63
+ pass === 'shadow'
64
+ ? shadowProgramIds.get(`${row.renderableIndex}:${material.materialHandle ?? -1}`)
65
+ : logicalMaterialShaderId;
66
+ const directPassNames = new Set(
67
+ Object.entries(material.materialProgramKeys ?? {})
68
+ .filter(([, key]) => key === directMaterialShaderId)
69
+ .map(([passName]) => passName),
70
+ );
71
+ const sceneIndexProgramKey = Object.entries(sceneIndexProgramKeys ?? {}).find(
72
+ ([passName, program]) => program.pass === pass && directPassNames.has(passName),
73
+ )?.[1]?.specializationKey;
74
+ // A modern publication may expose a scene-index pair for its
75
+ // forward Pass but omit ShadowCaster. Keep that material on its
76
+ // explicit CPU shadow lane; do not reinterpret its direct WGSL
77
+ // as a scene-index source and fail later at pipeline creation.
78
+ if (sceneIndexProgramKeys !== undefined && sceneIndexProgramKey === undefined) {
79
+ continue;
80
+ }
81
+ const materialShaderId =
82
+ pass === 'shadow'
83
+ ? (sceneIndexProgramKey ??
84
+ shadowProgramIds.get(`${row.renderableIndex}:${material.materialHandle ?? -1}`) ??
85
+ logicalMaterialShaderId)
86
+ : (sceneIndexProgramKey ?? logicalMaterialShaderId);
87
+ const artifact = resolve(materialShaderId, {
88
+ vertexColorAvailable: prepared.vertexInputs.some((input) => input.semantic === 'color'),
89
+ deformation: prepared.identity.deformation,
90
+ pass,
91
+ address: 'scene-index',
92
+ ...(pass === 'forward'
93
+ ? {
94
+ clustered: clustered,
95
+ reflectionFallback: reflectionFallback,
96
+ }
97
+ : {}),
98
+ });
99
+ const programEntry =
100
+ pass === 'shadow'
101
+ ? shadowDispatch.find(
102
+ (entry) =>
103
+ entry.tags.LightMode === 'ShadowCaster' &&
104
+ entry.renderableIndex === row.renderableIndex &&
105
+ entry.materialHandle === (material.materialHandle ?? -1),
106
+ )
107
+ : forwardProgramEntries.get(`${row.renderableIndex}:${material.materialHandle ?? -1}`);
108
+ const selectedArtifact =
109
+ artifact === undefined
110
+ ? undefined
111
+ : programEntry === undefined
112
+ ? artifact
113
+ : {
114
+ ...artifact,
115
+ ...(programEntry.vertexEntry === undefined
116
+ ? {}
117
+ : { vertexEntry: programEntry.vertexEntry }),
118
+ ...(programEntry.fragmentEntry === undefined
119
+ ? {}
120
+ : { fragmentEntry: programEntry.fragmentEntry }),
121
+ };
122
+ // A scene-index program has a distinct registry key but the
123
+ // prepared draw keeps the logical material identity for batching
124
+ // and admission. Carry both facts: `material` remains the logical
125
+ // identity, while `specializationKey` tells the raster owner which
126
+ // published WGSL key must be used for pipeline construction.
127
+ const selectedIdentityArtifact =
128
+ selectedArtifact === undefined || sceneIndexProgramKey === undefined
129
+ ? selectedArtifact
130
+ : {
131
+ ...selectedArtifact,
132
+ material: logicalMaterialShaderId,
133
+ specializationKey: sceneIndexProgramKey,
134
+ };
135
+ const receipt = selectedIdentityArtifact?.receipt;
136
+ const shadowVertexInputsMatch =
137
+ pass !== 'shadow' ||
138
+ (receipt !== undefined &&
139
+ receipt.vertexInputs.length === prepared.vertexInputs.length &&
140
+ receipt.vertexInputs.every(
141
+ (input, index) =>
142
+ input.semantic === prepared.vertexInputs[index]?.semantic &&
143
+ input.location === prepared.vertexInputs[index]?.location &&
144
+ input.format === prepared.vertexInputs[index]?.format,
145
+ ));
146
+ if (
147
+ selectedIdentityArtifact === undefined ||
148
+ receipt === undefined ||
149
+ !shadowVertexInputsMatch ||
150
+ (pass !== 'shadow' &&
151
+ (receipt.receiptIdentity !== prepared.receiptIdentity ||
152
+ receipt.generation !== prepared.receiptGeneration))
153
+ ) {
154
+ continue;
155
+ }
156
+ target.set(
157
+ gpuDrivenMaterialArtifactKey({
158
+ material: prepared.identity.material,
159
+ deformation: prepared.identity.deformation,
160
+ receiptIdentity: prepared.receiptIdentity,
161
+ receiptGeneration: prepared.receiptGeneration,
162
+ }),
163
+ selectedIdentityArtifact,
164
+ );
165
+ }
166
+ }
167
+ };
168
+ collectArtifacts(rows, 'forward', materialArtifacts);
169
+ collectArtifacts(shadowRows, 'forward', materialArtifacts);
170
+ collectArtifacts(rows, 'shadow', shadowMaterialArtifacts);
171
+ collectArtifacts(shadowRows, 'shadow', shadowMaterialArtifacts);
172
+ const firstArtifact = materialArtifacts.values().next().value as
173
+ | MaterialShaderArtifact
174
+ | undefined;
175
+ const firstSkinArtifact = [...materialArtifacts.values()].find(
176
+ (artifact) => artifact.receipt?.skinPaletteAddress !== undefined,
177
+ );
178
+ return { materialArtifacts, shadowMaterialArtifacts, firstArtifact, firstSkinArtifact };
179
+ }
@@ -0,0 +1,41 @@
1
+ import { resolveReflectionProbeBinding } from '../record/frame-lighting';
2
+ import type { ValidatedRenderable } from '../record/frame-snapshot';
3
+ import type { ReflectionProbeRecordState } from '../record/render-context';
4
+ import type { DispatchEntry, RenderableSnapshot } from '../render-system-extract';
5
+
6
+ export function materialBindingKey(
7
+ source: Pick<RenderableSnapshot, 'worldId' | 'entityKey'>,
8
+ handle: number,
9
+ ): string {
10
+ return `${source.worldId}:${source.entityKey}:${handle}`;
11
+ }
12
+
13
+ /** Per-object bindings and pass membership are part of an indirect batch's resource identity. */
14
+ export function projectMaterialBindingClasses(
15
+ rows: readonly ValidatedRenderable[],
16
+ dispatch: readonly DispatchEntry[],
17
+ probes?: ReflectionProbeRecordState,
18
+ ): ReadonlyMap<string, string> {
19
+ const deferred = new Set(
20
+ dispatch
21
+ .filter((entry) => entry.tags.LightMode === 'Deferred')
22
+ .map((entry) => `${entry.renderableIndex}:${entry.materialHandle}`),
23
+ );
24
+ const classes = new Map<string, string>();
25
+ for (const row of rows) {
26
+ const selection = probes?.selections.get(`${row.source.worldId}:${row.source.entityKey}`);
27
+ const probe =
28
+ selection === undefined
29
+ ? undefined
30
+ : resolveReflectionProbeBinding(selection, probes?.table).probeIndex;
31
+ const materials =
32
+ row.source.materials.length > 0 ? row.source.materials : [row.source.material];
33
+ for (const material of materials) {
34
+ const handle = material.materialHandle ?? -1;
35
+ const passClass = deferred.has(`${row.renderableIndex}:${handle}`) ? '' : 'forward';
36
+ if (probe !== undefined || passClass !== '')
37
+ classes.set(materialBindingKey(row.source, handle), `${passClass}|probe:${probe ?? ''}`);
38
+ }
39
+ }
40
+ return classes;
41
+ }
@@ -32,6 +32,8 @@ export interface PreparedGpuDrivenDraw {
32
32
  readonly deformation: 'rigid' | 'skin';
33
33
  };
34
34
  readonly receiptGeneration: number;
35
+ /** Content/layout identity that separates ABI variants (for example COLOR_0). */
36
+ readonly receiptIdentity?: string;
35
37
  readonly directEntry: string;
36
38
  readonly sceneIndexEntry: string;
37
39
  readonly materialRow: MaterialShaderArtifactReceipt['materialRow'];
@@ -111,6 +113,10 @@ function hasMaterialResources(
111
113
  // is a readiness requirement; this keeps numeric-only PBR on the same GPU
112
114
  // lane while still rejecting an authored handle that failed to resolve.
113
115
  return receipt.resourceSlots.every((slot) => {
116
+ // Storage-buffer resources have no MaterialSnapshot handle surface yet;
117
+ // admitting them as samplers would silently turn an authored resource
118
+ // failure into a prepared draw with an unrelated binding.
119
+ if (slot.kind === 'storage-buffer') return false;
114
120
  if (slot.kind === 'texture') {
115
121
  if (!authoredTextures?.has(slot.parameter)) return true;
116
122
  return (
@@ -169,11 +175,15 @@ export function prepareGpuDrivenDraw(
169
175
  actual: input.geometry.vertexInputs.map((input) => input.semantic).join(','),
170
176
  });
171
177
  }
172
- if (receipt.alphaMask.cutoff.length === 0) {
178
+ const hasAlphaMask = receipt.alphaMask.cutoff.length > 0 || receipt.alphaMask.source.length > 0;
179
+ if (
180
+ hasAlphaMask &&
181
+ (receipt.alphaMask.cutoff.length === 0 || receipt.alphaMask.source.length === 0)
182
+ ) {
173
183
  return failure('alpha-mask-mismatch', {
174
184
  reason: 'alpha-mask-receipt-missing',
175
185
  owner: 'material',
176
- expected: 'alphaMask.cutoff',
186
+ expected: 'alphaMask.cutoff and alphaMask.source for a masked Pass',
177
187
  });
178
188
  }
179
189
  if (!hasMaterialResources(input.snapshot, receipt)) {
@@ -215,6 +225,7 @@ export function prepareGpuDrivenDraw(
215
225
  deformation,
216
226
  },
217
227
  receiptGeneration: receipt.generation,
228
+ receiptIdentity: receipt.receiptIdentity,
218
229
  directEntry: receipt.directEntry,
219
230
  sceneIndexEntry: receipt.sceneIndexEntry,
220
231
  materialRow: receipt.materialRow,