@forgeax/engine-render 0.1.27 → 0.1.29

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 (655) hide show
  1. package/README.md +842 -124
  2. package/dist/assembly/atmosphere-shader-prewarm.d.ts +6 -0
  3. package/dist/assembly/atmosphere-shader-prewarm.d.ts.map +1 -0
  4. package/dist/assembly/dynamic-geometry-host.d.ts +52 -0
  5. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -0
  6. package/dist/assembly/dynamic-geometry-runtime.d.ts +20 -0
  7. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +3 -1
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/gpu-driven-owner.d.ts +10 -0
  11. package/dist/assembly/gpu-driven-owner.d.ts.map +1 -0
  12. package/dist/assembly/host-contract.d.ts +19 -0
  13. package/dist/assembly/host-contract.d.ts.map +1 -1
  14. package/dist/assembly/material/assembly.d.ts +7 -0
  15. package/dist/assembly/material/assembly.d.ts.map +1 -1
  16. package/dist/assembly/material-shader-policy.d.ts +3 -1
  17. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  18. package/dist/assembly/recovery/failure-location.d.ts +12 -0
  19. package/dist/assembly/recovery/failure-location.d.ts.map +1 -0
  20. package/dist/assembly/recovery/renderer-recover.d.ts +6 -4
  21. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  22. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  23. package/dist/assembly/renderer-feature-inspection.d.ts +5 -0
  24. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -0
  25. package/dist/assembly/renderer-frame-transaction.d.ts +19 -0
  26. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  27. package/dist/assembly/renderer-helpers.d.ts +3 -0
  28. package/dist/assembly/renderer-helpers.d.ts.map +1 -1
  29. package/dist/assembly/renderer-inspection.d.ts +4 -0
  30. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  31. package/dist/assembly/shader-prewarm-policy.d.ts +6 -0
  32. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -1
  33. package/dist/assembly/skin-palette-owner.d.ts +8 -0
  34. package/dist/assembly/skin-palette-owner.d.ts.map +1 -0
  35. package/dist/assembly/ssr-shader-prewarm.d.ts +6 -0
  36. package/dist/assembly/ssr-shader-prewarm.d.ts.map +1 -0
  37. package/dist/assembly/webgpu-device-loss.d.ts +15 -0
  38. package/dist/assembly/webgpu-device-loss.d.ts.map +1 -0
  39. package/dist/assembly/webgpu-pbr-ready.d.ts +38 -0
  40. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -0
  41. package/dist/assembly/webgpu-ready-mipmap.d.ts +11 -0
  42. package/dist/assembly/webgpu-ready-mipmap.d.ts.map +1 -0
  43. package/dist/assembly/webgpu-ready.d.ts +7 -3
  44. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  45. package/dist/assembly/webgpu-renderer-bootstrap.d.ts +10 -0
  46. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -0
  47. package/dist/assembly/webgpu-renderer-contract.d.ts +62 -0
  48. package/dist/assembly/webgpu-renderer-contract.d.ts.map +1 -0
  49. package/dist/assembly/webgpu-renderer-guards.d.ts +9 -0
  50. package/dist/assembly/webgpu-renderer-guards.d.ts.map +1 -0
  51. package/dist/assembly/webgpu-renderer-observation.d.ts +6 -0
  52. package/dist/assembly/webgpu-renderer-observation.d.ts.map +1 -0
  53. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts +12 -0
  54. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts.map +1 -0
  55. package/dist/assembly/webgpu-renderer.d.ts +7 -141
  56. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  57. package/dist/assembly/webgpu-vertex-layouts.d.ts +28 -84
  58. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -1
  59. package/dist/authoring.mjs +3 -3
  60. package/dist/{chunk-ADHHVYLW.mjs → chunk-33XZFTQU.mjs} +245 -39
  61. package/dist/chunk-33XZFTQU.mjs.map +1 -0
  62. package/dist/{chunk-GF523LHF.mjs → chunk-BLXGONR2.mjs} +30 -12
  63. package/dist/chunk-BLXGONR2.mjs.map +1 -0
  64. package/dist/{chunk-KM2NOX2I.mjs → chunk-BQVQOQJH.mjs} +958 -254
  65. package/dist/chunk-BQVQOQJH.mjs.map +1 -0
  66. package/dist/{chunk-HKXTW355.mjs → chunk-BTWH7BP4.mjs} +386 -17
  67. package/dist/chunk-BTWH7BP4.mjs.map +1 -0
  68. package/dist/{chunk-TMIASV2N.mjs → chunk-FUODTTIA.mjs} +3934 -676
  69. package/dist/chunk-FUODTTIA.mjs.map +1 -0
  70. package/dist/{chunk-FJ6P52EE.mjs → chunk-ILQ5MEZF.mjs} +3 -17
  71. package/dist/chunk-ILQ5MEZF.mjs.map +1 -0
  72. package/dist/{chunk-E34VL5VI.mjs → chunk-JQTSEI3Z.mjs} +3 -3
  73. package/dist/{chunk-E34VL5VI.mjs.map → chunk-JQTSEI3Z.mjs.map} +1 -1
  74. package/dist/{chunk-GE3SDD72.mjs → chunk-NICXSHAT.mjs} +16 -38
  75. package/dist/chunk-NICXSHAT.mjs.map +1 -0
  76. package/dist/{chunk-ZZ4YQ474.mjs → chunk-NRNXBLIJ.mjs} +577 -7
  77. package/dist/chunk-NRNXBLIJ.mjs.map +1 -0
  78. package/dist/{chunk-JDWARUOI.mjs → chunk-PA5DRJNI.mjs} +17 -3
  79. package/dist/chunk-PA5DRJNI.mjs.map +1 -0
  80. package/dist/{chunk-XDP6XE5V.mjs → chunk-S4DNKQYQ.mjs} +1118 -446
  81. package/dist/chunk-S4DNKQYQ.mjs.map +1 -0
  82. package/dist/components/camera.d.ts +62 -2
  83. package/dist/components/camera.d.ts.map +1 -1
  84. package/dist/components/index.d.ts +1 -0
  85. package/dist/components/index.d.ts.map +1 -1
  86. package/dist/components/instances.d.ts +6 -20
  87. package/dist/components/instances.d.ts.map +1 -1
  88. package/dist/components/scene-instance.d.ts +2 -1
  89. package/dist/components/scene-instance.d.ts.map +1 -1
  90. package/dist/components/screen-space-reflection.d.ts +14 -0
  91. package/dist/components/screen-space-reflection.d.ts.map +1 -0
  92. package/dist/construct-renderer.mjs +13805 -7186
  93. package/dist/construct-renderer.mjs.map +1 -1
  94. package/dist/device/gpu-residency.d.ts +11 -17
  95. package/dist/device/gpu-residency.d.ts.map +1 -1
  96. package/dist/device/mesh-residency-lifetime.d.ts +21 -0
  97. package/dist/device/mesh-residency-lifetime.d.ts.map +1 -0
  98. package/dist/dynamic-geometry.d.ts +125 -0
  99. package/dist/dynamic-geometry.d.ts.map +1 -0
  100. package/dist/environment/background.d.ts +6 -0
  101. package/dist/environment/background.d.ts.map +1 -0
  102. package/dist/errors/gpu-driven.d.ts +6 -3
  103. package/dist/errors/gpu-driven.d.ts.map +1 -1
  104. package/dist/errors/render.d.ts +7 -4
  105. package/dist/errors/render.d.ts.map +1 -1
  106. package/dist/extract/camera.d.ts.map +1 -1
  107. package/dist/extract/gpu-driven.d.ts +33 -3
  108. package/dist/extract/gpu-driven.d.ts.map +1 -1
  109. package/dist/features/host.d.ts +23 -1
  110. package/dist/features/host.d.ts.map +1 -1
  111. package/dist/features/noise-texture.d.ts +17 -0
  112. package/dist/features/noise-texture.d.ts.map +1 -0
  113. package/dist/features/plan.d.ts +83 -2
  114. package/dist/features/plan.d.ts.map +1 -1
  115. package/dist/features/prepared-gpu-work.d.ts +37 -4
  116. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  117. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  118. package/dist/features/prepared-graphics.d.ts +5 -0
  119. package/dist/features/prepared-graphics.d.ts.map +1 -1
  120. package/dist/features/render-graph-compute.d.ts +5 -3
  121. package/dist/features/render-graph-compute.d.ts.map +1 -1
  122. package/dist/features/render-graph-contribution.d.ts +11 -3
  123. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  124. package/dist/features/render-graph-raster.d.ts +19 -3
  125. package/dist/features/render-graph-raster.d.ts.map +1 -1
  126. package/dist/features/types.d.ts +11 -0
  127. package/dist/features/types.d.ts.map +1 -1
  128. package/dist/fullscreen-post-process-pass.d.ts +2 -0
  129. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  130. package/dist/gpu-driven/batch-topology.d.ts +34 -1
  131. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  132. package/dist/gpu-driven/pbr-program.d.ts +10 -0
  133. package/dist/gpu-driven/pbr-program.d.ts.map +1 -0
  134. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  135. package/dist/gpu-driven/production-raster.d.ts +144 -23
  136. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  137. package/dist/gpu-driven/shadow-views.d.ts +126 -0
  138. package/dist/gpu-driven/shadow-views.d.ts.map +1 -0
  139. package/dist/gpu-driven/view-gpu.d.ts +18 -2
  140. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  141. package/dist/gpu-scene-schema.d.ts +2 -8
  142. package/dist/gpu-scene-schema.d.ts.map +1 -1
  143. package/dist/gpu-scene.d.ts +14 -0
  144. package/dist/gpu-scene.d.ts.map +1 -1
  145. package/dist/gpu-texture-usage.d.ts +1 -0
  146. package/dist/gpu-texture-usage.d.ts.map +1 -1
  147. package/dist/hdrp-buffers.d.ts +6 -0
  148. package/dist/hdrp-buffers.d.ts.map +1 -1
  149. package/dist/ibl/frame-resources.d.ts +8 -0
  150. package/dist/ibl/frame-resources.d.ts.map +1 -0
  151. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  152. package/dist/index.d.ts +21 -8
  153. package/dist/index.d.ts.map +1 -1
  154. package/dist/index.mjs +8 -7
  155. package/dist/index.mjs.map +1 -1
  156. package/dist/inspection-types.d.ts +164 -0
  157. package/dist/inspection-types.d.ts.map +1 -1
  158. package/dist/instances-derived-bounds.d.ts +11 -0
  159. package/dist/instances-derived-bounds.d.ts.map +1 -1
  160. package/dist/instances.d.ts +89 -44
  161. package/dist/instances.d.ts.map +1 -1
  162. package/dist/internal.d.ts +3 -0
  163. package/dist/internal.d.ts.map +1 -1
  164. package/dist/internal.mjs +118 -10
  165. package/dist/internal.mjs.map +1 -1
  166. package/dist/lifecycle.d.ts +2 -1
  167. package/dist/lifecycle.d.ts.map +1 -1
  168. package/dist/material-row.d.ts +5 -0
  169. package/dist/material-row.d.ts.map +1 -0
  170. package/dist/materials.d.ts +15 -0
  171. package/dist/materials.d.ts.map +1 -1
  172. package/dist/pbr-pipeline.d.ts +21 -5
  173. package/dist/pbr-pipeline.d.ts.map +1 -1
  174. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  175. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts +12 -0
  176. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts.map +1 -0
  177. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts +70 -0
  178. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts.map +1 -0
  179. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts +75 -0
  180. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts.map +1 -0
  181. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts +111 -0
  182. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts.map +1 -0
  183. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts +16 -0
  184. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts.map +1 -0
  185. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts +14 -0
  186. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts.map +1 -0
  187. package/dist/pipeline/standard-output/auto-exposure/state.d.ts +46 -0
  188. package/dist/pipeline/standard-output/auto-exposure/state.d.ts.map +1 -0
  189. package/dist/pipeline/standard-output/color-transform.d.ts +27 -0
  190. package/dist/pipeline/standard-output/color-transform.d.ts.map +1 -0
  191. package/dist/pipeline/standard-output/graph.d.ts +15 -0
  192. package/dist/pipeline/standard-output/graph.d.ts.map +1 -0
  193. package/dist/pipeline/standard-output/lut-admission.d.ts +29 -0
  194. package/dist/pipeline/standard-output/lut-admission.d.ts.map +1 -0
  195. package/dist/pipeline/standard-output/lut-gpu.d.ts +48 -0
  196. package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -0
  197. package/dist/pipeline/standard-output/lut-state.d.ts +68 -0
  198. package/dist/pipeline/standard-output/lut-state.d.ts.map +1 -0
  199. package/dist/pipeline/standard-output/resources.d.ts +10 -0
  200. package/dist/pipeline/standard-output/resources.d.ts.map +1 -0
  201. package/dist/pipeline/standard-output/types.d.ts +39 -0
  202. package/dist/pipeline/standard-output/types.d.ts.map +1 -0
  203. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  204. package/dist/pipeline/standard-post.d.ts +2 -2
  205. package/dist/pipeline/standard-post.d.ts.map +1 -1
  206. package/dist/pipeline-builder.d.ts +2 -1
  207. package/dist/pipeline-builder.d.ts.map +1 -1
  208. package/dist/pipeline-spec-types.d.ts +4 -0
  209. package/dist/pipeline-spec-types.d.ts.map +1 -1
  210. package/dist/pipeline-spec.d.ts +1 -9
  211. package/dist/pipeline-spec.d.ts.map +1 -1
  212. package/dist/points-lines/standard-owner.d.ts +1 -1
  213. package/dist/points-lines/standard-owner.d.ts.map +1 -1
  214. package/dist/prepare/prepared-graphics-resolver.d.ts +6 -1
  215. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  216. package/dist/record/dynamic-geometry-consumption.d.ts +15 -0
  217. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -0
  218. package/dist/record/frame-lighting.d.ts +3 -3
  219. package/dist/record/frame-lighting.d.ts.map +1 -1
  220. package/dist/record/frame-snapshot.d.ts +106 -5
  221. package/dist/record/frame-snapshot.d.ts.map +1 -1
  222. package/dist/record/frame-targets.d.ts.map +1 -1
  223. package/dist/record/frame.d.ts +39 -4
  224. package/dist/record/frame.d.ts.map +1 -1
  225. package/dist/record/main-pass-geometry.d.ts +7 -8
  226. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  227. package/dist/record/main-pass-material.d.ts +38 -1
  228. package/dist/record/main-pass-material.d.ts.map +1 -1
  229. package/dist/record/main-pass-sprite-draws.d.ts +2 -10
  230. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  231. package/dist/record/main-pass.d.ts +12 -2
  232. package/dist/record/main-pass.d.ts.map +1 -1
  233. package/dist/record/mesh-ssbo.d.ts +15 -6
  234. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  235. package/dist/record/prepared-material-bindings.d.ts +19 -0
  236. package/dist/record/prepared-material-bindings.d.ts.map +1 -0
  237. package/dist/record/recovery-pipeline.d.ts +2 -9
  238. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  239. package/dist/record/render-context.d.ts +76 -26
  240. package/dist/record/render-context.d.ts.map +1 -1
  241. package/dist/record/shadow-pass.d.ts +62 -7
  242. package/dist/record/shadow-pass.d.ts.map +1 -1
  243. package/dist/record/skybox-post-pass.d.ts +1 -1
  244. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  245. package/dist/record/typed-frame-graph.d.ts +35 -7
  246. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  247. package/dist/record/view-ubo.d.ts +1 -0
  248. package/dist/record/view-ubo.d.ts.map +1 -1
  249. package/dist/recovery/render-system-candidate.d.ts +10 -2
  250. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  251. package/dist/recovery/render-system-roots.d.ts +11 -0
  252. package/dist/recovery/render-system-roots.d.ts.map +1 -0
  253. package/dist/recovery/types.d.ts +76 -0
  254. package/dist/recovery/types.d.ts.map +1 -0
  255. package/dist/reflection/record-owner.d.ts +0 -2
  256. package/dist/reflection/record-owner.d.ts.map +1 -1
  257. package/dist/render-contract.d.ts +185 -30
  258. package/dist/render-contract.d.ts.map +1 -1
  259. package/dist/render-graph-primitives.d.ts +1 -0
  260. package/dist/render-graph-primitives.d.ts.map +1 -1
  261. package/dist/render-pipeline.d.ts +64 -5
  262. package/dist/render-pipeline.d.ts.map +1 -1
  263. package/dist/render-system-extract-tail.d.ts +7 -5
  264. package/dist/render-system-extract-tail.d.ts.map +1 -1
  265. package/dist/render-system-extract.d.ts +101 -41
  266. package/dist/render-system-extract.d.ts.map +1 -1
  267. package/dist/render-system-projections.d.ts +22 -0
  268. package/dist/render-system-projections.d.ts.map +1 -0
  269. package/dist/render-system.d.ts +34 -77
  270. package/dist/render-system.d.ts.map +1 -1
  271. package/dist/scene/render-scene-types.d.ts +14 -7
  272. package/dist/scene/render-scene-types.d.ts.map +1 -1
  273. package/dist/scene/render-scene.d.ts +29 -11
  274. package/dist/scene/render-scene.d.ts.map +1 -1
  275. package/dist/scene/shadow-visibility.d.ts +5 -0
  276. package/dist/scene/shadow-visibility.d.ts.map +1 -0
  277. package/dist/ssr/admission.d.ts +70 -1
  278. package/dist/ssr/admission.d.ts.map +1 -1
  279. package/dist/ssr/compose.d.ts +13 -0
  280. package/dist/ssr/compose.d.ts.map +1 -0
  281. package/dist/ssr/errors.d.ts +35 -0
  282. package/dist/ssr/errors.d.ts.map +1 -1
  283. package/dist/ssr/graph.d.ts +60 -0
  284. package/dist/ssr/graph.d.ts.map +1 -0
  285. package/dist/ssr/history.d.ts +97 -0
  286. package/dist/ssr/history.d.ts.map +1 -0
  287. package/dist/ssr/hiz.d.ts +37 -0
  288. package/dist/ssr/hiz.d.ts.map +1 -0
  289. package/dist/ssr/inspection.d.ts +42 -0
  290. package/dist/ssr/inspection.d.ts.map +1 -0
  291. package/dist/ssr/resources.d.ts +58 -0
  292. package/dist/ssr/resources.d.ts.map +1 -0
  293. package/dist/ssr/temporal.d.ts +38 -0
  294. package/dist/ssr/temporal.d.ts.map +1 -0
  295. package/dist/systems/skin-palette-allocator.d.ts +32 -10
  296. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  297. package/dist/systems/skin-palette-types.d.ts +15 -0
  298. package/dist/systems/skin-palette-types.d.ts.map +1 -1
  299. package/dist/systems/transparent-dispatch.d.ts +9 -0
  300. package/dist/systems/transparent-dispatch.d.ts.map +1 -0
  301. package/dist/targets/material-source.d.ts +7 -0
  302. package/dist/targets/material-source.d.ts.map +1 -1
  303. package/dist/temporal/frame-coordinator.d.ts +7 -0
  304. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  305. package/dist/temporal/gpu.d.ts +8 -0
  306. package/dist/temporal/gpu.d.ts.map +1 -1
  307. package/dist/temporal/history.d.ts.map +1 -1
  308. package/dist/temporal/index.mjs +6 -6
  309. package/dist/temporal/inspection.d.ts.map +1 -1
  310. package/dist/temporal/standard-scene-data.d.ts +2 -1
  311. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  312. package/dist/temporal/temporal-view.d.ts +2 -0
  313. package/dist/temporal/temporal-view.d.ts.map +1 -1
  314. package/dist/typed-render-graph-primitives.d.ts +35 -1
  315. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  316. package/dist/typed-shadow-passes.d.ts +3 -1
  317. package/dist/typed-shadow-passes.d.ts.map +1 -1
  318. package/dist/volume/temporal.d.ts +6 -0
  319. package/dist/volume/temporal.d.ts.map +1 -1
  320. package/package.json +21 -20
  321. package/src/__tests__/auto-exposure-gpu-evidence.ts +291 -0
  322. package/src/__tests__/auto-exposure-inspection.unit.test.ts +80 -0
  323. package/src/__tests__/auto-exposure-transaction.integration.test.ts +99 -0
  324. package/src/__tests__/auto-exposure.browser.test.ts +33 -0
  325. package/src/__tests__/auto-exposure.dawn.test.ts +33 -0
  326. package/src/__tests__/detached-inspection.integration.test.ts +5 -0
  327. package/src/__tests__/device-scope-lifecycle.unit.test.ts +18 -0
  328. package/src/__tests__/device-scope-stale-matrix.unit.test.ts +18 -0
  329. package/src/__tests__/docs-gate-green.unit.test.ts +3 -3
  330. package/src/__tests__/dynamic-geometry-host.unit.test.ts +1123 -0
  331. package/src/__tests__/dynamic-geometry.unit.test.ts +233 -0
  332. package/src/__tests__/factory-contract.integration.test.ts +73 -22
  333. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +19 -0
  334. package/src/__tests__/feature-depth-input.dawn.test.ts +121 -0
  335. package/src/__tests__/feature-noise.unit.test.ts +38 -0
  336. package/src/__tests__/fixtures/taa-resolve-pinned-baseline.wgsl +301 -0
  337. package/src/__tests__/frame-camera-selection.unit.test.ts +131 -0
  338. package/src/__tests__/frame-plan-contract.unit.test.ts +24 -0
  339. package/src/__tests__/frame-targets.unit.test.ts +24 -0
  340. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +3 -0
  341. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +79 -0
  342. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +287 -6
  343. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +122 -142
  344. package/src/__tests__/gpu-driven-indirect-raster.browser.test.ts +10 -1
  345. package/src/__tests__/gpu-driven-lod-cache.unit.test.ts +264 -0
  346. package/src/__tests__/gpu-driven-mesh-world-identity.unit.test.ts +349 -0
  347. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +1417 -0
  348. package/src/__tests__/gpu-driven-pbr.integration.test.ts +21 -0
  349. package/src/__tests__/gpu-driven-production-fixture.ts +96 -0
  350. package/src/__tests__/gpu-driven-production.integration.test.ts +1075 -93
  351. package/src/__tests__/gpu-driven-scaling.unit.test.ts +1 -3
  352. package/src/__tests__/gpu-driven-shadow-views.browser.test.ts +63 -0
  353. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +294 -0
  354. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +97 -0
  355. package/src/__tests__/gpu-driven-shadow-views.unit.test.ts +168 -0
  356. package/src/__tests__/gpu-driven-skin-contract.test-d.ts +11 -0
  357. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +72 -20
  358. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +30 -9
  359. package/src/__tests__/gpu-driven-view.unit.test.ts +159 -52
  360. package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -1
  361. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +42 -0
  362. package/src/__tests__/gpu-scene-render-graph-gpu.ts +19 -5
  363. package/src/__tests__/gpu-scene.dawn.test.ts +40 -13
  364. package/src/__tests__/gpu-scene.unit.test.ts +559 -33
  365. package/src/__tests__/instances-culling.integration.test.ts +36 -10
  366. package/src/__tests__/instances-derived-bounds.unit.test.ts +54 -0
  367. package/src/__tests__/instances-store.unit.test.ts +242 -39
  368. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +6 -0
  369. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +13 -16
  370. package/src/__tests__/lut-lkg-transaction.integration.test.ts +122 -0
  371. package/src/__tests__/material-contract-inventory.unit.test.ts +5 -6
  372. package/src/__tests__/material-dispatch-shader-id.unit.test.ts +51 -0
  373. package/src/__tests__/material-lane-selection.unit.test.ts +60 -0
  374. package/src/__tests__/material-numeric-upload.unit.test.ts +53 -0
  375. package/src/__tests__/material-texture-handles.unit.test.ts +18 -0
  376. package/src/__tests__/material-texture-source.unit.test.ts +87 -0
  377. package/src/__tests__/materials-authoring-validation.unit.test.ts +41 -0
  378. package/src/__tests__/materials-standard-contract.unit.test.ts +52 -2
  379. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +4 -0
  380. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +53 -0
  381. package/src/__tests__/normal-fallback.dawn.test.ts +122 -0
  382. package/src/__tests__/particle-material-input-layout.unit.test.ts +104 -0
  383. package/src/__tests__/physical-clearcoat.integration.test.ts +37 -3
  384. package/src/__tests__/pipeline-texture-specialization.unit.test.ts +138 -0
  385. package/src/__tests__/point-shadow-inspection.unit.test.ts +2 -0
  386. package/src/__tests__/prepared-gpu-driven-pbr-contract.test-d.ts +31 -0
  387. package/src/__tests__/prepared-gpu-driven-pbr-contract.unit.test.ts +39 -0
  388. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +35 -4
  389. package/src/__tests__/prepared-graphics-capability.unit.test.ts +21 -0
  390. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +6 -2
  391. package/src/__tests__/profiler-phase-catalog.test.ts +5 -0
  392. package/src/__tests__/public-surface.test-d.ts +3 -0
  393. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +30 -12
  394. package/src/__tests__/reflection-fallback-async-publication.unit.test.ts +104 -0
  395. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +2 -1
  396. package/src/__tests__/render-error-code-owner.test-d.ts +4 -0
  397. package/src/__tests__/render-error-detail-ssot.test-d.ts +22 -0
  398. package/src/__tests__/render-error-exhaustive.test-d.ts +15 -0
  399. package/src/__tests__/render-feature-gpu-work.integration.test.ts +285 -2
  400. package/src/__tests__/render-feature-plan.unit.test.ts +245 -0
  401. package/src/__tests__/render-feature-shadow-order.unit.test.ts +228 -0
  402. package/src/__tests__/render-feature-zero-work.unit.test.ts +35 -1
  403. package/src/__tests__/render-graph-fallback.integration.test.ts +2 -2
  404. package/src/__tests__/render-owner-cohesion.unit.test.ts +55 -0
  405. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +426 -0
  406. package/src/__tests__/render-scene-projection.unit.test.ts +914 -23
  407. package/src/__tests__/render-scene-resource-updates.integration.test.ts +214 -0
  408. package/src/__tests__/render-scene-skin-lifecycle.integration.test.ts +173 -0
  409. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +133 -0
  410. package/src/__tests__/render-target-material-source.test-d.ts +27 -3
  411. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +61 -1
  412. package/src/__tests__/renderer-frame-transaction.integration.test.ts +923 -5
  413. package/src/__tests__/shader-manifest-fixture.ts +39 -0
  414. package/src/__tests__/skin-motion-regression.unit.test.ts +0 -33
  415. package/src/__tests__/skin-palette-persistent.integration.test.ts +28 -0
  416. package/src/__tests__/skin-palette-persistent.unit.test.ts +247 -0
  417. package/src/__tests__/skinned-shadow-caster.test.ts +118 -14
  418. package/src/__tests__/specular-environment-scale.dawn.test.ts +156 -0
  419. package/src/__tests__/ssr-admission-matrix.integration.test.ts +7 -1
  420. package/src/__tests__/ssr-confidence-compose.unit.test.ts +84 -0
  421. package/src/__tests__/ssr-coordinate-oracle.unit.test.ts +28 -0
  422. package/src/__tests__/ssr-format-probe-gating.integration.test.ts +82 -0
  423. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +91 -0
  424. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +112 -0
  425. package/src/__tests__/ssr-gpu-dispatch.ts +946 -0
  426. package/src/__tests__/ssr-graph-topology.unit.test.ts +104 -0
  427. package/src/__tests__/ssr-history-filter.dawn.test.ts +198 -0
  428. package/src/__tests__/ssr-hit-color.dawn.test.ts +273 -0
  429. package/src/__tests__/ssr-hiz-odd-reduction.dawn.test.ts +569 -0
  430. package/src/__tests__/ssr-hiz-rescue.dawn.test.ts +279 -0
  431. package/src/__tests__/ssr-hiz.browser.test.ts +150 -0
  432. package/src/__tests__/ssr-hiz.dawn.test.ts +150 -0
  433. package/src/__tests__/ssr-hiz.unit.test.ts +56 -0
  434. package/src/__tests__/ssr-inspection.unit.test.ts +105 -0
  435. package/src/__tests__/ssr-memory-budget.unit.test.ts +45 -0
  436. package/src/__tests__/ssr-performance-sequence-aggregation.unit.test.ts +15 -0
  437. package/src/__tests__/ssr-public-surface.test-d.ts +37 -0
  438. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +387 -0
  439. package/src/__tests__/ssr-reflection-mip-reducer.dawn.test.ts +309 -0
  440. package/src/__tests__/ssr-reprojection-probe.ts +90 -0
  441. package/src/__tests__/ssr-roughness-mip.dawn.test.ts +163 -0
  442. package/src/__tests__/ssr-shader-prewarm.unit.test.ts +59 -0
  443. package/src/__tests__/ssr-spatial.integration.test.ts +141 -0
  444. package/src/__tests__/ssr-stable-lattice.dawn.test.ts +475 -0
  445. package/src/__tests__/ssr-temporal-history.unit.test.ts +202 -0
  446. package/src/__tests__/ssr-texel-crossing.dawn.test.ts +448 -0
  447. package/src/__tests__/ssr-zero-work.integration.test.ts +87 -0
  448. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +44 -12
  449. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +29 -0
  450. package/src/__tests__/standard-output-chain-contract.unit.test.ts +99 -0
  451. package/src/__tests__/standard-output-chain.integration.test.ts +73 -0
  452. package/src/__tests__/standard-output-gpu-evidence.ts +205 -0
  453. package/src/__tests__/standard-output.browser.test.ts +15 -0
  454. package/src/__tests__/standard-output.dawn.test.ts +15 -0
  455. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +38 -0
  456. package/src/__tests__/standard-pipeline.integration.test.ts +320 -3
  457. package/src/__tests__/standard-profile-oracle.unit.test.ts +22 -0
  458. package/src/__tests__/standard-surface-lighting.unit.test.ts +41 -0
  459. package/src/__tests__/standard-texture-specialization.dawn.test.ts +141 -0
  460. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +6 -3
  461. package/src/__tests__/taa-coverage-edge.dawn.test.ts +193 -0
  462. package/src/__tests__/taa-history-clipping.dawn.test.ts +328 -0
  463. package/src/__tests__/taa-neighborhood-equivalence.dawn.test.ts +778 -0
  464. package/src/__tests__/taa-stability-feedback.browser.test.ts +23 -0
  465. package/src/__tests__/taa-stability-feedback.dawn.test.ts +68 -0
  466. package/src/__tests__/taa-stability-feedback.ts +341 -0
  467. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +49 -7
  468. package/src/__tests__/temporal-reset-matrix.unit.test.ts +9 -0
  469. package/src/__tests__/temporal-transaction.integration.test.ts +28 -0
  470. package/src/__tests__/texture-usage-surface.unit.test.ts +4 -2
  471. package/src/__tests__/typed-pipeline-topology.unit.test.ts +109 -4
  472. package/src/__tests__/typed-scene-pass-material-cache.integration.test.ts +126 -0
  473. package/src/__tests__/vertex-layouts-ssot.unit.test.ts +47 -0
  474. package/src/__tests__/visibility-extract.perf.test.ts +20 -8
  475. package/src/__tests__/visibility-instances.integration.test.ts +19 -7
  476. package/src/__tests__/visibility-producer-matrix.integration.test.ts +20 -9
  477. package/src/assembly/atmosphere-shader-prewarm.ts +38 -0
  478. package/src/assembly/dynamic-geometry-host.ts +887 -0
  479. package/src/assembly/dynamic-geometry-runtime.ts +71 -0
  480. package/src/assembly/factory.ts +4 -0
  481. package/src/assembly/gpu-driven-owner.ts +29 -0
  482. package/src/assembly/host-contract.ts +36 -0
  483. package/src/assembly/material/assembly.ts +38 -0
  484. package/src/assembly/material-shader-policy.ts +71 -28
  485. package/src/assembly/recovery/failure-location.ts +62 -0
  486. package/src/assembly/recovery/renderer-recover.ts +19 -17
  487. package/src/assembly/renderer-facade.ts +29 -0
  488. package/src/assembly/renderer-feature-inspection.ts +13 -0
  489. package/src/assembly/renderer-frame-transaction.ts +63 -0
  490. package/src/assembly/renderer-helpers.ts +26 -0
  491. package/src/assembly/renderer-inspection.ts +6 -0
  492. package/src/assembly/shader-prewarm-policy.ts +32 -0
  493. package/src/assembly/skin-palette-owner.ts +21 -0
  494. package/src/assembly/ssr-shader-prewarm.ts +41 -0
  495. package/src/assembly/webgpu-device-loss.ts +75 -0
  496. package/src/assembly/webgpu-pbr-ready.ts +232 -0
  497. package/src/assembly/webgpu-ready-mipmap.ts +26 -0
  498. package/src/assembly/webgpu-ready.ts +142 -121
  499. package/src/assembly/webgpu-renderer-bootstrap.ts +85 -0
  500. package/src/assembly/webgpu-renderer-contract.ts +69 -0
  501. package/src/assembly/webgpu-renderer-guards.ts +32 -0
  502. package/src/assembly/webgpu-renderer-observation.ts +43 -0
  503. package/src/assembly/webgpu-renderer-recovery-failure.ts +62 -0
  504. package/src/assembly/webgpu-renderer.ts +675 -614
  505. package/src/assembly/webgpu-vertex-layouts.ts +233 -87
  506. package/src/components/__tests__/camera-exposure.contract.unit.test.ts +153 -0
  507. package/src/components/camera.ts +287 -7
  508. package/src/components/index.ts +1 -0
  509. package/src/components/instances.ts +45 -18
  510. package/src/components/scene-instance.ts +2 -1
  511. package/src/components/screen-space-reflection.ts +21 -0
  512. package/src/components/sprite-animation.ts +3 -3
  513. package/src/components/sprite-instances.ts +1 -1
  514. package/src/device/gpu-residency.ts +96 -93
  515. package/src/device/mesh-residency-lifetime.ts +73 -0
  516. package/src/dynamic-geometry.ts +736 -0
  517. package/src/environment/background.ts +263 -0
  518. package/src/errors/gpu-driven.ts +39 -5
  519. package/src/errors/render.ts +27 -3
  520. package/src/extract/camera.ts +26 -1
  521. package/src/extract/gpu-driven.ts +185 -10
  522. package/src/features/host.ts +237 -19
  523. package/src/features/noise-texture.ts +91 -0
  524. package/src/features/plan.ts +553 -24
  525. package/src/features/prepared-gpu-work.ts +631 -49
  526. package/src/features/prepared-graphics-store.ts +31 -0
  527. package/src/features/prepared-graphics.ts +5 -0
  528. package/src/features/render-graph-compute.ts +41 -8
  529. package/src/features/render-graph-contribution.ts +141 -3
  530. package/src/features/render-graph-raster.ts +143 -30
  531. package/src/features/types.ts +11 -0
  532. package/src/fullscreen-post-process-pass.ts +3 -0
  533. package/src/gpu-driven/batch-topology.ts +424 -102
  534. package/src/gpu-driven/pbr-program.ts +31 -0
  535. package/src/gpu-driven/prepared-draw.ts +29 -4
  536. package/src/gpu-driven/production-raster.ts +2267 -694
  537. package/src/gpu-driven/shadow-views.ts +539 -0
  538. package/src/gpu-driven/view-gpu.ts +188 -41
  539. package/src/gpu-scene-schema.ts +52 -11
  540. package/src/gpu-scene.ts +186 -99
  541. package/src/gpu-texture-usage.ts +1 -0
  542. package/src/hdrp-buffers.ts +72 -36
  543. package/src/ibl/frame-resources.ts +38 -0
  544. package/src/ibl/skylight-bind-group.ts +5 -4
  545. package/src/index.ts +125 -1
  546. package/src/inspection-types.ts +191 -0
  547. package/src/instances-derived-bounds.ts +258 -34
  548. package/src/instances.ts +459 -150
  549. package/src/internal.ts +22 -0
  550. package/src/lifecycle.ts +2 -0
  551. package/src/material-row.ts +145 -0
  552. package/src/materials.ts +44 -8
  553. package/src/pbr-pipeline.ts +159 -17
  554. package/src/pipeline/standard-forward-lane.ts +52 -24
  555. package/src/pipeline/standard-output/__tests__/lut-admission.unit.test.ts +103 -0
  556. package/src/pipeline/standard-output/auto-exposure/__tests__/capability.unit.test.ts +40 -0
  557. package/src/pipeline/standard-output/auto-exposure/__tests__/gpu.contract.unit.test.ts +125 -0
  558. package/src/pipeline/standard-output/auto-exposure/__tests__/graph.contract.unit.test.ts +94 -0
  559. package/src/pipeline/standard-output/auto-exposure/__tests__/oracle.test.ts +108 -0
  560. package/src/pipeline/standard-output/auto-exposure/__tests__/preset.test.ts +26 -0
  561. package/src/pipeline/standard-output/auto-exposure/capability.ts +38 -0
  562. package/src/pipeline/standard-output/auto-exposure/gpu.ts +567 -0
  563. package/src/pipeline/standard-output/auto-exposure/graph.ts +221 -0
  564. package/src/pipeline/standard-output/auto-exposure/inspection.ts +231 -0
  565. package/src/pipeline/standard-output/auto-exposure/oracle.ts +291 -0
  566. package/src/pipeline/standard-output/auto-exposure/preset.ts +28 -0
  567. package/src/pipeline/standard-output/auto-exposure/state.ts +156 -0
  568. package/src/pipeline/standard-output/color-transform.ts +124 -0
  569. package/src/pipeline/standard-output/graph.ts +139 -0
  570. package/src/pipeline/standard-output/lut-admission.ts +121 -0
  571. package/src/pipeline/standard-output/lut-gpu.ts +399 -0
  572. package/src/pipeline/standard-output/lut-state.ts +176 -0
  573. package/src/pipeline/standard-output/resources.ts +121 -0
  574. package/src/pipeline/standard-output/types.ts +58 -0
  575. package/src/pipeline/standard-pipeline.ts +192 -59
  576. package/src/pipeline/standard-post.ts +186 -27
  577. package/src/pipeline-builder.ts +16 -6
  578. package/src/pipeline-spec-types.ts +4 -0
  579. package/src/pipeline-spec.ts +37 -29
  580. package/src/points-lines/standard-owner.ts +6 -14
  581. package/src/prepare/prepared-graphics-resolver.ts +9 -2
  582. package/src/record/__tests__/material-slot-plan.unit.test.ts +48 -0
  583. package/src/record/__tests__/mesh-ssbo.unit.test.ts +40 -1
  584. package/src/record/__tests__/recovery-pipeline.unit.test.ts +2 -29
  585. package/src/record/__tests__/sprite-material-abi.unit.test.ts +36 -5
  586. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +45 -0
  587. package/src/record/__tests__/view-ubo-layout.unit.test.ts +5 -0
  588. package/src/record/dynamic-geometry-consumption.ts +59 -0
  589. package/src/record/frame-lighting.ts +12 -11
  590. package/src/record/frame-snapshot.ts +133 -23
  591. package/src/record/frame-targets.ts +9 -0
  592. package/src/record/frame.ts +727 -133
  593. package/src/record/main-pass-geometry.ts +162 -98
  594. package/src/record/main-pass-material.ts +115 -26
  595. package/src/record/main-pass-sprite-draws.ts +18 -107
  596. package/src/record/main-pass.ts +78 -8
  597. package/src/record/material-uniforms.ts +1 -1
  598. package/src/record/mesh-ssbo.ts +59 -15
  599. package/src/record/prepared-material-bindings.ts +234 -0
  600. package/src/record/recovery-pipeline.ts +59 -136
  601. package/src/record/render-context.ts +92 -22
  602. package/src/record/shadow-pass.ts +1231 -209
  603. package/src/record/skybox-post-pass.ts +14 -3
  604. package/src/record/typed-frame-graph.ts +1006 -53
  605. package/src/record/view-ubo.ts +11 -0
  606. package/src/recovery/render-system-candidate.ts +90 -66
  607. package/src/recovery/render-system-roots.ts +113 -0
  608. package/src/recovery/types.ts +77 -0
  609. package/src/reflection/record-owner.ts +26 -54
  610. package/src/render-contract.ts +219 -25
  611. package/src/render-graph-primitives.ts +6 -2
  612. package/src/render-pipeline.ts +70 -1
  613. package/src/render-system-extract-tail.ts +409 -160
  614. package/src/render-system-extract.ts +376 -102
  615. package/src/render-system-projections.ts +241 -0
  616. package/src/render-system.ts +692 -791
  617. package/src/scene/render-scene-types.ts +13 -4
  618. package/src/scene/render-scene.ts +569 -355
  619. package/src/scene/shadow-visibility.ts +21 -0
  620. package/src/ssr/admission.ts +380 -14
  621. package/src/ssr/compose.ts +144 -0
  622. package/src/ssr/errors.ts +48 -0
  623. package/src/ssr/graph.ts +777 -0
  624. package/src/ssr/history.ts +436 -0
  625. package/src/ssr/hiz.ts +124 -0
  626. package/src/ssr/inspection.ts +124 -0
  627. package/src/ssr/resources.ts +202 -0
  628. package/src/ssr/temporal.ts +164 -0
  629. package/src/systems/skin-palette-allocator.ts +495 -148
  630. package/src/systems/skin-palette-types.ts +17 -0
  631. package/src/systems/transparent-dispatch.ts +90 -0
  632. package/src/targets/material-source.ts +34 -0
  633. package/src/temporal/coverage.ts +1 -1
  634. package/src/temporal/frame-coordinator.ts +23 -2
  635. package/src/temporal/gpu.ts +74 -12
  636. package/src/temporal/history.ts +3 -1
  637. package/src/temporal/inspection.ts +2 -1
  638. package/src/temporal/standard-scene-data.ts +3 -1
  639. package/src/temporal/temporal-view.ts +3 -0
  640. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +204 -139
  641. package/src/typed-render-graph-primitives.ts +266 -74
  642. package/src/typed-shadow-passes.ts +122 -12
  643. package/src/volume/temporal.ts +10 -0
  644. package/dist/chunk-ADHHVYLW.mjs.map +0 -1
  645. package/dist/chunk-FJ6P52EE.mjs.map +0 -1
  646. package/dist/chunk-GE3SDD72.mjs.map +0 -1
  647. package/dist/chunk-GF523LHF.mjs.map +0 -1
  648. package/dist/chunk-HKXTW355.mjs.map +0 -1
  649. package/dist/chunk-JDWARUOI.mjs.map +0 -1
  650. package/dist/chunk-KM2NOX2I.mjs.map +0 -1
  651. package/dist/chunk-TMIASV2N.mjs.map +0 -1
  652. package/dist/chunk-XDP6XE5V.mjs.map +0 -1
  653. package/dist/chunk-ZZ4YQ474.mjs.map +0 -1
  654. package/src/__tests__/instances-world-ownership.integration.test.ts +0 -62
  655. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +0 -36
@@ -9,8 +9,20 @@ import {
9
9
  type RhiRenderPassEncoder,
10
10
  } from '@forgeax/engine-rhi';
11
11
  import type { MaterialRenderState, PassSelector } from '@forgeax/engine-types';
12
- import type { GpuBuffer } from '../gpu-resource';
12
+ import { gpuDrivenShadowDrawKey } from '../extract/gpu-driven';
13
+ import type { SubmissionPlan } from '../gpu-driven/batch-topology';
14
+ import type { GpuDrivenShadowBatchProjection } from '../gpu-driven/production-raster';
15
+ import { type ShadowViewIdentity, shadowViewIdentityKey } from '../gpu-driven/shadow-views';
16
+ import type { GpuDrivenView } from '../gpu-driven/view-gpu';
17
+ import { GpuBuffer } from '../gpu-resource';
18
+ import {
19
+ GPU_BUFFER_USAGE_COPY_DST,
20
+ GPU_BUFFER_USAGE_STORAGE,
21
+ GPU_BUFFER_USAGE_UNIFORM,
22
+ } from '../gpu-usage';
13
23
  import { assembleMaterialWithSkylightEntries } from '../ibl/skylight-bind-group';
24
+ import type { InstanceBufferCacheEntry } from '../instance-buffer-cache';
25
+ import { instanceUploadRangesForResident } from '../instances';
14
26
  import {
15
27
  buildPbrMaterialUserRegionEntries,
16
28
  createPbrSkinMeshBindGroupEntries,
@@ -19,11 +31,18 @@ import {
19
31
  shadowCasterVariantSet,
20
32
  } from '../pbr-pipeline';
21
33
  import { COOKIE_MATRIX_BYTES } from '../prepare/extended-lighting/resources';
22
- import type { DispatchEntry, ExtractedLights } from '../render-system-extract';
34
+ import type {
35
+ DispatchEntry,
36
+ ExtractedLights,
37
+ RenderableSnapshot,
38
+ ShadowCasterMembership,
39
+ } from '../render-system-extract';
23
40
  import { matchPass } from '../systems/pass-selector';
41
+ import { instanceCollectionCacheKey, worldEntityKey } from './frame-snapshot';
24
42
  import {
25
- resolveGeometryInstanceBuffer,
43
+ reportInstanceResidency,
26
44
  resolveGeometryInstancesBindGroup,
45
+ resolveStorageInstanceChunks,
27
46
  } from './main-pass-geometry';
28
47
  import {
29
48
  buildPerSubmeshMaterialBg,
@@ -32,9 +51,12 @@ import {
32
51
  } from './main-pass-material';
33
52
  import {
34
53
  getOrCreateFromChain,
54
+ INSTANCE_UBO_FULL_ARRAY_BYTES,
55
+ MAX_UNIFORM_INSTANCES,
35
56
  MESH_PER_ENTITY_STRIDE,
36
57
  MESH_SSBO_BYTES,
37
58
  MESH_UBO_FULL_ARRAY_BYTES,
59
+ packInstanceStorageBuffer,
38
60
  } from './mesh-ssbo';
39
61
  import type { _InternalRenderPipelineContext } from './render-context';
40
62
  import { MATERIAL_PER_ENTITY_STRIDE, STANDARD_PBR_UBO_SIZE } from './render-context';
@@ -43,6 +65,32 @@ import { POINTS_LINES_VIEW_BYTES, pointShadowViewOffset, VIEW_UNIFORM_BYTES } fr
43
65
  export const SHADOW_CASTER_SLOT_STRIDE = 256;
44
66
  export const SHADOW_CASTER_BUFFER_SIZE = SHADOW_CASTER_SLOT_STRIDE * 8;
45
67
 
68
+ /**
69
+ * Renderer-owned capable shadow submission. The view/counter/indirect ABI is
70
+ * shared with the main GPU-driven lane; this record only supplies the
71
+ * prepared per-batch raster state that cannot be inferred from a raw scene.
72
+ * When present, the capable lane emits one indirect command per topology batch
73
+ * and never walks `validatedOrdered`.
74
+ */
75
+ export interface GpuDrivenShadowBatch {
76
+ readonly pipeline: RenderPipeline;
77
+ readonly meshBindGroup: BindGroup;
78
+ readonly visibleBindGroup: BindGroup;
79
+ readonly deformation: 'rigid' | 'skin';
80
+ /** Receipt-derived material BG for alpha-mask batches; opaque uses the pass singleton. */
81
+ readonly alphaMaskMaterialGroup?: BindGroup | undefined;
82
+ readonly vertexBuffer: Buffer;
83
+ readonly indexBuffer?: Buffer | undefined;
84
+ readonly indexFormat?: 'uint16' | 'uint32' | undefined;
85
+ }
86
+
87
+ export interface GpuDrivenShadowSubmission {
88
+ readonly view: GpuDrivenView;
89
+ readonly plan: SubmissionPlan;
90
+ readonly batches: ReadonlyMap<number, GpuDrivenShadowBatch>;
91
+ readonly instancesBindGroup?: BindGroup | undefined;
92
+ }
93
+
46
94
  export function directionalShadowCasterOffset(cascadeIndex: number): number {
47
95
  return SHADOW_CASTER_SLOT_STRIDE * cascadeIndex;
48
96
  }
@@ -253,43 +301,76 @@ function ensureTypedShadowViewBg(
253
301
  }
254
302
 
255
303
  interface ShadowDispatch {
304
+ readonly passIndex: number;
305
+ readonly materialHandle: number;
256
306
  readonly vertexEntry: string | undefined;
257
307
  readonly fragmentEntry: string | undefined;
258
308
  readonly materialShaderId: string;
259
309
  readonly renderState: MaterialRenderState | undefined;
310
+ readonly materialSlot: number | undefined;
311
+ readonly paramSnapshot: DispatchEntry['paramSnapshot'];
260
312
  }
261
313
 
262
- type ShadowDispatchMap = ReadonlyMap<number, ReadonlyMap<number, ShadowDispatch>>;
314
+ type ShadowDispatchMap = ReadonlyMap<number, ReadonlyMap<number, readonly ShadowDispatch[]>>;
315
+
316
+ function shadowDispatchEntries(c: _InternalRenderPipelineContext): readonly DispatchEntry[] {
317
+ return c.shadowDispatch ?? c.dispatch;
318
+ }
263
319
 
264
320
  export function shadowShaderMap(c: _InternalRenderPipelineContext): ShadowDispatchMap {
265
- const shaders = new Map<number, Map<number, ShadowDispatch>>();
266
- for (const entry of c.dispatch) {
267
- if (entry.tags.LightMode === 'ShadowCaster' && entry.materialShaderId !== undefined) {
268
- let byMaterial = shaders.get(entry.renderableIndex);
269
- if (byMaterial === undefined) {
270
- byMaterial = new Map<number, ShadowDispatch>();
271
- shaders.set(entry.renderableIndex, byMaterial);
272
- }
273
- byMaterial.set(entry.materialHandle, {
274
- vertexEntry: entry.vertexEntry,
275
- fragmentEntry: entry.fragmentEntry,
276
- materialShaderId:
277
- entry.materialShaderId === 'forgeax::default-standard-pbr'
278
- ? SHADOW_CASTER_SHADER_ID
279
- : entry.materialShaderId,
280
- renderState: entry.renderState,
281
- });
321
+ const shaders = new Map<number, Map<number, ShadowDispatch[]>>();
322
+ const shadowRows = c.shadowDispatch !== undefined;
323
+ const rows = (shadowRows ? c.shadowValidatedOrdered : c.validatedOrdered) ?? [];
324
+ const base = shadowRows ? c.validatedOrdered.length : 0;
325
+ const slotsByRenderable = new Map(
326
+ rows.map((row, index) => [row.renderableIndex, c.materialSlotIndices?.[base + index] ?? []]),
327
+ );
328
+ for (const entry of shadowDispatchEntries(c)) {
329
+ if (entry.tags.LightMode !== 'ShadowCaster' || entry.materialShaderId === undefined) continue;
330
+ let byMaterial = shaders.get(entry.renderableIndex);
331
+ if (byMaterial === undefined) {
332
+ byMaterial = new Map<number, ShadowDispatch[]>();
333
+ shaders.set(entry.renderableIndex, byMaterial);
282
334
  }
335
+ const candidateSlots = slotsByRenderable.get(entry.renderableIndex) ?? [];
336
+ const materialSlot = candidateSlots.find(
337
+ (slot) => c.materialSlots?.[slot]?.materialHandle === entry.materialHandle,
338
+ );
339
+ const dispatches = byMaterial.get(entry.materialHandle) ?? [];
340
+ dispatches.push({
341
+ passIndex: entry.passIndex,
342
+ materialHandle: entry.materialHandle,
343
+ vertexEntry: entry.vertexEntry,
344
+ fragmentEntry: entry.fragmentEntry,
345
+ materialShaderId:
346
+ entry.materialShaderId === 'forgeax::default-standard-pbr'
347
+ ? SHADOW_CASTER_SHADER_ID
348
+ : entry.materialShaderId,
349
+ renderState: entry.renderState,
350
+ materialSlot,
351
+ paramSnapshot: entry.paramSnapshot,
352
+ });
353
+ byMaterial.set(entry.materialHandle, dispatches);
283
354
  }
284
355
  return shaders;
285
356
  }
286
357
 
287
- function shadowPipeline(c: _InternalRenderPipelineContext): RenderPipeline | null {
358
+ function shadowRasterState(
359
+ state: MaterialRenderState | undefined,
360
+ reflected: boolean,
361
+ ): MaterialRenderState | undefined {
362
+ return reflected ? { ...state, frontFace: state?.frontFace === 'cw' ? 'ccw' : 'cw' } : state;
363
+ }
364
+
365
+ function shadowPipeline(
366
+ c: _InternalRenderPipelineContext,
367
+ reflected = false,
368
+ ): RenderPipeline | null {
288
369
  return (
289
370
  c.runtime.getMaterialShaderPipeline?.(
290
371
  SHADOW_CASTER_SHADER_ID,
291
372
  false,
292
- undefined,
373
+ shadowRasterState(undefined, reflected),
293
374
  'triangle-list',
294
375
  undefined,
295
376
  shadowCasterVariantSet(c.runtime.device.caps.storageBuffer, false),
@@ -307,32 +388,6 @@ function shadowPipeline(c: _InternalRenderPipelineContext): RenderPipeline | nul
307
388
  );
308
389
  }
309
390
 
310
- function skinnedShadowPipeline(
311
- c: _InternalRenderPipelineContext,
312
- entry: _InternalRenderPipelineContext['validatedOrdered'][number],
313
- ): RenderPipeline | null {
314
- return (
315
- c.runtime.getMaterialShaderPipeline?.(
316
- SHADOW_CASTER_SHADER_ID,
317
- false,
318
- undefined,
319
- entry.mesh.submeshes[0]?.topology ?? 'triangle-list',
320
- entry.mesh.indexFormat,
321
- shadowCasterVariantSet(c.runtime.device.caps.storageBuffer, true),
322
- 'shadow-caster',
323
- undefined,
324
- 1,
325
- undefined,
326
- undefined,
327
- undefined,
328
- undefined,
329
- entry.mesh.layoutProjection,
330
- undefined,
331
- 'pbr-skin',
332
- ) ?? null
333
- );
334
- }
335
-
336
391
  function skinnedShadowMeshBindGroup(
337
392
  c: _InternalRenderPipelineContext,
338
393
  entry: _InternalRenderPipelineContext['validatedOrdered'][number],
@@ -363,13 +418,371 @@ function skinnedShadowMeshBindGroup(
363
418
  );
364
419
  }
365
420
 
421
+ /**
422
+ * Record the capable shadow lane from the shared view's indirect arguments.
423
+ * The submission is an explicit prepared contract: missing view buffers or a
424
+ * missing batch raster projection are terminal rather than a reason to fall
425
+ * back to a CPU caster walk. The caller may then keep the existing CPU path
426
+ * when no capable submission was published for the current view.
427
+ */
428
+ export function recordGpuDrivenShadowIndirect(
429
+ pass: RhiRenderPassEncoder,
430
+ submission: GpuDrivenShadowSubmission,
431
+ ): void {
432
+ const indirect = submission.view.indirectBuffer;
433
+ if (indirect === undefined) {
434
+ throw new RhiError({
435
+ code: 'internal-error',
436
+ expected: 'GPU-driven shadow view indirect buffer',
437
+ hint: 'publish a prepared view generation before recording a capable shadow pass',
438
+ });
439
+ }
440
+ const indexed = new Map<number, GpuDrivenShadowBatch>();
441
+ for (const [batchId, batch] of submission.batches) indexed.set(batchId, batch);
442
+ let currentPipeline: RenderPipeline | undefined;
443
+ let currentVertex: Buffer | undefined;
444
+ let currentIndex: Buffer | undefined;
445
+ for (const batch of submission.plan.batches) {
446
+ const raster = indexed.get(batch.batchId);
447
+ if (raster === undefined) {
448
+ throw new RhiError({
449
+ code: 'internal-error',
450
+ expected: `prepared shadow raster batch ${batch.batchId}`,
451
+ hint: 'keep the shadow view plan and prepared batch projection on one generation',
452
+ });
453
+ }
454
+ if (currentPipeline !== raster.pipeline) {
455
+ pass.setPipeline(raster.pipeline);
456
+ currentPipeline = raster.pipeline;
457
+ }
458
+ if (currentVertex !== raster.vertexBuffer) {
459
+ pass.setVertexBuffer(0, raster.vertexBuffer);
460
+ currentVertex = raster.vertexBuffer;
461
+ }
462
+ if (raster.alphaMaskMaterialGroup !== undefined) {
463
+ // Alpha-mask shadow variants sample the receipt-owned base-color
464
+ // texture. Opaque batches keep the pass-level singleton bound by the
465
+ // caller, so group(1) remains one stable layout for every draw.
466
+ pass.setBindGroup(1, raster.alphaMaskMaterialGroup, [0]);
467
+ }
468
+ if (raster.indexBuffer !== undefined) {
469
+ if (raster.indexFormat === undefined) {
470
+ throw new RhiError({
471
+ code: 'internal-error',
472
+ expected: `index format for prepared shadow raster batch ${batch.batchId}`,
473
+ hint: 'publish the geometry layout projection with the capable shadow batch',
474
+ });
475
+ }
476
+ if (currentIndex !== raster.indexBuffer) {
477
+ pass.setIndexBuffer(raster.indexBuffer, raster.indexFormat);
478
+ currentIndex = raster.indexBuffer;
479
+ }
480
+ pass.setBindGroup(2, raster.meshBindGroup, raster.deformation === 'skin' ? [0, 0, 0] : [0]);
481
+ pass.setBindGroup(3, raster.visibleBindGroup);
482
+ pass.drawIndexedIndirect(indirect, batch.indirectOffset);
483
+ } else {
484
+ pass.setBindGroup(2, raster.meshBindGroup, raster.deformation === 'skin' ? [0, 0, 0] : [0]);
485
+ pass.setBindGroup(3, raster.visibleBindGroup);
486
+ pass.drawIndirect(indirect, batch.indirectOffset);
487
+ }
488
+ }
489
+ }
490
+
491
+ function shadowDrawIndicesBySubmesh(
492
+ source: RenderableSnapshot,
493
+ mesh: _InternalRenderPipelineContext['validatedOrdered'][number]['mesh'],
494
+ ): ReadonlyMap<number, number> {
495
+ const draws = source.gpuDrivenDraws;
496
+ if (draws === undefined || draws.length === 0) return new Map();
497
+ const indices = new Map<number, number>();
498
+ let hasStableSourceIdentity = false;
499
+ for (const [compactIndex, draw] of draws.entries()) {
500
+ const drawItemIndex = draw.drawItemIndex;
501
+ if (drawItemIndex === undefined) continue;
502
+ hasStableSourceIdentity = true;
503
+ if (mesh.submeshes[drawItemIndex] !== undefined) indices.set(drawItemIndex, compactIndex);
504
+ }
505
+ if (hasStableSourceIdentity) return indices;
506
+
507
+ // Legacy snapshots predating drawItemIndex are matched by their compact
508
+ // geometry facts. New extracted snapshots never take this path.
509
+ let nonIndexedFirst = 0;
510
+ let drawCursor = 0;
511
+ for (const [submeshIndex, submesh] of mesh.submeshes.entries()) {
512
+ const first = mesh.indexed ? submesh.indexOffset : nonIndexedFirst;
513
+ const count = mesh.indexed ? submesh.indexCount : submesh.vertexCount;
514
+ if (!mesh.indexed) nonIndexedFirst += submesh.vertexCount;
515
+ const draw = draws[drawCursor];
516
+ if (
517
+ draw !== undefined &&
518
+ draw.first === first &&
519
+ draw.count === count &&
520
+ draw.materialSlot === submesh.materialSlot &&
521
+ draw.topology === submesh.topology
522
+ ) {
523
+ indices.set(submeshIndex, drawCursor);
524
+ drawCursor += 1;
525
+ }
526
+ }
527
+ return indices;
528
+ }
529
+
530
+ function isGpuShadowClaimedSubmesh(
531
+ source: RenderableSnapshot,
532
+ renderableIndex: number,
533
+ submeshIndex: number,
534
+ drawIndices: ReadonlyMap<number, number>,
535
+ gpuDrivenShadowDrawKeys: ReadonlySet<string> | undefined,
536
+ shadowDispatchByRenderableIdx: ShadowDispatchMap,
537
+ shadowCasterMembership: readonly ShadowCasterMembership[] | undefined,
538
+ shadowCasterWorldKeys: readonly number[] | undefined,
539
+ ): boolean {
540
+ if (gpuDrivenShadowDrawKeys === undefined) return false;
541
+ const drawItemIndex = drawIndices.get(submeshIndex);
542
+ const draw = drawItemIndex === undefined ? undefined : source.gpuDrivenDraws?.[drawItemIndex];
543
+ if (drawItemIndex === undefined || draw === undefined) return false;
544
+ const material = source.materials[draw.materialSlot] ?? source.material;
545
+ const worldEntity = worldEntityKey(
546
+ shadowCasterWorldKeys?.[source.worldId] ?? source.worldId,
547
+ source.entityKey,
548
+ );
549
+ const sourceDrawItemIndex = draw.drawItemIndex ?? drawItemIndex;
550
+ if (shadowCasterMembership !== undefined) {
551
+ const matching = shadowCasterMembership.filter(
552
+ (membership) =>
553
+ membership.worldEntity === worldEntity &&
554
+ membership.materialHandle === (material.materialHandle ?? -1) &&
555
+ membership.drawItemIndex === sourceDrawItemIndex,
556
+ );
557
+ if (
558
+ matching.length === 0 ||
559
+ matching.some((membership) => membership.cpuReason !== undefined)
560
+ ) {
561
+ return false;
562
+ }
563
+ return matching.every((membership) =>
564
+ gpuDrivenShadowDrawKeys.has(
565
+ gpuDrivenShadowDrawKey(
566
+ membership.worldEntity,
567
+ membership.materialHandle,
568
+ membership.drawItemIndex,
569
+ membership.passIndex,
570
+ ),
571
+ ),
572
+ );
573
+ }
574
+ const dispatches =
575
+ shadowDispatchByRenderableIdx.get(renderableIndex)?.get(material.materialHandle ?? 0) ?? [];
576
+ if (dispatches.length > 0) {
577
+ return dispatches.every((dispatch) =>
578
+ gpuDrivenShadowDrawKeys.has(
579
+ gpuDrivenShadowDrawKey(
580
+ worldEntity,
581
+ material.materialHandle ?? -1,
582
+ draw.drawItemIndex ?? drawItemIndex,
583
+ dispatch.passIndex,
584
+ ),
585
+ ),
586
+ );
587
+ }
588
+ const base = gpuDrivenShadowDrawKey(
589
+ worldEntity,
590
+ material.materialHandle ?? -1,
591
+ sourceDrawItemIndex,
592
+ 0,
593
+ ).split(':');
594
+ return [...gpuDrivenShadowDrawKeys].some((key) => {
595
+ const parts = key.split(':');
596
+ return (
597
+ parts.length === 4 && parts[0] === base[0] && parts[1] === base[1] && parts[2] === base[2]
598
+ );
599
+ });
600
+ }
601
+
602
+ interface GpuDrivenShadowRecordResult {
603
+ readonly claimedKeys: ReadonlySet<string>;
604
+ }
605
+
606
+ function shadowCasterKeys(c: _InternalRenderPipelineContext): ReadonlySet<string> {
607
+ if (c.shadowCasterMembership !== undefined) {
608
+ const keys = new Set<string>();
609
+ for (const membership of c.shadowCasterMembership) {
610
+ keys.add(
611
+ gpuDrivenShadowDrawKey(
612
+ membership.worldEntity,
613
+ membership.materialHandle,
614
+ membership.drawItemIndex,
615
+ membership.passIndex,
616
+ ),
617
+ );
618
+ }
619
+ return keys;
620
+ }
621
+ return c.gpuDrivenShadowDrawKeys ?? new Set<string>();
622
+ }
623
+
624
+ function validateShadowOwnership(
625
+ c: _InternalRenderPipelineContext,
626
+ identity: ShadowViewIdentity,
627
+ claimedKeys: ReadonlySet<string> | undefined,
628
+ recordedKeys: ReadonlySet<string>,
629
+ ): void {
630
+ if (
631
+ c.shadowCasterMembership === undefined &&
632
+ (c.gpuDrivenShadowDrawKeys === undefined || c.gpuDrivenShadowDrawKeys.size === 0)
633
+ )
634
+ return;
635
+ const expectedKeys = shadowCasterKeys(c);
636
+ const claimed = claimedKeys ?? new Set<string>();
637
+ const overlap = [...claimed].filter((key) => recordedKeys.has(key));
638
+ const union = new Set([...claimed, ...recordedKeys]);
639
+ const missing = [...expectedKeys].filter((key) => !union.has(key));
640
+ const extra = [...union].filter((key) => !expectedKeys.has(key));
641
+ if (overlap.length === 0 && missing.length === 0 && extra.length === 0) return;
642
+ throw new RhiError({
643
+ code: 'internal-error',
644
+ expected: `disjoint ShadowCaster ownership for ${shadowViewIdentityKey(identity)}`,
645
+ hint: `claimed=${[...claimed].sort().join(',')} recorded=${[...recordedKeys].sort().join(',')} missing=${missing.sort().join(',')} extra=${extra.sort().join(',')}`,
646
+ });
647
+ }
648
+
649
+ function hasUnclaimedShadowCasters(
650
+ c: _InternalRenderPipelineContext,
651
+ claimedKeys: ReadonlySet<string> | undefined,
652
+ ): boolean {
653
+ const extractedKeys =
654
+ c.shadowCasterMembership === undefined
655
+ ? [...(c.gpuDrivenShadowDrawKeys ?? [])]
656
+ : c.shadowCasterMembership.map((membership) =>
657
+ gpuDrivenShadowDrawKey(
658
+ membership.worldEntity,
659
+ membership.materialHandle,
660
+ membership.drawItemIndex,
661
+ membership.passIndex,
662
+ ),
663
+ );
664
+ if (extractedKeys.length === 0) return false;
665
+ return claimedKeys === undefined || extractedKeys.some((key) => !claimedKeys.has(key));
666
+ }
667
+
668
+ function hasExpectedShadowCasters(c: _InternalRenderPipelineContext): boolean {
669
+ if ((c.shadowCasterMembership?.length ?? 0) > 0) return true;
670
+ if ((c.gpuDrivenShadowDrawKeys?.size ?? 0) > 0) return true;
671
+ return shadowDispatchEntries(c).some((entry) => entry.tags.LightMode === 'ShadowCaster');
672
+ }
673
+
674
+ function recordGpuDrivenShadowIfPublished(
675
+ c: _InternalRenderPipelineContext,
676
+ pass: RhiRenderPassEncoder,
677
+ identity: ShadowViewIdentity,
678
+ ): GpuDrivenShadowRecordResult | undefined {
679
+ const viewSubmission = c.gpuDrivenShadowViews?.submission(identity);
680
+ const projections = c.gpuDrivenShadowBatchProjections?.get(shadowViewIdentityKey(identity));
681
+ if (viewSubmission === undefined) {
682
+ return undefined;
683
+ }
684
+ if (projections === undefined) {
685
+ throw new RhiError({
686
+ code: 'rhi-not-available',
687
+ expected: `GPU-driven shadow projection for ${shadowViewIdentityKey(identity)}`,
688
+ hint: 'terminate the capable shadow frame instead of falling through to CPU caster enumeration',
689
+ });
690
+ }
691
+ const batches = new Map<number, GpuDrivenShadowBatch>();
692
+ for (const batch of viewSubmission.plan.batches) {
693
+ const projection: GpuDrivenShadowBatchProjection | undefined = projections.get(batch.batchId);
694
+ if (projection === undefined) {
695
+ throw new RhiError({
696
+ code: 'internal-error',
697
+ expected: `projected mesh binding for GPU-driven shadow batch ${batch.batchId}`,
698
+ hint: 'keep shadow batch scene rows aligned with the shared SubmissionPlan',
699
+ });
700
+ }
701
+ const pipeline = c.runtime.getMaterialShaderPipeline?.(
702
+ SHADOW_CASTER_SHADER_ID,
703
+ false,
704
+ shadowRasterState(undefined, identity.kind === 'point'),
705
+ batch.key.topology,
706
+ batch.key.drawKind === 'indexed' ? projection.mesh.indexFormat : undefined,
707
+ shadowCasterVariantSet(
708
+ c.runtime.device.caps.storageBuffer,
709
+ projection.deformation === 'skin',
710
+ true,
711
+ batch.key.admission === 'alpha-mask',
712
+ ),
713
+ 'shadow-caster',
714
+ undefined,
715
+ undefined,
716
+ undefined,
717
+ undefined,
718
+ undefined,
719
+ undefined,
720
+ projection.mesh.layoutProjection,
721
+ undefined,
722
+ projection.deformation === 'skin' ? 'gpu-driven-skin' : 'gpu-driven-pbr',
723
+ 'vs_scene_index',
724
+ );
725
+ if (pipeline === null || pipeline === undefined) {
726
+ throw new RhiError({
727
+ code: 'rhi-not-available',
728
+ expected: `GPU-driven shadow pipeline variant for batch ${batch.batchId}`,
729
+ hint: 'publish the exact scene-index shadow variant before entering the capable lane',
730
+ });
731
+ }
732
+ if (batch.key.drawKind === 'indexed' && projection.mesh.indexBuffer === null) {
733
+ throw new RhiError({
734
+ code: 'internal-error',
735
+ expected: `indexed mesh buffer for GPU-driven shadow batch ${batch.batchId}`,
736
+ hint: 'keep the geometry residency and topology draw kind aligned before capable recording',
737
+ });
738
+ }
739
+ batches.set(batch.batchId, {
740
+ pipeline,
741
+ meshBindGroup: projection.meshBindGroup,
742
+ visibleBindGroup: projection.visibleBindGroup,
743
+ deformation: projection.deformation,
744
+ ...(batch.key.admission === 'alpha-mask'
745
+ ? { alphaMaskMaterialGroup: ensureGpuDrivenAlphaMaskMaterialBg(c, projection) }
746
+ : {}),
747
+ vertexBuffer: projection.mesh.vertexBuffer.handle,
748
+ ...(batch.key.drawKind === 'indexed' && projection.mesh.indexBuffer !== null
749
+ ? {
750
+ indexBuffer: projection.mesh.indexBuffer.handle,
751
+ indexFormat: projection.mesh.indexFormat,
752
+ }
753
+ : {}),
754
+ });
755
+ }
756
+ const submission: GpuDrivenShadowSubmission = {
757
+ view: viewSubmission.view,
758
+ plan: viewSubmission.plan,
759
+ batches,
760
+ };
761
+ recordGpuDrivenShadowIndirect(pass, submission);
762
+ const viewKey = shadowViewIdentityKey(identity);
763
+ const viewClaims = c.gpuDrivenShadowDrawKeysByView;
764
+ return {
765
+ claimedKeys:
766
+ viewClaims === undefined
767
+ ? (c.gpuDrivenShadowDrawKeys ?? new Set<string>())
768
+ : (viewClaims.get(viewKey) ?? new Set<string>()),
769
+ };
770
+ }
771
+
366
772
  export function encodeDirectionalShadowPass(
367
773
  c: _InternalRenderPipelineContext,
368
774
  pass: RhiRenderPassEncoder,
369
775
  cascadeIndex: number,
370
776
  viewport: { readonly x: number; readonly y: number; readonly w: number; readonly h: number },
777
+ drawFeatures?: (view: BindGroup) => void,
371
778
  ): void {
372
- if (c.directionalShadowCacheReuse || c.meshBindGroup === null) return;
779
+ const directionalIdentity = { kind: 'directional' as const, index: cascadeIndex };
780
+ if (
781
+ (c.gpuDrivenShadowViews?.cacheHit(directionalIdentity) === true ||
782
+ (c.gpuDrivenShadowViews === undefined && c.directionalShadowCacheReuse)) &&
783
+ drawFeatures === undefined
784
+ )
785
+ return;
373
786
  const pipeline = shadowPipeline(c);
374
787
  const viewBg = ensureTypedShadowViewBg(
375
788
  c,
@@ -377,25 +790,94 @@ export function encodeDirectionalShadowPass(
377
790
  directionalShadowCasterOffset(cascadeIndex),
378
791
  `view-shadow-directional-${cascadeIndex}`,
379
792
  );
380
- const materialBg = ensureSpotShadowMaterialBg(c);
381
- if (pipeline === null || viewBg === null || materialBg === null) {
793
+ const materialBg = viewBg === null ? null : ensureSpotShadowMaterialBg(c);
794
+ const capable = c.gpuDrivenShadowViews?.submission(directionalIdentity) !== undefined;
795
+ if (viewBg === null || materialBg === null) {
382
796
  c.frameState.directionalShadowCacheRecorded = false;
797
+ if (capable || hasExpectedShadowCasters(c)) {
798
+ throw new RhiError({
799
+ code: 'rhi-not-available',
800
+ expected: 'directional ShadowCaster bind groups',
801
+ hint: 'publish the view/material resources before recording the shadow lane',
802
+ });
803
+ }
383
804
  return;
384
805
  }
806
+ c.frameState.directionalShadowCacheRecorded = true;
385
807
  pass.setViewport(viewport.x, viewport.y, viewport.w, viewport.h, 0, 1);
386
- pass.setPipeline(pipeline);
808
+ drawFeatures?.(viewBg);
387
809
  pass.setBindGroup(0, viewBg, [0, 0]);
388
810
  pass.setBindGroup(1, materialBg, [0]);
389
- const complete = recordShadowCasterDraws(
811
+ const gpuRecord = recordGpuDrivenShadowIfPublished(c, pass, directionalIdentity);
812
+ const gpuRecorded = gpuRecord !== undefined;
813
+ const claimedKeys = gpuRecord?.claimedKeys;
814
+ if (capable && !gpuRecorded) {
815
+ throw new RhiError({
816
+ code: 'rhi-not-available',
817
+ expected: 'capable directional GPU-driven shadow projection',
818
+ hint: 'terminate the capable shadow frame instead of enumerating CPU casters',
819
+ });
820
+ }
821
+ if (!hasUnclaimedShadowCasters(c, claimedKeys)) {
822
+ if (gpuRecorded || (!capable && !hasExpectedShadowCasters(c))) {
823
+ validateShadowOwnership(c, directionalIdentity, claimedKeys, new Set());
824
+ return;
825
+ }
826
+ }
827
+ if (pipeline === null) {
828
+ if (gpuRecorded && !hasUnclaimedShadowCasters(c, claimedKeys)) {
829
+ validateShadowOwnership(c, directionalIdentity, claimedKeys, new Set());
830
+ return;
831
+ }
832
+ if (gpuRecorded) {
833
+ throw new RhiError({
834
+ code: 'rhi-not-available',
835
+ expected: 'CPU residual directional shadow pipeline',
836
+ hint: 'publish the legacy residual caster pipeline alongside the partial GPU shadow lane',
837
+ });
838
+ }
839
+ throw new RhiError({
840
+ code: 'rhi-not-available',
841
+ expected: 'directional ShadowCaster pipeline',
842
+ hint: 'publish the CPU ShadowCaster pipeline before recording residual casters',
843
+ });
844
+ }
845
+ pass.setPipeline(pipeline);
846
+ const legacyMeshBindGroup = c.meshBindGroup;
847
+ if (legacyMeshBindGroup === null) {
848
+ if (gpuRecorded && !hasUnclaimedShadowCasters(c, claimedKeys)) {
849
+ validateShadowOwnership(c, directionalIdentity, claimedKeys, new Set());
850
+ return;
851
+ }
852
+ if (gpuRecorded) {
853
+ throw new RhiError({
854
+ code: 'rhi-not-available',
855
+ expected: 'CPU residual directional shadow mesh binding',
856
+ hint: 'publish legacy mesh resources for the unclaimed ShadowCaster residual',
857
+ });
858
+ }
859
+ throw new RhiError({
860
+ code: 'rhi-not-available',
861
+ expected: 'directional ShadowCaster mesh binding',
862
+ hint: 'publish the CPU mesh binding before recording residual casters',
863
+ });
864
+ }
865
+ if (gpuRecorded) pass.setBindGroup(1, materialBg, [0]);
866
+ const recordedKeys = recordShadowCasterDraws(
390
867
  c,
391
868
  pass,
392
869
  pipeline,
393
- c.meshBindGroup,
394
- buildMatchedRenderableIndices(c.dispatch, { LightMode: ['ShadowCaster'] }),
870
+ legacyMeshBindGroup,
871
+ buildMatchedRenderableIndices(shadowDispatchEntries(c), { LightMode: ['ShadowCaster'] }),
872
+ buildMatchedMaterialHandlesByRenderable(shadowDispatchEntries(c), {
873
+ LightMode: ['ShadowCaster'],
874
+ }),
395
875
  shadowShaderMap(c),
876
+ claimedKeys,
396
877
  );
878
+ validateShadowOwnership(c, directionalIdentity, claimedKeys, recordedKeys);
397
879
  c.frameState.directionalShadowCacheRecorded =
398
- cascadeIndex === 0 ? complete : c.frameState.directionalShadowCacheRecorded && complete;
880
+ cascadeIndex === 0 ? true : c.frameState.directionalShadowCacheRecorded;
399
881
  }
400
882
 
401
883
  export function encodePointShadowPass(
@@ -403,35 +885,110 @@ export function encodePointShadowPass(
403
885
  pass: RhiRenderPassEncoder,
404
886
  snapshotIndex: number,
405
887
  face: number,
888
+ drawFeatures?: (view: BindGroup) => void,
406
889
  ): void {
407
890
  const snapshot = c.frameState.pointShadowSnapshots[snapshotIndex];
408
- if (snapshot === undefined || c.meshBindGroup === null) return;
409
- const pipeline = shadowPipeline(c);
891
+ if (snapshot === undefined) return;
892
+ const pipeline = shadowPipeline(c, true);
410
893
  const viewBg = ensureTypedShadowViewBg(
411
894
  c,
412
895
  pointShadowViewOffset(snapshot.shadowAtlasLayer, face),
413
896
  0,
414
897
  `view-shadow-point-${snapshot.shadowAtlasLayer}-${face}`,
415
898
  );
416
- const materialBg = ensureSpotShadowMaterialBg(c);
417
- if (pipeline === null || viewBg === null || materialBg === null) return;
418
- pass.setPipeline(pipeline);
899
+ const materialBg = viewBg === null ? null : ensureSpotShadowMaterialBg(c);
900
+ const pointIdentity = { kind: 'point' as const, index: snapshotIndex, face };
901
+ const capable = c.gpuDrivenShadowViews?.submission(pointIdentity) !== undefined;
902
+ if (viewBg === null || materialBg === null) {
903
+ if (capable || hasExpectedShadowCasters(c))
904
+ throw new RhiError({
905
+ code: 'rhi-not-available',
906
+ expected: 'point ShadowCaster bind groups',
907
+ hint: 'publish the view/material resources before recording the shadow lane',
908
+ });
909
+ return;
910
+ }
911
+ if (pipeline === null && !capable && !hasExpectedShadowCasters(c) && drawFeatures === undefined)
912
+ return;
913
+ drawFeatures?.(viewBg);
419
914
  pass.setBindGroup(0, viewBg, [0, 0]);
420
915
  pass.setBindGroup(1, materialBg, [0]);
421
- recordShadowCasterDraws(
916
+ const gpuRecord = recordGpuDrivenShadowIfPublished(c, pass, pointIdentity);
917
+ const gpuRecorded = gpuRecord !== undefined;
918
+ const claimedKeys = gpuRecord?.claimedKeys;
919
+ if (capable && !gpuRecorded) {
920
+ throw new RhiError({
921
+ code: 'rhi-not-available',
922
+ expected: 'capable point GPU-driven shadow projection',
923
+ hint: 'terminate the capable shadow frame instead of enumerating CPU casters',
924
+ });
925
+ }
926
+ if (!hasUnclaimedShadowCasters(c, claimedKeys)) {
927
+ if (gpuRecorded || (!capable && !hasExpectedShadowCasters(c))) {
928
+ validateShadowOwnership(c, pointIdentity, claimedKeys, new Set());
929
+ return;
930
+ }
931
+ }
932
+ if (pipeline === null) {
933
+ if (gpuRecorded && !hasUnclaimedShadowCasters(c, claimedKeys)) {
934
+ validateShadowOwnership(c, pointIdentity, claimedKeys, new Set());
935
+ return;
936
+ }
937
+ if (gpuRecorded) {
938
+ throw new RhiError({
939
+ code: 'rhi-not-available',
940
+ expected: 'CPU residual point shadow pipeline',
941
+ hint: 'publish the legacy residual caster pipeline alongside the partial GPU shadow lane',
942
+ });
943
+ }
944
+ throw new RhiError({
945
+ code: 'rhi-not-available',
946
+ expected: 'point ShadowCaster pipeline',
947
+ hint: 'publish the CPU ShadowCaster pipeline before recording residual casters',
948
+ });
949
+ }
950
+ pass.setPipeline(pipeline);
951
+ const legacyMeshBindGroup = c.meshBindGroup;
952
+ if (legacyMeshBindGroup === null) {
953
+ if (gpuRecorded && !hasUnclaimedShadowCasters(c, claimedKeys)) {
954
+ validateShadowOwnership(c, pointIdentity, claimedKeys, new Set());
955
+ return;
956
+ }
957
+ if (gpuRecorded) {
958
+ throw new RhiError({
959
+ code: 'rhi-not-available',
960
+ expected: 'CPU residual point shadow mesh binding',
961
+ hint: 'publish legacy mesh resources for the unclaimed ShadowCaster residual',
962
+ });
963
+ }
964
+ throw new RhiError({
965
+ code: 'rhi-not-available',
966
+ expected: 'point ShadowCaster mesh binding',
967
+ hint: 'publish the CPU mesh binding before recording residual casters',
968
+ });
969
+ }
970
+ if (gpuRecorded) pass.setBindGroup(1, materialBg, [0]);
971
+ const recordedKeys = recordShadowCasterDraws(
422
972
  c,
423
973
  pass,
424
974
  pipeline,
425
- c.meshBindGroup,
426
- buildMatchedRenderableIndices(c.dispatch, { LightMode: ['ShadowCaster'] }),
975
+ legacyMeshBindGroup,
976
+ buildMatchedRenderableIndices(shadowDispatchEntries(c), { LightMode: ['ShadowCaster'] }),
977
+ buildMatchedMaterialHandlesByRenderable(shadowDispatchEntries(c), {
978
+ LightMode: ['ShadowCaster'],
979
+ }),
427
980
  shadowShaderMap(c),
981
+ claimedKeys,
982
+ true,
428
983
  );
984
+ validateShadowOwnership(c, pointIdentity, claimedKeys, recordedKeys);
429
985
  }
430
986
 
431
987
  export function encodeSpotShadowPass(
432
988
  c: _InternalRenderPipelineContext,
433
989
  pass: RhiRenderPassEncoder,
434
990
  snapshotIndex: number,
991
+ drawFeatures?: (view: BindGroup) => void,
435
992
  ): void {
436
993
  const snapshot = c.frameState.spotShadowSnapshots.filter(
437
994
  (candidate) => candidate.shadowAtlasTile >= 0 && candidate.lightViewProj !== undefined,
@@ -439,8 +996,7 @@ export function encodeSpotShadowPass(
439
996
  if (
440
997
  snapshot === undefined ||
441
998
  snapshot.shadowAtlasTile < 0 ||
442
- snapshot.lightViewProj === undefined ||
443
- c.meshBindGroup === null
999
+ snapshot.lightViewProj === undefined
444
1000
  ) {
445
1001
  return;
446
1002
  }
@@ -451,8 +1007,20 @@ export function encodeSpotShadowPass(
451
1007
  spotShadowCasterOffset(snapshot.shadowAtlasTile),
452
1008
  `view-shadow-spot-${snapshot.shadowAtlasTile}`,
453
1009
  );
454
- const materialBg = ensureSpotShadowMaterialBg(c);
455
- if (pipeline === null || viewBg === null || materialBg === null) return;
1010
+ const materialBg = viewBg === null ? null : ensureSpotShadowMaterialBg(c);
1011
+ const spotIdentity = { kind: 'spot' as const, index: snapshotIndex };
1012
+ const capable = c.gpuDrivenShadowViews?.submission(spotIdentity) !== undefined;
1013
+ if (viewBg === null || materialBg === null) {
1014
+ if (capable || hasExpectedShadowCasters(c))
1015
+ throw new RhiError({
1016
+ code: 'rhi-not-available',
1017
+ expected: 'spot ShadowCaster bind groups',
1018
+ hint: 'publish the view/material resources before recording the shadow lane',
1019
+ });
1020
+ return;
1021
+ }
1022
+ if (pipeline === null && !capable && !hasExpectedShadowCasters(c) && drawFeatures === undefined)
1023
+ return;
456
1024
  const tileSize = c.pipelineState.perPassResources.shadowMapSize;
457
1025
  const tile = snapshot.shadowAtlasTile;
458
1026
  pass.setViewport(
@@ -463,21 +1031,81 @@ export function encodeSpotShadowPass(
463
1031
  0,
464
1032
  1,
465
1033
  );
466
- pass.setPipeline(pipeline);
1034
+ drawFeatures?.(viewBg);
467
1035
  pass.setBindGroup(0, viewBg, [0, 0]);
468
1036
  pass.setBindGroup(1, materialBg, [0]);
469
1037
  // Reuse the shared caster recorder. The compact spot-only loop used to
470
1038
  // depend on instance bindings created by a directional pass, so a scene
471
1039
  // containing only a SpotLight opened the atlas pass but emitted no caster
472
1040
  // draws.
473
- recordShadowCasterDraws(
1041
+ const gpuRecord = recordGpuDrivenShadowIfPublished(c, pass, spotIdentity);
1042
+ const gpuRecorded = gpuRecord !== undefined;
1043
+ const claimedKeys = gpuRecord?.claimedKeys;
1044
+ if (capable && !gpuRecorded) {
1045
+ throw new RhiError({
1046
+ code: 'rhi-not-available',
1047
+ expected: 'capable spot GPU-driven shadow projection',
1048
+ hint: 'terminate the capable shadow frame instead of enumerating CPU casters',
1049
+ });
1050
+ }
1051
+ if (!hasUnclaimedShadowCasters(c, claimedKeys)) {
1052
+ if (gpuRecorded || (!capable && !hasExpectedShadowCasters(c))) {
1053
+ validateShadowOwnership(c, spotIdentity, claimedKeys, new Set());
1054
+ return;
1055
+ }
1056
+ }
1057
+ if (pipeline === null) {
1058
+ if (gpuRecorded && !hasUnclaimedShadowCasters(c, claimedKeys)) {
1059
+ validateShadowOwnership(c, spotIdentity, claimedKeys, new Set());
1060
+ return;
1061
+ }
1062
+ if (gpuRecorded) {
1063
+ throw new RhiError({
1064
+ code: 'rhi-not-available',
1065
+ expected: 'CPU residual spot shadow pipeline',
1066
+ hint: 'publish the legacy residual caster pipeline alongside the partial GPU shadow lane',
1067
+ });
1068
+ }
1069
+ throw new RhiError({
1070
+ code: 'rhi-not-available',
1071
+ expected: 'spot ShadowCaster pipeline',
1072
+ hint: 'publish the CPU ShadowCaster pipeline before recording residual casters',
1073
+ });
1074
+ }
1075
+ pass.setPipeline(pipeline);
1076
+ const legacyMeshBindGroup = c.meshBindGroup;
1077
+ if (legacyMeshBindGroup === null) {
1078
+ if (gpuRecorded && !hasUnclaimedShadowCasters(c, claimedKeys)) {
1079
+ validateShadowOwnership(c, spotIdentity, claimedKeys, new Set());
1080
+ return;
1081
+ }
1082
+ if (gpuRecorded) {
1083
+ throw new RhiError({
1084
+ code: 'rhi-not-available',
1085
+ expected: 'CPU residual spot shadow mesh binding',
1086
+ hint: 'publish legacy mesh resources for the unclaimed ShadowCaster residual',
1087
+ });
1088
+ }
1089
+ throw new RhiError({
1090
+ code: 'rhi-not-available',
1091
+ expected: 'spot ShadowCaster mesh binding',
1092
+ hint: 'publish the CPU mesh binding before recording residual casters',
1093
+ });
1094
+ }
1095
+ if (gpuRecorded) pass.setBindGroup(1, materialBg, [0]);
1096
+ const recordedKeys = recordShadowCasterDraws(
474
1097
  c,
475
1098
  pass,
476
1099
  pipeline,
477
- c.meshBindGroup,
478
- buildMatchedRenderableIndices(c.dispatch, { LightMode: ['ShadowCaster'] }),
1100
+ legacyMeshBindGroup,
1101
+ buildMatchedRenderableIndices(shadowDispatchEntries(c), { LightMode: ['ShadowCaster'] }),
1102
+ buildMatchedMaterialHandlesByRenderable(shadowDispatchEntries(c), {
1103
+ LightMode: ['ShadowCaster'],
1104
+ }),
479
1105
  shadowShaderMap(c),
1106
+ claimedKeys,
480
1107
  );
1108
+ validateShadowOwnership(c, spotIdentity, claimedKeys, recordedKeys);
481
1109
  }
482
1110
 
483
1111
  /**
@@ -541,16 +1169,41 @@ export function buildMatchedRenderableIndices(
541
1169
  * filtering is insufficient for geometry recording: a Deferred graph pass
542
1170
  * must not draw the same renderable's Forward-only material. Keep the
543
1171
  * renderable key in the result so mixed-material meshes retain their
544
- * per-submesh routing.
1172
+ * per-submesh routing. An optional exclusion selector removes a material
1173
+ * handle when that same renderable/material pair also contributes to another
1174
+ * graph lane (for example Deferred Standard geometry after SSR composition).
1175
+ * An inclusion selector intersects membership by the same renderable/material
1176
+ * pair without replacing the selected shader pass or its authored entries.
545
1177
  */
546
1178
  export function buildMatchedMaterialHandlesByRenderable(
547
1179
  dispatch: readonly DispatchEntry[],
548
1180
  selector: PassSelector,
1181
+ excludeSelector?: PassSelector,
1182
+ includeSelector?: PassSelector,
549
1183
  ): ReadonlyMap<number, ReadonlySet<number>> | null {
550
1184
  if (dispatch.length === 0) return null;
551
1185
  const matched = filterDispatchBySelector(dispatch, selector);
1186
+ const included =
1187
+ includeSelector === undefined
1188
+ ? undefined
1189
+ : new Set(
1190
+ filterDispatchBySelector(dispatch, includeSelector).map(
1191
+ (entry) => `${entry.renderableIndex}:${entry.materialHandle}`,
1192
+ ),
1193
+ );
1194
+ const excluded =
1195
+ excludeSelector === undefined
1196
+ ? undefined
1197
+ : new Set(
1198
+ filterDispatchBySelector(dispatch, excludeSelector).map(
1199
+ (entry) => `${entry.renderableIndex}:${entry.materialHandle}`,
1200
+ ),
1201
+ );
552
1202
  const handlesByRenderable = new Map<number, Set<number>>();
553
1203
  for (const entry of matched) {
1204
+ if (included !== undefined && !included.has(`${entry.renderableIndex}:${entry.materialHandle}`))
1205
+ continue;
1206
+ if (excluded?.has(`${entry.renderableIndex}:${entry.materialHandle}`)) continue;
554
1207
  const handles = handlesByRenderable.get(entry.renderableIndex);
555
1208
  if (handles === undefined) {
556
1209
  handlesByRenderable.set(entry.renderableIndex, new Set([entry.materialHandle]));
@@ -569,25 +1222,36 @@ export function buildMatchedMaterialHandlesByRenderable(
569
1222
  * and issues the per-submesh depth draws. `shadowPass` view/material bind
570
1223
  * groups (@group 0/1) are already set by the caller; this loop owns @group
571
1224
  * 2/3 + the vertex/index/pipeline de-dup state. Receives the explicit
572
- * `_InternalRenderPipelineContext` (`c`) plus the caller-resolved shadow
573
- * pipeline, mesh bind group, pass-selector match set, and per-material
1225
+ * `_InternalRenderPipelineContext` (`c`) plus the caller-resolved
1226
+ * mesh bind group, pass-selector match set, and per-material
574
1227
  * ShadowCaster dispatch map so no cross-function mutable state is introduced.
575
1228
  */
576
- function recordShadowCasterDraws(
1229
+ export function recordShadowCasterDraws(
577
1230
  c: _InternalRenderPipelineContext,
578
1231
  shadowPass: RhiRenderPassEncoder,
579
1232
  shadowPipeline: RenderPipeline,
580
1233
  shadowMeshBindGroup: BindGroup,
581
1234
  matchedIndices: Set<number> | null,
1235
+ matchedMaterialHandles: ReadonlyMap<number, ReadonlySet<number>> | null,
582
1236
  shadowDispatchByRenderableIdx: ShadowDispatchMap,
583
- ): boolean {
584
- const { runtime, pipelineState, validatedOrdered } = c;
1237
+ gpuDrivenShadowDrawKeys: ReadonlySet<string> | undefined,
1238
+ reflected = false,
1239
+ ): ReadonlySet<string> {
1240
+ const {
1241
+ runtime,
1242
+ pipelineState,
1243
+ validatedOrdered: mainValidatedOrdered,
1244
+ shadowValidatedOrdered,
1245
+ } = c;
1246
+ const validatedOrdered = shadowValidatedOrdered ?? mainValidatedOrdered;
1247
+ const meshSsboBase = c.shadowMeshSsboBase ?? 0;
585
1248
  // M-3 / w12: vertexBuffer/indexBuffer state locals migrate to GpuBuffer
586
1249
  // (the wrapper) -- the de-dup compare uses wrapper identity (one wrapper
587
1250
  // per RHI handle from gpuStore), and `.handle` is passed to the RHI
588
1251
  // setVertexBuffer / setIndexBuffer call.
589
1252
  let shadowLastVertexBuffer: GpuBuffer | null = null;
590
1253
  let shadowLastIndexBuffer: GpuBuffer | null = null;
1254
+ const recordedKeys = new Set<string>();
591
1255
  // bug-20260619-csm RC-3 (D-3): track the currently-bound shadow PSO so
592
1256
  // per-entity setPipeline only fires on change (same de-dup discipline as
593
1257
  // vertex/index buffers above). The default-shadow-caster PSO is already
@@ -595,7 +1259,6 @@ function recordShadowCasterDraws(
595
1259
  // ShadowCaster PSO when a material supplies one.
596
1260
  let shadowLastPipeline: RenderPipeline = shadowPipeline;
597
1261
  let materialDeps: PerSubmeshMaterialBgDeps | undefined;
598
- let complete = true;
599
1262
 
600
1263
  for (let i = 0; i < validatedOrdered.length; i++) {
601
1264
  const entry = validatedOrdered[i];
@@ -604,17 +1267,30 @@ function recordShadowCasterDraws(
604
1267
  // feat-20260609 M2: skip entities that don't match the pass selector.
605
1268
  if (matchedIndices !== null && !matchedIndices.has(entry.renderableIndex)) continue;
606
1269
 
607
- if (entry.source.skin !== undefined) {
608
- const skinPipeline = skinnedShadowPipeline(c, entry);
609
- if (skinPipeline === null) {
610
- complete = false;
611
- continue;
1270
+ const drawIndices = shadowDrawIndicesBySubmesh(entry.source, entry.mesh);
1271
+ const shadowSubmeshes = entry.mesh.submeshes.flatMap((submesh, submeshIndex) => {
1272
+ if (submesh.topology !== 'triangle-list' && submesh.topology !== 'triangle-strip') {
1273
+ return [];
612
1274
  }
613
- if (skinPipeline !== shadowLastPipeline) {
614
- shadowPass.setPipeline(skinPipeline);
615
- shadowLastPipeline = skinPipeline;
1275
+ if (matchedMaterialHandles !== null) {
1276
+ const handles = matchedMaterialHandles.get(entry.renderableIndex);
1277
+ const material = entry.source.materials[submesh.materialSlot] ?? entry.source.material;
1278
+ if (handles === undefined || !handles.has(material.materialHandle ?? -1)) return [];
616
1279
  }
617
- }
1280
+ return isGpuShadowClaimedSubmesh(
1281
+ entry.source,
1282
+ entry.renderableIndex,
1283
+ submeshIndex,
1284
+ drawIndices,
1285
+ gpuDrivenShadowDrawKeys,
1286
+ shadowDispatchByRenderableIdx,
1287
+ c.shadowCasterMembership,
1288
+ c.shadowCasterWorldKeys,
1289
+ )
1290
+ ? []
1291
+ : [{ submesh, submeshIndex }];
1292
+ });
1293
+ if (shadowSubmeshes.length === 0) continue;
618
1294
  // feat-20260604-mesh-topology-debug-draw M5 / w14 (AC-09, D-A6): the
619
1295
  // shadow caster PSO is triangle-list; it only projects triangle faces.
620
1296
  // line-list / line-strip / point-list meshes have no surface to cast a
@@ -624,14 +1300,6 @@ function recordShadowCasterDraws(
624
1300
  //
625
1301
  // feat-20260608 M4 / w16: per-submesh shadow draw — iterate submeshes
626
1302
  // and skip non-triangle submeshes individually (each submesh may differ).
627
- const shadowSubmeshes = entry.mesh.submeshes;
628
- const hasAnyShadowSubmesh = shadowSubmeshes.some(
629
- (sm) => sm.topology === 'triangle-list' || sm.topology === 'triangle-strip',
630
- );
631
- if (!hasAnyShadowSubmesh) {
632
- continue;
633
- }
634
-
635
1303
  if (entry.mesh.vertexBuffer !== shadowLastVertexBuffer) {
636
1304
  shadowPass.setVertexBuffer(0, entry.mesh.vertexBuffer.handle);
637
1305
  shadowLastVertexBuffer = entry.mesh.vertexBuffer;
@@ -646,39 +1314,250 @@ function recordShadowCasterDraws(
646
1314
 
647
1315
  if (entry.source.skin !== undefined) {
648
1316
  const skinMeshBindGroup = skinnedShadowMeshBindGroup(c, entry);
649
- if (skinMeshBindGroup === null) continue;
1317
+ if (skinMeshBindGroup === null) {
1318
+ throw new RhiError({
1319
+ code: 'rhi-not-available',
1320
+ expected: 'CPU skinned ShadowCaster mesh binding',
1321
+ hint: 'publish the skin mesh/palette bind group before recording the residual',
1322
+ });
1323
+ }
650
1324
  shadowPass.setBindGroup(
651
1325
  2,
652
1326
  skinMeshBindGroup,
653
- pbrSkinMeshDynamicOffsets(i * MESH_PER_ENTITY_STRIDE, entry.source.skin.byteOffset),
1327
+ pbrSkinMeshDynamicOffsets(
1328
+ (meshSsboBase + i) * MESH_PER_ENTITY_STRIDE,
1329
+ entry.source.skin.byteOffset,
1330
+ ),
654
1331
  );
655
1332
  } else {
656
- shadowPass.setBindGroup(2, shadowMeshBindGroup, [i * MESH_PER_ENTITY_STRIDE]);
1333
+ shadowPass.setBindGroup(2, shadowMeshBindGroup, [
1334
+ (meshSsboBase + i) * MESH_PER_ENTITY_STRIDE,
1335
+ ]);
657
1336
  }
658
1337
 
659
- // Main and shadow passes share one instance residency owner and upload receipt.
660
- const shadowInstanceDraws = resolveGeometryInstanceBuffer(
661
- c,
662
- entry,
663
- [
664
- {
665
- instanceBuffer: pipelineState.identityInstanceBuffer,
666
- instanceBindGroup: resolveGeometryInstancesBindGroup(
667
- c,
668
- pipelineState.identityInstanceBuffer,
669
- ),
670
- instanceCount: 1,
671
- probeOffset: -1,
672
- },
673
- ],
674
- false,
675
- );
676
- if (shadowInstanceDraws === null) continue;
1338
+ // C1 + C2 (w12): per-entity instance buffer + instanceCount
1339
+ let shadowInstanceDraws: readonly { readonly buffer: Buffer; readonly count: number }[] = [
1340
+ { buffer: pipelineState.identityInstanceBuffer, count: 1 },
1341
+ ];
1342
+ const shadowInst = entry.source.instances;
1343
+ if (shadowInst?.instanceCount === 0) continue;
1344
+ if (shadowInst !== undefined) {
1345
+ const uniformFallback = runtime.device.caps.storageBuffer === false;
1346
+ // Uniform fallback uses resident chunks because the shader's uniform
1347
+ // array is capped at MAX_UNIFORM_INSTANCES entries.
1348
+ if (uniformFallback && shadowInst.instanceCount > MAX_UNIFORM_INSTANCES) {
1349
+ const ownerKey = instanceCollectionCacheKey(entry.source.worldId, shadowInst);
1350
+ const chunks = c.frameState.instanceBufferChunks;
1351
+ const activeKeys = new Set<string>();
1352
+ const draws: { buffer: Buffer; count: number }[] = [];
1353
+ const strideBytes = 16 * Float32Array.BYTES_PER_ELEMENT;
1354
+ const dirtyRanges = shadowInst.dirtyRanges ?? [{ start: 0, end: shadowInst.instanceCount }];
1355
+ const uploadedRanges: { start: number; end: number }[] = [];
1356
+ let uploadedBytes = 0;
1357
+ let failed = false;
1358
+ for (let start = 0; start < shadowInst.instanceCount; start += MAX_UNIFORM_INSTANCES) {
1359
+ const count = Math.min(MAX_UNIFORM_INSTANCES, shadowInst.instanceCount - start);
1360
+ const chunkKey = `${ownerKey}:${start}`;
1361
+ activeKeys.add(chunkKey);
1362
+ const payloadBytes = count * strideBytes;
1363
+ const previous = chunks?.get(chunkKey);
1364
+ let active =
1365
+ previous !== undefined &&
1366
+ previous.uploadedArchVersion === shadowInst.archVersion &&
1367
+ previous.uploadedByteLength === payloadBytes
1368
+ ? previous
1369
+ : undefined;
1370
+ let activeIsNew = false;
1371
+ if (active === undefined) {
1372
+ const created = runtime.device.createBuffer({
1373
+ size: INSTANCE_UBO_FULL_ARRAY_BYTES,
1374
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
1375
+ mappedAtCreation: false,
1376
+ });
1377
+ if (!created.ok) {
1378
+ runtime.errorRegistry.fire(created.error);
1379
+ failed = true;
1380
+ break;
1381
+ }
1382
+ if (previous !== undefined && !previous.buffer.isDestroyed) {
1383
+ const destroyed = previous.buffer.destroy();
1384
+ if (!destroyed.ok) runtime.errorRegistry.fire(destroyed.error);
1385
+ }
1386
+ active = {
1387
+ buffer: new GpuBuffer(runtime.device, created.value),
1388
+ uploadedArchVersion: shadowInst.archVersion,
1389
+ uploadedByteLength: payloadBytes,
1390
+ };
1391
+ activeIsNew = true;
1392
+ chunks?.set(chunkKey, active);
1393
+ if (chunks === undefined) c.frameState.transientInstanceBuffers.push(active);
1394
+ }
1395
+ const localRanges = dirtyRanges
1396
+ .map((range) => ({
1397
+ start: Math.max(start, range.start) - start,
1398
+ end: Math.min(start + count, range.end) - start,
1399
+ }))
1400
+ .filter((range) => range.end > range.start);
1401
+ const rangesToUpload = instanceUploadRangesForResident(activeIsNew, localRanges, {
1402
+ start: 0,
1403
+ end: count,
1404
+ });
1405
+ const needsUpload =
1406
+ activeIsNew ||
1407
+ (shadowInst.revision !== undefined && active.uploadedRevision !== shadowInst.revision);
1408
+ if (needsUpload && rangesToUpload.length > 0) {
1409
+ for (const range of rangesToUpload) {
1410
+ const source = shadowInst.transforms.subarray(
1411
+ (start + range.start) * 16,
1412
+ (start + range.end) * 16,
1413
+ );
1414
+ const written = runtime.device.queue.writeBuffer(
1415
+ active.buffer.handle,
1416
+ range.start * strideBytes,
1417
+ source,
1418
+ );
1419
+ if (!written.ok) {
1420
+ runtime.errorRegistry.fire(written.error);
1421
+ failed = true;
1422
+ break;
1423
+ }
1424
+ uploadedRanges.push({ start: start + range.start, end: start + range.end });
1425
+ uploadedBytes += source.byteLength;
1426
+ }
1427
+ if (failed) break;
1428
+ }
1429
+ if (needsUpload && shadowInst.revision !== undefined) {
1430
+ active = { ...active, uploadedRevision: shadowInst.revision };
1431
+ chunks?.set(chunkKey, active);
1432
+ }
1433
+ draws.push({ buffer: active.buffer.handle, count });
1434
+ }
1435
+ shadowInstanceDraws = failed
1436
+ ? [{ buffer: pipelineState.identityInstanceBuffer, count: 1 }]
1437
+ : draws;
1438
+ if (chunks !== undefined) {
1439
+ const prefix = `${ownerKey}:`;
1440
+ for (const [key, stale] of chunks.entries()) {
1441
+ if (!key.startsWith(prefix) || activeKeys.has(key)) continue;
1442
+ if (!stale.buffer.isDestroyed) {
1443
+ const destroyed = stale.buffer.destroy();
1444
+ if (!destroyed.ok) runtime.errorRegistry.fire(destroyed.error);
1445
+ }
1446
+ chunks.delete(key);
1447
+ }
1448
+ }
1449
+ if (!failed) {
1450
+ reportInstanceResidency(c, shadowInst, {
1451
+ lane: 'chunked-uniform',
1452
+ requestedBytes: shadowInst.instanceCount * strideBytes,
1453
+ supportedBytes: MAX_UNIFORM_INSTANCES * strideBytes,
1454
+ uploadRanges: uploadedRanges,
1455
+ uploadedBytes,
1456
+ });
1457
+ }
1458
+ } else {
1459
+ const bufUsage = uniformFallback
1460
+ ? GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST
1461
+ : GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST;
1462
+ const instancePayload = uniformFallback
1463
+ ? shadowInst.transforms
1464
+ : packInstanceStorageBuffer(shadowInst.transforms);
1465
+ const requestedBytes = instancePayload.byteLength;
1466
+ const cap = runtime.device.limits.maxStorageBufferBindingSize;
1467
+ if (!uniformFallback && typeof cap === 'number' && cap > 0 && requestedBytes > cap) {
1468
+ const chunked = resolveStorageInstanceChunks(c, entry, shadowInst, cap, undefined, -1);
1469
+ if (chunked !== null) {
1470
+ shadowInstanceDraws = chunked.map((draw) => ({
1471
+ buffer: draw.instanceBuffer,
1472
+ count: draw.instanceCount,
1473
+ }));
1474
+ }
1475
+ } else {
1476
+ const instanceCacheKey = instanceCollectionCacheKey(entry.source.worldId, shadowInst);
1477
+ const cached = c.frameState.instanceBuffers.get(instanceCacheKey);
1478
+ let active: InstanceBufferCacheEntry | null = null;
1479
+ let activeIsNew = false;
1480
+ if (
1481
+ cached !== undefined &&
1482
+ cached.uploadedArchVersion === shadowInst.archVersion &&
1483
+ cached.uploadedByteLength === requestedBytes &&
1484
+ (shadowInst.revision === undefined
1485
+ ? cached.uploadedRevision === undefined
1486
+ : cached.uploadedRevision === shadowInst.revision)
1487
+ ) {
1488
+ active = cached;
1489
+ } else if (requestedBytes > 0) {
1490
+ const bufRes = runtime.device.createBuffer({
1491
+ size: requestedBytes,
1492
+ usage: bufUsage,
1493
+ mappedAtCreation: false,
1494
+ });
1495
+ if (!bufRes.ok) {
1496
+ runtime.errorRegistry.fire(bufRes.error);
1497
+ } else {
1498
+ // feat-20260619 M4 / F12: destroy the old cached buffer
1499
+ // before replacing it with the new one (D-6).
1500
+ if (cached !== undefined && !cached.buffer.isDestroyed) {
1501
+ const r = cached.buffer.destroy();
1502
+ if (!r.ok) runtime.errorRegistry.fire(r.error);
1503
+ }
1504
+ const newBuffer = new GpuBuffer(runtime.device, bufRes.value);
1505
+ active = {
1506
+ buffer: newBuffer,
1507
+ uploadedArchVersion: shadowInst.archVersion,
1508
+ uploadedByteLength: requestedBytes,
1509
+ };
1510
+ activeIsNew = true;
1511
+ if (active !== null) {
1512
+ // Keep the collection identity in the worldEntityKey namespace.
1513
+ c.frameState.instanceBuffers.set(instanceCacheKey, active);
1514
+ }
1515
+ }
1516
+ }
1517
+ if (active !== null) {
1518
+ const needsUpload =
1519
+ activeIsNew ||
1520
+ (shadowInst.revision !== undefined &&
1521
+ active.uploadedRevision !== shadowInst.revision);
1522
+ if (needsUpload) {
1523
+ const writeRes = runtime.device.queue.writeBuffer(
1524
+ active.buffer.handle,
1525
+ 0,
1526
+ instancePayload,
1527
+ );
1528
+ if (!writeRes.ok) {
1529
+ runtime.errorRegistry.fire(writeRes.error);
1530
+ continue;
1531
+ }
1532
+ if (shadowInst.revision !== undefined) {
1533
+ const published = { ...active, uploadedRevision: shadowInst.revision };
1534
+ c.frameState.instanceBuffers.set(instanceCacheKey, published);
1535
+ active = published;
1536
+ }
1537
+ }
1538
+ reportInstanceResidency(c, shadowInst, {
1539
+ lane: uniformFallback ? 'direct-uniform' : 'direct-storage',
1540
+ requestedBytes,
1541
+ supportedBytes: uniformFallback
1542
+ ? MAX_UNIFORM_INSTANCES * Float32Array.BYTES_PER_ELEMENT * 16
1543
+ : typeof cap === 'number'
1544
+ ? cap
1545
+ : undefined,
1546
+ uploadRanges: needsUpload ? [{ start: 0, end: shadowInst.instanceCount }] : [],
1547
+ uploadedBytes: needsUpload ? instancePayload.byteLength : 0,
1548
+ });
1549
+ shadowInstanceDraws = [
1550
+ { buffer: active.buffer.handle, count: Math.max(1, shadowInst.instanceCount) },
1551
+ ];
1552
+ }
1553
+ }
1554
+ }
1555
+ }
677
1556
 
678
1557
  // feat-20260608 M4 / w16: per-submesh shadow draw loop.
679
1558
  // Only draw submeshes whose topology is triangle-list or triangle-strip
680
1559
  // (line-list / point-list submeshes cast no shadow and are skipped).
681
- for (const sm of shadowSubmeshes) {
1560
+ for (const { submesh: sm, submeshIndex } of shadowSubmeshes) {
682
1561
  if (sm.topology !== 'triangle-list' && sm.topology !== 'triangle-strip') {
683
1562
  continue;
684
1563
  }
@@ -689,107 +1568,172 @@ function recordShadowCasterDraws(
689
1568
  // two-sided casters (such as the Three.js teapot) populate the atlas
690
1569
  // from both faces. Custom ShadowCaster shaders retain their own path.
691
1570
  const submeshMaterial = entry.source.materials[sm.materialSlot] ?? entry.source.material;
692
- const shadowDispatch = shadowDispatchByRenderableIdx
693
- .get(entry.renderableIndex)
694
- ?.get(submeshMaterial.materialHandle ?? 0);
695
- const entryShadowShaderId = shadowDispatch?.materialShaderId;
696
- const entryShadowRenderState = shadowDispatch?.renderState;
697
- // Canonical skin casters retain their palette layout. Authored casters
698
- // select their own program below with the same geometry layout.
699
- let entryShadowPipeline: RenderPipeline | null =
700
- entry.source.skin !== undefined ? shadowLastPipeline : shadowPipeline;
701
- if (
702
- entryShadowShaderId !== undefined &&
703
- (entryShadowShaderId !== 'forgeax::default-shadow-caster' ||
704
- (entryShadowRenderState !== undefined && entry.source.skin === undefined))
705
- ) {
706
- // Pending or failed authored programs must not draw using a different
707
- // caster. Retry preparation on the next frame with the same program.
708
- entryShadowPipeline =
709
- runtime.getMaterialShaderPipeline?.(
710
- entryShadowShaderId,
711
- false, // isHdr — shadow depth pass is always LDR
712
- entryShadowRenderState,
713
- 'triangle-list', // topology — shadow PSO targets triangle-list
714
- undefined, // indexFormat
715
- undefined, // variantSet — shadow caster has no variant axes
716
- 'shadow-caster', // passKind
717
- undefined, // meshAttributes
718
- 1,
719
- undefined, // colorFormatOverride
720
- undefined, // shaderUvSetCount
721
- undefined, // depthFormatOverride
722
- undefined, // vertexLayout
723
- entry.mesh.layoutProjection,
724
- undefined, // shaderModuleMode
725
- entry.source.skin === undefined ? 'pbr' : 'pbr-skin',
726
- undefined, // additionalColorFormats
727
- shadowDispatch?.vertexEntry,
728
- shadowDispatch?.fragmentEntry,
729
- ) ?? null;
730
- }
731
- if (entryShadowPipeline === null) {
732
- complete = false;
733
- continue;
734
- }
735
- if (entryShadowPipeline !== shadowLastPipeline && entryShadowPipeline !== null) {
736
- shadowPass.setPipeline(entryShadowPipeline);
737
- shadowLastPipeline = entryShadowPipeline;
738
- }
739
- if (entryShadowShaderId !== undefined && entryShadowShaderId !== SHADOW_CASTER_SHADER_ID) {
740
- materialDeps ??= {
741
- runtime,
742
- pipelineState,
743
- world: c.world,
744
- store: c.store,
745
- materialSlice: STANDARD_PBR_UBO_SIZE,
746
- videoHighPerfAvailable: probeVideoHighPerfUpload(runtime.device),
747
- skylightResources: prepareMaterialSkylight(c).skylightResources,
748
- resolveRenderTargetTextureSource: runtime.resolveRenderTargetTextureSource,
749
- materialBgShared: c.frameState.materialBgShared,
750
- materialBgAssemblyCache: c.materialBgAssemblyCache,
751
- frameState: c.frameState,
752
- bindGroupCounts: c.bindGroupCounts,
753
- };
754
- const materialSlot =
755
- c.materialSlotIndices[i]?.[sm.materialSlot] ?? c.materialSlotIndices[i]?.[0];
756
- if (materialSlot === undefined) {
1571
+ const shadowDispatches =
1572
+ shadowDispatchByRenderableIdx
1573
+ .get(entry.renderableIndex)
1574
+ ?.get(submeshMaterial.materialHandle ?? 0) ?? [];
1575
+ const compactDrawIndex = drawIndices.get(submeshIndex);
1576
+ const draw =
1577
+ compactDrawIndex === undefined
1578
+ ? undefined
1579
+ : entry.source.gpuDrivenDraws?.[compactDrawIndex];
1580
+ const drawItemIndex = draw?.drawItemIndex ?? compactDrawIndex ?? submeshIndex;
1581
+ const worldEntity = worldEntityKey(
1582
+ c.shadowCasterWorldKeys?.[entry.source.worldId] ?? entry.source.worldId,
1583
+ entry.source.entityKey,
1584
+ );
1585
+ const residualDispatches =
1586
+ gpuDrivenShadowDrawKeys === undefined
1587
+ ? shadowDispatches
1588
+ : shadowDispatches.filter(
1589
+ (dispatch) =>
1590
+ !gpuDrivenShadowDrawKeys.has(
1591
+ gpuDrivenShadowDrawKey(
1592
+ worldEntity,
1593
+ submeshMaterial.materialHandle ?? -1,
1594
+ drawItemIndex,
1595
+ dispatch.passIndex,
1596
+ ),
1597
+ ),
1598
+ );
1599
+ const passes = residualDispatches.length === 0 ? [undefined] : residualDispatches;
1600
+ if (gpuDrivenShadowDrawKeys !== undefined && residualDispatches.length === 0) continue;
1601
+ // Deformation selects the mesh/palette layout, never the material program.
1602
+ // Each residual keeps its authored shader, entries, state and bindings.
1603
+ for (const shadowDispatch of passes) {
1604
+ const entryShadowShaderId = shadowDispatch?.materialShaderId;
1605
+ const entryShadowRenderState = shadowRasterState(shadowDispatch?.renderState, reflected);
1606
+ const skinned = entry.source.skin !== undefined;
1607
+ let entryShadowPipeline: RenderPipeline | null = shadowPipeline;
1608
+ const needsDedicatedPipeline =
1609
+ shadowDispatch !== undefined || skinned || sm.topology !== 'triangle-list';
1610
+ if (shadowDispatch !== undefined && entryShadowShaderId === undefined) {
757
1611
  throw new RhiError({
758
- code: 'rhi-descriptor-invalid',
759
- expected: 'a prepared material slot for each shadow submesh',
760
- hint: 'repair material slot preparation before recording shadows',
1612
+ code: 'rhi-not-available',
1613
+ expected: 'material shader identity for CPU ShadowCaster residual',
1614
+ hint: 'publish a producer-owned ShadowCaster shader identity before recording the residual',
761
1615
  });
762
1616
  }
763
- const materialGroup = buildPerSubmeshMaterialBg(
764
- materialDeps,
765
- submeshMaterial,
766
- entry.source.entityKey,
767
- entry.world ?? c.world,
768
- entryShadowShaderId,
769
- );
770
- shadowPass.setBindGroup(1, materialGroup, [materialSlot * MATERIAL_PER_ENTITY_STRIDE]);
771
- } else {
772
- const materialGroup = ensureSpotShadowMaterialBg(c);
773
- if (materialGroup === null) continue;
774
- shadowPass.setBindGroup(1, materialGroup, [0]);
775
- }
776
- for (const instanceDraw of shadowInstanceDraws) {
777
- shadowPass.setBindGroup(3, instanceDraw.instanceBindGroup);
778
- if (entry.mesh.indexed) {
779
- shadowPass.drawIndexed(sm.indexCount, instanceDraw.instanceCount, sm.indexOffset, 0, 0);
1617
+ if (needsDedicatedPipeline) {
1618
+ entryShadowPipeline =
1619
+ runtime.getMaterialShaderPipeline?.(
1620
+ entryShadowShaderId ?? SHADOW_CASTER_SHADER_ID,
1621
+ false,
1622
+ entryShadowRenderState,
1623
+ sm.topology,
1624
+ entry.mesh.indexFormat,
1625
+ shadowCasterVariantSet(
1626
+ runtime.device.caps.storageBuffer,
1627
+ skinned,
1628
+ false,
1629
+ Number(shadowDispatch?.paramSnapshot?.alphaCutoff ?? 0) > 0,
1630
+ ),
1631
+ 'shadow-caster',
1632
+ undefined, // meshAttributes
1633
+ 1, // sampleCount
1634
+ undefined,
1635
+ undefined,
1636
+ undefined,
1637
+ undefined,
1638
+ entry.mesh.layoutProjection,
1639
+ // Built-in variants reuse validated prewarm modules, including
1640
+ // adapters with no synchronous shader factory. Authored residual
1641
+ // programs use their synchronous manifest producer at first draw.
1642
+ entryShadowShaderId === undefined || entryShadowShaderId === SHADOW_CASTER_SHADER_ID
1643
+ ? 'validated'
1644
+ : 'immediate',
1645
+ skinned ? 'pbr-skin' : 'pbr',
1646
+ undefined, // vertexEntryPoint
1647
+ undefined, // additionalColorFormats
1648
+ shadowDispatch?.vertexEntry,
1649
+ shadowDispatch?.fragmentEntry,
1650
+ ) ?? null;
1651
+ }
1652
+ if (entryShadowPipeline === null) {
1653
+ throw new RhiError({
1654
+ code: 'rhi-not-available',
1655
+ expected: `CPU ShadowCaster pipeline ${entryShadowShaderId ?? '<missing>'}`,
1656
+ hint: 'publish the exact residual ShadowCaster pipeline; do not substitute the default shader',
1657
+ });
1658
+ }
1659
+ if (entryShadowPipeline !== shadowLastPipeline && entryShadowPipeline !== null) {
1660
+ shadowPass.setPipeline(entryShadowPipeline);
1661
+ shadowLastPipeline = entryShadowPipeline;
1662
+ }
1663
+ if (shadowDispatch !== undefined && entryShadowShaderId !== undefined) {
1664
+ materialDeps ??= {
1665
+ runtime,
1666
+ pipelineState,
1667
+ world: c.world,
1668
+ store: c.store,
1669
+ materialSlice: STANDARD_PBR_UBO_SIZE,
1670
+ videoHighPerfAvailable: probeVideoHighPerfUpload(runtime.device),
1671
+ skylightResources: prepareMaterialSkylight(c).skylightResources,
1672
+ resolveRenderTargetTextureSource: runtime.resolveRenderTargetTextureSource,
1673
+ materialBgShared: c.frameState.materialBgShared,
1674
+ materialBgAssemblyCache: c.materialBgAssemblyCache,
1675
+ frameState: c.frameState,
1676
+ bindGroupCounts: c.bindGroupCounts,
1677
+ };
1678
+ const materialSlot =
1679
+ shadowDispatch?.materialSlot ??
1680
+ c.materialSlotIndices[i]?.[sm.materialSlot] ??
1681
+ c.materialSlotIndices[i]?.[0];
1682
+ if (materialSlot === undefined) {
1683
+ throw new RhiError({
1684
+ code: 'rhi-descriptor-invalid',
1685
+ expected: 'a prepared material slot for each shadow submesh',
1686
+ hint: 'repair material slot preparation before recording shadows',
1687
+ });
1688
+ }
1689
+ const materialGroup = buildPerSubmeshMaterialBg(
1690
+ materialDeps,
1691
+ submeshMaterial,
1692
+ entry.source.entityKey,
1693
+ entry.world ?? c.world,
1694
+ entryShadowShaderId,
1695
+ );
1696
+ shadowPass.setBindGroup(1, materialGroup, [materialSlot * MATERIAL_PER_ENTITY_STRIDE]);
780
1697
  } else {
781
- shadowPass.draw(sm.vertexCount, instanceDraw.instanceCount, 0, 0);
1698
+ const materialGroup = ensureSpotShadowMaterialBg(c);
1699
+ if (materialGroup === null) {
1700
+ throw new RhiError({
1701
+ code: 'rhi-not-available',
1702
+ expected: 'CPU residual shadow material bind group',
1703
+ hint: 'publish the residual material resources before recording shadows',
1704
+ });
1705
+ }
1706
+ shadowPass.setBindGroup(1, materialGroup, [0]);
1707
+ }
1708
+ for (const instanceDraw of shadowInstanceDraws) {
1709
+ const shadowInstancesBg = resolveGeometryInstancesBindGroup(c, instanceDraw.buffer);
1710
+ shadowPass.setBindGroup(3, shadowInstancesBg);
1711
+ if (entry.mesh.indexed) {
1712
+ shadowPass.drawIndexed(sm.indexCount, instanceDraw.count, sm.indexOffset, 0, 0);
1713
+ } else {
1714
+ shadowPass.draw(sm.vertexCount, instanceDraw.count, 0, 0);
1715
+ }
1716
+ }
1717
+ if (shadowDispatch !== undefined) {
1718
+ recordedKeys.add(
1719
+ gpuDrivenShadowDrawKey(
1720
+ worldEntity,
1721
+ submeshMaterial.materialHandle ?? -1,
1722
+ drawItemIndex,
1723
+ shadowDispatch.passIndex,
1724
+ ),
1725
+ );
782
1726
  }
783
1727
  }
784
1728
  }
785
1729
  }
786
- return complete;
1730
+ return recordedKeys;
787
1731
  }
788
1732
 
789
1733
  /**
790
- * Build (or reuse) the dummy `shadow-material-singleton` @group(1) BG for the
791
- * spot shadow caster pass. The vertex-only shadow_caster shader never consumes
792
- * @group(1) but the PSO's BGL must validate. Reuses the same singleton Map
1734
+ * Build (or reuse) the neutral `shadow-material-singleton` @group(1) BG for
1735
+ * default casters without an authored material dispatch. Authored opaque and
1736
+ * masked residuals bind their own prepared material slots instead. Reuses the same singleton Map
793
1737
  * entry encodeDirectionalShadowPass / encodePointShadowPass write so the three paths share
794
1738
  * one allocation per frame (D-6).
795
1739
  */
@@ -841,6 +1785,22 @@ function ensureSpotShadowMaterialBg(c: _InternalRenderPipelineContext): BindGrou
841
1785
  intensityBuffer: fb.intensityBuffer,
842
1786
  })
843
1787
  : fallbackEntries;
1788
+ // Binding(46) is the optional scene-material storage table. The material
1789
+ // BGL intentionally omits it on the uniform-only WebGL2 lane, so emitting
1790
+ // an entry unconditionally creates a deferred-invalid bind group in wgpu's
1791
+ // downlevel backend and only surfaces at Queue::submit. GPU-driven alpha
1792
+ // batches build their own receipt-backed group and are storage-capable by
1793
+ // admission; this singleton only needs the optional slot when that same
1794
+ // capability is present.
1795
+ if (runtime.device.caps.storageBuffer) {
1796
+ merged.push({
1797
+ binding: 46,
1798
+ resource: {
1799
+ kind: 'buffer',
1800
+ value: { buffer: pipelineState.meshStorageBuffer.buffer },
1801
+ },
1802
+ });
1803
+ }
844
1804
  const r = runtime.device.createBindGroup({
845
1805
  label: 'shadow-material-bg',
846
1806
  layout: pipelineState.materialBindGroupLayout,
@@ -855,3 +1815,65 @@ function ensureSpotShadowMaterialBg(c: _InternalRenderPipelineContext): BindGrou
855
1815
  frameState.singletonMaterialCache.set('shadow-material-singleton', r.value);
856
1816
  return r.value;
857
1817
  }
1818
+
1819
+ /**
1820
+ * Assemble the material group for a GPU-driven alpha-mask caster from the
1821
+ * same material snapshot/residency owner as the main PBR pass. The shadow
1822
+ * shader only consumes the base-color pair and the scene-material table, but
1823
+ * the complete merged layout is still required by the shared pipeline.
1824
+ */
1825
+ function ensureGpuDrivenAlphaMaskMaterialBg(
1826
+ c: _InternalRenderPipelineContext,
1827
+ projection: GpuDrivenShadowBatchProjection,
1828
+ ): BindGroup {
1829
+ const sceneMaterialBuffer = c.gpuDrivenStandardPbrFrameResources?.sceneMaterialBuffer;
1830
+ if (sceneMaterialBuffer === undefined) {
1831
+ throw new RhiError({
1832
+ code: 'rhi-not-available',
1833
+ expected: 'GPU Scene material buffer for alpha-mask shadow recording',
1834
+ hint: 'publish the receipt-derived scene material table before the capable shadow pass',
1835
+ });
1836
+ }
1837
+ const materialKey = `shadow-material-alpha:${projection.materialEntityKey}:${projection.material.materialHandle ?? -1}:${c.store.materialResourceEpoch}`;
1838
+ const cached = c.frameState.singletonMaterialCache.get(materialKey);
1839
+ if (cached !== undefined) return cached;
1840
+ const fallback = c.pipelineState.skylightFallback;
1841
+ if (fallback === null) {
1842
+ throw new RhiError({
1843
+ code: 'webgpu-runtime-error',
1844
+ expected: 'pipelineState.skylightFallback for alpha-mask shadow material layout',
1845
+ hint: 'create the merged Standard PBR material resources before recording shadows',
1846
+ });
1847
+ }
1848
+ const skylightResources = {
1849
+ irradianceView: fallback.irradianceView,
1850
+ irradianceSampler: fallback.sampler,
1851
+ prefilterView: fallback.prefilterView,
1852
+ prefilterSampler: fallback.sampler,
1853
+ brdfLutView: fallback.brdfLutView,
1854
+ brdfLutSampler: fallback.sampler,
1855
+ intensityBuffer: fallback.intensityBuffer,
1856
+ } satisfies import('../ibl/skylight-bind-group').SkylightBindGroupResources;
1857
+ const deps: PerSubmeshMaterialBgDeps = {
1858
+ runtime: c.runtime,
1859
+ pipelineState: c.pipelineState,
1860
+ world: c.world,
1861
+ store: c.store,
1862
+ materialSlice: STANDARD_PBR_UBO_SIZE,
1863
+ videoHighPerfAvailable: false,
1864
+ skylightResources,
1865
+ materialBgShared: c.frameState.materialBgShared,
1866
+ materialBgAssemblyCache: c.materialBgAssemblyCache,
1867
+ sceneMaterialBuffer,
1868
+ bindGroupCounts: c.bindGroupCounts,
1869
+ frameState: c.frameState,
1870
+ };
1871
+ const bindGroup = buildPerSubmeshMaterialBg(
1872
+ deps,
1873
+ projection.material,
1874
+ projection.materialEntityKey,
1875
+ c.world,
1876
+ );
1877
+ c.frameState.singletonMaterialCache.set(materialKey, bindGroup);
1878
+ return bindGroup;
1879
+ }