@forgeax/engine-render 0.1.28 → 0.1.30

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 (768) hide show
  1. package/README.md +856 -126
  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/device-feature-admission.d.ts +4 -3
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -1
  6. package/dist/assembly/dynamic-geometry-host.d.ts +27 -0
  7. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  8. package/dist/assembly/dynamic-geometry-runtime.d.ts +20 -0
  9. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -0
  10. package/dist/assembly/factory.d.ts +3 -1
  11. package/dist/assembly/factory.d.ts.map +1 -1
  12. package/dist/assembly/gpu-driven-owner.d.ts +10 -0
  13. package/dist/assembly/gpu-driven-owner.d.ts.map +1 -0
  14. package/dist/assembly/host-contract.d.ts +14 -0
  15. package/dist/assembly/host-contract.d.ts.map +1 -1
  16. package/dist/assembly/material/assembly.d.ts +31 -1
  17. package/dist/assembly/material/assembly.d.ts.map +1 -1
  18. package/dist/assembly/material/pipeline-helpers.d.ts +24 -0
  19. package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -0
  20. package/dist/assembly/material-shader-policy.d.ts +3 -1
  21. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  22. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
  23. package/dist/assembly/recovery/failure-location.d.ts +12 -0
  24. package/dist/assembly/recovery/failure-location.d.ts.map +1 -0
  25. package/dist/assembly/recovery/recovery-attempt.d.ts +1 -6
  26. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -1
  27. package/dist/assembly/recovery/renderer-recover.d.ts +7 -4
  28. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  29. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  30. package/dist/assembly/renderer-feature-inspection.d.ts +5 -0
  31. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -0
  32. package/dist/assembly/renderer-frame-transaction.d.ts +19 -0
  33. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  34. package/dist/assembly/renderer-helpers.d.ts +3 -0
  35. package/dist/assembly/renderer-helpers.d.ts.map +1 -1
  36. package/dist/assembly/renderer-inspection.d.ts +4 -0
  37. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  38. package/dist/assembly/shader-prewarm-policy.d.ts +7 -1
  39. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -1
  40. package/dist/assembly/skin-palette-owner.d.ts +8 -0
  41. package/dist/assembly/skin-palette-owner.d.ts.map +1 -0
  42. package/dist/assembly/ssr-shader-prewarm.d.ts +6 -0
  43. package/dist/assembly/ssr-shader-prewarm.d.ts.map +1 -0
  44. package/dist/assembly/webgpu-device-loss.d.ts +15 -0
  45. package/dist/assembly/webgpu-device-loss.d.ts.map +1 -0
  46. package/dist/assembly/webgpu-pbr-ready.d.ts +38 -0
  47. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -0
  48. package/dist/assembly/webgpu-ready-mipmap.d.ts +11 -0
  49. package/dist/assembly/webgpu-ready-mipmap.d.ts.map +1 -0
  50. package/dist/assembly/webgpu-ready.d.ts +7 -3
  51. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  52. package/dist/assembly/webgpu-renderer-bootstrap.d.ts +10 -0
  53. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -0
  54. package/dist/assembly/webgpu-renderer-contract.d.ts +62 -0
  55. package/dist/assembly/webgpu-renderer-contract.d.ts.map +1 -0
  56. package/dist/assembly/webgpu-renderer-guards.d.ts +9 -0
  57. package/dist/assembly/webgpu-renderer-guards.d.ts.map +1 -0
  58. package/dist/assembly/webgpu-renderer-observation.d.ts +6 -0
  59. package/dist/assembly/webgpu-renderer-observation.d.ts.map +1 -0
  60. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts +12 -0
  61. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts.map +1 -0
  62. package/dist/assembly/webgpu-renderer.d.ts +7 -141
  63. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  64. package/dist/assembly/webgpu-vertex-layouts.d.ts +28 -84
  65. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -1
  66. package/dist/authoring.mjs +2 -2
  67. package/dist/capture/scene-projection.d.ts +41 -0
  68. package/dist/capture/scene-projection.d.ts.map +1 -0
  69. package/dist/{chunk-J52NMQKR.mjs → chunk-7MQ3XBSP.mjs} +1098 -844
  70. package/dist/chunk-7MQ3XBSP.mjs.map +1 -0
  71. package/dist/{chunk-GF523LHF.mjs → chunk-BLXGONR2.mjs} +30 -12
  72. package/dist/chunk-BLXGONR2.mjs.map +1 -0
  73. package/dist/{chunk-VSL23LZE.mjs → chunk-C3DDSXYK.mjs} +1188 -463
  74. package/dist/chunk-C3DDSXYK.mjs.map +1 -0
  75. package/dist/{chunk-IQMKJLNU.mjs → chunk-FUUO4UND.mjs} +443 -30
  76. package/dist/chunk-FUUO4UND.mjs.map +1 -0
  77. package/dist/{chunk-J6KACNOH.mjs → chunk-JRBDO2KB.mjs} +617 -7
  78. package/dist/chunk-JRBDO2KB.mjs.map +1 -0
  79. package/dist/{chunk-GE3SDD72.mjs → chunk-MDQJPY3T.mjs} +20 -40
  80. package/dist/chunk-MDQJPY3T.mjs.map +1 -0
  81. package/dist/{chunk-MYJ5RPJO.mjs → chunk-NO4WZCAH.mjs} +3 -3
  82. package/dist/chunk-NO4WZCAH.mjs.map +1 -0
  83. package/dist/{chunk-RBDBZ5R7.mjs → chunk-PA5DRJNI.mjs} +2 -2
  84. package/dist/chunk-PA5DRJNI.mjs.map +1 -0
  85. package/dist/{chunk-MBU4ZVE7.mjs → chunk-PSEQ6QPH.mjs} +4655 -892
  86. package/dist/chunk-PSEQ6QPH.mjs.map +1 -0
  87. package/dist/{chunk-JAASCMXA.mjs → chunk-S5UE2T5P.mjs} +289 -41
  88. package/dist/chunk-S5UE2T5P.mjs.map +1 -0
  89. package/dist/components/camera.d.ts +62 -2
  90. package/dist/components/camera.d.ts.map +1 -1
  91. package/dist/components/directional-light.d.ts +2 -2
  92. package/dist/components/dynamic-resolution.d.ts +15 -0
  93. package/dist/components/dynamic-resolution.d.ts.map +1 -0
  94. package/dist/components/index.d.ts +2 -0
  95. package/dist/components/index.d.ts.map +1 -1
  96. package/dist/components/instances.d.ts +6 -20
  97. package/dist/components/instances.d.ts.map +1 -1
  98. package/dist/components/light-helpers.d.ts.map +1 -1
  99. package/dist/components/reflection-probe.d.ts +1 -0
  100. package/dist/components/reflection-probe.d.ts.map +1 -1
  101. package/dist/components/scene-instance.d.ts +2 -1
  102. package/dist/components/scene-instance.d.ts.map +1 -1
  103. package/dist/components/screen-space-reflection.d.ts +14 -0
  104. package/dist/components/screen-space-reflection.d.ts.map +1 -0
  105. package/dist/components/skylight.d.ts +4 -1
  106. package/dist/components/skylight.d.ts.map +1 -1
  107. package/dist/construct-renderer.mjs +14809 -7570
  108. package/dist/construct-renderer.mjs.map +1 -1
  109. package/dist/device/device-scope.d.ts +2 -0
  110. package/dist/device/device-scope.d.ts.map +1 -1
  111. package/dist/device/gpu-residency.d.ts +3 -14
  112. package/dist/device/gpu-residency.d.ts.map +1 -1
  113. package/dist/dynamic-geometry.d.ts.map +1 -1
  114. package/dist/environment/background.d.ts +7 -0
  115. package/dist/environment/background.d.ts.map +1 -0
  116. package/dist/environment/ibl.d.ts +11 -0
  117. package/dist/environment/ibl.d.ts.map +1 -0
  118. package/dist/environment/storage.d.ts +14 -0
  119. package/dist/environment/storage.d.ts.map +1 -0
  120. package/dist/errors/gpu-driven.d.ts +6 -3
  121. package/dist/errors/gpu-driven.d.ts.map +1 -1
  122. package/dist/errors/render.d.ts +41 -4
  123. package/dist/errors/render.d.ts.map +1 -1
  124. package/dist/extract/camera.d.ts.map +1 -1
  125. package/dist/extract/contracts.d.ts +7 -0
  126. package/dist/extract/contracts.d.ts.map +1 -1
  127. package/dist/extract/gpu-driven.d.ts +50 -3
  128. package/dist/extract/gpu-driven.d.ts.map +1 -1
  129. package/dist/features/host.d.ts +23 -1
  130. package/dist/features/host.d.ts.map +1 -1
  131. package/dist/features/noise-texture.d.ts +17 -0
  132. package/dist/features/noise-texture.d.ts.map +1 -0
  133. package/dist/features/plan.d.ts +83 -2
  134. package/dist/features/plan.d.ts.map +1 -1
  135. package/dist/features/prepared-gpu-work.d.ts +37 -4
  136. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  137. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  138. package/dist/features/prepared-graphics.d.ts +5 -0
  139. package/dist/features/prepared-graphics.d.ts.map +1 -1
  140. package/dist/features/render-graph-compute.d.ts +5 -3
  141. package/dist/features/render-graph-compute.d.ts.map +1 -1
  142. package/dist/features/render-graph-contribution.d.ts +11 -3
  143. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  144. package/dist/features/render-graph-raster.d.ts +19 -3
  145. package/dist/features/render-graph-raster.d.ts.map +1 -1
  146. package/dist/features/types.d.ts +11 -0
  147. package/dist/features/types.d.ts.map +1 -1
  148. package/dist/fullscreen-post-process-pass.d.ts +2 -0
  149. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  150. package/dist/gpu-driven/batch-topology.d.ts +34 -1
  151. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  152. package/dist/gpu-driven/material-artifacts.d.ts +24 -0
  153. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -0
  154. package/dist/gpu-driven/material-bindings.d.ts +7 -0
  155. package/dist/gpu-driven/material-bindings.d.ts.map +1 -0
  156. package/dist/gpu-driven/pbr-program.d.ts +10 -0
  157. package/dist/gpu-driven/pbr-program.d.ts.map +1 -0
  158. package/dist/gpu-driven/prepared-draw.d.ts +2 -0
  159. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  160. package/dist/gpu-driven/production-raster.d.ts +169 -27
  161. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  162. package/dist/gpu-driven/shadow-views.d.ts +126 -0
  163. package/dist/gpu-driven/shadow-views.d.ts.map +1 -0
  164. package/dist/gpu-driven/view-gpu.d.ts +18 -2
  165. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  166. package/dist/gpu-resource.d.ts +5 -2
  167. package/dist/gpu-resource.d.ts.map +1 -1
  168. package/dist/gpu-scene-schema.d.ts +2 -8
  169. package/dist/gpu-scene-schema.d.ts.map +1 -1
  170. package/dist/gpu-scene.d.ts +14 -0
  171. package/dist/gpu-scene.d.ts.map +1 -1
  172. package/dist/gpu-texture-usage.d.ts +1 -0
  173. package/dist/gpu-texture-usage.d.ts.map +1 -1
  174. package/dist/hdrp-buffers.d.ts +9 -0
  175. package/dist/hdrp-buffers.d.ts.map +1 -1
  176. package/dist/ibl/IblPipelineCache.d.ts +2 -0
  177. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  178. package/dist/ibl/frame-resources.d.ts +8 -0
  179. package/dist/ibl/frame-resources.d.ts.map +1 -0
  180. package/dist/ibl/skylight-bind-group.d.ts +3 -1
  181. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  182. package/dist/index.d.ts +87 -75
  183. package/dist/index.d.ts.map +1 -1
  184. package/dist/index.mjs +10 -8
  185. package/dist/index.mjs.map +1 -1
  186. package/dist/inspection-types.d.ts +180 -1
  187. package/dist/inspection-types.d.ts.map +1 -1
  188. package/dist/instances-derived-bounds.d.ts +11 -0
  189. package/dist/instances-derived-bounds.d.ts.map +1 -1
  190. package/dist/instances.d.ts +89 -44
  191. package/dist/instances.d.ts.map +1 -1
  192. package/dist/internal.d.ts +3 -0
  193. package/dist/internal.d.ts.map +1 -1
  194. package/dist/internal.mjs +117 -9
  195. package/dist/internal.mjs.map +1 -1
  196. package/dist/lifecycle.d.ts +2 -1
  197. package/dist/lifecycle.d.ts.map +1 -1
  198. package/dist/material-row.d.ts +15 -0
  199. package/dist/material-row.d.ts.map +1 -0
  200. package/dist/materials.d.ts +15 -0
  201. package/dist/materials.d.ts.map +1 -1
  202. package/dist/pbr-pipeline.d.ts +23 -5
  203. package/dist/pbr-pipeline.d.ts.map +1 -1
  204. package/dist/pipeline/render-extent.d.ts +51 -0
  205. package/dist/pipeline/render-extent.d.ts.map +1 -0
  206. package/dist/pipeline/standard-build-context.d.ts +7 -0
  207. package/dist/pipeline/standard-build-context.d.ts.map +1 -0
  208. package/dist/pipeline/standard-forward-lane.d.ts +3 -2
  209. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  210. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts +12 -0
  211. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts.map +1 -0
  212. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts +70 -0
  213. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts.map +1 -0
  214. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts +75 -0
  215. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts.map +1 -0
  216. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts +111 -0
  217. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts.map +1 -0
  218. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts +16 -0
  219. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts.map +1 -0
  220. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts +14 -0
  221. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts.map +1 -0
  222. package/dist/pipeline/standard-output/auto-exposure/state.d.ts +46 -0
  223. package/dist/pipeline/standard-output/auto-exposure/state.d.ts.map +1 -0
  224. package/dist/pipeline/standard-output/color-transform.d.ts +27 -0
  225. package/dist/pipeline/standard-output/color-transform.d.ts.map +1 -0
  226. package/dist/pipeline/standard-output/graph.d.ts +15 -0
  227. package/dist/pipeline/standard-output/graph.d.ts.map +1 -0
  228. package/dist/pipeline/standard-output/lut-admission.d.ts +29 -0
  229. package/dist/pipeline/standard-output/lut-admission.d.ts.map +1 -0
  230. package/dist/pipeline/standard-output/lut-gpu.d.ts +48 -0
  231. package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -0
  232. package/dist/pipeline/standard-output/lut-state.d.ts +68 -0
  233. package/dist/pipeline/standard-output/lut-state.d.ts.map +1 -0
  234. package/dist/pipeline/standard-output/resources.d.ts +10 -0
  235. package/dist/pipeline/standard-output/resources.d.ts.map +1 -0
  236. package/dist/pipeline/standard-output/types.d.ts +39 -0
  237. package/dist/pipeline/standard-output/types.d.ts.map +1 -0
  238. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  239. package/dist/pipeline/standard-post.d.ts +2 -2
  240. package/dist/pipeline/standard-post.d.ts.map +1 -1
  241. package/dist/pipeline/standard-profile.d.ts +2 -1
  242. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  243. package/dist/pipeline-builder.d.ts +5 -2
  244. package/dist/pipeline-builder.d.ts.map +1 -1
  245. package/dist/pipeline-spec-types.d.ts +4 -0
  246. package/dist/pipeline-spec-types.d.ts.map +1 -1
  247. package/dist/pipeline-spec.d.ts +1 -9
  248. package/dist/pipeline-spec.d.ts.map +1 -1
  249. package/dist/plugin.d.ts.map +1 -1
  250. package/dist/points-lines/standard-owner.d.ts +1 -1
  251. package/dist/points-lines/standard-owner.d.ts.map +1 -1
  252. package/dist/post-process-errors.d.ts +9 -2
  253. package/dist/post-process-errors.d.ts.map +1 -1
  254. package/dist/prepare/extended-lighting/resources.d.ts +3 -3
  255. package/dist/prepare/prepared-graphics-resolver.d.ts +6 -1
  256. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  257. package/dist/record/dynamic-geometry-consumption.d.ts +15 -0
  258. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -0
  259. package/dist/record/frame-lighting.d.ts +3 -3
  260. package/dist/record/frame-lighting.d.ts.map +1 -1
  261. package/dist/record/frame-snapshot.d.ts +109 -13
  262. package/dist/record/frame-snapshot.d.ts.map +1 -1
  263. package/dist/record/frame-targets.d.ts.map +1 -1
  264. package/dist/record/frame.d.ts +41 -5
  265. package/dist/record/frame.d.ts.map +1 -1
  266. package/dist/record/main-pass-geometry.d.ts +13 -9
  267. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  268. package/dist/record/main-pass-material.d.ts +43 -3
  269. package/dist/record/main-pass-material.d.ts.map +1 -1
  270. package/dist/record/main-pass-sprite-draws.d.ts +2 -10
  271. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  272. package/dist/record/main-pass.d.ts +14 -2
  273. package/dist/record/main-pass.d.ts.map +1 -1
  274. package/dist/record/mesh-ssbo.d.ts +15 -6
  275. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  276. package/dist/record/prepared-material-bindings.d.ts +19 -0
  277. package/dist/record/prepared-material-bindings.d.ts.map +1 -0
  278. package/dist/record/recovery-pipeline.d.ts +1 -0
  279. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  280. package/dist/record/render-context.d.ts +101 -22
  281. package/dist/record/render-context.d.ts.map +1 -1
  282. package/dist/record/shadow-pass.d.ts +62 -7
  283. package/dist/record/shadow-pass.d.ts.map +1 -1
  284. package/dist/record/skybox-post-pass.d.ts +1 -1
  285. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  286. package/dist/record/typed-frame-graph.d.ts +39 -8
  287. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  288. package/dist/record/view-ubo.d.ts +1 -0
  289. package/dist/record/view-ubo.d.ts.map +1 -1
  290. package/dist/recovery/render-system-candidate.d.ts +10 -2
  291. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  292. package/dist/recovery/render-system-roots.d.ts +11 -0
  293. package/dist/recovery/render-system-roots.d.ts.map +1 -0
  294. package/dist/recovery/types.d.ts +76 -0
  295. package/dist/recovery/types.d.ts.map +1 -0
  296. package/dist/reflection/background.d.ts +14 -0
  297. package/dist/reflection/background.d.ts.map +1 -0
  298. package/dist/reflection/filter.d.ts.map +1 -1
  299. package/dist/reflection/projection.d.ts +1 -0
  300. package/dist/reflection/projection.d.ts.map +1 -1
  301. package/dist/reflection/record-owner.d.ts +4 -4
  302. package/dist/reflection/record-owner.d.ts.map +1 -1
  303. package/dist/render-contract.d.ts +211 -53
  304. package/dist/render-contract.d.ts.map +1 -1
  305. package/dist/render-graph-primitives.d.ts +1 -0
  306. package/dist/render-graph-primitives.d.ts.map +1 -1
  307. package/dist/render-pipeline.d.ts +81 -6
  308. package/dist/render-pipeline.d.ts.map +1 -1
  309. package/dist/render-system-extract-tail.d.ts +15 -6
  310. package/dist/render-system-extract-tail.d.ts.map +1 -1
  311. package/dist/render-system-extract.d.ts +128 -41
  312. package/dist/render-system-extract.d.ts.map +1 -1
  313. package/dist/render-system-projections.d.ts +22 -0
  314. package/dist/render-system-projections.d.ts.map +1 -0
  315. package/dist/render-system.d.ts +27 -77
  316. package/dist/render-system.d.ts.map +1 -1
  317. package/dist/scene/render-scene-types.d.ts +14 -7
  318. package/dist/scene/render-scene-types.d.ts.map +1 -1
  319. package/dist/scene/render-scene.d.ts +31 -11
  320. package/dist/scene/render-scene.d.ts.map +1 -1
  321. package/dist/scene/shadow-visibility.d.ts +5 -0
  322. package/dist/scene/shadow-visibility.d.ts.map +1 -0
  323. package/dist/ssao-config.d.ts +8 -6
  324. package/dist/ssao-config.d.ts.map +1 -1
  325. package/dist/ssr/admission.d.ts +70 -1
  326. package/dist/ssr/admission.d.ts.map +1 -1
  327. package/dist/ssr/compose.d.ts +13 -0
  328. package/dist/ssr/compose.d.ts.map +1 -0
  329. package/dist/ssr/errors.d.ts +35 -0
  330. package/dist/ssr/errors.d.ts.map +1 -1
  331. package/dist/ssr/graph.d.ts +60 -0
  332. package/dist/ssr/graph.d.ts.map +1 -0
  333. package/dist/ssr/history.d.ts +97 -0
  334. package/dist/ssr/history.d.ts.map +1 -0
  335. package/dist/ssr/hiz.d.ts +37 -0
  336. package/dist/ssr/hiz.d.ts.map +1 -0
  337. package/dist/ssr/inspection.d.ts +42 -0
  338. package/dist/ssr/inspection.d.ts.map +1 -0
  339. package/dist/ssr/resources.d.ts +58 -0
  340. package/dist/ssr/resources.d.ts.map +1 -0
  341. package/dist/ssr/temporal.d.ts +38 -0
  342. package/dist/ssr/temporal.d.ts.map +1 -0
  343. package/dist/systems/skin-palette-allocator.d.ts +32 -10
  344. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  345. package/dist/systems/skin-palette-types.d.ts +15 -0
  346. package/dist/systems/skin-palette-types.d.ts.map +1 -1
  347. package/dist/systems/transparent-dispatch.d.ts +9 -0
  348. package/dist/systems/transparent-dispatch.d.ts.map +1 -0
  349. package/dist/targets/material-source.d.ts +7 -0
  350. package/dist/targets/material-source.d.ts.map +1 -1
  351. package/dist/temporal/frame-coordinator.d.ts +7 -0
  352. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  353. package/dist/temporal/gpu.d.ts +8 -0
  354. package/dist/temporal/gpu.d.ts.map +1 -1
  355. package/dist/temporal/history.d.ts.map +1 -1
  356. package/dist/temporal/index.mjs +6 -6
  357. package/dist/temporal/inspection.d.ts.map +1 -1
  358. package/dist/temporal/standard-scene-data.d.ts +4 -2
  359. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  360. package/dist/temporal/target-coverage-attachment.d.ts +15 -0
  361. package/dist/temporal/target-coverage-attachment.d.ts.map +1 -0
  362. package/dist/temporal/temporal-view.d.ts +6 -0
  363. package/dist/temporal/temporal-view.d.ts.map +1 -1
  364. package/dist/temporal/view.d.ts +6 -0
  365. package/dist/temporal/view.d.ts.map +1 -1
  366. package/dist/typed-render-graph-primitives.d.ts +41 -1
  367. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  368. package/dist/typed-shadow-passes.d.ts +3 -1
  369. package/dist/typed-shadow-passes.d.ts.map +1 -1
  370. package/dist/volume/component.d.ts +1 -0
  371. package/dist/volume/component.d.ts.map +1 -1
  372. package/dist/volume/temporal.d.ts +6 -0
  373. package/dist/volume/temporal.d.ts.map +1 -1
  374. package/package.json +21 -20
  375. package/src/__tests__/auto-exposure-gpu-evidence.ts +291 -0
  376. package/src/__tests__/auto-exposure-inspection.unit.test.ts +80 -0
  377. package/src/__tests__/auto-exposure-transaction.integration.test.ts +99 -0
  378. package/src/__tests__/auto-exposure.browser.test.ts +33 -0
  379. package/src/__tests__/auto-exposure.dawn.test.ts +33 -0
  380. package/src/__tests__/capture-scene-projection.unit.test.ts +24 -0
  381. package/src/__tests__/detached-inspection.integration.test.ts +5 -0
  382. package/src/__tests__/device-feature-admission.unit.test.ts +15 -24
  383. package/src/__tests__/device-loss-fanout.unit.test.ts +43 -0
  384. package/src/__tests__/device-scope-lifecycle.unit.test.ts +18 -0
  385. package/src/__tests__/device-scope-stale-matrix.unit.test.ts +18 -0
  386. package/src/__tests__/docs-gate-green.unit.test.ts +3 -3
  387. package/src/__tests__/dynamic-geometry-batch.unit.test.ts +157 -0
  388. package/src/__tests__/dynamic-geometry.unit.test.ts +36 -0
  389. package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +81 -0
  390. package/src/__tests__/factory-contract.integration.test.ts +231 -9
  391. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +19 -0
  392. package/src/__tests__/feature-depth-input.dawn.test.ts +121 -0
  393. package/src/__tests__/feature-noise.unit.test.ts +38 -0
  394. package/src/__tests__/fixtures/taa-resolve-pinned-baseline.wgsl +301 -0
  395. package/src/__tests__/frame-camera-selection.unit.test.ts +131 -0
  396. package/src/__tests__/frame-observation.contract.test.ts +21 -0
  397. package/src/__tests__/frame-plan-contract.unit.test.ts +24 -0
  398. package/src/__tests__/frame-targets.unit.test.ts +24 -0
  399. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +3 -0
  400. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +79 -0
  401. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +307 -7
  402. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +122 -142
  403. package/src/__tests__/gpu-driven-indirect-raster.browser.test.ts +10 -1
  404. package/src/__tests__/gpu-driven-lod-cache.unit.test.ts +261 -0
  405. package/src/__tests__/gpu-driven-mesh-world-identity.unit.test.ts +346 -0
  406. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +1422 -0
  407. package/src/__tests__/gpu-driven-pbr.integration.test.ts +21 -0
  408. package/src/__tests__/gpu-driven-production-fixture.ts +96 -0
  409. package/src/__tests__/gpu-driven-production.integration.test.ts +1076 -106
  410. package/src/__tests__/gpu-driven-scaling.unit.test.ts +1 -3
  411. package/src/__tests__/gpu-driven-shadow-views.browser.test.ts +63 -0
  412. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +294 -0
  413. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +97 -0
  414. package/src/__tests__/gpu-driven-shadow-views.unit.test.ts +168 -0
  415. package/src/__tests__/gpu-driven-skin-contract.test-d.ts +11 -0
  416. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +72 -20
  417. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +30 -9
  418. package/src/__tests__/gpu-driven-view.unit.test.ts +160 -52
  419. package/src/__tests__/gpu-pass-timing-browser-runner.ts +15 -0
  420. package/src/__tests__/gpu-pass-timing.browser.test.ts +20 -2
  421. package/src/__tests__/gpu-resource-scope.unit.test.ts +71 -0
  422. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +42 -0
  423. package/src/__tests__/gpu-scene-render-graph-gpu.ts +19 -5
  424. package/src/__tests__/gpu-scene.dawn.test.ts +40 -13
  425. package/src/__tests__/gpu-scene.unit.test.ts +613 -33
  426. package/src/__tests__/instances-culling.integration.test.ts +36 -10
  427. package/src/__tests__/instances-derived-bounds.unit.test.ts +54 -0
  428. package/src/__tests__/instances-store.unit.test.ts +242 -39
  429. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +6 -0
  430. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +13 -16
  431. package/src/__tests__/lut-lkg-transaction.integration.test.ts +122 -0
  432. package/src/__tests__/material-contract-inventory.unit.test.ts +5 -6
  433. package/src/__tests__/material-dispatch-shader-id.unit.test.ts +51 -0
  434. package/src/__tests__/material-lane-selection.unit.test.ts +60 -0
  435. package/src/__tests__/material-numeric-upload.unit.test.ts +53 -0
  436. package/src/__tests__/material-texture-handles.unit.test.ts +18 -0
  437. package/src/__tests__/material-texture-source.unit.test.ts +87 -0
  438. package/src/__tests__/materials-authoring-validation.unit.test.ts +41 -0
  439. package/src/__tests__/materials-standard-contract.unit.test.ts +57 -3
  440. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +4 -0
  441. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +8 -0
  442. package/src/__tests__/morph-neutral-admission.unit.test.ts +19 -0
  443. package/src/__tests__/normal-fallback.dawn.test.ts +122 -0
  444. package/src/__tests__/particle-material-input-layout.unit.test.ts +104 -0
  445. package/src/__tests__/physical-clearcoat.integration.test.ts +42 -3
  446. package/src/__tests__/pipeline-texture-specialization.unit.test.ts +138 -0
  447. package/src/__tests__/point-shadow-inspection.unit.test.ts +2 -0
  448. package/src/__tests__/post-process-error-policy-owner.unit.test.ts +6 -5
  449. package/src/__tests__/prepared-gpu-driven-pbr-contract.test-d.ts +31 -0
  450. package/src/__tests__/prepared-gpu-driven-pbr-contract.unit.test.ts +39 -0
  451. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +251 -9
  452. package/src/__tests__/prepared-graphics-capability.unit.test.ts +21 -0
  453. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +6 -2
  454. package/src/__tests__/profiler-phase-catalog.test.ts +5 -0
  455. package/src/__tests__/public-surface.test-d.ts +3 -0
  456. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +30 -12
  457. package/src/__tests__/reflection-fallback-async-publication.unit.test.ts +171 -0
  458. package/src/__tests__/reflection-probe-authoring.unit.test.ts +12 -10
  459. package/src/__tests__/reflection-probe-box.dawn.test.ts +71 -0
  460. package/src/__tests__/reflection-probe-filter.unit.test.ts +9 -0
  461. package/src/__tests__/reflection-probe-orientation.dawn.test.ts +294 -0
  462. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +5 -14
  463. package/src/__tests__/reflection-probe-updates.unit.test.ts +129 -0
  464. package/src/__tests__/render-error-code-owner.test-d.ts +7 -0
  465. package/src/__tests__/render-error-detail-ssot.test-d.ts +27 -1
  466. package/src/__tests__/render-error-exhaustive.test-d.ts +15 -0
  467. package/src/__tests__/render-extent.unit.test.ts +63 -0
  468. package/src/__tests__/render-feature-gpu-work.integration.test.ts +285 -2
  469. package/src/__tests__/render-feature-plan.unit.test.ts +245 -0
  470. package/src/__tests__/render-feature-shadow-order.unit.test.ts +228 -0
  471. package/src/__tests__/render-feature-zero-work.unit.test.ts +35 -1
  472. package/src/__tests__/render-graph-fallback.integration.test.ts +2 -2
  473. package/src/__tests__/render-owner-cohesion.unit.test.ts +55 -0
  474. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +426 -0
  475. package/src/__tests__/render-scene-projection.unit.test.ts +915 -23
  476. package/src/__tests__/render-scene-resource-updates.integration.test.ts +214 -0
  477. package/src/__tests__/render-scene-skin-lifecycle.integration.test.ts +173 -0
  478. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +133 -0
  479. package/src/__tests__/render-target-material-source.test-d.ts +27 -3
  480. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +233 -9
  481. package/src/__tests__/renderer-frame-transaction.integration.test.ts +923 -5
  482. package/src/__tests__/shader-manifest-fixture.ts +45 -0
  483. package/src/__tests__/skin-motion-regression.unit.test.ts +0 -33
  484. package/src/__tests__/skin-palette-persistent.integration.test.ts +28 -0
  485. package/src/__tests__/skin-palette-persistent.unit.test.ts +247 -0
  486. package/src/__tests__/skinned-shadow-caster.test.ts +125 -14
  487. package/src/__tests__/specular-environment-scale.dawn.test.ts +158 -0
  488. package/src/__tests__/ssao-config.unit.test.ts +7 -0
  489. package/src/__tests__/ssao-surface.dawn.test.ts +252 -0
  490. package/src/__tests__/ssr-admission-matrix.integration.test.ts +7 -1
  491. package/src/__tests__/ssr-confidence-compose.unit.test.ts +84 -0
  492. package/src/__tests__/ssr-coordinate-oracle.unit.test.ts +28 -0
  493. package/src/__tests__/ssr-cube-silhouette.dawn.test.ts +270 -0
  494. package/src/__tests__/ssr-format-probe-gating.integration.test.ts +82 -0
  495. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +99 -0
  496. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +116 -0
  497. package/src/__tests__/ssr-gpu-dispatch.ts +951 -0
  498. package/src/__tests__/ssr-graph-topology.unit.test.ts +104 -0
  499. package/src/__tests__/ssr-history-filter.dawn.test.ts +198 -0
  500. package/src/__tests__/ssr-hit-color.dawn.test.ts +273 -0
  501. package/src/__tests__/ssr-hiz-odd-reduction.dawn.test.ts +569 -0
  502. package/src/__tests__/ssr-hiz-rescue.dawn.test.ts +279 -0
  503. package/src/__tests__/ssr-hiz.browser.test.ts +150 -0
  504. package/src/__tests__/ssr-hiz.dawn.test.ts +150 -0
  505. package/src/__tests__/ssr-hiz.unit.test.ts +56 -0
  506. package/src/__tests__/ssr-inspection.unit.test.ts +105 -0
  507. package/src/__tests__/ssr-memory-budget.unit.test.ts +45 -0
  508. package/src/__tests__/ssr-performance-sequence-aggregation.unit.test.ts +15 -0
  509. package/src/__tests__/ssr-public-surface.test-d.ts +37 -0
  510. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +387 -0
  511. package/src/__tests__/ssr-reflection-mip-reducer.dawn.test.ts +309 -0
  512. package/src/__tests__/ssr-reprojection-probe.ts +90 -0
  513. package/src/__tests__/ssr-roughness-mip.dawn.test.ts +163 -0
  514. package/src/__tests__/ssr-shader-prewarm.unit.test.ts +59 -0
  515. package/src/__tests__/ssr-spatial.integration.test.ts +141 -0
  516. package/src/__tests__/ssr-stable-lattice.dawn.test.ts +475 -0
  517. package/src/__tests__/ssr-temporal-history.unit.test.ts +202 -0
  518. package/src/__tests__/ssr-texel-crossing.dawn.test.ts +448 -0
  519. package/src/__tests__/ssr-zero-work.integration.test.ts +87 -0
  520. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +84 -12
  521. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +29 -0
  522. package/src/__tests__/standard-output-chain-contract.unit.test.ts +99 -0
  523. package/src/__tests__/standard-output-chain.integration.test.ts +73 -0
  524. package/src/__tests__/standard-output-gpu-evidence.ts +205 -0
  525. package/src/__tests__/standard-output.browser.test.ts +15 -0
  526. package/src/__tests__/standard-output.dawn.test.ts +15 -0
  527. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +275 -0
  528. package/src/__tests__/standard-pipeline.integration.test.ts +320 -3
  529. package/src/__tests__/standard-profile-oracle.unit.test.ts +49 -0
  530. package/src/__tests__/standard-surface-lighting.unit.test.ts +41 -0
  531. package/src/__tests__/standard-texture-specialization.dawn.test.ts +141 -0
  532. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +6 -3
  533. package/src/__tests__/taa-coverage-edge.dawn.test.ts +199 -0
  534. package/src/__tests__/taa-history-clipping.dawn.test.ts +328 -0
  535. package/src/__tests__/taa-neighborhood-equivalence.dawn.test.ts +782 -0
  536. package/src/__tests__/taa-stability-feedback.browser.test.ts +23 -0
  537. package/src/__tests__/taa-stability-feedback.dawn.test.ts +68 -0
  538. package/src/__tests__/taa-stability-feedback.ts +342 -0
  539. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +49 -7
  540. package/src/__tests__/temporal-reset-matrix.unit.test.ts +9 -0
  541. package/src/__tests__/temporal-transaction.integration.test.ts +28 -0
  542. package/src/__tests__/temporal-view.unit.test.ts +17 -0
  543. package/src/__tests__/texture-usage-surface.unit.test.ts +4 -2
  544. package/src/__tests__/transmission-prewarm.unit.test.ts +57 -0
  545. package/src/__tests__/typed-pipeline-topology.unit.test.ts +109 -4
  546. package/src/__tests__/typed-scene-pass-material-cache.integration.test.ts +126 -0
  547. package/src/__tests__/vertex-layouts-ssot.unit.test.ts +47 -0
  548. package/src/__tests__/visibility-extract.perf.test.ts +20 -8
  549. package/src/__tests__/visibility-instances.integration.test.ts +19 -7
  550. package/src/__tests__/visibility-producer-matrix.integration.test.ts +20 -9
  551. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +62 -13
  552. package/src/__tests__/volumetric-fog-topology.unit.test.ts +7 -7
  553. package/src/assembly/atmosphere-shader-prewarm.ts +39 -0
  554. package/src/assembly/device-feature-admission.ts +9 -4
  555. package/src/assembly/dynamic-geometry-host.ts +284 -163
  556. package/src/assembly/dynamic-geometry-runtime.ts +71 -0
  557. package/src/assembly/factory.ts +4 -0
  558. package/src/assembly/gpu-driven-owner.ts +29 -0
  559. package/src/assembly/host-contract.ts +19 -0
  560. package/src/assembly/material/assembly.ts +383 -1
  561. package/src/assembly/material/pipeline-helpers.ts +100 -0
  562. package/src/assembly/material-shader-policy.ts +110 -46
  563. package/src/assembly/recovery/device-loss-fanout.ts +5 -0
  564. package/src/assembly/recovery/failure-location.ts +62 -0
  565. package/src/assembly/recovery/recovery-attempt.ts +2 -29
  566. package/src/assembly/recovery/renderer-recover.ts +16 -17
  567. package/src/assembly/renderer-facade.ts +34 -1
  568. package/src/assembly/renderer-feature-inspection.ts +13 -0
  569. package/src/assembly/renderer-frame-transaction.ts +63 -0
  570. package/src/assembly/renderer-helpers.ts +26 -0
  571. package/src/assembly/renderer-inspection.ts +6 -0
  572. package/src/assembly/shader-prewarm-policy.ts +46 -1
  573. package/src/assembly/skin-palette-owner.ts +21 -0
  574. package/src/assembly/ssr-shader-prewarm.ts +41 -0
  575. package/src/assembly/webgpu-device-loss.ts +75 -0
  576. package/src/assembly/webgpu-pbr-ready.ts +232 -0
  577. package/src/assembly/webgpu-ready-mipmap.ts +26 -0
  578. package/src/assembly/webgpu-ready.ts +161 -131
  579. package/src/assembly/webgpu-renderer-bootstrap.ts +85 -0
  580. package/src/assembly/webgpu-renderer-contract.ts +69 -0
  581. package/src/assembly/webgpu-renderer-guards.ts +32 -0
  582. package/src/assembly/webgpu-renderer-observation.ts +43 -0
  583. package/src/assembly/webgpu-renderer-recovery-failure.ts +62 -0
  584. package/src/assembly/webgpu-renderer.ts +744 -730
  585. package/src/assembly/webgpu-vertex-layouts.ts +233 -87
  586. package/src/capture/scene-projection.ts +37 -0
  587. package/src/components/__tests__/camera-exposure.contract.unit.test.ts +153 -0
  588. package/src/components/__tests__/dynamic-resolution-schema.unit.test.ts +65 -0
  589. package/src/components/__tests__/light-radius.integration.test.ts +59 -0
  590. package/src/components/camera.ts +287 -7
  591. package/src/components/directional-light.ts +3 -3
  592. package/src/components/dynamic-resolution.ts +67 -0
  593. package/src/components/index.ts +2 -0
  594. package/src/components/instances.ts +45 -18
  595. package/src/components/light-helpers.ts +6 -1
  596. package/src/components/reflection-probe.ts +1 -0
  597. package/src/components/scene-instance.ts +2 -1
  598. package/src/components/screen-space-reflection.ts +21 -0
  599. package/src/components/skylight.ts +4 -1
  600. package/src/components/sprite-animation.ts +2 -2
  601. package/src/components/sprite-instances.ts +1 -1
  602. package/src/device/device-scope.ts +13 -7
  603. package/src/device/gpu-residency.ts +45 -42
  604. package/src/dynamic-geometry.ts +13 -8
  605. package/src/environment/background.ts +281 -0
  606. package/src/environment/ibl.ts +211 -0
  607. package/src/environment/storage.ts +63 -0
  608. package/src/errors/gpu-driven.ts +49 -8
  609. package/src/errors/render.ts +91 -3
  610. package/src/extract/camera.ts +37 -1
  611. package/src/extract/contracts.ts +10 -0
  612. package/src/extract/gpu-driven.ts +240 -10
  613. package/src/features/host.ts +237 -19
  614. package/src/features/noise-texture.ts +91 -0
  615. package/src/features/plan.ts +553 -24
  616. package/src/features/prepared-gpu-work.ts +631 -49
  617. package/src/features/prepared-graphics-store.ts +31 -0
  618. package/src/features/prepared-graphics.ts +5 -0
  619. package/src/features/render-graph-compute.ts +41 -8
  620. package/src/features/render-graph-contribution.ts +141 -3
  621. package/src/features/render-graph-raster.ts +143 -30
  622. package/src/features/types.ts +11 -0
  623. package/src/fullscreen-post-process-pass.ts +3 -0
  624. package/src/gpu-driven/batch-topology.ts +452 -108
  625. package/src/gpu-driven/material-artifacts.ts +179 -0
  626. package/src/gpu-driven/material-bindings.ts +41 -0
  627. package/src/gpu-driven/pbr-program.ts +31 -0
  628. package/src/gpu-driven/prepared-draw.ts +42 -6
  629. package/src/gpu-driven/production-raster.ts +2806 -716
  630. package/src/gpu-driven/shadow-views.ts +539 -0
  631. package/src/gpu-driven/view-gpu.ts +188 -41
  632. package/src/gpu-resource.ts +21 -2
  633. package/src/gpu-scene-schema.ts +61 -11
  634. package/src/gpu-scene.ts +197 -99
  635. package/src/gpu-texture-usage.ts +1 -0
  636. package/src/hdrp-buffers.ts +141 -36
  637. package/src/ibl/IblPipelineCache.ts +5 -0
  638. package/src/ibl/frame-resources.ts +38 -0
  639. package/src/ibl/skylight-bind-group.ts +32 -21
  640. package/src/index.ts +194 -75
  641. package/src/inspection-types.ts +207 -1
  642. package/src/instances-derived-bounds.ts +258 -34
  643. package/src/instances.ts +459 -150
  644. package/src/internal.ts +22 -0
  645. package/src/lifecycle.ts +2 -0
  646. package/src/material-row.ts +194 -0
  647. package/src/materials.ts +44 -8
  648. package/src/pbr-pipeline.ts +172 -19
  649. package/src/pipeline/render-extent.ts +111 -0
  650. package/src/pipeline/standard-build-context.ts +7 -0
  651. package/src/pipeline/standard-forward-lane.ts +74 -29
  652. package/src/pipeline/standard-output/__tests__/lut-admission.unit.test.ts +103 -0
  653. package/src/pipeline/standard-output/auto-exposure/__tests__/capability.unit.test.ts +40 -0
  654. package/src/pipeline/standard-output/auto-exposure/__tests__/gpu.contract.unit.test.ts +125 -0
  655. package/src/pipeline/standard-output/auto-exposure/__tests__/graph.contract.unit.test.ts +94 -0
  656. package/src/pipeline/standard-output/auto-exposure/__tests__/oracle.test.ts +108 -0
  657. package/src/pipeline/standard-output/auto-exposure/__tests__/preset.test.ts +26 -0
  658. package/src/pipeline/standard-output/auto-exposure/capability.ts +38 -0
  659. package/src/pipeline/standard-output/auto-exposure/gpu.ts +567 -0
  660. package/src/pipeline/standard-output/auto-exposure/graph.ts +221 -0
  661. package/src/pipeline/standard-output/auto-exposure/inspection.ts +231 -0
  662. package/src/pipeline/standard-output/auto-exposure/oracle.ts +291 -0
  663. package/src/pipeline/standard-output/auto-exposure/preset.ts +28 -0
  664. package/src/pipeline/standard-output/auto-exposure/state.ts +156 -0
  665. package/src/pipeline/standard-output/color-transform.ts +124 -0
  666. package/src/pipeline/standard-output/graph.ts +139 -0
  667. package/src/pipeline/standard-output/lut-admission.ts +121 -0
  668. package/src/pipeline/standard-output/lut-gpu.ts +399 -0
  669. package/src/pipeline/standard-output/lut-state.ts +176 -0
  670. package/src/pipeline/standard-output/resources.ts +121 -0
  671. package/src/pipeline/standard-output/types.ts +58 -0
  672. package/src/pipeline/standard-pipeline.ts +232 -70
  673. package/src/pipeline/standard-post.ts +190 -27
  674. package/src/pipeline/standard-profile.ts +2 -1
  675. package/src/pipeline-builder.ts +28 -9
  676. package/src/pipeline-spec-types.ts +4 -0
  677. package/src/pipeline-spec.ts +37 -29
  678. package/src/plugin.ts +4 -0
  679. package/src/points-lines/standard-owner.ts +6 -14
  680. package/src/post-process-errors.ts +26 -18
  681. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +2 -2
  682. package/src/prepare/extended-lighting/resources.ts +3 -3
  683. package/src/prepare/prepared-graphics-resolver.ts +9 -2
  684. package/src/record/__tests__/main-pass-geometry.unit.test.ts +68 -0
  685. package/src/record/__tests__/material-bg-cache.unit.test.ts +45 -0
  686. package/src/record/__tests__/material-slot-plan.unit.test.ts +57 -0
  687. package/src/record/__tests__/mesh-ssbo.unit.test.ts +40 -1
  688. package/src/record/__tests__/recovery-pipeline.unit.test.ts +20 -1
  689. package/src/record/__tests__/sprite-material-abi.unit.test.ts +41 -6
  690. package/src/record/__tests__/temporal-payload.unit.test.ts +2 -0
  691. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +45 -0
  692. package/src/record/__tests__/view-ubo-layout.unit.test.ts +5 -0
  693. package/src/record/dynamic-geometry-consumption.ts +59 -0
  694. package/src/record/frame-lighting.ts +15 -12
  695. package/src/record/frame-snapshot.ts +146 -34
  696. package/src/record/frame-targets.ts +9 -0
  697. package/src/record/frame.ts +838 -170
  698. package/src/record/main-pass-geometry.ts +209 -103
  699. package/src/record/main-pass-material.ts +157 -40
  700. package/src/record/main-pass-sprite-draws.ts +16 -107
  701. package/src/record/main-pass.ts +141 -12
  702. package/src/record/material-uniforms.ts +1 -1
  703. package/src/record/mesh-ssbo.ts +59 -15
  704. package/src/record/prepared-material-bindings.ts +238 -0
  705. package/src/record/recovery-pipeline.ts +69 -90
  706. package/src/record/render-context.ts +123 -24
  707. package/src/record/shadow-pass.ts +1389 -230
  708. package/src/record/skybox-post-pass.ts +14 -3
  709. package/src/record/typed-frame-graph.ts +1064 -47
  710. package/src/record/view-ubo.ts +12 -1
  711. package/src/recovery/render-system-candidate.ts +135 -68
  712. package/src/recovery/render-system-roots.ts +113 -0
  713. package/src/recovery/types.ts +77 -0
  714. package/src/reflection/background.ts +151 -0
  715. package/src/reflection/filter.ts +3 -0
  716. package/src/reflection/projection.ts +16 -6
  717. package/src/reflection/record-owner.ts +308 -140
  718. package/src/render-contract.ts +237 -49
  719. package/src/render-graph-primitives.ts +34 -5
  720. package/src/render-pipeline.ts +90 -2
  721. package/src/render-system-extract-tail.ts +431 -162
  722. package/src/render-system-extract.ts +478 -119
  723. package/src/render-system-projections.ts +241 -0
  724. package/src/render-system.ts +715 -828
  725. package/src/scene/render-scene-types.ts +13 -4
  726. package/src/scene/render-scene.ts +577 -355
  727. package/src/scene/shadow-visibility.ts +21 -0
  728. package/src/ssao-config.ts +25 -8
  729. package/src/ssr/admission.ts +380 -14
  730. package/src/ssr/compose.ts +144 -0
  731. package/src/ssr/errors.ts +48 -0
  732. package/src/ssr/graph.ts +797 -0
  733. package/src/ssr/history.ts +436 -0
  734. package/src/ssr/hiz.ts +124 -0
  735. package/src/ssr/inspection.ts +124 -0
  736. package/src/ssr/resources.ts +202 -0
  737. package/src/ssr/temporal.ts +164 -0
  738. package/src/systems/skin-palette-allocator.ts +495 -148
  739. package/src/systems/skin-palette-types.ts +17 -0
  740. package/src/systems/transparent-dispatch.ts +90 -0
  741. package/src/targets/material-source.ts +34 -0
  742. package/src/temporal/coverage.ts +1 -1
  743. package/src/temporal/frame-coordinator.ts +23 -2
  744. package/src/temporal/gpu.ts +79 -12
  745. package/src/temporal/history.ts +3 -1
  746. package/src/temporal/inspection.ts +2 -1
  747. package/src/temporal/standard-scene-data.ts +10 -2
  748. package/src/temporal/target-coverage-attachment.ts +72 -0
  749. package/src/temporal/temporal-view.ts +11 -2
  750. package/src/temporal/view.ts +12 -2
  751. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +219 -139
  752. package/src/typed-render-graph-primitives.ts +304 -74
  753. package/src/typed-shadow-passes.ts +122 -12
  754. package/src/volume/component.ts +1 -0
  755. package/src/volume/passes.ts +3 -3
  756. package/src/volume/temporal.ts +10 -0
  757. package/dist/chunk-GE3SDD72.mjs.map +0 -1
  758. package/dist/chunk-GF523LHF.mjs.map +0 -1
  759. package/dist/chunk-IQMKJLNU.mjs.map +0 -1
  760. package/dist/chunk-J52NMQKR.mjs.map +0 -1
  761. package/dist/chunk-J6KACNOH.mjs.map +0 -1
  762. package/dist/chunk-JAASCMXA.mjs.map +0 -1
  763. package/dist/chunk-MBU4ZVE7.mjs.map +0 -1
  764. package/dist/chunk-MYJ5RPJO.mjs.map +0 -1
  765. package/dist/chunk-RBDBZ5R7.mjs.map +0 -1
  766. package/dist/chunk-VSL23LZE.mjs.map +0 -1
  767. package/src/__tests__/instances-world-ownership.integration.test.ts +0 -62
  768. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +0 -36
@@ -14,7 +14,6 @@ import type {
14
14
  RenderPipeline,
15
15
  Result,
16
16
  RhiDevice,
17
- ShaderModule,
18
17
  TextureFormat,
19
18
  } from '@forgeax/engine-rhi';
20
19
  import { err, ok, RhiError } from '@forgeax/engine-rhi';
@@ -22,24 +21,51 @@ import type { MaterialShaderArtifact } from '@forgeax/engine-shader';
22
21
  import type { MaterialRenderState } from '@forgeax/engine-types';
23
22
  import type { DeviceScope, LifecycleResourceSpec } from '../device/device-scope';
24
23
  import type { MeshGpuHandles } from '../device/gpu-residency';
25
- import { GPU_SCENE_WGSL } from '../gpu-scene-schema';
24
+ import { GpuDrivenPreparationError } from '../errors/gpu-driven';
25
+ import {
26
+ gpuDrivenDrawKey,
27
+ gpuDrivenMaterialArtifactKey,
28
+ gpuDrivenShadowDrawKey,
29
+ gpuDrivenSourceDrawItemIndex,
30
+ } from '../extract/gpu-driven';
31
+ import { GPU_SCENE_LAYOUTS } from '../gpu-scene-schema';
26
32
  import {
27
33
  GPU_BUFFER_USAGE_COPY_DST,
28
34
  GPU_BUFFER_USAGE_INDEX,
29
35
  GPU_BUFFER_USAGE_STORAGE,
30
- GPU_BUFFER_USAGE_UNIFORM,
31
36
  GPU_BUFFER_USAGE_VERTEX,
32
37
  } from '../gpu-usage';
33
- import { assembleMaterialWithSkylightEntries } from '../ibl/skylight-bind-group';
34
- import type { GpuDrivenProductionInspection } from '../inspection-types';
38
+ import type {
39
+ GpuDrivenLaneReason,
40
+ GpuDrivenLaneSummary,
41
+ GpuDrivenPreparationFailureInspection,
42
+ GpuDrivenProductionInspection,
43
+ GpuDrivenStructureMetrics,
44
+ } from '../inspection-types';
45
+ import { isStandardPbrMaterialShader, resolvePipelineGroup2Contract } from '../pbr-pipeline';
35
46
  import type { PipelineBuilderShaderModuleFactory } from '../pipeline-builder';
47
+ import type { ValidatedRenderable } from '../record/frame-snapshot';
36
48
  import { worldEntityKey } from '../record/frame-snapshot';
37
49
  import { geometryRenderStateForTopology } from '../record/main-pass-material';
38
- import { MESH_SSBO_BYTES } from '../record/mesh-ssbo';
39
- import type { PipelineState } from '../record/render-context';
50
+ import { MESH_SSBO_BYTES, packInstanceStorageBuffer } from '../record/mesh-ssbo';
51
+ import {
52
+ MATERIAL_PER_ENTITY_STRIDE,
53
+ type PipelineState,
54
+ type RenderSystemInternals,
55
+ } from '../record/render-context';
40
56
  import type { CameraSnapshot } from '../render-contract';
41
- import type { RenderPipelineFrame, RenderPipelineGpuDrivenProjection } from '../render-pipeline';
42
- import type { RenderableSnapshot } from '../render-system-extract';
57
+ import type {
58
+ GpuDrivenSceneMeshBinding,
59
+ GpuDrivenStandardPbrFrameResources,
60
+ RenderPipelineFrame,
61
+ RenderPipelineGpuDrivenProjection,
62
+ } from '../render-pipeline';
63
+ import type {
64
+ DispatchEntry,
65
+ MaterialSnapshot,
66
+ RenderableSnapshot,
67
+ ShadowCasterMembership,
68
+ } from '../render-system-extract';
43
69
  import type { PersistentGpuDrivenState } from '../scene/render-scene';
44
70
  import { selectLod } from '../scene/visibility/lod-selector';
45
71
  import type { OcclusionFrameProjection } from '../scene/visibility/occlusion-runtime';
@@ -49,20 +75,24 @@ import {
49
75
  projectedHeightForCandidate,
50
76
  type SubmissionPlan,
51
77
  } from './batch-topology';
78
+ import { materialBindingKey } from './material-bindings';
79
+ import { type GpuDrivenPbrProgram, resolveStandardPbrProgram } from './pbr-program';
52
80
  import {
53
- GPU_DRIVEN_VIEW_WGSL,
54
- type GpuDrivenLodSelectionInspection,
55
- GpuDrivenView,
56
- selectGpuLodLane,
57
- } from './view-gpu';
58
-
59
- const VERTEX_STAGE = 0x1;
60
- const FRAGMENT_STAGE = 0x2;
61
- const EMPTY_RESOURCE_CLASS = JSON.stringify({ textures: [], samplers: [], video: [] });
81
+ type ShadowViewIdentity,
82
+ type ShadowViewProjection,
83
+ ShadowViewStatePool,
84
+ type ShadowViewUpdateInput,
85
+ shadowViewIdentityKey,
86
+ } from './shadow-views';
87
+ import { type GpuDrivenLodSelectionInspection, GpuDrivenView, selectGpuLodLane } from './view-gpu';
62
88
 
63
89
  export interface StandardPbrSceneCandidate {
64
90
  readonly primitiveIndex: number;
65
91
  readonly world: Float32Array;
92
+ /** Optional per-instance local transform selected by BatchTopology. */
93
+ readonly instanceTransform?: Float32Array;
94
+ /** Optional GPU Scene material row carried by the shadow projection. */
95
+ readonly materialIndex?: number;
66
96
  }
67
97
 
68
98
  export interface StandardPbrSceneProjection {
@@ -73,6 +103,24 @@ export interface StandardPbrSceneProjection {
73
103
  readonly rows: Float32Array;
74
104
  }
75
105
 
106
+ export interface GpuDrivenShadowBatchProjection {
107
+ readonly mesh: MeshGpuHandles;
108
+ readonly meshBindGroup: BindGroup;
109
+ readonly visibleBindGroup: BindGroup;
110
+ readonly deformation: 'rigid' | 'skin';
111
+ /** Actual producer-selected shadow-pass artifact for this batch. */
112
+ readonly shadowArtifact: MaterialShaderArtifact;
113
+ /** Pass entry points selected by the authored ShadowCaster dispatch. */
114
+ readonly shadowVertexEntry?: string;
115
+ readonly shadowFragmentEntry?: string;
116
+ /** Material snapshot used to assemble the receipt-owned alpha-mask BG. */
117
+ readonly material: MaterialSnapshot;
118
+ /** Composite entity identity used only for video-resource routing. */
119
+ readonly materialEntityKey: number;
120
+ /** Geometry-owned COLOR_0 fact shared by main and shadow ABI variants. */
121
+ readonly vertexColorAvailable: boolean;
122
+ }
123
+
76
124
  /**
77
125
  * Build the producer-owned Standard PBR scene-index projection. The receipt is
78
126
  * the only source of the vertex entry and layout identity; callers cannot
@@ -97,12 +145,16 @@ export function projectStandardPbrScene(input: {
97
145
  for (let index = 0; index < input.candidates.length; index += 1) {
98
146
  const candidate = input.candidates[index];
99
147
  if (candidate === undefined) continue;
148
+ const world =
149
+ candidate.instanceTransform === undefined
150
+ ? candidate.world
151
+ : mat4.multiply(mat4.create(), candidate.world, candidate.instanceTransform);
100
152
  const base = index * 48;
101
153
  for (let value = 0; value < 16; value += 1) {
102
- rows[base + value] = candidate.world[value] ?? 0;
103
- rows[base + 28 + value] = candidate.world[value] ?? 0;
154
+ rows[base + value] = world[value] ?? 0;
155
+ rows[base + 28 + value] = world[value] ?? 0;
104
156
  }
105
- const normalMatrix = mat3.normalMatrix(normal, candidate.world);
157
+ const normalMatrix = mat3.normalMatrix(normal, world);
106
158
  for (let column = 0; column < 3; column += 1) {
107
159
  const source = column * 3;
108
160
  const target = base + 16 + column * 4;
@@ -110,6 +162,11 @@ export function projectStandardPbrScene(input: {
110
162
  rows[target + 1] = normalMatrix[source + 1] ?? 0;
111
163
  rows[target + 2] = normalMatrix[source + 2] ?? 0;
112
164
  }
165
+ // The final temporal metadata lane is otherwise unused by the shadow
166
+ // vertex path. Carry the concrete scene-material row there so skinned
167
+ // GPU-driven alpha-mask shadows can keep the palette base in visible.y
168
+ // without introducing a second visible-stream ABI.
169
+ rows[base + 47] = candidate.materialIndex ?? 0;
113
170
  }
114
171
  return {
115
172
  ok: true,
@@ -183,53 +240,11 @@ export function adaptStandardPbrFrameResources(input: {
183
240
  };
184
241
  }
185
242
 
186
- export const GPU_DRIVEN_RIGID_UNLIT_WGSL = /* wgsl */ `
187
- ${GPU_SCENE_WGSL}
188
-
189
- struct ViewData {
190
- slots: array<vec4<f32>, 49>,
191
- };
192
-
193
- @group(0) @binding(0) var<uniform> view: ViewData;
194
- @group(1) @binding(0) var<storage, read> primitives: array<GpuScenePrimitive>;
195
- @group(1) @binding(1) var<storage, read> instances: array<GpuSceneInstance>;
196
- @group(1) @binding(2) var<storage, read> materials: array<GpuSceneMaterial>;
197
- @group(1) @binding(3) var<storage, read> visibleItems: array<vec2<u32>>;
198
- @group(1) @binding(4) var<storage, read> transforms: array<GpuSceneTransform>;
199
-
200
- struct VertexInput {
201
- @location(0) position: vec3<f32>,
202
- };
203
-
204
- struct VertexOutput {
205
- @builtin(position) position: vec4<f32>,
206
- @location(0) color: vec4<f32>,
207
- };
208
-
209
- @vertex
210
- fn vs_main(input: VertexInput, @builtin(instance_index) instanceIndex: u32) -> VertexOutput {
211
- let viewProjection = mat4x4<f32>(view.slots[0], view.slots[1], view.slots[2], view.slots[3]);
212
- let visible = visibleItems[instanceIndex];
213
- let sceneInstance = instances[visible.x];
214
- let primitiveIndex = sceneInstance.primitiveIndex;
215
- let primitive = primitives[primitiveIndex];
216
- let material = materials[visible.y];
217
- var output: VertexOutput;
218
- let world = transforms[primitive.transformIndex].currentWorld * transforms[sceneInstance.transformIndex].currentWorld;
219
- output.position = viewProjection * world * vec4<f32>(input.position, 1.0);
220
- output.color = material.params0;
221
- return output;
222
- }
223
-
224
- @fragment
225
- fn fs_main(input: VertexOutput) -> @location(0) vec4<f32> {
226
- return input.color;
227
- }
228
- `;
229
-
230
243
  interface PreparedBatch {
231
244
  readonly batch: GpuDrivenBatch;
232
245
  readonly mesh: MeshGpuHandles;
246
+ /** Exact producer artifact selected by every draw in this homogeneous batch. */
247
+ readonly artifact: MaterialShaderArtifact;
233
248
  readonly renderState: MaterialRenderState | undefined;
234
249
  /** Stable World attachment key used to join same-submit GPU counters. */
235
250
  readonly worldKey: number;
@@ -238,13 +253,6 @@ interface PreparedBatch {
238
253
  readonly slotGeneration: number;
239
254
  }
240
255
 
241
- interface ProjectedBatch extends PreparedBatch {
242
- readonly bindings: BindGroup;
243
- readonly pipeline: RenderPipeline;
244
- readonly vertex: GraphBuffer;
245
- readonly index?: GraphBuffer;
246
- }
247
-
248
256
  function bufferBinding(
249
257
  buffer: import('@forgeax/engine-rhi').Buffer,
250
258
  offset?: number,
@@ -282,46 +290,118 @@ function activeFrustum(camera: CameraSnapshot): Float32Array {
282
290
  );
283
291
  }
284
292
 
293
+ function hasFiniteOrderedBounds(bounds: Float32Array | undefined): boolean {
294
+ if (bounds === undefined || bounds.length < 6) return false;
295
+ for (let index = 0; index < 6; index += 1) {
296
+ if (!Number.isFinite(bounds[index])) return false;
297
+ }
298
+ const minX = bounds[0];
299
+ const minY = bounds[1];
300
+ const minZ = bounds[2];
301
+ const maxX = bounds[3];
302
+ const maxY = bounds[4];
303
+ const maxZ = bounds[5];
304
+ return (
305
+ minX !== undefined &&
306
+ minY !== undefined &&
307
+ minZ !== undefined &&
308
+ maxX !== undefined &&
309
+ maxY !== undefined &&
310
+ maxZ !== undefined &&
311
+ minX <= maxX &&
312
+ minY <= maxY &&
313
+ minZ <= maxZ
314
+ );
315
+ }
316
+
317
+ function skinLaneReason(
318
+ snapshot: RenderableSnapshot,
319
+ ): 'skin-bounds-missing' | 'skin-address-missing' | undefined {
320
+ if (snapshot.skin === undefined) return undefined;
321
+ if (!hasFiniteOrderedBounds(snapshot.skin.bounds)) return 'skin-bounds-missing';
322
+ if (
323
+ snapshot.skin.storageOrUniform !== 'storage' ||
324
+ !Number.isInteger(snapshot.skin.customDataStart) ||
325
+ snapshot.skin.customDataStart < 0 ||
326
+ snapshot.skin.buffer === undefined
327
+ ) {
328
+ return 'skin-address-missing';
329
+ }
330
+ return undefined;
331
+ }
332
+
285
333
  /**
286
- * LOD projected height is a view fact, not a scene-topology fact. Keep the
287
- * filtered plan cache scene-stable while still rebuilding its candidate
288
- * heights whenever a camera input used by the selector changes.
334
+ * Derived projected heights invalidate the prepared plan only when a
335
+ * candidate has more than its implicit root LOD row. No-LOD plans have no
336
+ * view-dependent selection. Root matrix edits and camera edits feed the same
337
+ * height calculation; retaining the empty key for absent LOD avoids rebuilding
338
+ * their candidate and batch uploads on every camera frame.
289
339
  */
290
- function lodProjectionFingerprint(camera: CameraSnapshot): string {
291
- return JSON.stringify({
292
- position: Array.from(camera.position),
293
- world: Array.from(camera.world),
294
- projection: camera.projection,
295
- fov: camera.fov,
296
- aspect: camera.aspect,
297
- orthoHeight: camera.orthoTop - camera.orthoBottom,
298
- });
340
+ function lodProjectionFingerprint(
341
+ source: SubmissionPlan,
342
+ camera: CameraSnapshot,
343
+ slots: PersistentGpuDrivenState['slots'],
344
+ ): string {
345
+ const primitives = new Set<number>();
346
+ for (const batch of source.batches) {
347
+ for (const candidate of batch.candidates) {
348
+ if ((candidate.lodCoverages?.length ?? 0) > 1) primitives.add(candidate.primitiveIndex);
349
+ }
350
+ }
351
+ if (primitives.size === 0) return '';
352
+ const heights: number[] = [];
353
+ for (const slot of slots) {
354
+ if (primitives.has(slot.slot))
355
+ heights.push(slot.slot, projectedHeightForCandidate(slot, camera));
356
+ }
357
+ return JSON.stringify(heights);
299
358
  }
300
359
 
301
- function productionEligible(snapshot: RenderableSnapshot, batch: GpuDrivenBatch): boolean {
360
+ function preparedPbrCandidateEligible(
361
+ snapshot: RenderableSnapshot,
362
+ batch: GpuDrivenBatch,
363
+ candidate: GpuDrivenBatch['candidates'][number],
364
+ artifact: MaterialShaderArtifact | undefined,
365
+ ): boolean {
302
366
  const material = snapshot.materials[batch.key.materialSlot] ?? snapshot.material;
303
- const shader = material.materialShaderId ?? 'forgeax::default-unlit';
367
+ const prepared = candidate.prepared;
368
+ if (prepared === undefined) return false;
369
+ const receipt = artifact?.receipt;
370
+ const publishedAbiMatches =
371
+ receipt !== undefined &&
372
+ artifact?.material === prepared.identity.material &&
373
+ (prepared.receiptIdentity === undefined ||
374
+ (prepared.receiptIdentity === receipt.receiptIdentity &&
375
+ prepared.receiptGeneration === receipt.generation)) &&
376
+ receipt.reflection.layoutIdentity === artifact?.layoutIdentity;
377
+ const alphaBlend = material.transparent === true || material.renderState?.blend !== undefined;
378
+ const cpuOnlyMaterialResources =
379
+ (material.textureSources?.size ?? 0) > 0 || (material.videoTextureFields?.size ?? 0) > 0;
380
+ // Transmission has a separate CPU semantic pass today. Keep it out of the
381
+ // GPU-owned candidate set so the residual transmission draw is not swallowed
382
+ // by the generic Standard PBR lane.
383
+ const transmission =
384
+ isStandardPbrMaterialShader(prepared.identity.material) &&
385
+ typeof material.paramSnapshot?.transmission === 'number' &&
386
+ material.paramSnapshot.transmission > 0;
387
+ const skinReady =
388
+ prepared.identity.deformation !== 'skin' ||
389
+ (artifact?.receipt?.skinPaletteAddress !== undefined &&
390
+ snapshot.skin?.storageOrUniform === 'storage' &&
391
+ snapshot.skin.customDataStart >= 0 &&
392
+ Number.isInteger(snapshot.skin.customDataStart) &&
393
+ snapshot.skin.buffer !== undefined &&
394
+ hasFiniteOrderedBounds(snapshot.skin.bounds));
304
395
  return (
305
- shader === 'forgeax::default-unlit' &&
306
- // The production rigid shader owns triangle-list raster only. Lines and
307
- // points stay with their CPU/specialized topology owners.
308
- batch.key.topology === 'triangle-list' &&
309
- batch.key.materialResourceClass === EMPTY_RESOURCE_CLASS &&
310
- material.baseColorTexture === undefined &&
311
- material.transparent !== true
396
+ publishedAbiMatches &&
397
+ !alphaBlend &&
398
+ !cpuOnlyMaterialResources &&
399
+ !transmission &&
400
+ (prepared.identity.deformation === 'rigid' || prepared.identity.deformation === 'skin') &&
401
+ skinReady
312
402
  );
313
403
  }
314
404
 
315
- /**
316
- * Prepared Standard PBR draws have an explicit receipt identity on their
317
- * batch key. Unprepared batches stay on the rigid production shader; the
318
- * presence of a frame-wide PBR artifact is not permission to reinterpret an
319
- * unlit batch with the PBR bind-group layout.
320
- */
321
- function isStandardPbrBatch(batch: GpuDrivenBatch): boolean {
322
- return batch.key.preparedIdentity !== undefined;
323
- }
324
-
325
405
  function meshSupportsBatch(mesh: MeshGpuHandles, batch: GpuDrivenBatch): boolean {
326
406
  if (
327
407
  batch.candidates.some((candidate) => {
@@ -341,6 +421,160 @@ function meshSupportsBatch(mesh: MeshGpuHandles, batch: GpuDrivenBatch): boolean
341
421
  : !mesh.indexed;
342
422
  }
343
423
 
424
+ function sceneCandidate(
425
+ slot: PersistentGpuDrivenState['slots'][number],
426
+ candidate: GpuDrivenBatch['candidates'][number],
427
+ materialIndex?: number,
428
+ ): StandardPbrSceneCandidate {
429
+ const transforms = slot.snapshot.instances?.transforms;
430
+ return {
431
+ primitiveIndex: candidate.primitiveIndex,
432
+ world: slot.snapshot.transform.world,
433
+ ...(transforms === undefined
434
+ ? {}
435
+ : {
436
+ instanceTransform: transforms.subarray(
437
+ candidate.instanceOrdinal * 16,
438
+ candidate.instanceOrdinal * 16 + 16,
439
+ ),
440
+ }),
441
+ ...(materialIndex === undefined ? {} : { materialIndex }),
442
+ };
443
+ }
444
+
445
+ function shadowCandidateKey(candidate: GpuDrivenBatch['candidates'][number]): string {
446
+ return `${candidate.primitiveIndex}:${candidate.drawItemIndex}:${candidate.instanceOrdinal}`;
447
+ }
448
+
449
+ function gpuDrivenProjectionKey(
450
+ batch: GpuDrivenBatch,
451
+ deformation: 'rigid' | 'skin',
452
+ artifact: MaterialShaderArtifact,
453
+ ): string {
454
+ const receipt = artifact.receipt;
455
+ return `${batch.batchId}:${batch.generation}:${gpuDrivenMaterialArtifactKey({
456
+ material: artifact.material,
457
+ deformation,
458
+ receiptIdentity: receipt?.receiptIdentity,
459
+ receiptGeneration: receipt?.generation ?? 0,
460
+ })}:${artifact.variantSet ?? ''}:${materialArtifactProgramIdentity(artifact)}:${artifact.vertexEntry ?? receipt?.sceneIndexEntry ?? ''}:${artifact.fragmentEntry ?? ''}`;
461
+ }
462
+
463
+ function gpuDrivenShadowProjectionKey(
464
+ viewKey: string,
465
+ batch: GpuDrivenBatch,
466
+ deformation: 'rigid' | 'skin',
467
+ artifact: MaterialShaderArtifact,
468
+ ): string {
469
+ return `shadow:${viewKey}:${gpuDrivenProjectionKey(batch, deformation, artifact)}`;
470
+ }
471
+
472
+ function sourceDrawForCandidate(
473
+ slot: PersistentGpuDrivenState['slots'][number],
474
+ candidate: GpuDrivenBatch['candidates'][number],
475
+ ): NonNullable<RenderableSnapshot['gpuDrivenDraws']>[number] | undefined {
476
+ return slot.snapshot.gpuDrivenDraws?.find(
477
+ (draw, compactIndex) =>
478
+ gpuDrivenSourceDrawItemIndex(draw, compactIndex) === candidate.drawItemIndex,
479
+ );
480
+ }
481
+
482
+ const materialArtifactProgramIdentities = new WeakMap<
483
+ MaterialShaderArtifact,
484
+ { readonly source: string; readonly identity: string }
485
+ >();
486
+
487
+ /** Hash a publication once; replacement source invalidates the same artifact key. */
488
+ function materialArtifactProgramIdentity(artifact: MaterialShaderArtifact): string {
489
+ const source = artifact.specializationKey ?? artifact.wgsl;
490
+ const cached = materialArtifactProgramIdentities.get(artifact);
491
+ if (cached?.source === source) return cached.identity;
492
+ let hash = 2166136261;
493
+ for (let index = 0; index < source.length; index += 1) {
494
+ hash ^= source.charCodeAt(index);
495
+ hash = Math.imul(hash, 16777619);
496
+ }
497
+ const identity = `${source.length}:${hash >>> 0}`;
498
+ materialArtifactProgramIdentities.set(artifact, { source, identity });
499
+ return identity;
500
+ }
501
+
502
+ function shadowKeysForCandidate(
503
+ slot: PersistentGpuDrivenState['slots'][number],
504
+ candidate: GpuDrivenBatch['candidates'][number],
505
+ shadowCasterDrawKeys: ReadonlySet<string> | undefined,
506
+ shadowCasterMembership: readonly ShadowCasterMembership[] | undefined,
507
+ ): readonly string[] {
508
+ const draw = sourceDrawForCandidate(slot, candidate);
509
+ if (draw === undefined) return [];
510
+ const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
511
+ const worldEntity = worldEntityKey(slot.snapshot.worldId, slot.snapshot.entityKey);
512
+ const base = gpuDrivenDrawKey(
513
+ worldEntity,
514
+ material.materialHandle ?? -1,
515
+ candidate.drawItemIndex,
516
+ );
517
+ if (shadowCasterMembership !== undefined) {
518
+ const keys = new Set<string>();
519
+ for (const membership of shadowCasterMembership) {
520
+ if (
521
+ membership.worldEntity === worldEntity &&
522
+ membership.materialHandle === (material.materialHandle ?? -1) &&
523
+ membership.drawItemIndex === candidate.drawItemIndex
524
+ ) {
525
+ keys.add(
526
+ gpuDrivenShadowDrawKey(
527
+ membership.worldEntity,
528
+ membership.materialHandle,
529
+ membership.drawItemIndex,
530
+ membership.passIndex,
531
+ ),
532
+ );
533
+ }
534
+ }
535
+ return [...keys];
536
+ }
537
+ if (shadowCasterDrawKeys === undefined) return [];
538
+ return [...shadowCasterDrawKeys].filter((key) => {
539
+ const parts = key.split(':');
540
+ return parts.length === 4 && `${parts[0]}:${parts[1]}:${parts[2]}` === base;
541
+ });
542
+ }
543
+
544
+ function shadowMembershipIsGpuCompatible(
545
+ slot: PersistentGpuDrivenState['slots'][number],
546
+ candidate: GpuDrivenBatch['candidates'][number],
547
+ shadowCasterDrawKeys: ReadonlySet<string> | undefined,
548
+ shadowCasterMembership: readonly ShadowCasterMembership[] | undefined,
549
+ ): boolean {
550
+ const keys = shadowKeysForCandidate(
551
+ slot,
552
+ candidate,
553
+ shadowCasterDrawKeys,
554
+ shadowCasterMembership,
555
+ );
556
+ if (keys.length !== 1) return false;
557
+ if (shadowCasterMembership === undefined) return true;
558
+ const draw = sourceDrawForCandidate(slot, candidate);
559
+ if (draw === undefined) return false;
560
+ const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
561
+ const worldEntity = worldEntityKey(slot.snapshot.worldId, slot.snapshot.entityKey);
562
+ const matching = shadowCasterMembership.filter(
563
+ (membership) =>
564
+ membership.worldEntity === worldEntity &&
565
+ membership.materialHandle === (material.materialHandle ?? -1) &&
566
+ membership.drawItemIndex === candidate.drawItemIndex,
567
+ );
568
+ if (matching.length !== 1) return false;
569
+ const membership = matching[0];
570
+ if (membership?.cpuReason !== undefined) return false;
571
+ if (membership?.renderState !== undefined) return false;
572
+ // Older synthetic membership fixtures omit the producer flag and remain
573
+ // compatible; extracted records always carry it. A false flag is an
574
+ // authored/producer decision and must keep the draw on its CPU lane.
575
+ return membership?.gpuDrivenEligible !== false;
576
+ }
577
+
344
578
  function selectedLodLevel(candidate: GpuDrivenCandidate): number {
345
579
  const coverages = candidate.lodCoverages;
346
580
  if (coverages === undefined || coverages.length <= 1) return 0;
@@ -368,28 +602,41 @@ function selectedLodRange(
368
602
  );
369
603
  }
370
604
 
371
- /**
372
- * One raster indirect command is emitted for each selector batch that has at
373
- * least one admitted candidate. Selector-only batches remain in the compute
374
- * and readback plan, but they must not inflate the raster command count.
375
- */
376
605
  function admittedRasterBatchCount(plan: SubmissionPlan): number {
377
606
  return plan.batches.reduce((count, batch) => count + (batch.visibleCapacity > 0 ? 1 : 0), 0);
378
607
  }
379
608
 
609
+ function gpuDrawEntityCount(drawKeys: ReadonlySet<string>): number {
610
+ const entities = new Set<string>();
611
+ for (const key of drawKeys) {
612
+ const separator = key.indexOf(':');
613
+ if (separator > 0) entities.add(key.slice(0, separator));
614
+ }
615
+ return entities.size;
616
+ }
617
+
380
618
  function filteredPlan(
381
619
  source: SubmissionPlan,
382
620
  slots: PersistentGpuDrivenState['slots'],
383
- meshes: ReadonlyMap<number, MeshGpuHandles>,
384
- camera: CameraSnapshot,
621
+ meshBySlot: ReadonlyMap<number, MeshGpuHandles>,
622
+ shadowCasterDrawKeys?: ReadonlySet<string>,
623
+ skinArtifact?: MaterialShaderArtifact,
624
+ shadowCasterMembership?: readonly ShadowCasterMembership[],
625
+ camera?: CameraSnapshot,
385
626
  activeEntityKeys?: ReadonlySet<number>,
386
627
  worldKeys?: readonly number[],
628
+ materialArtifact?: MaterialShaderArtifact,
629
+ materialArtifacts?: ReadonlyMap<string, MaterialShaderArtifact>,
630
+ materialBindingClasses?: ReadonlyMap<string, string>,
387
631
  ): {
388
632
  readonly plan: SubmissionPlan;
389
633
  readonly batches: readonly PreparedBatch[];
390
- readonly entityKeys: ReadonlySet<number>;
391
- /** No source primitive can enter this lane without a topology/material change. */
392
- readonly topologyInvariantEmpty: boolean;
634
+ /** True only when every source draw item/instance has a main GPU candidate. */
635
+ readonly ownsAllDrawItems: boolean;
636
+ /** Concrete entity/material/draw identities claimed by this projection. */
637
+ readonly drawKeys: ReadonlySet<string>;
638
+ /** Extracted concrete ShadowCaster keys actually claimed by this plan. */
639
+ readonly shadowDrawKeys: ReadonlySet<string>;
393
640
  } {
394
641
  const slotByPrimitive = new Map(slots.map((slot) => [slot.slot, slot] as const));
395
642
  const participatingByPrimitive = new Map<number, Set<GpuDrivenBatch>>();
@@ -403,47 +650,79 @@ function filteredPlan(
403
650
  }
404
651
  }
405
652
  }
406
- const entityKeys = new Set<number>();
653
+ const eligibleMainCandidates = new Set<string>();
654
+ const eligibleArtifacts = new Map<string, MaterialShaderArtifact>();
655
+ const drawKeys = new Set<string>();
656
+ const eligibleShadowCandidates = new Set<string>();
657
+ const shadowDrawKeys = new Set<string>();
658
+ const shadowSelectionProvided =
659
+ shadowCasterDrawKeys !== undefined || shadowCasterMembership !== undefined;
407
660
  const worldKeyFor = (slot: PersistentGpuDrivenState['slots'][number]): number =>
408
661
  worldKeys?.[slot.snapshot.worldId] ?? slot.snapshot.worldId;
409
662
  const activeKey = (slot: PersistentGpuDrivenState['slots'][number]): number =>
410
663
  worldEntityKey(worldKeyFor(slot), slot.snapshot.entityKey);
411
- const eligiblePrimitives = new Set<number>();
412
- let topologyEligiblePrimitiveCount = 0;
413
664
  for (const slot of slots) {
414
- const mesh = meshes.get(slot.snapshot.assetHandle);
415
- const participating = participatingByPrimitive.get(slot.slot);
416
- const participatingBatches = participating === undefined ? [] : [...participating];
417
- const topologyEligible =
418
- participatingBatches.length > 0 &&
419
- participatingBatches.length === (slot.snapshot.gpuDrivenDraws?.length ?? 0) &&
420
- participatingBatches.every((batch) => productionEligible(slot.snapshot, batch));
421
- if (topologyEligible) topologyEligiblePrimitiveCount += 1;
422
- if (
423
- mesh !== undefined &&
424
- topologyEligible &&
425
- participatingBatches.every((batch) => meshSupportsBatch(mesh, batch))
426
- ) {
427
- eligiblePrimitives.add(slot.slot);
428
- // Record validation still keys extracted renderables by their current
429
- // stable world/entity key. Suppressed candidates remain in the GPU
430
- // selector, but only admitted entities are owned by the compacted draw.
431
- if (activeEntityKeys === undefined || activeEntityKeys.has(activeKey(slot))) {
432
- entityKeys.add(activeKey(slot));
665
+ const mesh = meshBySlot.get(slot.slot);
666
+ const participating = participatingByPrimitive.get(slot.slot) ?? new Set<GpuDrivenBatch>();
667
+ for (const batch of participating) {
668
+ for (const candidate of batch.candidates) {
669
+ if (candidate.primitiveIndex !== slot.slot) continue;
670
+ const draw = sourceDrawForCandidate(slot, candidate);
671
+ const selectedArtifact = artifactForPreparedCandidate(
672
+ candidate,
673
+ materialArtifacts,
674
+ materialArtifact,
675
+ skinArtifact,
676
+ );
677
+ const mainEligible =
678
+ draw !== undefined &&
679
+ preparedPbrCandidateEligible(slot.snapshot, batch, candidate, selectedArtifact) &&
680
+ mesh !== undefined &&
681
+ meshSupportsBatch(mesh, batch);
682
+ const candidateKey = shadowCandidateKey(candidate);
683
+ if (mainEligible && selectedArtifact !== undefined) {
684
+ eligibleArtifacts.set(candidateKey, selectedArtifact);
685
+ }
686
+ if (!shadowSelectionProvided && mainEligible && draw !== undefined) {
687
+ eligibleMainCandidates.add(candidateKey);
688
+ const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
689
+ drawKeys.add(
690
+ gpuDrivenDrawKey(
691
+ worldEntityKey(worldKeyFor(slot), slot.snapshot.entityKey),
692
+ material.materialHandle ?? -1,
693
+ candidate.drawItemIndex,
694
+ ),
695
+ );
696
+ }
697
+ if (
698
+ shadowSelectionProvided &&
699
+ mainEligible &&
700
+ shadowMembershipIsGpuCompatible(
701
+ slot,
702
+ candidate,
703
+ shadowCasterDrawKeys,
704
+ shadowCasterMembership,
705
+ )
706
+ ) {
707
+ eligibleShadowCandidates.add(shadowCandidateKey(candidate));
708
+ for (const key of shadowKeysForCandidate(
709
+ slot,
710
+ candidate,
711
+ shadowCasterDrawKeys,
712
+ shadowCasterMembership,
713
+ )) {
714
+ shadowDrawKeys.add(key);
715
+ }
716
+ }
433
717
  }
434
718
  }
435
719
  }
436
- // BatchTopology deliberately keeps LOD candidates individually addressable
437
- // so topology updates never alias a candidate's selector state. Once the
438
- // camera has projected the candidates, the renderer can safely coalesce
439
- // candidates that have identical immutable geometry/LOD facts and the same
440
- // selected range. The GPU selector uses the same no-history inputs below,
441
- // so one indirect range remains correct for every candidate in a group.
442
720
  const groups = new Map<
443
721
  string,
444
722
  {
445
723
  readonly source: GpuDrivenBatch;
446
724
  readonly mesh: MeshGpuHandles;
725
+ readonly artifact: MaterialShaderArtifact;
447
726
  readonly renderState: MaterialRenderState | undefined;
448
727
  readonly candidates: GpuDrivenCandidate[];
449
728
  }
@@ -451,23 +730,34 @@ function filteredPlan(
451
730
  for (const batch of source.batches) {
452
731
  const candidates = batch.candidates.filter((candidate) => {
453
732
  const slot = slotByPrimitive.get(candidate.primitiveIndex);
454
- if (slot === undefined || !eligiblePrimitives.has(slot.slot)) return false;
455
- const mesh = meshes.get(slot.snapshot.assetHandle);
733
+ if (slot === undefined) return false;
734
+ if (
735
+ !shadowSelectionProvided
736
+ ? !eligibleMainCandidates.has(shadowCandidateKey(candidate))
737
+ : !eligibleShadowCandidates.has(shadowCandidateKey(candidate))
738
+ )
739
+ return false;
740
+ const mesh = meshBySlot.get(slot.slot);
456
741
  return mesh !== undefined && meshSupportsBatch(mesh, batch);
457
742
  });
458
743
  if (candidates.length === 0) continue;
459
744
  for (const candidate of candidates) {
460
745
  const slot = slotByPrimitive.get(candidate.primitiveIndex);
461
746
  if (slot === undefined) continue;
462
- const mesh = meshes.get(slot.snapshot.assetHandle);
747
+ const mesh = meshBySlot.get(slot.slot);
463
748
  if (mesh === undefined) continue;
749
+ const artifact = eligibleArtifacts.get(shadowCandidateKey(candidate));
750
+ if (artifact === undefined) continue;
464
751
  const physicalLodRanges = mesh.lodRanges
465
752
  ?.slice(1)
466
753
  .map((ranges) => ranges[candidate.drawItemIndex]);
467
754
  const projected = Object.freeze({
468
755
  ...candidate,
469
756
  submitAdmission: activeEntityKeys === undefined || activeEntityKeys.has(activeKey(slot)),
470
- projectedHeight: projectedHeightForCandidate(slot, camera),
757
+ projectedHeight:
758
+ camera === undefined
759
+ ? (candidate.projectedHeight ?? Number.NaN)
760
+ : projectedHeightForCandidate(slot, camera),
471
761
  ...(physicalLodRanges?.every((range) => range !== undefined)
472
762
  ? { lodRanges: physicalLodRanges as NonNullable<typeof candidate.lodRanges> }
473
763
  : {}),
@@ -475,7 +765,23 @@ function filteredPlan(
475
765
  const selectedRange = selectedLodRange(batch, projected);
476
766
  const groupKey = JSON.stringify({
477
767
  key: batch.key,
768
+ artifact: [
769
+ artifact.material,
770
+ artifact.layoutIdentity,
771
+ artifact.receipt?.receiptIdentity ?? '',
772
+ artifact.receipt?.generation ?? 0,
773
+ artifact.variantSet ?? '',
774
+ materialArtifactProgramIdentity(artifact),
775
+ ],
478
776
  worldKey: worldKeyFor(slot),
777
+ bindingClass:
778
+ materialBindingClasses?.get(
779
+ materialBindingKey(
780
+ slot.snapshot,
781
+ (slot.snapshot.materials[batch.key.materialSlot] ?? slot.snapshot.material)
782
+ .materialHandle ?? -1,
783
+ ),
784
+ ) ?? '',
479
785
  lodCoverages: projected.lodCoverages ?? null,
480
786
  lodRanges: projected.lodRanges ?? null,
481
787
  selectedRange,
@@ -487,6 +793,7 @@ function filteredPlan(
487
793
  groups.set(groupKey, {
488
794
  source: batch,
489
795
  mesh,
796
+ artifact,
490
797
  renderState: geometryRenderStateForTopology(batch.key.topology, material.renderState),
491
798
  candidates: [projected],
492
799
  });
@@ -495,7 +802,6 @@ function filteredPlan(
495
802
  }
496
803
  }
497
804
  }
498
-
499
805
  const batches: PreparedBatch[] = [];
500
806
  let visibleBase = 0;
501
807
  const filteredBatches: GpuDrivenBatch[] = [];
@@ -521,35 +827,190 @@ function filteredPlan(
521
827
  });
522
828
  visibleBase += admittedCount;
523
829
  filteredBatches.push(filtered);
524
- const firstCandidate = group.candidates[0];
830
+ // Keep every selector batch id unique even when no current entity is
831
+ // admitted to raster. The zero-admission batch still feeds LOD telemetry,
832
+ // while the following raster batch must not reuse its indirect slot.
833
+ batchId += 1;
834
+ // A selector batch can retain facet-suppressed candidates for LOD
835
+ // telemetry. Raster ownership, however, must be represented by an
836
+ // admitted candidate; using the first retained row can point material
837
+ // routing at an entity that the main-pass producer intentionally omitted.
838
+ const firstCandidate = group.candidates.find(
839
+ (candidate) =>
840
+ (candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
841
+ .submitAdmission !== false,
842
+ );
525
843
  const firstSlot =
526
844
  firstCandidate === undefined ? undefined : slotByPrimitive.get(firstCandidate.primitiveIndex);
527
845
  if (firstCandidate === undefined || firstSlot === undefined) continue;
528
846
  batches.push({
529
847
  batch: filtered,
530
848
  mesh: group.mesh,
849
+ artifact: group.artifact,
531
850
  renderState: group.renderState,
532
851
  worldKey: worldKeyFor(firstSlot),
533
852
  primitiveSlot: firstSlot.slot,
534
853
  slotGeneration: firstSlot.generation,
535
854
  });
536
- batchId += 1;
537
855
  }
856
+ const ownershipSlots =
857
+ activeEntityKeys === undefined
858
+ ? slots
859
+ : slots.filter((slot) => activeEntityKeys.has(activeKey(slot)));
860
+ const ownsAllDrawItems =
861
+ !shadowSelectionProvided &&
862
+ ownershipSlots.every((slot) => {
863
+ const draws = slot.snapshot.gpuDrivenDraws ?? [];
864
+ if (draws.length === 0) return false;
865
+ const instanceCount = slot.snapshot.instances?.instanceCount ?? 1;
866
+ const candidates = source.batches.flatMap((batch) =>
867
+ batch.candidates.filter((candidate) => candidate.primitiveIndex === slot.slot),
868
+ );
869
+ return (
870
+ candidates.length === draws.length * instanceCount &&
871
+ candidates.every((candidate) => {
872
+ if (!eligibleMainCandidates.has(shadowCandidateKey(candidate))) return false;
873
+ const draw = sourceDrawForCandidate(slot, candidate);
874
+ if (draw === undefined) return false;
875
+ const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
876
+ return drawKeys.has(
877
+ gpuDrivenDrawKey(
878
+ worldEntityKey(worldKeyFor(slot), slot.snapshot.entityKey),
879
+ material.materialHandle ?? -1,
880
+ candidate.drawItemIndex,
881
+ ),
882
+ );
883
+ })
884
+ );
885
+ });
538
886
  return {
539
887
  plan: Object.freeze({
540
888
  revision: source.revision,
889
+ payloadRevision: source.payloadRevision,
541
890
  batches: Object.freeze(filteredBatches),
542
891
  candidateCount: filteredBatches.reduce((total, batch) => total + batch.candidates.length, 0),
543
892
  visibleCapacity: visibleBase,
544
893
  }),
545
894
  batches: Object.freeze(batches),
546
- entityKeys,
547
- topologyInvariantEmpty: topologyEligiblePrimitiveCount === 0,
895
+ ownsAllDrawItems,
896
+ drawKeys,
897
+ shadowDrawKeys,
548
898
  };
549
899
  }
550
900
 
901
+ function shadowCasterMembershipSignature(
902
+ drawKeys: ReadonlySet<string> | undefined,
903
+ membership: readonly ShadowCasterMembership[] | undefined,
904
+ ): string {
905
+ const keySignature = drawKeys === undefined ? '*' : [...drawKeys].sort().join(',');
906
+ if (membership === undefined) return keySignature;
907
+ const membershipSignature = membership
908
+ .map((entry) =>
909
+ [
910
+ entry.worldEntity,
911
+ entry.renderableIndex,
912
+ entry.drawItemIndex,
913
+ entry.materialHandle,
914
+ entry.passIndex,
915
+ entry.materialShaderId ?? '',
916
+ JSON.stringify(entry.renderState ?? null),
917
+ entry.cpuReason ?? '',
918
+ entry.gpuDrivenEligible === undefined ? '' : String(entry.gpuDrivenEligible),
919
+ ].join(':'),
920
+ )
921
+ .sort()
922
+ .join(',');
923
+ return `${keySignature}|membership=${membershipSignature}`;
924
+ }
925
+
551
926
  type FilteredProductionPlan = ReturnType<typeof filteredPlan>;
552
927
 
928
+ function sceneRequiresGpuDrivenArtifact(scene: PersistentGpuDrivenState): boolean {
929
+ return scene.plan.batches.some((batch) =>
930
+ batch.candidates.some((candidate) => {
931
+ return (
932
+ candidate.prepared?.receiptIdentity !== undefined &&
933
+ (candidate.prepared?.identity.deformation === 'rigid' ||
934
+ candidate.prepared?.identity.deformation === 'skin')
935
+ );
936
+ }),
937
+ );
938
+ }
939
+
940
+ function missingMaterialArtifact(
941
+ scene: PersistentGpuDrivenState,
942
+ artifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined,
943
+ rigidFallback: MaterialShaderArtifact | undefined,
944
+ skinFallback: MaterialShaderArtifact | undefined,
945
+ ): GpuDrivenPreparationError | undefined {
946
+ for (const batch of scene.plan.batches) {
947
+ for (const candidate of batch.candidates) {
948
+ const prepared = candidate.prepared;
949
+ if (prepared?.receiptIdentity === undefined) continue;
950
+ if (
951
+ artifactForPreparedCandidate(candidate, artifacts, rigidFallback, skinFallback) !==
952
+ undefined
953
+ )
954
+ continue;
955
+ return new GpuDrivenPreparationError('missing-material-receipt', {
956
+ reason: 'material-receipt-missing',
957
+ owner: 'material',
958
+ expected: `published artifact for ${prepared.identity.material} (${prepared.receiptIdentity}@${prepared.receiptGeneration})`,
959
+ });
960
+ }
961
+ }
962
+ return undefined;
963
+ }
964
+
965
+ function nonCpuPreparationFailure(
966
+ scene: PersistentGpuDrivenState,
967
+ ): { readonly error: GpuDrivenPreparationError; readonly drawKeys: readonly string[] } | undefined {
968
+ const drawKeys: string[] = [];
969
+ let first: GpuDrivenPreparationError | undefined;
970
+ for (const slot of scene.slots) {
971
+ const draws = slot.snapshot.gpuDrivenDraws ?? [];
972
+ for (const [compactIndex, draw] of draws.entries()) {
973
+ const error = draw.preparationError;
974
+ if (error === undefined || error.detail.recovery === 'route-cpu-lane') continue;
975
+ const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
976
+ drawKeys.push(
977
+ gpuDrivenDrawKey(
978
+ worldEntityKey(slot.snapshot.worldId, slot.snapshot.entityKey),
979
+ material.materialHandle ?? -1,
980
+ gpuDrivenSourceDrawItemIndex(draw, compactIndex),
981
+ ),
982
+ );
983
+ first ??= error;
984
+ }
985
+ }
986
+ return first === undefined ? undefined : { error: first, drawKeys };
987
+ }
988
+
989
+ function preparationFailureInspection(
990
+ error: GpuDrivenPreparationError,
991
+ ): GpuDrivenPreparationFailureInspection {
992
+ return Object.freeze({
993
+ code: error.code,
994
+ expected: error.expected,
995
+ hint: error.hint,
996
+ detail: error.detail,
997
+ });
998
+ }
999
+
1000
+ function shadowOwnershipFailureInspection(
1001
+ viewPass: GpuDrivenLaneSummary['viewPass'],
1002
+ ): GpuDrivenPreparationFailureInspection {
1003
+ return preparationFailureInspection(
1004
+ new GpuDrivenPreparationError('shadow-ownership', {
1005
+ reason: 'shadow-ownership-missing',
1006
+ owner: 'shadow',
1007
+ expected: 'one GPU-compatible owner for every declared ShadowCaster pass',
1008
+ actual: `${viewPass} contains a residual ShadowCaster draw item`,
1009
+ recovery: 'route-cpu-lane',
1010
+ }),
1011
+ );
1012
+ }
1013
+
553
1014
  export interface GpuDrivenWorldLodSelectionInspection {
554
1015
  readonly worldKey: number;
555
1016
  readonly primitiveSlot: number;
@@ -570,302 +1031,537 @@ function sameEntityKeySet(
570
1031
  return true;
571
1032
  }
572
1033
 
573
- class GpuDrivenRigidRaster {
574
- private readonly pipelines = new Map<string, RenderPipeline>();
575
-
576
- private constructor(
577
- private readonly device: RhiDevice,
578
- private readonly layout: BindGroupLayout,
579
- private readonly pipelineLayout: import('@forgeax/engine-rhi').PipelineLayout,
580
- private readonly module: ShaderModule,
581
- ) {}
1034
+ type MaterialPipelineFactory = NonNullable<RenderSystemInternals['getMaterialShaderPipelineEntry']>;
582
1035
 
583
- static create(input: {
584
- readonly device: RhiDevice;
585
- readonly shaderModuleFactory: PipelineBuilderShaderModuleFactory;
586
- readonly viewBindGroupLayout: BindGroupLayout;
587
- }): Result<GpuDrivenRigidRaster, RhiError> {
588
- const layout = input.device.createBindGroupLayout({
589
- label: 'gpu-driven-rigid-raster-bgl',
590
- entries: [
591
- { binding: 0, visibility: VERTEX_STAGE, buffer: { type: 'read-only-storage' } },
592
- {
593
- binding: 1,
594
- visibility: VERTEX_STAGE,
595
- buffer: { type: 'read-only-storage' },
596
- },
597
- {
598
- binding: 2,
599
- visibility: VERTEX_STAGE | FRAGMENT_STAGE,
600
- buffer: { type: 'read-only-storage' },
601
- },
602
- { binding: 3, visibility: VERTEX_STAGE, buffer: { type: 'read-only-storage' } },
603
- { binding: 4, visibility: VERTEX_STAGE, buffer: { type: 'read-only-storage' } },
604
- ],
605
- });
606
- if (!layout.ok) return layout;
607
- const pipelineLayout = input.device.createPipelineLayout({
608
- label: 'gpu-driven-rigid-raster-pl',
609
- bindGroupLayouts: [input.viewBindGroupLayout, layout.value],
610
- });
611
- if (!pipelineLayout.ok) return pipelineLayout;
612
- const module = input.shaderModuleFactory.createShaderModule({
613
- label: 'gpu-driven-rigid-unlit',
614
- code: GPU_DRIVEN_RIGID_UNLIT_WGSL,
615
- });
616
- if (!module.ok) return module;
617
- return ok(
618
- new GpuDrivenRigidRaster(input.device, layout.value, pipelineLayout.value, module.value),
619
- );
620
- }
1036
+ function artifactForPreparedCandidate(
1037
+ candidate: GpuDrivenBatch['candidates'][number],
1038
+ artifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined,
1039
+ rigidFallback: MaterialShaderArtifact | undefined,
1040
+ skinFallback: MaterialShaderArtifact | undefined,
1041
+ ): MaterialShaderArtifact | undefined {
1042
+ const prepared = candidate.prepared;
1043
+ if (prepared === undefined) return undefined;
1044
+ const exact = artifacts?.get(
1045
+ gpuDrivenMaterialArtifactKey({
1046
+ material: prepared.identity.material,
1047
+ deformation: prepared.identity.deformation,
1048
+ receiptIdentity: prepared.receiptIdentity,
1049
+ receiptGeneration: prepared.receiptGeneration,
1050
+ }),
1051
+ );
1052
+ if (exact !== undefined) return exact;
1053
+ // A map supplied by the real record path is authoritative. A receipt-backed
1054
+ // draw missing from it is a producer/load failure, not permission to pair
1055
+ // the draw with another material's artifact.
1056
+ if (artifacts !== undefined && prepared.receiptIdentity !== undefined) return undefined;
1057
+ const fallback = prepared.identity.deformation === 'skin' ? skinFallback : rigidFallback;
1058
+ return fallback?.material === prepared.identity.material ? fallback : undefined;
1059
+ }
621
1060
 
622
- pipeline(
623
- format: TextureFormat,
624
- sampleCount: 1 | 4,
625
- vertexStride: number,
626
- topology: GpuDrivenBatch['key']['topology'],
627
- stripIndexFormat: 'uint16' | 'uint32' | undefined,
628
- renderState: MaterialRenderState | undefined,
629
- ): Result<RenderPipeline, RhiError> {
630
- const key = `${format}|${sampleCount}|${vertexStride}|${topology}|${stripIndexFormat ?? ''}|${JSON.stringify(renderState ?? null)}`;
631
- const cached = this.pipelines.get(key);
632
- if (cached !== undefined) return ok(cached);
633
- const created = this.device.createRenderPipeline({
634
- label: `gpu-driven-rigid-unlit.${key}`,
635
- layout: this.pipelineLayout,
636
- vertex: {
637
- module: this.module,
638
- entryPoint: 'vs_main',
639
- buffers: [
640
- {
641
- arrayStride: vertexStride,
642
- stepMode: 'vertex',
643
- attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }],
644
- },
645
- ],
646
- },
647
- fragment: {
648
- module: this.module,
649
- entryPoint: 'fs_main',
650
- targets: [
651
- {
652
- format,
653
- ...(renderState?.blend === undefined ? {} : { blend: renderState.blend }),
654
- },
655
- ],
656
- },
657
- primitive: {
658
- topology,
659
- cullMode: renderState?.cullMode ?? 'back',
660
- frontFace: renderState?.frontFace ?? 'ccw',
661
- ...(stripIndexFormat === undefined ? {} : { stripIndexFormat }),
662
- },
663
- depthStencil: {
664
- format: 'depth24plus-stencil8',
665
- depthWriteEnabled: renderState?.depthWriteEnabled ?? true,
666
- depthCompare: renderState?.depthCompare ?? 'less',
667
- ...(renderState?.stencil === undefined
668
- ? {}
669
- : {
670
- stencilFront: renderState.stencil,
671
- stencilBack: renderState.stencil,
672
- stencilReadMask: renderState.stencilReadMask,
673
- stencilWriteMask: renderState.stencilWriteMask,
674
- }),
675
- },
676
- ...(sampleCount === 4
677
- ? {
678
- multisample: {
679
- count: 4,
680
- alphaToCoverageEnabled: renderState?.alphaToCoverageEnabled ?? false,
681
- },
682
- }
683
- : {}),
684
- });
685
- if (!created.ok) return created;
686
- this.pipelines.set(key, created.value);
687
- return created;
688
- }
1061
+ function shadowArtifactForPreparedCandidate(
1062
+ candidate: GpuDrivenBatch['candidates'][number],
1063
+ artifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined,
1064
+ fallback: MaterialShaderArtifact | undefined,
1065
+ ): MaterialShaderArtifact | undefined {
1066
+ const prepared = candidate.prepared;
1067
+ if (prepared === undefined) return undefined;
1068
+ const exact = artifacts?.get(
1069
+ gpuDrivenMaterialArtifactKey({
1070
+ material: prepared.identity.material,
1071
+ deformation: prepared.identity.deformation,
1072
+ receiptIdentity: prepared.receiptIdentity,
1073
+ receiptGeneration: prepared.receiptGeneration,
1074
+ }),
1075
+ );
1076
+ if (exact !== undefined) return exact;
1077
+ // Direct production fixtures predate the separate shadow publication map.
1078
+ // They are allowed to reuse the already validated artifact; a real frame
1079
+ // passes the authoritative map and therefore fails closed on a missing
1080
+ // shadow program instead of silently selecting the forward program.
1081
+ return artifacts === undefined ? fallback : undefined;
1082
+ }
689
1083
 
690
- bindGroup(
691
- scene: PersistentGpuDrivenState['scene'],
692
- view: GpuDrivenView,
693
- batch: GpuDrivenBatch,
694
- ): Result<BindGroup, RhiError> {
695
- const visible = view.visibleBuffer;
696
- if (visible === undefined) {
697
- return err(
698
- new RhiError({
699
- code: 'internal-error',
700
- expected: 'GPU-driven view buffers exist after a successful view update',
701
- hint: 'prepare the GPU-driven view before projecting its raster batches',
702
- }),
703
- );
704
- }
705
- return this.device.createBindGroup({
706
- label: `gpu-driven-rigid-batch-${batch.batchId}`,
707
- layout: this.layout,
708
- entries: [
709
- { binding: 0, resource: bufferBinding(scene.primitiveBuffer) },
710
- { binding: 1, resource: bufferBinding(scene.instanceBuffer) },
711
- { binding: 2, resource: bufferBinding(scene.materialBuffer) },
712
- {
713
- binding: 3,
714
- resource: bufferBinding(visible, batch.visibleBase * 8, batch.visibleCapacity * 8),
715
- },
716
- {
717
- binding: 4,
718
- resource: bufferBinding(scene.transformBuffer),
719
- },
720
- ],
721
- });
722
- }
1084
+ function materialArtifactSignature(
1085
+ artifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined,
1086
+ rigidFallback: MaterialShaderArtifact | undefined,
1087
+ skinFallback: MaterialShaderArtifact | undefined,
1088
+ ): string {
1089
+ const rows =
1090
+ artifacts === undefined
1091
+ ? []
1092
+ : [...artifacts.entries()]
1093
+ .map(([key, artifact]) => [
1094
+ key,
1095
+ artifact.material,
1096
+ artifact.layoutIdentity,
1097
+ artifact.receipt?.receiptIdentity ?? '',
1098
+ artifact.receipt?.generation ?? 0,
1099
+ artifact.variantSet ?? '',
1100
+ materialArtifactProgramIdentity(artifact),
1101
+ artifact.vertexEntry ?? artifact.receipt?.sceneIndexEntry ?? '',
1102
+ artifact.fragmentEntry ?? '',
1103
+ ])
1104
+ .sort((left, right) => String(left[0] ?? '').localeCompare(String(right[0] ?? '')));
1105
+ return JSON.stringify({
1106
+ rows,
1107
+ rigid:
1108
+ rigidFallback === undefined
1109
+ ? undefined
1110
+ : [
1111
+ rigidFallback.material,
1112
+ rigidFallback.layoutIdentity,
1113
+ rigidFallback.receipt?.receiptIdentity ?? '',
1114
+ rigidFallback.receipt?.generation ?? 0,
1115
+ rigidFallback.variantSet ?? '',
1116
+ materialArtifactProgramIdentity(rigidFallback),
1117
+ rigidFallback.vertexEntry ?? rigidFallback.receipt?.sceneIndexEntry ?? '',
1118
+ rigidFallback.fragmentEntry ?? '',
1119
+ ],
1120
+ skin:
1121
+ skinFallback === undefined
1122
+ ? undefined
1123
+ : [
1124
+ skinFallback.material,
1125
+ skinFallback.layoutIdentity,
1126
+ skinFallback.receipt?.receiptIdentity ?? '',
1127
+ skinFallback.receipt?.generation ?? 0,
1128
+ skinFallback.variantSet ?? '',
1129
+ materialArtifactProgramIdentity(skinFallback),
1130
+ skinFallback.vertexEntry ?? skinFallback.receipt?.sceneIndexEntry ?? '',
1131
+ skinFallback.fragmentEntry ?? '',
1132
+ ],
1133
+ });
723
1134
  }
724
1135
 
725
1136
  /** The GPU-driven bridge to the existing four-group Standard PBR pipeline. */
726
- class StandardPbrRasterAdapter {
1137
+ class MaterialAbiRasterAdapter {
727
1138
  private readonly pipelines = new Map<string, RenderPipeline>();
728
- private readonly projectionBuffers: Buffer[] = [];
1139
+ private readonly projectionBuffers = new Set<Buffer>();
1140
+ private readonly projectionBindings = new Map<
1141
+ string,
1142
+ {
1143
+ readonly buffer: Buffer;
1144
+ readonly bindGroup: BindGroup;
1145
+ readonly capacityBytes: number;
1146
+ rows: Float32Array;
1147
+ }
1148
+ >();
1149
+ /**
1150
+ * Projection rows are graph-generation resources. A new topology may stage
1151
+ * a replacement before the graph is promoted; keeping staged bindings out
1152
+ * of the active map lets an aborted graph leave the last submitted rows
1153
+ * intact. Superseded buffers are fence-retired on promotion.
1154
+ */
1155
+ private readonly stagedProjectionBindings = new Map<
1156
+ string,
1157
+ {
1158
+ readonly buffer: Buffer;
1159
+ readonly bindGroup: BindGroup;
1160
+ readonly capacityBytes: number;
1161
+ rows: Float32Array;
1162
+ }
1163
+ >();
1164
+ private readonly pendingProjectionRetirements = new Set<Buffer>();
1165
+ private readonly destroyedProjectionBuffers = new Set<Buffer>();
1166
+ private projectionDisposed = false;
1167
+ private stagedProjectionKeys = new Set<string>();
1168
+ private readonly activeProjectionRowsAtStage = new Map<string, Float32Array>();
1169
+ private readonly visibleWindowBindGroups = new Map<Buffer, Map<string, BindGroup>>();
729
1170
 
730
1171
  private constructor(
731
1172
  private readonly device: RhiDevice,
732
1173
  private readonly pipelineLayout: import('@forgeax/engine-rhi').PipelineLayout,
733
1174
  private readonly meshLayout: BindGroupLayout,
734
- private readonly module: ShaderModule,
735
- private readonly materialBindGroup: BindGroup,
736
- private readonly instancesBindGroup: BindGroup,
1175
+ private readonly instancesBindGroupLayout: BindGroupLayout,
1176
+ private readonly programs: ReadonlyMap<string, GpuDrivenPbrProgram> | undefined,
1177
+ private instancesBindGroup: BindGroup,
1178
+ private readonly instanceBuffer: Buffer,
1179
+ private readonly instanceBufferSize: number,
1180
+ private visibleBuffer: Buffer,
737
1181
  private readonly artifact: MaterialShaderArtifact,
1182
+ private readonly deformation: 'rigid' | 'skin',
1183
+ private readonly pipelineFactory: MaterialPipelineFactory | undefined,
1184
+ private paletteBuffer: Buffer | undefined,
1185
+ private paletteBindingWindowBytes: number | undefined,
738
1186
  ) {}
739
1187
 
740
1188
  static create(input: {
741
1189
  readonly device: RhiDevice;
742
- readonly shaderModuleFactory: PipelineBuilderShaderModuleFactory;
743
1190
  readonly pipelineState: PipelineState;
744
1191
  readonly artifact: MaterialShaderArtifact;
745
- }): Result<StandardPbrRasterAdapter, RhiError> {
746
- const pipelineLayout = input.pipelineState.pbrPipelineLayout;
747
- const fallback = input.pipelineState.skylightFallback;
748
- if (pipelineLayout === null || fallback === null) {
1192
+ readonly visibleBuffer: Buffer;
1193
+ readonly deformation?: 'rigid' | 'skin';
1194
+ readonly paletteBuffer?: Buffer;
1195
+ readonly paletteBindingWindowBytes?: number;
1196
+ /** Renderer-owned factory for the selected artifact's actual PSO. */
1197
+ readonly pipelineFactory?: MaterialPipelineFactory;
1198
+ }): Result<MaterialAbiRasterAdapter, RhiError | GpuDrivenPreparationError> {
1199
+ const deformation = input.deformation ?? 'rigid';
1200
+ const receipt = input.artifact.receipt;
1201
+ if (receipt === undefined) {
749
1202
  return err(
750
1203
  new RhiError({
751
1204
  code: 'rhi-not-available',
752
- expected: 'Standard PBR pipeline layout and Skylight fallback',
753
- hint: 'initialize the Standard PBR pipeline before enabling GPU-driven raster',
1205
+ expected: 'published MaterialProgramAbi receipt for the selected material artifact',
1206
+ hint: 'recook and publish the material before creating a GPU-driven raster owner',
754
1207
  }),
755
1208
  );
756
1209
  }
757
- const module = input.shaderModuleFactory.createShaderModule({
758
- label: 'standard-pbr-gpu-driven-scene-index',
759
- code: input.artifact.wgsl,
760
- });
761
- if (!module.ok) return module;
762
- const materialBuffer = input.device.createBuffer({
763
- label: 'standard-pbr-gpu-driven-material-row',
764
- size: 384,
765
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
766
- });
767
- if (!materialBuffer.ok) return materialBuffer;
768
- const materialPayload = new Float32Array(96);
769
- materialPayload[0] = 1;
770
- materialPayload[1] = 1;
771
- materialPayload[2] = 1;
772
- materialPayload[3] = 1;
773
- materialPayload[5] = 1;
774
- materialPayload[6] = 1;
775
- const materialWrite = input.device.queue.writeBuffer(materialBuffer.value, 0, materialPayload);
776
- if (!materialWrite.ok) return materialWrite;
777
- const baseEntries = [
778
- { binding: 0, resource: bufferBinding(materialBuffer.value, 0, 384) },
779
- ...Array.from({ length: 8 }, (_, index) => [
780
- {
781
- binding: index * 2 + 1,
782
- resource: { kind: 'sampler' as const, value: fallback.sampler },
783
- },
784
- {
785
- binding: index * 2 + 2,
786
- resource: {
787
- kind: 'textureView' as const,
788
- value:
789
- index === 2
790
- ? input.pipelineState.defaultNormalTextureView
791
- : input.pipelineState.defaultWhiteTextureView,
792
- },
793
- },
794
- ]).flat(),
795
- ];
796
- const materialEntries = assembleMaterialWithSkylightEntries(
797
- baseEntries,
798
- {
799
- irradianceView: fallback.irradianceView,
800
- irradianceSampler: fallback.sampler,
801
- prefilterView: fallback.prefilterView,
802
- prefilterSampler: fallback.sampler,
803
- brdfLutView: fallback.brdfLutView,
804
- brdfLutSampler: fallback.sampler,
805
- intensityBuffer: fallback.intensityBuffer,
806
- },
807
- { sampler: fallback.sampler, backdropView: input.pipelineState.defaultWhiteTextureView },
808
- );
809
- const materialBindGroup = input.device.createBindGroup({
810
- label: 'standard-pbr-gpu-driven-material-bg',
811
- layout: input.pipelineState.materialBindGroupLayout,
812
- entries: materialEntries,
813
- });
814
- if (!materialBindGroup.ok) return materialBindGroup;
815
- const instancesBindGroup = input.device.createBindGroup({
816
- label: 'standard-pbr-gpu-driven-identity-instances-bg',
817
- layout: input.pipelineState.instancesBindGroupLayout,
818
- entries: [
819
- {
820
- binding: 0,
821
- resource: bufferBinding(input.pipelineState.identityInstanceBuffer),
822
- },
823
- ],
824
- });
825
- if (!instancesBindGroup.ok) return instancesBindGroup;
826
- return ok(
827
- new StandardPbrRasterAdapter(
828
- input.device,
829
- pipelineLayout,
830
- input.pipelineState.meshBindGroupLayout,
831
- module.value,
832
- materialBindGroup.value,
1210
+ const materialRowMatchesSceneTable =
1211
+ receipt.materialRow.byteLength === GPU_SCENE_LAYOUTS.material.stride &&
1212
+ new Set(receipt.materialRow.fields).size === receipt.materialRow.fields.length;
1213
+ if (!materialRowMatchesSceneTable) {
1214
+ return err(
1215
+ new GpuDrivenPreparationError('material-abi-not-executable', {
1216
+ reason: 'material-abi-not-executable',
1217
+ owner: 'material',
1218
+ expected: `scene-index material row ${GPU_SCENE_LAYOUTS.material.stride} bytes with unique producer fields`,
1219
+ actual: `row ${receipt.materialRow.byteLength} bytes with fields ${receipt.materialRow.fields.join(',')}`,
1220
+ }),
1221
+ );
1222
+ }
1223
+ if (
1224
+ receipt.reflection.layoutIdentity !== input.artifact.layoutIdentity ||
1225
+ receipt.directEntry.length === 0 ||
1226
+ receipt.sceneIndexEntry.length === 0 ||
1227
+ receipt.resourceSlots.some((slot) => slot.group !== 1)
1228
+ ) {
1229
+ return err(
1230
+ new RhiError({
1231
+ code: 'rhi-descriptor-invalid',
1232
+ expected: `complete selected MaterialProgramAbi reflection, scene row stride ${GPU_SCENE_LAYOUTS.material.stride}, and resource slots`,
1233
+ hint: 'repair the producer publication or use the owner for its non-scene material layout',
1234
+ }),
1235
+ );
1236
+ }
1237
+ // Scene-index shaders declare the dedicated slot-3 ABI. Keep the legacy
1238
+ // fields as a compatibility fallback for synthetic fixtures; the real
1239
+ // factory publishes the dedicated handles whenever the capable storage
1240
+ // lane is enabled, so production draws never bind the direct-draw BGL.
1241
+ const pipelineLayout =
1242
+ deformation === 'skin'
1243
+ ? input.pipelineState.gpuDrivenPbrSkinPipelineLayout
1244
+ : input.pipelineState.gpuDrivenPbrPipelineLayout;
1245
+ const instancesBindGroupLayout = input.pipelineState.gpuDrivenInstancesBindGroupLayout;
1246
+ if (pipelineLayout == null || instancesBindGroupLayout == null) {
1247
+ return err(
1248
+ new RhiError({
1249
+ code: 'rhi-not-available',
1250
+ expected: `${deformation === 'skin' ? 'skinned ' : ''}published material pipeline layout`,
1251
+ hint: 'initialize the producer material pipeline before enabling GPU-driven raster',
1252
+ }),
1253
+ );
1254
+ }
1255
+ const programs = input.pipelineState.gpuDrivenPbrPrograms;
1256
+ if (
1257
+ input.pipelineFactory === undefined &&
1258
+ (programs === undefined ||
1259
+ resolveStandardPbrProgram(
1260
+ programs,
1261
+ input.artifact.material,
1262
+ input.artifact.receipt?.vertexInputs.some((attribute) => attribute.semantic === 'color'),
1263
+ ) === undefined)
1264
+ ) {
1265
+ return err(
1266
+ new RhiError({
1267
+ code: 'rhi-not-available',
1268
+ expected: `producer-owned ${deformation === 'skin' ? 'skinned ' : ''}material shader module for the current device generation`,
1269
+ hint: 'publish the selected material module from buildReadyWebGPU before enabling GPU-driven raster',
1270
+ }),
1271
+ );
1272
+ }
1273
+ const meshLayout =
1274
+ deformation === 'skin'
1275
+ ? input.pipelineState.pbrSkinMeshBindGroupLayout
1276
+ : input.pipelineState.meshBindGroupLayout;
1277
+ if (meshLayout === null) {
1278
+ return err(
1279
+ new RhiError({
1280
+ code: 'rhi-not-available',
1281
+ expected: `${deformation === 'skin' ? 'skinned ' : ''}material mesh bind-group layout`,
1282
+ hint: 'publish the exact mesh-array layout selected by the material receipt',
1283
+ }),
1284
+ );
1285
+ }
1286
+ if (
1287
+ deformation === 'skin' &&
1288
+ (input.paletteBuffer === undefined || input.paletteBindingWindowBytes === undefined)
1289
+ ) {
1290
+ return err(
1291
+ new RhiError({
1292
+ code: 'rhi-not-available',
1293
+ expected: 'persistent skin palette buffer and binding window',
1294
+ hint: 'keep the palette receipt and the GPU-driven skinned projection on one frame identity',
1295
+ }),
1296
+ );
1297
+ }
1298
+ const identity = new Float32Array(16);
1299
+ identity[0] = 1;
1300
+ identity[5] = 1;
1301
+ identity[10] = 1;
1302
+ identity[15] = 1;
1303
+ const instancePayload = packInstanceStorageBuffer(identity);
1304
+ const instanceBuffer = input.device.createBuffer({
1305
+ label: 'standard-pbr-gpu-driven-instance-data',
1306
+ size: instancePayload.byteLength,
1307
+ usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
1308
+ });
1309
+ if (!instanceBuffer.ok) return instanceBuffer;
1310
+ const instanceWrite = input.device.queue.writeBuffer(instanceBuffer.value, 0, instancePayload);
1311
+ if (!instanceWrite.ok) return instanceWrite;
1312
+ const instancesBindGroup = input.device.createBindGroup({
1313
+ label: 'standard-pbr-gpu-driven-visible-instances-bg',
1314
+ layout: instancesBindGroupLayout,
1315
+ entries: [
1316
+ {
1317
+ binding: 0,
1318
+ resource: bufferBinding(instanceBuffer.value, 0, instancePayload.byteLength),
1319
+ },
1320
+ {
1321
+ binding: 1,
1322
+ resource: bufferBinding(input.visibleBuffer),
1323
+ },
1324
+ {
1325
+ binding: 2,
1326
+ resource: bufferBinding(input.visibleBuffer),
1327
+ },
1328
+ ],
1329
+ });
1330
+ if (!instancesBindGroup.ok) return instancesBindGroup;
1331
+ return ok(
1332
+ new MaterialAbiRasterAdapter(
1333
+ input.device,
1334
+ pipelineLayout,
1335
+ meshLayout,
1336
+ instancesBindGroupLayout,
1337
+ programs,
833
1338
  instancesBindGroup.value,
1339
+ instanceBuffer.value,
1340
+ instancePayload.byteLength,
1341
+ input.visibleBuffer,
834
1342
  input.artifact,
1343
+ deformation,
1344
+ input.pipelineFactory,
1345
+ input.paletteBuffer,
1346
+ input.paletteBindingWindowBytes,
835
1347
  ),
836
1348
  );
837
1349
  }
838
1350
 
1351
+ ensurePaletteBuffer(
1352
+ paletteBuffer: Buffer | undefined,
1353
+ paletteBindingWindowBytes: number | undefined,
1354
+ ): Result<void, RhiError> {
1355
+ if (this.deformation !== 'skin') return ok(undefined);
1356
+ if (paletteBuffer === undefined || paletteBindingWindowBytes === undefined) {
1357
+ return err(
1358
+ new RhiError({
1359
+ code: 'rhi-not-available',
1360
+ expected: 'persistent skin palette buffer and binding window',
1361
+ hint: 'publish a storage-backed SkinPaletteReceipt before the GPU-driven skinned lane',
1362
+ }),
1363
+ );
1364
+ }
1365
+ this.paletteBuffer = paletteBuffer;
1366
+ this.paletteBindingWindowBytes = paletteBindingWindowBytes;
1367
+ return ok(undefined);
1368
+ }
1369
+
1370
+ ensureVisibleBuffer(visibleBuffer: Buffer): Result<void, RhiError> {
1371
+ if (this.visibleBuffer === visibleBuffer) return ok(undefined);
1372
+ const created = this.device.createBindGroup({
1373
+ label: 'standard-pbr-gpu-driven-visible-instances-bg',
1374
+ layout: this.instancesBindGroupLayout,
1375
+ entries: [
1376
+ {
1377
+ binding: 0,
1378
+ resource: bufferBinding(this.instanceBuffer, 0, this.instanceBufferSize),
1379
+ },
1380
+ { binding: 1, resource: bufferBinding(visibleBuffer) },
1381
+ { binding: 2, resource: bufferBinding(visibleBuffer) },
1382
+ ],
1383
+ });
1384
+ if (!created.ok) return created;
1385
+ this.instancesBindGroup = created.value;
1386
+ this.visibleBuffer = visibleBuffer;
1387
+ return ok(undefined);
1388
+ }
1389
+
1390
+ visibleWindowBindGroup(
1391
+ visibleBase: number,
1392
+ visibleCapacity: number,
1393
+ ): Result<BindGroup, RhiError> {
1394
+ return this.visibleWindowBindGroupForBuffer(this.visibleBuffer, visibleBase, visibleCapacity);
1395
+ }
1396
+
1397
+ visibleWindowBindGroupForBuffer(
1398
+ visibleBuffer: Buffer,
1399
+ visibleBase: number,
1400
+ visibleCapacity: number,
1401
+ ): Result<BindGroup, RhiError> {
1402
+ const offset = visibleBase * 8;
1403
+ const size = visibleCapacity * 8;
1404
+ if (offset % 256 !== 0 || size <= 0) {
1405
+ return err(
1406
+ new RhiError({
1407
+ code: 'rhi-descriptor-invalid',
1408
+ expected: '256-byte-aligned non-empty visible window',
1409
+ hint: `allocate each GPU-driven batch visible window on the portable storage-buffer boundary (base=${visibleBase}, capacity=${visibleCapacity}, offset=${offset}, size=${size})`,
1410
+ }),
1411
+ );
1412
+ }
1413
+ let groups = this.visibleWindowBindGroups.get(visibleBuffer);
1414
+ if (groups === undefined) {
1415
+ groups = new Map();
1416
+ this.visibleWindowBindGroups.set(visibleBuffer, groups);
1417
+ }
1418
+ const key = `${visibleBase}:${visibleCapacity}`;
1419
+ const cached = groups.get(key);
1420
+ if (cached !== undefined) return ok(cached);
1421
+ const created = this.device.createBindGroup({
1422
+ label: `standard-pbr-gpu-driven-visible-window-${visibleBase}`,
1423
+ layout: this.instancesBindGroupLayout,
1424
+ entries: [
1425
+ {
1426
+ binding: 0,
1427
+ resource: bufferBinding(this.instanceBuffer, 0, this.instanceBufferSize),
1428
+ },
1429
+ { binding: 1, resource: bufferBinding(visibleBuffer, offset, size) },
1430
+ { binding: 2, resource: bufferBinding(visibleBuffer, offset, size) },
1431
+ ],
1432
+ });
1433
+ if (!created.ok) return created;
1434
+ groups.set(key, created.value);
1435
+ return ok(created.value);
1436
+ }
1437
+
839
1438
  pipeline(
840
1439
  format: TextureFormat,
841
1440
  sampleCount: 1 | 4,
842
1441
  layoutProjection: MeshGpuHandles['layoutProjection'],
1442
+ shaderUvSetCount: number,
843
1443
  topology: GpuDrivenBatch['key']['topology'],
844
1444
  stripIndexFormat: 'uint16' | 'uint32' | undefined,
845
1445
  renderState: MaterialRenderState | undefined,
1446
+ selectedArtifact: MaterialShaderArtifact = this.artifact,
1447
+ additionalColorFormats: readonly TextureFormat[] = [],
846
1448
  ): Result<RenderPipeline, RhiError> {
847
- const projectedLayout = deriveVertexBufferLayoutFromProjection(
848
- layoutProjection,
849
- this.artifact.uvSetCount === undefined
1449
+ const vertexLayout = deriveVertexBufferLayoutFromProjection(layoutProjection, {
1450
+ shaderUvSetCount,
1451
+ })[0];
1452
+ if (vertexLayout === undefined) {
1453
+ return err(
1454
+ new RhiError({
1455
+ code: 'shader-compile-failed',
1456
+ expected: 'non-empty geometry VertexLayoutProjection',
1457
+ hint: 'prepare a receipt-compatible mesh projection before Standard PBR raster',
1458
+ }),
1459
+ );
1460
+ }
1461
+ const receipt = selectedArtifact.receipt;
1462
+ if (receipt === undefined) {
1463
+ return err(
1464
+ new RhiError({
1465
+ code: 'rhi-not-available',
1466
+ expected: 'selected MaterialProgramAbi receipt for GPU-driven raster',
1467
+ hint: 'keep the draw on the producer recovery path until its ABI is published',
1468
+ }),
1469
+ );
1470
+ }
1471
+ const program =
1472
+ this.programs === undefined
850
1473
  ? undefined
851
- : { shaderUvSetCount: this.artifact.uvSetCount },
852
- )[0];
853
- const vertexStride = projectedLayout?.arrayStride ?? layoutProjection.arrayStride;
854
- const attributes = projectedLayout?.attributes ?? layoutProjection.attributes;
855
- const key = `${format}|${sampleCount}|${vertexStride}|${topology}|${stripIndexFormat ?? ''}|${JSON.stringify(renderState ?? null)}|${this.artifact.layoutIdentity}`;
1474
+ : resolveStandardPbrProgram(
1475
+ this.programs,
1476
+ this.artifact.material,
1477
+ layoutProjection.attributes.some((attribute) => attribute.key === 'color'),
1478
+ );
1479
+ if (this.pipelineFactory === undefined && program === undefined) {
1480
+ return err(
1481
+ new RhiError({
1482
+ code: 'rhi-not-available',
1483
+ expected: 'producer-owned Standard PBR program matching the geometry vertex layout',
1484
+ hint: 'publish both vertex-color variants for the current device generation',
1485
+ }),
1486
+ );
1487
+ }
1488
+ const key = `${format}:${additionalColorFormats.join(',')}|${sampleCount}|${vertexLayout.arrayStride}|${topology}|${stripIndexFormat ?? ''}|${JSON.stringify(renderState ?? null)}|${selectedArtifact.material}|${selectedArtifact.layoutIdentity}|${receipt.receiptIdentity}|${receipt.generation}|${selectedArtifact.variantSet ?? ''}|${materialArtifactProgramIdentity(selectedArtifact)}|${selectedArtifact.vertexEntry ?? receipt.sceneIndexEntry}|${selectedArtifact.fragmentEntry ?? ''}|${layoutProjection.digest}|uv${shaderUvSetCount}`;
856
1489
  const cached = this.pipelines.get(key);
857
1490
  if (cached !== undefined) return ok(cached);
1491
+ if (this.pipelineFactory !== undefined) {
1492
+ const pipelineEntry = this.pipelineFactory(
1493
+ selectedArtifact.specializationKey ?? selectedArtifact.material,
1494
+ false,
1495
+ renderState,
1496
+ topology,
1497
+ stripIndexFormat,
1498
+ selectedArtifact.variantSet,
1499
+ 'forward',
1500
+ undefined,
1501
+ sampleCount,
1502
+ format,
1503
+ selectedArtifact.uvSetCount ?? shaderUvSetCount,
1504
+ 'depth24plus-stencil8',
1505
+ undefined,
1506
+ layoutProjection,
1507
+ // GPU-driven receipts are already producer-validated. Use the
1508
+ // synchronous shader-module adapter for this submission seam so a
1509
+ // newly loaded authored material cannot lose its first draw to the
1510
+ // ordinary async pre-bake warm-up window.
1511
+ 'immediate',
1512
+ this.deformation === 'skin' ? 'gpu-driven-skin' : 'gpu-driven-pbr',
1513
+ receipt.sceneIndexEntry,
1514
+ additionalColorFormats,
1515
+ selectedArtifact.vertexEntry,
1516
+ selectedArtifact.fragmentEntry,
1517
+ );
1518
+ if (pipelineEntry === null) {
1519
+ return err(
1520
+ new RhiError({
1521
+ code: 'rhi-not-available',
1522
+ expected: `producer-owned scene-index pipeline for material '${selectedArtifact.material}'`,
1523
+ hint: 'publish a matching program, entry, layout and resource contract before GPU admission',
1524
+ }),
1525
+ );
1526
+ }
1527
+ const expectedGroup2Contract = resolvePipelineGroup2Contract(selectedArtifact.wgsl);
1528
+ if (pipelineEntry.group2Contract !== expectedGroup2Contract) {
1529
+ return err(
1530
+ new RhiError({
1531
+ code: 'rhi-descriptor-invalid',
1532
+ expected: `GPU-driven ${this.deformation === 'skin' ? 'skinned ' : ''}material pipeline group(2) ${expectedGroup2Contract} contract`,
1533
+ hint: 'custom attachment or incompatible group(2) layouts remain on their specialized lane',
1534
+ }),
1535
+ );
1536
+ }
1537
+ this.pipelines.set(key, pipelineEntry.pipeline);
1538
+ return ok(pipelineEntry.pipeline);
1539
+ }
1540
+ if (
1541
+ program === undefined ||
1542
+ selectedArtifact.material !== this.artifact.material ||
1543
+ selectedArtifact.layoutIdentity !== this.artifact.layoutIdentity ||
1544
+ selectedArtifact.receipt?.receiptIdentity !== this.artifact.receipt?.receiptIdentity
1545
+ ) {
1546
+ return err(
1547
+ new RhiError({
1548
+ code: 'rhi-not-available',
1549
+ expected: 'selected material pipeline factory for the requested ABI artifact',
1550
+ hint: 'do not pair a receipt-backed custom or color variant with the legacy fixed module',
1551
+ }),
1552
+ );
1553
+ }
858
1554
  const pipeline = this.device.createRenderPipeline({
859
1555
  label: `standard-pbr-gpu-driven.${key}`,
860
1556
  layout: this.pipelineLayout,
861
1557
  vertex: {
862
- module: this.module,
863
- entryPoint: this.artifact.receipt?.sceneIndexEntry ?? 'vs_scene_index',
1558
+ module: program.module,
1559
+ entryPoint: selectedArtifact.vertexEntry ?? receipt.sceneIndexEntry,
864
1560
  buffers: [
865
1561
  {
866
- arrayStride: vertexStride,
1562
+ arrayStride: vertexLayout.arrayStride,
867
1563
  stepMode: 'vertex',
868
- attributes: attributes.map(({ shaderLocation, offset, format }) => ({
1564
+ attributes: vertexLayout.attributes.map(({ shaderLocation, offset, format }) => ({
869
1565
  shaderLocation,
870
1566
  offset,
871
1567
  format: format as GPUVertexFormat,
@@ -874,9 +1570,9 @@ class StandardPbrRasterAdapter {
874
1570
  ],
875
1571
  },
876
1572
  fragment: {
877
- module: this.module,
1573
+ module: program.module,
878
1574
  entryPoint: 'fs_main',
879
- targets: [{ format }],
1575
+ targets: [format, ...additionalColorFormats].map((format) => ({ format })),
880
1576
  },
881
1577
  primitive: {
882
1578
  topology,
@@ -896,47 +1592,380 @@ class StandardPbrRasterAdapter {
896
1592
  return pipeline;
897
1593
  }
898
1594
 
899
- projectionBindGroup(rows: Float32Array): Result<BindGroup, RhiError> {
1595
+ /**
1596
+ * Drop only program-derived caches when a publication switches. Resource
1597
+ * buffers stay alive until their normal fence retirement, while the next
1598
+ * draw must obtain a PSO for the new artifact rather than reuse the old one.
1599
+ */
1600
+ invalidatePipelineCache(): void {
1601
+ this.pipelines.clear();
1602
+ }
1603
+
1604
+ projectionBindGroup(
1605
+ rows: Float32Array,
1606
+ key?: string,
1607
+ capacityRows?: number,
1608
+ ): Result<BindGroup, RhiError> {
1609
+ if (key !== undefined) {
1610
+ const capacityBytes = Math.max(
1611
+ MESH_SSBO_BYTES,
1612
+ (capacityRows ?? Math.ceil(rows.byteLength / MESH_SSBO_BYTES)) * MESH_SSBO_BYTES,
1613
+ );
1614
+ const retained = this.stagedProjectionBindings.get(key) ?? this.projectionBindings.get(key);
1615
+ if (retained !== undefined && retained.capacityBytes >= capacityBytes) {
1616
+ const write = this.writeProjectionRows(key, retained, rows);
1617
+ if (!write.ok) return write;
1618
+ return ok(retained.bindGroup);
1619
+ }
1620
+ const created = this.createProjectionBinding(rows, capacityBytes);
1621
+ if (!created.ok) return created;
1622
+ this.stagedProjectionBindings.set(key, created.value);
1623
+ return ok(created.value.bindGroup);
1624
+ }
1625
+ const created = this.createProjectionBinding(rows, Math.max(MESH_SSBO_BYTES, rows.byteLength));
1626
+ if (!created.ok) return created;
1627
+ return ok(created.value.bindGroup);
1628
+ }
1629
+
1630
+ refreshProjectionRows(key: string, rows: Float32Array): Result<void, RhiError> {
1631
+ const retained = this.stagedProjectionBindings.get(key) ?? this.projectionBindings.get(key);
1632
+ if (retained === undefined) return ok(undefined);
1633
+ if (rows.byteLength > retained.capacityBytes) {
1634
+ const capacityBytes = Math.max(
1635
+ MESH_SSBO_BYTES,
1636
+ Math.ceil(rows.byteLength / MESH_SSBO_BYTES) * MESH_SSBO_BYTES,
1637
+ );
1638
+ const replacement = this.createProjectionBinding(rows, capacityBytes);
1639
+ if (!replacement.ok) return replacement;
1640
+ const previousStaged = this.stagedProjectionBindings.get(key);
1641
+ if (previousStaged !== undefined && previousStaged.buffer !== replacement.value.buffer) {
1642
+ this.retireProjectionBuffer(previousStaged.buffer);
1643
+ }
1644
+ this.stagedProjectionBindings.set(key, replacement.value);
1645
+ return ok(undefined);
1646
+ }
1647
+ return this.writeProjectionRows(key, retained, rows);
1648
+ }
1649
+
1650
+ sceneMeshBinding(key: string): GpuDrivenSceneMeshBinding {
1651
+ const binding = this.stagedProjectionBindings.get(key) ?? this.projectionBindings.get(key);
1652
+ if (binding === undefined)
1653
+ throw new RhiError({
1654
+ code: 'rhi-not-available',
1655
+ expected: 'retained scene mesh projection',
1656
+ hint: 'prepare this batch projection before binding its lighting resources',
1657
+ });
1658
+ return {
1659
+ meshBuffer: binding.buffer,
1660
+ meshBytes: binding.capacityBytes,
1661
+ ...(this.deformation === 'skin' && this.paletteBuffer !== undefined
1662
+ ? { paletteBuffer: this.paletteBuffer }
1663
+ : {}),
1664
+ };
1665
+ }
1666
+
1667
+ /** Stage the projection keys required by the frame being prepared. */
1668
+ stageProjectionKeys(keys: ReadonlySet<string>): void {
1669
+ this.stagedProjectionKeys = new Set(keys);
1670
+ this.activeProjectionRowsAtStage.clear();
1671
+ }
1672
+
1673
+ /** Add a shadow projection key discovered while updating a view plan. */
1674
+ noteProjectionKey(key: string): void {
1675
+ this.stagedProjectionKeys.add(key);
1676
+ }
1677
+
1678
+ /** Promote staged projection resources after the command buffer submits. */
1679
+ commitProjectionBindings(): void {
1680
+ const keep = new Set(this.stagedProjectionKeys);
1681
+ for (const [key, staged] of this.stagedProjectionBindings) {
1682
+ if (!keep.has(key)) {
1683
+ this.retireProjectionBuffer(staged.buffer);
1684
+ continue;
1685
+ }
1686
+ const previous = this.projectionBindings.get(key);
1687
+ this.projectionBindings.set(key, staged);
1688
+ if (previous !== undefined && previous.buffer !== staged.buffer) {
1689
+ this.retireProjectionBuffer(previous.buffer);
1690
+ }
1691
+ }
1692
+ this.stagedProjectionBindings.clear();
1693
+ for (const [key, active] of this.projectionBindings) {
1694
+ if (!keep.has(key)) {
1695
+ this.projectionBindings.delete(key);
1696
+ this.retireProjectionBuffer(active.buffer);
1697
+ }
1698
+ }
1699
+ this.stagedProjectionKeys.clear();
1700
+ this.activeProjectionRowsAtStage.clear();
1701
+ }
1702
+
1703
+ /** Discard resources created for an aborted graph while retaining LKG rows. */
1704
+ abortProjectionBindings(): void {
1705
+ for (const [key, rows] of this.activeProjectionRowsAtStage) {
1706
+ const active = this.projectionBindings.get(key);
1707
+ if (active === undefined) continue;
1708
+ const restored = this.device.queue.writeBuffer(active.buffer, 0, rows);
1709
+ if (restored.ok) active.rows = new Float32Array(rows);
1710
+ }
1711
+ for (const staged of this.stagedProjectionBindings.values()) {
1712
+ this.retireProjectionBuffer(staged.buffer);
1713
+ }
1714
+ this.stagedProjectionBindings.clear();
1715
+ this.stagedProjectionKeys.clear();
1716
+ this.activeProjectionRowsAtStage.clear();
1717
+ }
1718
+
1719
+ private createProjectionBinding(
1720
+ rows: Float32Array,
1721
+ capacityBytes: number,
1722
+ ): Result<
1723
+ {
1724
+ readonly buffer: Buffer;
1725
+ readonly bindGroup: BindGroup;
1726
+ readonly capacityBytes: number;
1727
+ rows: Float32Array;
1728
+ },
1729
+ RhiError
1730
+ > {
900
1731
  const buffer = this.device.createBuffer({
901
1732
  label: 'standard-pbr-gpu-driven-scene-meshes',
902
- size: Math.max(MESH_SSBO_BYTES, rows.byteLength),
1733
+ size: capacityBytes,
903
1734
  usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
904
1735
  });
905
1736
  if (!buffer.ok) return buffer;
906
1737
  const write = this.device.queue.writeBuffer(buffer.value, 0, rows);
907
- if (!write.ok) return write;
908
- this.projectionBuffers.push(buffer.value);
909
- return this.device.createBindGroup({
1738
+ if (!write.ok) {
1739
+ this.destroyProjectionBuffer(buffer.value);
1740
+ return write;
1741
+ }
1742
+ this.projectionBuffers.add(buffer.value);
1743
+ const entries = [
1744
+ { binding: 0, resource: bufferBinding(buffer.value, 0, capacityBytes) },
1745
+ ...(this.deformation === 'skin' &&
1746
+ this.paletteBuffer !== undefined &&
1747
+ this.paletteBindingWindowBytes !== undefined
1748
+ ? [
1749
+ {
1750
+ binding: 1,
1751
+ // The scene-index second lane is a global palette row. Bind the
1752
+ // complete arena so rows beyond the legacy 255-joint window
1753
+ // remain addressable; the shader performs the row selection.
1754
+ resource: bufferBinding(this.paletteBuffer),
1755
+ },
1756
+ {
1757
+ binding: 2,
1758
+ // The dedicated GPU-driven skin layout keeps the current /
1759
+ // previous palette pair used by the direct skin layout. The
1760
+ // scene-index shader currently reads the current arena, but
1761
+ // the shared layout still requires a complete binding shape.
1762
+ resource: bufferBinding(this.paletteBuffer),
1763
+ },
1764
+ ]
1765
+ : []),
1766
+ ];
1767
+ const bindGroup = this.device.createBindGroup({
910
1768
  label: 'standard-pbr-gpu-driven-scene-mesh-bg',
911
1769
  layout: this.meshLayout,
912
- entries: [{ binding: 0, resource: bufferBinding(buffer.value, 0, rows.byteLength) }],
1770
+ entries,
913
1771
  });
1772
+ if (!bindGroup.ok) {
1773
+ this.destroyProjectionBuffer(buffer.value);
1774
+ return bindGroup;
1775
+ }
1776
+ return ok({
1777
+ buffer: buffer.value,
1778
+ bindGroup: bindGroup.value,
1779
+ capacityBytes,
1780
+ rows: new Float32Array(rows),
1781
+ });
1782
+ }
1783
+
1784
+ private writeProjectionRows(
1785
+ key: string,
1786
+ binding: {
1787
+ readonly buffer: Buffer;
1788
+ readonly bindGroup: BindGroup;
1789
+ readonly capacityBytes: number;
1790
+ rows: Float32Array;
1791
+ },
1792
+ rows: Float32Array,
1793
+ ): Result<void, RhiError> {
1794
+ // Active bindings are the last-known-good graph resources. Keep one CPU
1795
+ // snapshot so an aborted candidate can restore their exact contents
1796
+ // without allocating a second GPU buffer on every stable frame.
1797
+ if (
1798
+ this.stagedProjectionBindings.get(key) === undefined &&
1799
+ !this.activeProjectionRowsAtStage.has(key)
1800
+ ) {
1801
+ this.activeProjectionRowsAtStage.set(key, new Float32Array(binding.rows));
1802
+ }
1803
+ const write = this.device.queue.writeBuffer(binding.buffer, 0, rows);
1804
+ if (!write.ok) return write;
1805
+ binding.rows = new Float32Array(rows);
1806
+ return ok(undefined);
914
1807
  }
915
1808
 
916
1809
  dispose(): void {
917
- for (const buffer of this.projectionBuffers) this.device.destroyBuffer(buffer);
918
- this.projectionBuffers.length = 0;
1810
+ this.projectionDisposed = true;
1811
+ for (const binding of this.projectionBindings.values()) {
1812
+ this.destroyProjectionBuffer(binding.buffer);
1813
+ }
1814
+ for (const binding of this.stagedProjectionBindings.values()) {
1815
+ this.destroyProjectionBuffer(binding.buffer);
1816
+ }
1817
+ for (const buffer of this.projectionBuffers) this.destroyProjectionBuffer(buffer);
1818
+ for (const buffer of this.pendingProjectionRetirements) {
1819
+ this.destroyProjectionBuffer(buffer);
1820
+ }
1821
+ this.projectionBindings.clear();
1822
+ this.stagedProjectionBindings.clear();
1823
+ this.activeProjectionRowsAtStage.clear();
1824
+ this.projectionBuffers.clear();
1825
+ this.pendingProjectionRetirements.clear();
1826
+ this.device.destroyBuffer(this.instanceBuffer);
919
1827
  }
920
1828
 
921
- get frameGroups(): readonly [BindGroup, BindGroup] {
922
- return [this.materialBindGroup, this.instancesBindGroup];
1829
+ private retireProjectionBuffer(buffer: Buffer): void {
1830
+ if (this.destroyedProjectionBuffers.has(buffer)) return;
1831
+ this.projectionBuffers.delete(buffer);
1832
+ if (this.projectionDisposed) {
1833
+ this.destroyProjectionBuffer(buffer);
1834
+ return;
1835
+ }
1836
+ if (!this.pendingProjectionRetirements.add(buffer)) return;
1837
+ void this.device.queue.onSubmittedWorkDone().then(
1838
+ () => {
1839
+ this.pendingProjectionRetirements.delete(buffer);
1840
+ this.destroyProjectionBuffer(buffer);
1841
+ },
1842
+ () => {
1843
+ this.pendingProjectionRetirements.delete(buffer);
1844
+ this.destroyProjectionBuffer(buffer);
1845
+ },
1846
+ );
1847
+ }
1848
+
1849
+ private destroyProjectionBuffer(buffer: Buffer): void {
1850
+ if (this.destroyedProjectionBuffers.has(buffer)) return;
1851
+ this.destroyedProjectionBuffers.add(buffer);
1852
+ this.projectionBuffers.delete(buffer);
1853
+ this.device.destroyBuffer(buffer);
1854
+ }
1855
+
1856
+ get instancesFrameGroup(): BindGroup {
1857
+ return this.instancesBindGroup;
1858
+ }
1859
+
1860
+ get kind(): 'rigid' | 'skin' {
1861
+ return this.deformation;
923
1862
  }
924
1863
  }
925
1864
 
926
1865
  export interface PreparedGpuDrivenFrame {
927
1866
  readonly topologySignature: string;
928
- readonly entityKeys: ReadonlySet<number>;
929
- readonly ownsAllRenderables: boolean;
1867
+ /** True when every source draw item/instance is claimed by the GPU lane. */
1868
+ readonly ownsAllDrawItems: boolean;
1869
+ /** True only when the extracted ShadowCaster channel is fully represented. */
1870
+ readonly ownsAllShadowCasters?: boolean;
1871
+ /** Concrete main-pass draw identities claimed by the GPU projection. */
1872
+ readonly drawKeys?: ReadonlySet<string>;
1873
+ readonly standardPbrFrameResources: GpuDrivenStandardPbrFrameResources;
930
1874
  readonly occlusion?: OcclusionFrameProjection;
931
1875
  /** @internal Commits a replacement generation only after graph promotion. */
932
1876
  readonly _commitResourceReplacement: () => void;
1877
+ /** @internal Retires superseded view buffers when graph compilation promotes them. */
1878
+ readonly _commitGpuResourceReplacement?: () => void;
1879
+ /** @internal Discards staged shadow cache publication after a failed frame. */
1880
+ readonly _abortResourceReplacement?: () => void;
1881
+ /** Concrete ShadowCaster keys claimed by the GPU subset for this frame. */
1882
+ readonly shadowDrawKeys?: ReadonlySet<string>;
1883
+ /** View-scoped ShadowCaster claims consumed by each shadow record pass. */
1884
+ readonly shadowDrawKeysByView?: ReadonlyMap<string, ReadonlySet<string>>;
1885
+ readonly projectShadow?: (
1886
+ graph: RenderGraphBuilder<RenderPipelineFrame>,
1887
+ identity: ShadowViewIdentity,
1888
+ ) => Result<ShadowViewProjection | undefined, RenderGraphError>;
1889
+ readonly updateShadowViews?: (
1890
+ views: readonly Omit<ShadowViewUpdateInput, 'sourcePlan' | 'scene'>[],
1891
+ ) => Result<void, RhiError>;
1892
+ readonly shadowViewPool?: ShadowViewStatePool;
1893
+ readonly shadowBatchProjections?: ReadonlyMap<
1894
+ string,
1895
+ ReadonlyMap<number, GpuDrivenShadowBatchProjection>
1896
+ >;
933
1897
  project(
934
1898
  graph: RenderGraphBuilder<RenderPipelineFrame>,
935
1899
  format: TextureFormat,
936
1900
  sampleCount: 1 | 4,
1901
+ additionalColorFormats?: readonly TextureFormat[],
937
1902
  ): Result<RenderPipelineGpuDrivenProjection, RenderGraphError | RhiError>;
938
1903
  }
939
1904
 
1905
+ /**
1906
+ * Renderer-owned identity token for the validation that resolves renderable
1907
+ * assets to GPU meshes. The token is deliberately narrower than a frame
1908
+ * number: unchanged persistent scene snapshots can reuse the resolved rows,
1909
+ * while a mesh/device/pipeline generation change forces a fresh producer read.
1910
+ */
1911
+ export interface GpuDrivenResidencyValidationInput {
1912
+ /** Opaque renderer-computed world/source identity and generation fence. */
1913
+ readonly cacheKey: string;
1914
+ readonly renderables: readonly RenderableSnapshot[];
1915
+ readonly transparentDispatch: readonly DispatchEntry[];
1916
+ readonly pipelineState: PipelineState;
1917
+ readonly pipelineHandle: number;
1918
+ readonly meshResidencyEpoch: number;
1919
+ readonly deviceGeneration: number;
1920
+ readonly assetCatalogEpoch: number;
1921
+ }
1922
+
1923
+ interface GpuDrivenResidencyValidationCache extends GpuDrivenResidencyValidationInput {
1924
+ readonly value: readonly ValidatedRenderable[];
1925
+ readonly dispatchRenderableIndices: readonly (number | undefined)[];
1926
+ readonly renderableIdentities: readonly RenderableSnapshot[];
1927
+ readonly dispatchIdentities: readonly DispatchEntry[];
1928
+ }
1929
+
1930
+ interface GpuDrivenCpuValidationTelemetryCache {
1931
+ readonly cacheKey: string;
1932
+ readonly validated: readonly { readonly source: RenderableSnapshot }[];
1933
+ readonly gpuOwnedDrawKeys: ReadonlySet<string>;
1934
+ readonly gpuOwnedDrawCount: number;
1935
+ readonly totalDrawItems: number;
1936
+ readonly gpuOwnedRows: number;
1937
+ readonly cpuFallbackDrawItems: number;
1938
+ readonly cpuSemanticFallbackDrawItems: number;
1939
+ readonly cpuDeformationFallbackDrawItems: number;
1940
+ readonly cpuDeformationReasons: ReadonlyMap<
1941
+ 'skin-bounds-missing' | 'skin-address-missing' | 'resource-not-ready',
1942
+ number
1943
+ >;
1944
+ readonly blockedDrawItems: number;
1945
+ }
1946
+
1947
+ function sameIdentitySequence<T extends object>(
1948
+ expected: readonly T[],
1949
+ actual: readonly T[],
1950
+ ): boolean {
1951
+ if (expected.length !== actual.length) return false;
1952
+ for (let index = 0; index < expected.length; index += 1) {
1953
+ if (expected[index] !== actual[index]) return false;
1954
+ }
1955
+ return true;
1956
+ }
1957
+
1958
+ function sameDispatchRenderableIndices(
1959
+ expected: readonly (number | undefined)[],
1960
+ actual: readonly DispatchEntry[],
1961
+ ): boolean {
1962
+ if (expected.length !== actual.length) return false;
1963
+ for (let index = 0; index < expected.length; index += 1) {
1964
+ if (expected[index] !== actual[index]?.renderableIndex) return false;
1965
+ }
1966
+ return true;
1967
+ }
1968
+
940
1969
  export interface RecoveryCapabilityFacts {
941
1970
  readonly generation: number;
942
1971
  readonly shaderCompilation: boolean;
@@ -994,17 +2023,36 @@ export function classifyRecoveryCapability(
994
2023
  export class GpuDrivenProduction {
995
2024
  private disposed = false;
996
2025
  private view: GpuDrivenView | undefined;
997
- private raster: GpuDrivenRigidRaster | undefined;
998
- private standardRaster: StandardPbrRasterAdapter | undefined;
2026
+ private materialRaster: MaterialAbiRasterAdapter | undefined;
2027
+ private skinRaster: MaterialAbiRasterAdapter | undefined;
999
2028
  private readonly sceneIdentities = new WeakMap<object, number>();
1000
2029
  private nextSceneIdentity = 1;
1001
2030
  private filteredCache:
1002
2031
  | {
1003
2032
  readonly source: SubmissionPlan;
2033
+ readonly scene: PersistentGpuDrivenState['scene'];
2034
+ readonly slots: PersistentGpuDrivenState['slots'];
2035
+ readonly worldKeys: PersistentGpuDrivenState['worldKeys'];
1004
2036
  readonly meshResidencyEpoch: number;
1005
2037
  readonly lodProjectionFingerprint: string;
1006
2038
  readonly activeEntityRevision?: number;
1007
2039
  readonly activeEntityKeys?: ReadonlySet<number>;
2040
+ readonly skinArtifact?: MaterialShaderArtifact;
2041
+ readonly materialArtifactSignature: string;
2042
+ readonly value: FilteredProductionPlan;
2043
+ }
2044
+ | undefined;
2045
+ private shadowFilteredCache:
2046
+ | {
2047
+ readonly source: SubmissionPlan;
2048
+ readonly scene: PersistentGpuDrivenState['scene'];
2049
+ readonly slots: PersistentGpuDrivenState['slots'];
2050
+ readonly worldKeys: PersistentGpuDrivenState['worldKeys'];
2051
+ readonly lodProjectionFingerprint: string;
2052
+ readonly meshResidencyEpoch: number;
2053
+ readonly membership: string;
2054
+ readonly skinArtifact?: MaterialShaderArtifact;
2055
+ readonly materialArtifactSignature: string;
1008
2056
  readonly value: FilteredProductionPlan;
1009
2057
  }
1010
2058
  | undefined;
@@ -1014,13 +2062,61 @@ export class GpuDrivenProduction {
1014
2062
  private batchBindGroupCreates = 0;
1015
2063
  private validatedGpuOwnedRows = 0;
1016
2064
  private cpuFallbackDrawItems = 0;
2065
+ private cpuSemanticFallbackDrawItems = 0;
2066
+ private cpuDeformationFallbackDrawItems = 0;
2067
+ private readonly cpuDeformationReasons = new Map<
2068
+ 'skin-bounds-missing' | 'skin-address-missing' | 'resource-not-ready',
2069
+ number
2070
+ >();
2071
+ private blockedDrawItems = 0;
2072
+ private preparationFailure: GpuDrivenPreparationFailureInspection | undefined;
2073
+ private encodedIndirectDraws = 0;
1017
2074
  private telemetryPrepared = false;
1018
2075
  private telemetryCandidateCount = 0;
1019
2076
  private telemetryGeometryWork = 0;
1020
2077
  private telemetryRootGeometryWork = 0;
2078
+ private telemetryOverflow = false;
1021
2079
  private indirectDrawCount = 0;
1022
- private sceneRowsRequired = true;
2080
+ private gpuOwnedDrawItems = 0;
2081
+ private mainTotalDrawItems = 0;
2082
+ private lane: 'gpu' | 'cpu-semantic' | 'cpu-deformation' | 'blocked' = 'cpu-semantic';
2083
+ private laneReason: GpuDrivenLaneReason = 'capability';
2084
+ private submissionState: 'idle' | 'pending' | 'submitted' | 'aborted' = 'idle';
2085
+ private retryCount = 0;
2086
+ private lastKnownGoodGeneration: number | undefined;
2087
+ private overflowRecoveryRequired = false;
2088
+ private shadowViews: ShadowViewStatePool | undefined;
2089
+ private shadowSourcePlan: SubmissionPlan | undefined;
2090
+ private shadowScene: PersistentGpuDrivenState['scene'] | undefined;
2091
+ private shadowBatchProjections = new Map<
2092
+ string,
2093
+ ReadonlyMap<number, GpuDrivenShadowBatchProjection>
2094
+ >();
2095
+ private shadowDrawKeysByView = new Map<string, ReadonlySet<string>>();
2096
+ private shadowExpectedDrawKeys = new Set<string>();
2097
+ private shadowBatchProjectionSource: SubmissionPlan | undefined;
2098
+ private skinPaletteBuffer: Buffer | undefined;
2099
+ private committedPaletteContentRevision: number | undefined;
1023
2100
  private standardPbrProjection: StandardPbrSceneProjection | undefined;
2101
+ private committedProjectionSource:
2102
+ | {
2103
+ readonly filtered: FilteredProductionPlan;
2104
+ readonly scene: PersistentGpuDrivenState['scene'];
2105
+ readonly contentRevision: number;
2106
+ readonly materialArtifactSignature: string;
2107
+ }
2108
+ | undefined;
2109
+ private validatedResidencyCache: GpuDrivenResidencyValidationCache | undefined;
2110
+ private residencyValidationCacheHits = 0;
2111
+ private residencyValidationScans = 0;
2112
+ private cpuValidationTelemetryCache: GpuDrivenCpuValidationTelemetryCache | undefined;
2113
+ private cpuValidationCacheHits = 0;
2114
+ private cpuValidationScans = 0;
2115
+ private structureMetrics: GpuDrivenStructureMetrics = {
2116
+ worldEntitiesScanned: 0,
2117
+ sceneTableUploadBytes: 0,
2118
+ paletteUploadBytes: 0,
2119
+ };
1024
2120
  private signatureCache:
1025
2121
  | {
1026
2122
  readonly filtered: FilteredProductionPlan;
@@ -1034,8 +2130,67 @@ export class GpuDrivenProduction {
1034
2130
  constructor(
1035
2131
  private readonly device: RhiDevice,
1036
2132
  private readonly shaderModuleFactory: PipelineBuilderShaderModuleFactory,
2133
+ private readonly recoveryCount = 0,
1037
2134
  ) {}
1038
2135
 
2136
+ /**
2137
+ * Reuse the last producer-resolved residency rows when all source identity
2138
+ * and generation fences still match. Renderables and dispatch entries are
2139
+ * checked element-by-element as well as by their stable object identities so
2140
+ * an accidental in-place mutation cannot reuse stale renderable indices.
2141
+ */
2142
+ reuseResidencyValidation(
2143
+ input: GpuDrivenResidencyValidationInput,
2144
+ ): readonly ValidatedRenderable[] | undefined {
2145
+ const cached = this.validatedResidencyCache;
2146
+ if (
2147
+ cached === undefined ||
2148
+ cached.cacheKey !== input.cacheKey ||
2149
+ cached.pipelineState !== input.pipelineState ||
2150
+ cached.pipelineState.device !== this.device ||
2151
+ cached.pipelineHandle !== input.pipelineHandle ||
2152
+ cached.meshResidencyEpoch !== input.meshResidencyEpoch ||
2153
+ cached.deviceGeneration !== input.deviceGeneration ||
2154
+ cached.assetCatalogEpoch !== input.assetCatalogEpoch ||
2155
+ !sameIdentitySequence(cached.renderableIdentities, input.renderables) ||
2156
+ !sameIdentitySequence(cached.dispatchIdentities, input.transparentDispatch) ||
2157
+ !sameDispatchRenderableIndices(cached.dispatchRenderableIndices, input.transparentDispatch)
2158
+ ) {
2159
+ return undefined;
2160
+ }
2161
+ this.residencyValidationCacheHits += 1;
2162
+ return cached.value;
2163
+ }
2164
+
2165
+ /** Publish the producer-owned rows after a completed residency validation. */
2166
+ rememberResidencyValidation(
2167
+ input: GpuDrivenResidencyValidationInput,
2168
+ value: readonly ValidatedRenderable[],
2169
+ ): void {
2170
+ this.validatedResidencyCache = {
2171
+ ...input,
2172
+ value,
2173
+ renderableIdentities: input.renderables,
2174
+ dispatchIdentities: input.transparentDispatch,
2175
+ dispatchRenderableIndices: input.transparentDispatch.map((entry) => entry.renderableIndex),
2176
+ };
2177
+ this.residencyValidationScans += 1;
2178
+ }
2179
+
2180
+ private applyCpuValidationTelemetry(cache: GpuDrivenCpuValidationTelemetryCache): void {
2181
+ this.gpuOwnedDrawItems = cache.gpuOwnedDrawCount;
2182
+ this.mainTotalDrawItems = cache.totalDrawItems;
2183
+ this.validatedGpuOwnedRows = cache.gpuOwnedRows;
2184
+ this.cpuFallbackDrawItems = cache.cpuFallbackDrawItems;
2185
+ this.cpuSemanticFallbackDrawItems = cache.cpuSemanticFallbackDrawItems;
2186
+ this.cpuDeformationFallbackDrawItems = cache.cpuDeformationFallbackDrawItems;
2187
+ this.cpuDeformationReasons.clear();
2188
+ for (const [reason, count] of cache.cpuDeformationReasons) {
2189
+ this.cpuDeformationReasons.set(reason, count);
2190
+ }
2191
+ this.blockedDrawItems = cache.blockedDrawItems;
2192
+ }
2193
+
1039
2194
  static forDevice(
1040
2195
  device: RhiDevice,
1041
2196
  shaderModuleFactory: PipelineBuilderShaderModuleFactory,
@@ -1043,34 +2198,6 @@ export class GpuDrivenProduction {
1043
2198
  return new GpuDrivenProduction(device, shaderModuleFactory);
1044
2199
  }
1045
2200
 
1046
- /** Recovery has no frame retry: await generated programs before preparing the graph. */
1047
- static async forRecoveryDevice(
1048
- device: RhiDevice,
1049
- factory: PipelineBuilderShaderModuleFactory,
1050
- createShaderModule: (descriptor: {
1051
- code: string;
1052
- label?: string;
1053
- }) => Promise<Result<ShaderModule, RhiError>>,
1054
- ): Promise<Result<GpuDrivenProduction, RhiError>> {
1055
- const modules = new Map<string, ShaderModule>();
1056
- for (const descriptor of [
1057
- { label: 'gpu-driven-view', code: GPU_DRIVEN_VIEW_WGSL },
1058
- { label: 'gpu-driven-rigid-unlit', code: GPU_DRIVEN_RIGID_UNLIT_WGSL },
1059
- ]) {
1060
- const module = await createShaderModule(descriptor);
1061
- if (!module.ok) return module;
1062
- modules.set(descriptor.code, module.value);
1063
- }
1064
- return ok(
1065
- new GpuDrivenProduction(device, {
1066
- createShaderModule: (descriptor) => {
1067
- const module = modules.get(descriptor.code);
1068
- return module === undefined ? factory.createShaderModule(descriptor) : ok(module);
1069
- },
1070
- }),
1071
- );
1072
- }
1073
-
1074
2201
  createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown> {
1075
2202
  return {
1076
2203
  kind: 'scene-table',
@@ -1088,10 +2215,31 @@ export class GpuDrivenProduction {
1088
2215
  prepare(input: {
1089
2216
  readonly scene: PersistentGpuDrivenState | undefined;
1090
2217
  readonly camera: CameraSnapshot;
1091
- readonly meshes: ReadonlyMap<number, MeshGpuHandles>;
2218
+ /** GPU meshes projected by retained RenderScene slot, never raw asset handle. */
2219
+ readonly meshBySlot: ReadonlyMap<number, MeshGpuHandles>;
1092
2220
  readonly viewBindGroupLayout: BindGroupLayout;
1093
2221
  readonly meshResidencyEpoch: number;
1094
- readonly hdrp: boolean;
2222
+ readonly materialBindingClasses?: ReadonlyMap<string, string>;
2223
+ /** Cooked producer artifact selected from the frame material catalog. */
2224
+ readonly materialArtifact?: MaterialShaderArtifact;
2225
+ /** Compatibility input for callers that have not migrated their name. */
2226
+ readonly standardPbrArtifact?: MaterialShaderArtifact;
2227
+ /** Cooked producer artifact for the GPU skinning variant. */
2228
+ readonly materialSkinArtifact?: MaterialShaderArtifact;
2229
+ /** Compatibility input for callers that have not migrated their name. */
2230
+ readonly standardPbrSkinArtifact?: MaterialShaderArtifact;
2231
+ /** Per-draw producer artifacts keyed by prepared identity and receipt. */
2232
+ readonly materialArtifacts?: ReadonlyMap<string, MaterialShaderArtifact>;
2233
+ /** Per-draw shadow-pass artifacts keyed by the same prepared identity. */
2234
+ readonly shadowMaterialArtifacts?: ReadonlyMap<string, MaterialShaderArtifact>;
2235
+ readonly materialPipelineState?: PipelineState;
2236
+ readonly standardPbrPipelineState?: PipelineState;
2237
+ /** Existing renderer owner that builds the selected artifact PSO/BGL. */
2238
+ readonly materialPipelineFactory?: MaterialPipelineFactory;
2239
+ /** Extracted ShadowCaster ownership keyed by concrete draw item and pass. */
2240
+ readonly shadowCasterDrawKeys?: ReadonlySet<string>;
2241
+ /** Structured ShadowCaster ownership facts for exact pass admission. */
2242
+ readonly shadowCasterMembership?: readonly ShadowCasterMembership[];
1095
2243
  /** Entity keys admitted by the persistent visibility facet for this view. */
1096
2244
  readonly activeEntityKeys?: ReadonlySet<number>;
1097
2245
  /** Collision-free renderer-owned revision for the active visibility set. */
@@ -1099,20 +2247,37 @@ export class GpuDrivenProduction {
1099
2247
  /** Total extracted candidates, including facet-suppressed rows. */
1100
2248
  readonly telemetryCandidateCount?: number;
1101
2249
  readonly occlusion?: OcclusionFrameProjection;
1102
- /** Cooked producer artifact selected from the frame material catalog. */
1103
- readonly standardPbrArtifact?: MaterialShaderArtifact;
1104
- readonly standardPbrPipelineState?: PipelineState;
1105
- }): Result<PreparedGpuDrivenFrame | undefined, RhiError> {
2250
+ /** Per-frame deltas from the existing RenderScene/GPU Scene/palette owners. */
2251
+ readonly structureMetrics?: GpuDrivenStructureMetrics;
2252
+ }): Result<PreparedGpuDrivenFrame | undefined, RhiError | GpuDrivenPreparationError> {
1106
2253
  this.gpuOwnedSnapshotsMaterialized = 0;
1107
2254
  this.filteredPlanBuilds = 0;
1108
2255
  this.batchBindGroupCreates = 0;
1109
2256
  this.validatedGpuOwnedRows = 0;
1110
2257
  this.cpuFallbackDrawItems = 0;
2258
+ this.cpuSemanticFallbackDrawItems = 0;
2259
+ this.cpuDeformationFallbackDrawItems = 0;
2260
+ this.cpuDeformationReasons.clear();
2261
+ this.blockedDrawItems = 0;
2262
+ this.preparationFailure = undefined;
2263
+ this.encodedIndirectDraws = 0;
1111
2264
  this.telemetryPrepared = false;
1112
2265
  this.telemetryCandidateCount = input.telemetryCandidateCount ?? 0;
1113
2266
  this.telemetryGeometryWork = 0;
1114
2267
  this.telemetryRootGeometryWork = 0;
2268
+ this.telemetryOverflow = false;
1115
2269
  this.indirectDrawCount = 0;
2270
+ this.gpuOwnedDrawItems = 0;
2271
+ this.mainTotalDrawItems = 0;
2272
+ this.structureMetrics = input.structureMetrics ?? {
2273
+ worldEntitiesScanned: 0,
2274
+ sceneTableUploadBytes: 0,
2275
+ paletteUploadBytes: 0,
2276
+ };
2277
+ this.lane = 'cpu-semantic';
2278
+ this.laneReason = 'capability';
2279
+ this.submissionState = 'idle';
2280
+ this.shadowExpectedDrawKeys = new Set();
1116
2281
  const capability = classifyRecoveryCapability({
1117
2282
  generation: 0,
1118
2283
  shaderCompilation: true,
@@ -1122,26 +2287,103 @@ export class GpuDrivenProduction {
1122
2287
  multisample: true,
1123
2288
  });
1124
2289
  if (
1125
- input.hdrp ||
1126
2290
  input.scene === undefined ||
1127
- capability.status === 'disabled' ||
1128
- selectGpuLodLane(this.device.caps) === 'cpu'
2291
+ selectGpuLodLane(this.device.caps) === 'cpu' ||
2292
+ capability.status === 'disabled'
1129
2293
  ) {
1130
2294
  this.gpuOwnedEntityCount = 0;
1131
- this.sceneRowsRequired = false;
1132
2295
  return ok(undefined);
1133
2296
  }
1134
- const scene = input.scene;
1135
- const projectionFingerprint = lodProjectionFingerprint(input.camera);
1136
- // An empty filtered plan means this topology/mesh-residency pair has no
1137
- // renderables that this production lane can own. Camera and visibility
1138
- // inputs only affect admitted candidates and LOD selection, so they cannot
1139
- // turn that empty topology into an eligible one. Keep the negative result
1140
- // resident instead of rescanning every scene slot as the camera moves.
1141
- const cachedPlanIsTopologyInvariant = this.filteredCache?.value.topologyInvariantEmpty === true;
1142
- const cachedPlanIsCameraInvariant = cachedPlanIsTopologyInvariant;
2297
+ if (this.overflowRecoveryRequired && this.view !== undefined) {
2298
+ const recovered = this.view._recoverFromOverflow();
2299
+ if (!recovered.ok) {
2300
+ this.gpuOwnedEntityCount = 0;
2301
+ this.lane = 'blocked';
2302
+ this.laneReason = 'capacity';
2303
+ return recovered;
2304
+ }
2305
+ this.overflowRecoveryRequired = false;
2306
+ }
2307
+ const scene = input.scene;
2308
+ const preparationFailure = nonCpuPreparationFailure(scene);
2309
+ if (preparationFailure !== undefined) {
2310
+ this.gpuOwnedEntityCount = 0;
2311
+ // A capable candidate that cannot be prepared is a stopped promotion,
2312
+ // not a CPU-semantic lane. Keep the concrete rejected draw count in the
2313
+ // detached channel so an inspector can repair the producer and retry the
2314
+ // same view-pass without guessing from an error string.
2315
+ this.blockedDrawItems = preparationFailure.drawKeys.length;
2316
+ this.mainTotalDrawItems = Math.max(
2317
+ this.mainTotalDrawItems,
2318
+ scene.plan.batches.reduce((total, batch) => total + batch.candidates.length, 0),
2319
+ );
2320
+ this.lane = 'blocked';
2321
+ this.laneReason = 'resource-not-ready';
2322
+ this.preparationFailure = preparationFailureInspection(preparationFailure.error);
2323
+ return err(preparationFailure.error);
2324
+ }
2325
+ const materialArtifact = input.materialArtifact ?? input.standardPbrArtifact;
2326
+ const skinArtifact = input.materialSkinArtifact ?? input.standardPbrSkinArtifact;
2327
+ const materialPipelineState = input.materialPipelineState ?? input.standardPbrPipelineState;
2328
+ const artifactSignature = `${materialArtifactSignature(
2329
+ input.materialArtifacts,
2330
+ materialArtifact,
2331
+ skinArtifact,
2332
+ )}|shadow=${materialArtifactSignature(input.shadowMaterialArtifacts, undefined, undefined)}|bindings=${JSON.stringify([...(input.materialBindingClasses ?? [])])}`;
2333
+ if (
2334
+ (materialArtifact === undefined && input.materialArtifacts === undefined) ||
2335
+ materialPipelineState === undefined
2336
+ ) {
2337
+ this.gpuOwnedEntityCount = 0;
2338
+ this.laneReason = 'resource-not-ready';
2339
+ if (sceneRequiresGpuDrivenArtifact(input.scene)) {
2340
+ this.blockedDrawItems = input.scene.plan.batches.reduce(
2341
+ (total, batch) => total + batch.candidates.length,
2342
+ 0,
2343
+ );
2344
+ this.mainTotalDrawItems = Math.max(this.mainTotalDrawItems, this.blockedDrawItems);
2345
+ this.lane = 'blocked';
2346
+ const failure =
2347
+ materialArtifact === undefined && input.materialArtifacts === undefined
2348
+ ? new GpuDrivenPreparationError('missing-material-receipt', {
2349
+ reason: 'material-receipt-missing',
2350
+ owner: 'material',
2351
+ expected: 'producer-owned MaterialProgramAbi GPU-driven artifact',
2352
+ })
2353
+ : new GpuDrivenPreparationError('resource-not-ready', {
2354
+ reason: 'material-resource-missing',
2355
+ owner: 'material',
2356
+ expected: 'producer-owned GPU-driven material pipeline state',
2357
+ });
2358
+ this.preparationFailure = preparationFailureInspection(failure);
2359
+ return err(failure);
2360
+ }
2361
+ return ok(undefined);
2362
+ }
2363
+ const missingArtifact = missingMaterialArtifact(
2364
+ scene,
2365
+ input.materialArtifacts,
2366
+ materialArtifact,
2367
+ skinArtifact,
2368
+ );
2369
+ if (missingArtifact !== undefined) {
2370
+ this.gpuOwnedEntityCount = 0;
2371
+ this.lane = 'blocked';
2372
+ this.laneReason = 'resource-not-ready';
2373
+ this.blockedDrawItems = scene.plan.batches.reduce(
2374
+ (total, batch) => total + batch.candidates.length,
2375
+ 0,
2376
+ );
2377
+ this.preparationFailure = preparationFailureInspection(missingArtifact);
2378
+ return err(missingArtifact);
2379
+ }
2380
+ const projectionFingerprint =
2381
+ this.filteredCache?.source === scene.plan &&
2382
+ this.filteredCache.lodProjectionFingerprint === ''
2383
+ ? ''
2384
+ : lodProjectionFingerprint(scene.plan, input.camera, scene.slots);
1143
2385
  const cachedVisibilityMatches =
1144
- cachedPlanIsCameraInvariant || this.filteredCache === undefined
2386
+ this.filteredCache === undefined
1145
2387
  ? true
1146
2388
  : input.activeEntityRevision !== undefined &&
1147
2389
  this.filteredCache.activeEntityRevision !== undefined
@@ -1151,22 +2393,34 @@ export class GpuDrivenProduction {
1151
2393
  if (
1152
2394
  filtered === undefined ||
1153
2395
  this.filteredCache?.source !== scene.plan ||
1154
- (!cachedPlanIsTopologyInvariant &&
1155
- this.filteredCache.meshResidencyEpoch !== input.meshResidencyEpoch) ||
1156
- (!cachedPlanIsCameraInvariant &&
1157
- this.filteredCache.lodProjectionFingerprint !== projectionFingerprint) ||
1158
- !cachedVisibilityMatches
2396
+ this.filteredCache?.scene !== scene.scene ||
2397
+ this.filteredCache?.slots !== scene.slots ||
2398
+ this.filteredCache?.worldKeys !== scene.worldKeys ||
2399
+ this.filteredCache.meshResidencyEpoch !== input.meshResidencyEpoch ||
2400
+ this.filteredCache.lodProjectionFingerprint !== projectionFingerprint ||
2401
+ !cachedVisibilityMatches ||
2402
+ this.filteredCache.skinArtifact !== skinArtifact ||
2403
+ this.filteredCache.materialArtifactSignature !== artifactSignature
1159
2404
  ) {
1160
2405
  filtered = filteredPlan(
1161
2406
  scene.plan,
1162
2407
  scene.slots,
1163
- input.meshes,
2408
+ input.meshBySlot,
2409
+ undefined,
2410
+ skinArtifact,
2411
+ undefined,
1164
2412
  input.camera,
1165
2413
  input.activeEntityKeys,
1166
2414
  scene.worldKeys,
2415
+ materialArtifact,
2416
+ input.materialArtifacts,
2417
+ input.materialBindingClasses,
1167
2418
  );
1168
2419
  this.filteredCache = {
1169
2420
  source: scene.plan,
2421
+ scene: scene.scene,
2422
+ slots: scene.slots,
2423
+ worldKeys: scene.worldKeys,
1170
2424
  meshResidencyEpoch: input.meshResidencyEpoch,
1171
2425
  lodProjectionFingerprint: projectionFingerprint,
1172
2426
  ...(input.activeEntityRevision === undefined
@@ -1175,59 +2429,126 @@ export class GpuDrivenProduction {
1175
2429
  ...(input.activeEntityKeys === undefined
1176
2430
  ? {}
1177
2431
  : { activeEntityKeys: input.activeEntityKeys }),
2432
+ ...(skinArtifact === undefined ? {} : { skinArtifact }),
2433
+ materialArtifactSignature: artifactSignature,
1178
2434
  value: filtered,
1179
2435
  };
1180
2436
  this.gpuOwnedSnapshotsMaterialized = scene.slots.length;
1181
2437
  this.filteredPlanBuilds = 1;
1182
2438
  }
1183
- this.gpuOwnedEntityCount = filtered.entityKeys.size;
1184
- this.sceneRowsRequired = !filtered.topologyInvariantEmpty;
1185
- // Publish the command count from the current filtered plan on every
1186
- // prepare call. A cached graph does not call project() again on stable
1187
- // frames, so deriving this only while compiling the raster pass would
1188
- // transiently report zero after prepare() reset the per-frame counters.
2439
+ this.gpuOwnedEntityCount = gpuDrawEntityCount(filtered.drawKeys);
1189
2440
  this.indirectDrawCount = admittedRasterBatchCount(filtered.plan);
1190
- if (filtered.batches.length === 0) return ok(undefined);
1191
- this.standardPbrProjection = undefined;
1192
- const usesStandardPbrRaster = filtered.batches.every(({ batch }) => isStandardPbrBatch(batch));
1193
- if (usesStandardPbrRaster && input.standardPbrArtifact !== undefined) {
2441
+ if (filtered.batches.length === 0) {
2442
+ this.laneReason = 'unsupported';
2443
+ return ok(undefined);
2444
+ }
2445
+ this.lane = 'gpu';
2446
+ this.laneReason = 'none';
2447
+ this.submissionState = 'pending';
2448
+ const shadowMembership = shadowCasterMembershipSignature(
2449
+ input.shadowCasterDrawKeys,
2450
+ input.shadowCasterMembership,
2451
+ );
2452
+ let shadowFiltered = this.shadowFilteredCache?.value;
2453
+ if (
2454
+ shadowFiltered === undefined ||
2455
+ this.shadowFilteredCache?.source !== scene.plan ||
2456
+ this.shadowFilteredCache.scene !== scene.scene ||
2457
+ this.shadowFilteredCache.slots !== scene.slots ||
2458
+ this.shadowFilteredCache.worldKeys !== scene.worldKeys ||
2459
+ this.shadowFilteredCache.lodProjectionFingerprint !== projectionFingerprint ||
2460
+ this.shadowFilteredCache.meshResidencyEpoch !== input.meshResidencyEpoch ||
2461
+ this.shadowFilteredCache.membership !== shadowMembership ||
2462
+ this.shadowFilteredCache.skinArtifact !== skinArtifact ||
2463
+ this.shadowFilteredCache.materialArtifactSignature !== artifactSignature
2464
+ ) {
2465
+ const projectedShadowFiltered = filteredPlan(
2466
+ scene.plan,
2467
+ scene.slots,
2468
+ input.meshBySlot,
2469
+ input.shadowCasterDrawKeys,
2470
+ skinArtifact,
2471
+ input.shadowCasterMembership,
2472
+ input.camera,
2473
+ undefined,
2474
+ scene.worldKeys,
2475
+ materialArtifact,
2476
+ input.materialArtifacts,
2477
+ );
2478
+ shadowFiltered = projectedShadowFiltered;
2479
+ this.shadowFilteredCache = {
2480
+ source: scene.plan,
2481
+ scene: scene.scene,
2482
+ slots: scene.slots,
2483
+ worldKeys: scene.worldKeys,
2484
+ lodProjectionFingerprint: projectionFingerprint,
2485
+ meshResidencyEpoch: input.meshResidencyEpoch,
2486
+ membership: shadowMembership,
2487
+ ...(skinArtifact === undefined ? {} : { skinArtifact }),
2488
+ materialArtifactSignature: artifactSignature,
2489
+ value: shadowFiltered,
2490
+ };
2491
+ }
2492
+ this.shadowSourcePlan = shadowFiltered.plan;
2493
+ this.shadowScene = scene.scene;
2494
+ const shadowCasterKeys =
2495
+ input.shadowCasterMembership === undefined
2496
+ ? input.shadowCasterDrawKeys
2497
+ : new Set(
2498
+ input.shadowCasterMembership.map((membership) =>
2499
+ gpuDrivenShadowDrawKey(
2500
+ membership.worldEntity,
2501
+ membership.materialHandle,
2502
+ membership.drawItemIndex,
2503
+ membership.passIndex,
2504
+ ),
2505
+ ),
2506
+ );
2507
+ this.shadowExpectedDrawKeys = new Set(shadowCasterKeys ?? []);
2508
+ const ownsAllShadowCasters =
2509
+ shadowCasterKeys === undefined ||
2510
+ [...shadowCasterKeys].every((key) => shadowFiltered.shadowDrawKeys.has(key));
2511
+ if (this.shadowViews === undefined) {
2512
+ const created = ShadowViewStatePool.create({
2513
+ device: this.device,
2514
+ shaderModuleFactory: this.shaderModuleFactory,
2515
+ });
2516
+ if (!created.ok) return created;
2517
+ this.shadowViews = created.value;
2518
+ }
2519
+ const projectionSource = {
2520
+ filtered,
2521
+ scene: scene.scene,
2522
+ contentRevision: scene.scene.contentRevision,
2523
+ materialArtifactSignature: artifactSignature,
2524
+ };
2525
+ const previousSource = this.committedProjectionSource;
2526
+ const reuseProjectionRows =
2527
+ previousSource?.filtered === filtered &&
2528
+ previousSource.scene === scene.scene &&
2529
+ previousSource.contentRevision === scene.scene.contentRevision &&
2530
+ previousSource.materialArtifactSignature === artifactSignature;
2531
+ if (!reuseProjectionRows) {
1194
2532
  const slotsByPrimitive = new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
2533
+ const representative = filtered.batches[0];
2534
+ if (representative === undefined) return ok(undefined);
1195
2535
  const projection = projectStandardPbrScene({
1196
- artifact: input.standardPbrArtifact,
1197
- candidates: filtered.batches.flatMap(({ batch }) =>
1198
- batch.candidates.flatMap((candidate) => {
1199
- const slot = slotsByPrimitive.get(candidate.primitiveIndex);
1200
- return slot === undefined
1201
- ? []
1202
- : [
1203
- {
1204
- primitiveIndex: candidate.primitiveIndex,
1205
- world: slot.snapshot.transform.world,
1206
- },
1207
- ];
1208
- }),
1209
- ),
2536
+ artifact: representative.artifact,
2537
+ candidates: representative.batch.candidates.flatMap((candidate) => {
2538
+ const slot = slotsByPrimitive.get(candidate.primitiveIndex);
2539
+ return slot === undefined ? [] : [sceneCandidate(slot, candidate)];
2540
+ }),
1210
2541
  });
1211
2542
  if (!projection.ok) {
1212
2543
  return err(
1213
2544
  new RhiError({
1214
2545
  code: 'shader-compile-failed',
1215
- expected: 'receipt-derived Standard PBR scene projection',
2546
+ expected: 'receipt-derived material scene projection',
1216
2547
  hint: 'publish one validated MaterialShaderArtifact receipt before entering the GPU lane',
1217
2548
  }),
1218
2549
  );
1219
2550
  }
1220
2551
  this.standardPbrProjection = projection.value;
1221
- if (input.standardPbrPipelineState !== undefined && this.standardRaster === undefined) {
1222
- const created = StandardPbrRasterAdapter.create({
1223
- device: this.device,
1224
- shaderModuleFactory: this.shaderModuleFactory,
1225
- pipelineState: input.standardPbrPipelineState,
1226
- artifact: input.standardPbrArtifact,
1227
- });
1228
- if (!created.ok) return created;
1229
- this.standardRaster = created.value;
1230
- }
1231
2552
  }
1232
2553
  if (this.view === undefined) {
1233
2554
  const created = GpuDrivenView.create({
@@ -1237,22 +2558,210 @@ export class GpuDrivenProduction {
1237
2558
  if (!created.ok) return created;
1238
2559
  this.view = created.value;
1239
2560
  }
1240
- if (this.raster === undefined) {
1241
- const created = GpuDrivenRigidRaster.create({
1242
- device: this.device,
1243
- shaderModuleFactory: this.shaderModuleFactory,
1244
- viewBindGroupLayout: input.viewBindGroupLayout,
1245
- });
1246
- if (!created.ok) return created;
1247
- this.raster = created.value;
1248
- }
1249
2561
  const updated = this.view.update(filtered.plan, scene.scene, activeFrustum(input.camera));
1250
2562
  if (!updated.ok) return updated;
1251
2563
  this.telemetryPrepared = true;
1252
2564
  const view = this.view;
1253
- const raster = this.raster;
1254
- const standardRaster = this.standardRaster;
1255
- const standardArtifact = input.standardPbrArtifact;
2565
+ const visibleBuffer = view.visibleBuffer;
2566
+ if (visibleBuffer === undefined) {
2567
+ return err(
2568
+ new RhiError({
2569
+ code: 'internal-error',
2570
+ expected: 'GpuDrivenView visible buffer after update',
2571
+ hint: 'keep the visible-index binding request on the updated GPU scene view',
2572
+ }),
2573
+ );
2574
+ }
2575
+ const representativeArtifact = materialArtifact ?? filtered.batches[0]?.artifact;
2576
+ if (representativeArtifact === undefined) {
2577
+ return err(
2578
+ new RhiError({
2579
+ code: 'rhi-not-available',
2580
+ expected: 'one selected MaterialProgramAbi artifact for GPU-driven batches',
2581
+ hint: 'keep receipt-backed draws blocked until Catalog/load publishes their artifact',
2582
+ }),
2583
+ );
2584
+ }
2585
+ const committedArtifactChanged =
2586
+ this.committedProjectionSource !== undefined &&
2587
+ this.committedProjectionSource.materialArtifactSignature !== artifactSignature;
2588
+ if (committedArtifactChanged) {
2589
+ // ShadowViewStatePool's cache token is light/view scoped and cannot see
2590
+ // a material publication replacement. Drop the retained projections at
2591
+ // the same boundary as the main PSO cache so a cache hit cannot replay
2592
+ // the previous shadow program or BGL with the new receipt.
2593
+ this.shadowBatchProjections.clear();
2594
+ this.shadowBatchProjectionSource = undefined;
2595
+ this.shadowViews?.invalidate();
2596
+ }
2597
+ if (this.committedProjectionSource?.materialArtifactSignature !== artifactSignature) {
2598
+ this.materialRaster?.invalidatePipelineCache();
2599
+ this.skinRaster?.invalidatePipelineCache();
2600
+ }
2601
+ if (this.materialRaster === undefined) {
2602
+ const created = MaterialAbiRasterAdapter.create({
2603
+ device: this.device,
2604
+ pipelineState: materialPipelineState,
2605
+ artifact: representativeArtifact,
2606
+ visibleBuffer,
2607
+ ...(input.materialPipelineFactory === undefined
2608
+ ? {}
2609
+ : { pipelineFactory: input.materialPipelineFactory }),
2610
+ });
2611
+ if (!created.ok) return created;
2612
+ this.materialRaster = created.value;
2613
+ } else {
2614
+ const rebound = this.materialRaster.ensureVisibleBuffer(visibleBuffer);
2615
+ if (!rebound.ok) return rebound;
2616
+ }
2617
+ const materialRaster = this.materialRaster;
2618
+ if (materialRaster === undefined) {
2619
+ return err(
2620
+ new RhiError({
2621
+ code: 'internal-error',
2622
+ expected: 'material ABI raster adapter after prepared artifact admission',
2623
+ hint: 'keep the selected material artifact and PipelineState on one frame identity',
2624
+ }),
2625
+ );
2626
+ }
2627
+ const skinBatch = filtered.batches.find(
2628
+ ({ batch }) => batch.candidates[0]?.prepared?.identity.deformation === 'skin',
2629
+ );
2630
+ if (skinBatch !== undefined) {
2631
+ const skinSnapshot = scene.slots.find(
2632
+ (slot) => slot.slot === skinBatch.batch.candidates[0]?.primitiveIndex,
2633
+ )?.snapshot.skin;
2634
+ const skinAllocator = materialPipelineState.skinPaletteAllocator;
2635
+ const selectedSkinArtifact = skinArtifact ?? skinBatch.artifact;
2636
+ if (
2637
+ selectedSkinArtifact === undefined ||
2638
+ skinSnapshot === undefined ||
2639
+ skinAllocator === null ||
2640
+ skinAllocator === undefined
2641
+ ) {
2642
+ return err(
2643
+ new RhiError({
2644
+ code: 'rhi-not-available',
2645
+ expected: 'skinned material artifact and persistent palette owner',
2646
+ hint: 'keep missing skin producer resources on the CPU deformation path',
2647
+ }),
2648
+ );
2649
+ }
2650
+ if (this.skinRaster === undefined) {
2651
+ const created = MaterialAbiRasterAdapter.create({
2652
+ device: this.device,
2653
+ pipelineState: materialPipelineState,
2654
+ artifact: selectedSkinArtifact,
2655
+ visibleBuffer,
2656
+ deformation: 'skin',
2657
+ paletteBuffer: skinSnapshot.buffer,
2658
+ paletteBindingWindowBytes: skinAllocator.bindingWindowBytes,
2659
+ ...(input.materialPipelineFactory === undefined
2660
+ ? {}
2661
+ : { pipelineFactory: input.materialPipelineFactory }),
2662
+ });
2663
+ if (!created.ok) return created;
2664
+ this.skinRaster = created.value;
2665
+ } else {
2666
+ const rebound = this.skinRaster.ensureVisibleBuffer(visibleBuffer);
2667
+ if (!rebound.ok) return rebound;
2668
+ const palette = this.skinRaster.ensurePaletteBuffer(
2669
+ skinSnapshot.buffer,
2670
+ skinAllocator.bindingWindowBytes,
2671
+ );
2672
+ if (!palette.ok) return palette;
2673
+ }
2674
+ if (this.skinPaletteBuffer !== skinSnapshot.buffer) {
2675
+ this.skinPaletteBuffer = skinSnapshot.buffer;
2676
+ this.shadowBatchProjections.clear();
2677
+ this.shadowBatchProjectionSource = undefined;
2678
+ this.shadowViews?.invalidate();
2679
+ }
2680
+ }
2681
+ const skinRaster = this.skinRaster;
2682
+ const shadowViewPool = this.shadowViews;
2683
+ if (shadowViewPool === undefined) {
2684
+ return err(
2685
+ new RhiError({
2686
+ code: 'internal-error',
2687
+ expected: 'ShadowViewStatePool after GPU-driven preparation',
2688
+ hint: 'publish view-keyed shadow state with the prepared frame',
2689
+ }),
2690
+ );
2691
+ }
2692
+ // A pose can change while conservative bounds, allocation and scene rows
2693
+ // remain unchanged. Read the palette owner, never optional telemetry.
2694
+ const paletteContentRevision = materialPipelineState.skinPaletteAllocator?.contentRevision;
2695
+ if (paletteContentRevision !== this.committedPaletteContentRevision)
2696
+ shadowViewPool.invalidate();
2697
+ const abortProvisionalResources = (): void => {
2698
+ materialRaster.abortProjectionBindings();
2699
+ skinRaster?.abortProjectionBindings();
2700
+ shadowViewPool._abortResourceReplacement();
2701
+ };
2702
+ // The render graph is cached by topology, while transforms are frame
2703
+ // data. Refresh retained scene-row buffers here so a cached graph reads
2704
+ // the current world matrices without forcing a topology rebuild.
2705
+ let slotsByPrimitive: Map<number, PersistentGpuDrivenState['slots'][number]> | undefined;
2706
+ const projectionSlot = (index: number) => {
2707
+ slotsByPrimitive ??= new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
2708
+ return slotsByPrimitive.get(index);
2709
+ };
2710
+ // Start the provisional resource set before any retained row is updated;
2711
+ // an aborted graph can then restore the active rows from its snapshot.
2712
+ materialRaster.stageProjectionKeys(new Set());
2713
+ skinRaster?.stageProjectionKeys(new Set());
2714
+ for (const prepared of filtered.batches) {
2715
+ if (reuseProjectionRows) {
2716
+ const deformation = prepared.batch.candidates[0]?.prepared?.identity.deformation ?? 'rigid';
2717
+ const raster = deformation === 'skin' ? skinRaster : materialRaster;
2718
+ const artifact = prepared.artifact;
2719
+ if (raster !== undefined && artifact !== undefined)
2720
+ raster.noteProjectionKey(gpuDrivenProjectionKey(prepared.batch, deformation, artifact));
2721
+ continue;
2722
+ }
2723
+ const admittedCandidates = prepared.batch.candidates.filter(
2724
+ (candidate) =>
2725
+ (candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
2726
+ .submitAdmission !== false,
2727
+ );
2728
+ if (admittedCandidates.length === 0) continue;
2729
+ const deformation = admittedCandidates[0]?.prepared?.identity.deformation ?? 'rigid';
2730
+ const raster = deformation === 'skin' ? skinRaster : materialRaster;
2731
+ const artifact = prepared.artifact;
2732
+ if (raster === undefined) continue;
2733
+ const projectionKey = gpuDrivenProjectionKey(prepared.batch, deformation, artifact);
2734
+ raster.noteProjectionKey(projectionKey);
2735
+ const candidates = admittedCandidates.flatMap((candidate) => {
2736
+ const slot = projectionSlot(candidate.primitiveIndex);
2737
+ return slot === undefined ? [] : [sceneCandidate(slot, candidate)];
2738
+ });
2739
+ const projection = projectStandardPbrScene({ artifact, candidates });
2740
+ if (!projection.ok) {
2741
+ abortProvisionalResources();
2742
+ return err(
2743
+ new RhiError({
2744
+ code: 'shader-compile-failed',
2745
+ expected: 'receipt-derived material scene projection',
2746
+ hint: 'keep the retained projection rows on the current material receipt',
2747
+ }),
2748
+ );
2749
+ }
2750
+ const refreshed = raster.refreshProjectionRows(projectionKey, projection.value.rows);
2751
+ if (!refreshed.ok) {
2752
+ abortProvisionalResources();
2753
+ return refreshed;
2754
+ }
2755
+ }
2756
+ // Bindings created by the graph projection below are provisional until
2757
+ // the frame submits. A successful commit promotes exactly these keys and
2758
+ // fence-retires any superseded batch generation; an abort keeps the last
2759
+ // known-good active set and drops only staged bindings. Shadow view update
2760
+ // adds its keyed projections to the same staged set.
2761
+ if (this.shadowBatchProjectionSource !== shadowFiltered.plan) {
2762
+ this.shadowBatchProjections.clear();
2763
+ this.shadowBatchProjectionSource = shadowFiltered.plan;
2764
+ }
1256
2765
  const sceneIdentity = this.sceneIdentity(scene.scene);
1257
2766
  const viewInspection = view.inspect();
1258
2767
  let signature = this.signatureCache?.value;
@@ -1267,26 +2776,43 @@ export class GpuDrivenProduction {
1267
2776
  sceneIdentity,
1268
2777
  revision: filtered.plan.revision,
1269
2778
  meshResidencyEpoch: input.meshResidencyEpoch,
2779
+ materialArtifactSignature: artifactSignature,
1270
2780
  standardPbrLayoutIdentity: this.standardPbrProjection?.layoutIdentity,
1271
2781
  viewResourceGeneration: viewInspection.resourceGeneration,
1272
- batches: filtered.batches.map(({ batch, mesh }) => [
2782
+ batches: filtered.batches.map(({ batch, mesh, artifact }) => [
1273
2783
  batch.batchId,
1274
2784
  batch.generation,
1275
- batch.visibleCapacity,
1276
2785
  batch.visibleBase,
2786
+ batch.visibleCapacity,
1277
2787
  batch.candidates.map((candidate) => [
1278
2788
  candidate.primitiveIndex,
1279
2789
  candidate.generation,
1280
2790
  candidate.instanceOrdinal,
1281
2791
  ]),
2792
+ batch.candidates
2793
+ .filter(
2794
+ (candidate) =>
2795
+ (candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
2796
+ .submitAdmission !== false,
2797
+ )
2798
+ .map((candidate) => [
2799
+ candidate.primitiveIndex,
2800
+ candidate.generation,
2801
+ candidate.instanceOrdinal,
2802
+ ]),
1282
2803
  mesh.vboBytes,
1283
2804
  mesh.iboBytes,
1284
2805
  mesh.indexFormat,
1285
2806
  mesh.uvSetCount,
2807
+ artifact.material,
2808
+ artifact.layoutIdentity,
2809
+ artifact.receipt?.receiptIdentity,
2810
+ artifact.receipt?.generation,
2811
+ artifact.variantSet,
2812
+ artifact.wgsl.length,
1286
2813
  ]),
1287
2814
  capacities: [
1288
2815
  viewInspection.candidateCapacity,
1289
- viewInspection.visibleBufferCapacity,
1290
2816
  viewInspection.batchCapacity,
1291
2817
  viewInspection.indirectCapacity,
1292
2818
  ],
@@ -1299,31 +2825,102 @@ export class GpuDrivenProduction {
1299
2825
  value: signature,
1300
2826
  };
1301
2827
  }
2828
+ const shadowTopologySignature = () =>
2829
+ this.shadowViews
2830
+ ?.inspect()
2831
+ .map(
2832
+ (entry) =>
2833
+ `${shadowViewIdentityKey(entry.identity)}:${entry.cache}:${entry.generation}:${entry.sourceRevision}:${entry.resourceGeneration}`,
2834
+ )
2835
+ .join('|') ?? '';
2836
+ const production = this;
1302
2837
  return ok({
1303
- topologySignature: signature,
1304
- entityKeys: filtered.entityKeys,
1305
- ownsAllRenderables:
1306
- input.activeEntityKeys === undefined
1307
- ? filtered.entityKeys.size === scene.slots.length
1308
- : filtered.entityKeys.size === input.activeEntityKeys.size,
2838
+ get topologySignature() {
2839
+ const shadow = shadowTopologySignature();
2840
+ return shadow.length === 0 ? signature : `${signature}|shadow=${shadow}`;
2841
+ },
2842
+ ownsAllDrawItems: filtered.ownsAllDrawItems,
1309
2843
  ...(input.occlusion === undefined ? {} : { occlusion: input.occlusion }),
1310
- _commitResourceReplacement: () => view._commitResourceReplacement(),
1311
- project: (graph, format, sampleCount) => {
2844
+ ownsAllShadowCasters,
2845
+ drawKeys: filtered.drawKeys,
2846
+ shadowDrawKeys: shadowFiltered.shadowDrawKeys,
2847
+ get shadowDrawKeysByView() {
2848
+ return new Map(production.shadowDrawKeysByView);
2849
+ },
2850
+ standardPbrFrameResources: {
2851
+ materialBindGroups: [],
2852
+ materialSlotIndicesByEntity: new Map(),
2853
+ instancesBindGroup: materialRaster.instancesFrameGroup,
2854
+ materialStride: MATERIAL_PER_ENTITY_STRIDE,
2855
+ sceneMaterialBuffer: scene.scene.materialBuffer,
2856
+ },
2857
+ _commitGpuResourceReplacement: () => {
2858
+ view._commitResourceReplacement();
2859
+ },
2860
+ _commitResourceReplacement: () => {
2861
+ view._commitResourceReplacement();
2862
+ shadowViewPool._commitResourceReplacement();
2863
+ materialRaster.commitProjectionBindings();
2864
+ skinRaster?.commitProjectionBindings();
2865
+ this.committedProjectionSource = projectionSource;
2866
+ this.committedPaletteContentRevision = paletteContentRevision;
2867
+ this.submissionState = 'submitted';
2868
+ this.lastKnownGoodGeneration = view.inspect().resourceGeneration;
2869
+ },
2870
+ _abortResourceReplacement: () => {
2871
+ shadowViewPool._abortResourceReplacement();
2872
+ materialRaster.abortProjectionBindings();
2873
+ skinRaster?.abortProjectionBindings();
2874
+ this.committedProjectionSource = undefined;
2875
+ this.submissionState = 'aborted';
2876
+ this.retryCount += 1;
2877
+ },
2878
+ projectShadow: (graph, identity) => {
2879
+ if (!shadowViewPool.isActive(identity) || shadowViewPool.submission(identity) === undefined)
2880
+ return ok(undefined);
2881
+ return shadowViewPool.project(graph, identity);
2882
+ },
2883
+ updateShadowViews: (views) =>
2884
+ shadowFiltered.plan.batches.length > 0
2885
+ ? this.updateShadowViews(
2886
+ views,
2887
+ materialArtifact,
2888
+ skinArtifact,
2889
+ input.materialArtifacts,
2890
+ input.shadowMaterialArtifacts,
2891
+ shadowFiltered,
2892
+ scene,
2893
+ input.shadowCasterDrawKeys,
2894
+ input.shadowCasterMembership,
2895
+ )
2896
+ : this.disableShadowViews(),
2897
+ shadowViewPool,
2898
+ get shadowBatchProjections() {
2899
+ return new Map(
2900
+ [...production.shadowBatchProjections].map(
2901
+ ([key, projections]) => [key, new Map(projections)] as const,
2902
+ ),
2903
+ );
2904
+ },
2905
+ project: (graph, format, sampleCount, additionalColorFormats) => {
1312
2906
  this.indirectDrawCount = admittedRasterBatchCount(filtered.plan);
1313
2907
  const outputs = view.addPasses(graph);
1314
2908
  if (!outputs.ok) return outputs;
1315
- if (
1316
- usesStandardPbrRaster &&
1317
- standardRaster !== undefined &&
1318
- standardArtifact !== undefined
1319
- ) {
2909
+ if (materialRaster !== undefined && filtered.batches.length > 0) {
1320
2910
  const standardProjected: Array<{
1321
2911
  readonly batch: GpuDrivenBatch;
1322
2912
  readonly mesh: MeshGpuHandles;
2913
+ readonly materialEntityKey: number;
1323
2914
  readonly vertex: GraphBuffer;
1324
2915
  readonly index?: GraphBuffer;
1325
- readonly pipeline: RenderPipeline;
2916
+ readonly pipelineFor: (
2917
+ formats: readonly TextureFormat[],
2918
+ ) => Result<RenderPipeline, RhiError>;
1326
2919
  readonly meshBindGroup: BindGroup;
2920
+ readonly raster: MaterialAbiRasterAdapter;
2921
+ readonly deformation: 'rigid' | 'skin';
2922
+ readonly clustered: boolean;
2923
+ readonly projectionKey: string;
1327
2924
  }> = [];
1328
2925
  const standardAccesses: GraphAccess[] = [
1329
2926
  { resource: outputs.value.primitive, usage: 'storage-read' },
@@ -1333,42 +2930,72 @@ export class GpuDrivenProduction {
1333
2930
  { resource: outputs.value.transform, usage: 'storage-read' },
1334
2931
  { resource: outputs.value.indirect, usage: 'indirect-read' },
1335
2932
  ];
1336
- const slotsByPrimitive = new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
1337
2933
  const meshBuffers = new Map<
1338
- number,
2934
+ MeshGpuHandles,
1339
2935
  { readonly vertex: GraphBuffer; readonly index?: GraphBuffer }
1340
2936
  >();
1341
2937
  for (const prepared of filtered.batches) {
1342
- const candidates = prepared.batch.candidates.flatMap((candidate) => {
1343
- const slot = slotsByPrimitive.get(candidate.primitiveIndex);
1344
- return slot === undefined
1345
- ? []
1346
- : [
1347
- {
1348
- primitiveIndex: candidate.primitiveIndex,
1349
- world: slot.snapshot.transform.world,
1350
- },
1351
- ];
2938
+ if (prepared.batch.visibleCapacity === 0) continue;
2939
+ const admittedCandidates = prepared.batch.candidates.filter(
2940
+ (candidate) =>
2941
+ (candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
2942
+ .submitAdmission !== false,
2943
+ );
2944
+ if (admittedCandidates.length === 0) continue;
2945
+ const deformation = admittedCandidates[0]?.prepared?.identity.deformation ?? 'rigid';
2946
+ const raster = deformation === 'skin' ? skinRaster : materialRaster;
2947
+ const artifact = prepared.artifact;
2948
+ if (raster === undefined) {
2949
+ return err(
2950
+ new RhiError({
2951
+ code: 'rhi-not-available',
2952
+ expected: `${deformation === 'skin' ? 'skinned ' : ''}published material ABI raster owner`,
2953
+ hint: 'publish the selected material artifact before entering the GPU lane',
2954
+ }),
2955
+ );
2956
+ }
2957
+ // The visible stream is compacted after the persistent visibility
2958
+ // facet applies its active-entity admission. Project the mesh rows
2959
+ // from that same admitted sequence so visible.x remains the
2960
+ // batch-local row identity even when a suppressed candidate sits
2961
+ // between two submitted candidates.
2962
+ const candidates = admittedCandidates.flatMap((candidate) => {
2963
+ const slot = projectionSlot(candidate.primitiveIndex);
2964
+ return slot === undefined ? [] : [sceneCandidate(slot, candidate)];
1352
2965
  });
1353
2966
  const projection = projectStandardPbrScene({
1354
- artifact: standardArtifact,
2967
+ artifact,
1355
2968
  candidates,
1356
2969
  });
1357
2970
  if (!projection.ok) {
1358
2971
  return err(
1359
2972
  new RhiError({
1360
2973
  code: 'shader-compile-failed',
1361
- expected: 'receipt-derived Standard PBR scene projection',
2974
+ expected: 'receipt-derived material scene projection',
1362
2975
  hint: 'keep the frame artifact receipt and projected rows on one identity',
1363
2976
  }),
1364
2977
  );
1365
2978
  }
1366
- const meshBindGroup = standardRaster.projectionBindGroup(projection.value.rows);
2979
+ const meshBindGroup = raster.projectionBindGroup(
2980
+ projection.value.rows,
2981
+ gpuDrivenProjectionKey(prepared.batch, deformation, artifact),
2982
+ prepared.batch.candidates.length,
2983
+ );
1367
2984
  if (!meshBindGroup.ok) return meshBindGroup;
1368
- let buffers = meshBuffers.get(prepared.batch.key.assetHandle);
2985
+ const materialSlot = projectionSlot(admittedCandidates[0]?.primitiveIndex ?? -1);
2986
+ if (materialSlot === undefined) {
2987
+ return err(
2988
+ new RhiError({
2989
+ code: 'internal-error',
2990
+ expected: 'batch representative entity for global material slot projection',
2991
+ hint: 'preserve the retained scene identity when projecting the GPU batch',
2992
+ }),
2993
+ );
2994
+ }
2995
+ let buffers = meshBuffers.get(prepared.mesh);
1369
2996
  if (buffers === undefined) {
1370
2997
  const vertex = graph.importBuffer(
1371
- `gpu-driven.standard.${prepared.batch.key.assetHandle}.vertex`,
2998
+ `gpu-driven.standard.${prepared.worldKey}.${prepared.batch.key.assetHandle}.vertex`,
1372
2999
  {
1373
3000
  size: prepared.mesh.vboBytes,
1374
3001
  usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
@@ -1380,7 +3007,7 @@ export class GpuDrivenProduction {
1380
3007
  if (prepared.mesh.indexBuffer !== null) {
1381
3008
  const indexHandle = prepared.mesh.indexBuffer;
1382
3009
  const imported = graph.importBuffer(
1383
- `gpu-driven.standard.${prepared.batch.key.assetHandle}.index`,
3010
+ `gpu-driven.standard.${prepared.worldKey}.${prepared.batch.key.assetHandle}.index`,
1384
3011
  {
1385
3012
  size: prepared.mesh.iboBytes,
1386
3013
  usage: GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST,
@@ -1391,53 +3018,144 @@ export class GpuDrivenProduction {
1391
3018
  index = imported.value;
1392
3019
  }
1393
3020
  buffers = { vertex: vertex.value, ...(index === undefined ? {} : { index }) };
1394
- meshBuffers.set(prepared.batch.key.assetHandle, buffers);
3021
+ meshBuffers.set(prepared.mesh, buffers);
1395
3022
  standardAccesses.push({ resource: buffers.vertex, usage: 'vertex-read' });
1396
3023
  if (buffers.index !== undefined) {
1397
3024
  standardAccesses.push({ resource: buffers.index, usage: 'index-read' });
1398
3025
  }
1399
3026
  }
1400
- const pipeline = standardRaster.pipeline(
1401
- format,
1402
- sampleCount,
1403
- prepared.mesh.layoutProjection,
1404
- prepared.batch.key.topology,
1405
- prepared.batch.key.drawKind === 'indexed' &&
1406
- (prepared.batch.key.topology === 'line-strip' ||
1407
- prepared.batch.key.topology === 'triangle-strip')
1408
- ? prepared.mesh.indexFormat
1409
- : undefined,
1410
- prepared.renderState,
1411
- );
1412
- if (!pipeline.ok) return pipeline;
3027
+ const pipelineFor = (
3028
+ formats: readonly TextureFormat[],
3029
+ ): Result<RenderPipeline, RhiError> => {
3030
+ return raster.pipeline(
3031
+ formats[0] ?? format,
3032
+ sampleCount,
3033
+ prepared.mesh.layoutProjection,
3034
+ // The producer receipt owns the selected shader's UV ABI. The
3035
+ // Standard count is only a fallback for legacy synthetic
3036
+ // fixtures; using it for a custom program would add aliases
3037
+ // the shader never declares (or omit a declared custom set).
3038
+ artifact.uvSetCount ?? materialPipelineState.standardPbrShaderUvSetCount,
3039
+ prepared.batch.key.topology,
3040
+ prepared.batch.key.drawKind === 'indexed' &&
3041
+ (prepared.batch.key.topology === 'line-strip' ||
3042
+ prepared.batch.key.topology === 'triangle-strip')
3043
+ ? prepared.mesh.indexFormat
3044
+ : undefined,
3045
+ prepared.renderState,
3046
+ artifact,
3047
+ formats.slice(1),
3048
+ );
3049
+ };
3050
+ const initialPipeline = pipelineFor([format, ...(additionalColorFormats ?? [])]);
3051
+ if (!initialPipeline.ok) return initialPipeline;
1413
3052
  standardProjected.push({
1414
3053
  batch: prepared.batch,
1415
3054
  mesh: prepared.mesh,
3055
+ materialEntityKey: worldEntityKey(
3056
+ materialSlot.snapshot.worldId,
3057
+ materialSlot.snapshot.entityKey,
3058
+ ),
1416
3059
  ...buffers,
1417
- pipeline: pipeline.value,
3060
+ pipelineFor,
1418
3061
  meshBindGroup: meshBindGroup.value,
3062
+ raster,
3063
+ deformation,
3064
+ clustered: ['cluster', 'skin-cluster'].includes(
3065
+ resolvePipelineGroup2Contract(artifact.wgsl),
3066
+ ),
3067
+ projectionKey: gpuDrivenProjectionKey(prepared.batch, deformation, artifact),
1419
3068
  });
1420
3069
  }
1421
- const [materialBindGroup, instancesBindGroup] = standardRaster.frameGroups;
3070
+ if (
3071
+ standardProjected.length === 0 ||
3072
+ standardProjected.length !== filtered.batches.length
3073
+ ) {
3074
+ return err(
3075
+ new RhiError({
3076
+ code: 'internal-error',
3077
+ expected: 'one actual material ABI raster dispatch for each eligible batch',
3078
+ hint: 'repair the receipt-backed batch projection before encoding the frame',
3079
+ }),
3080
+ );
3081
+ }
3082
+ const rasterDispatches = Object.freeze(standardProjected);
1422
3083
  return ok({
1423
3084
  accesses: standardAccesses,
1424
- encode: (viewBindGroup, pass, resources: GraphResourceResolver) => {
1425
- // pbr-view-bgl has two dynamic uniform bindings: the frame view
1426
- // slot and the optional Points/Lines style slot. Standard
1427
- // draws do not use the latter, but WebGPU still requires one
1428
- // offset per dynamic binding.
3085
+ encode: (
3086
+ viewBindGroup,
3087
+ pass,
3088
+ resources: GraphResourceResolver,
3089
+ frameResources: GpuDrivenStandardPbrFrameResources,
3090
+ ) => {
3091
+ // The shared Standard view BGL carries two dynamic buffers:
3092
+ // the frame view UBO and the per-frame shadow/light table. GPU
3093
+ // indirect draws use the same zero offsets as direct draws;
3094
+ // supplying only one offset makes Dawn reject the command
3095
+ // buffer before the indirect work can reach the rasterizer.
1429
3096
  pass.setBindGroup(0, viewBindGroup, [0, 0]);
1430
- pass.setBindGroup(1, materialBindGroup, [0]);
1431
- pass.setBindGroup(3, instancesBindGroup);
1432
3097
  let currentPipeline: RenderPipeline | undefined;
1433
3098
  let currentVertex: GraphBuffer | undefined;
1434
3099
  let currentIndex: GraphBuffer | undefined;
1435
- for (const batch of standardProjected) {
1436
- if (currentPipeline !== batch.pipeline) {
1437
- pass.setPipeline(batch.pipeline);
1438
- currentPipeline = batch.pipeline;
3100
+ for (const batch of rasterDispatches) {
3101
+ const globalMaterialSlots = frameResources.materialSlotIndicesByEntity.get(
3102
+ batch.materialEntityKey,
3103
+ );
3104
+ const globalMaterialSlot =
3105
+ globalMaterialSlots?.[batch.batch.key.materialSlot] ??
3106
+ (frameResources.materialSlotIndicesByEntity.size === 0
3107
+ ? batch.batch.key.materialSlot
3108
+ : undefined);
3109
+ if (globalMaterialSlot === undefined) {
3110
+ throw new RhiError({
3111
+ code: 'rhi-not-available',
3112
+ expected: `frame-global material slot for entity ${batch.materialEntityKey}`,
3113
+ hint: 'populate materialSlotIndicesByEntity from the main-pass material producer',
3114
+ });
3115
+ }
3116
+ if (
3117
+ frameResources.selectedMaterialSlots !== undefined &&
3118
+ !frameResources.selectedMaterialSlots.has(globalMaterialSlot)
3119
+ )
3120
+ continue;
3121
+ const selectedPipeline = batch.pipelineFor(
3122
+ frameResources.colorFormats ?? [format, ...(additionalColorFormats ?? [])],
3123
+ );
3124
+ if (!selectedPipeline.ok) throw selectedPipeline.error;
3125
+ const pipeline = selectedPipeline.value;
3126
+ if (currentPipeline !== pipeline) {
3127
+ pass.setPipeline(pipeline);
3128
+ currentPipeline = pipeline;
1439
3129
  }
1440
- pass.setBindGroup(2, batch.meshBindGroup, [0]);
3130
+ const materialBindGroup = frameResources.materialBindGroups[globalMaterialSlot];
3131
+ if (materialBindGroup === undefined) {
3132
+ throw new RhiError({
3133
+ code: 'rhi-not-available',
3134
+ expected: `material bind group for global slot ${globalMaterialSlot}`,
3135
+ hint: 'keep GPU-driven material selection on the main-pass producer contract',
3136
+ });
3137
+ }
3138
+ pass.setBindGroup(1, materialBindGroup, [
3139
+ globalMaterialSlot * frameResources.materialStride,
3140
+ ]);
3141
+ const meshBindGroup = batch.clustered
3142
+ ? frameResources.resolveClusteredMeshBindGroup?.(
3143
+ batch.raster.sceneMeshBinding(batch.projectionKey),
3144
+ )
3145
+ : batch.meshBindGroup;
3146
+ if (meshBindGroup === undefined)
3147
+ throw new RhiError({
3148
+ code: 'rhi-not-available',
3149
+ expected: 'current scene-index lighting binding',
3150
+ hint: 'resolve clustered surface resources during the material pass',
3151
+ });
3152
+ pass.setBindGroup(2, meshBindGroup, batch.deformation === 'skin' ? [0, 0, 0] : [0]);
3153
+ const visibleWindow = batch.raster.visibleWindowBindGroup(
3154
+ batch.batch.visibleBase,
3155
+ batch.batch.visibleCapacity,
3156
+ );
3157
+ if (!visibleWindow.ok) throw visibleWindow.error;
3158
+ pass.setBindGroup(3, visibleWindow.value);
1441
3159
  if (currentVertex !== batch.vertex) {
1442
3160
  const vertex = resources.buffer(batch.vertex);
1443
3161
  if (!vertex.ok) throw vertex.error;
@@ -1457,129 +3175,261 @@ export class GpuDrivenProduction {
1457
3175
  } else {
1458
3176
  pass.drawIndirect(indirect.value, batch.batch.indirectOffset);
1459
3177
  }
3178
+ this.encodedIndirectDraws += 1;
1460
3179
  }
1461
3180
  },
1462
3181
  });
1463
3182
  }
1464
- const projected: ProjectedBatch[] = [];
1465
- const accesses: GraphAccess[] = [
1466
- { resource: outputs.value.primitive, usage: 'storage-read' },
1467
- { resource: outputs.value.instance, usage: 'storage-read' },
1468
- { resource: outputs.value.material, usage: 'storage-read' },
1469
- { resource: outputs.value.visible, usage: 'storage-read' },
1470
- { resource: outputs.value.transform, usage: 'storage-read' },
1471
- { resource: outputs.value.indirect, usage: 'indirect-read' },
1472
- ];
1473
- const meshBuffers = new Map<
1474
- number,
1475
- { readonly vertex: GraphBuffer; readonly index?: GraphBuffer }
1476
- >();
1477
- for (const prepared of filtered.batches) {
1478
- // A selector-only batch can have zero admitted candidates. Keep it
1479
- // in the GPU selector and readback counters, but do not create a
1480
- // raster bind group or encode a zero-instance indirect draw.
1481
- if (prepared.batch.visibleCapacity === 0) continue;
1482
- let buffers = meshBuffers.get(prepared.batch.key.assetHandle);
1483
- if (buffers === undefined) {
1484
- const vertex = graph.importBuffer(
1485
- `gpu-driven.mesh.${prepared.batch.key.assetHandle}.vertex`,
1486
- {
1487
- size: prepared.mesh.vboBytes,
1488
- usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
1489
- },
1490
- () => prepared.mesh.vertexBuffer.handle,
3183
+ return err(
3184
+ new RhiError({
3185
+ code: 'internal-error',
3186
+ expected: 'material ABI raster projection branch',
3187
+ hint: 'preserve the selected-artifact branch as the only raster owner',
3188
+ }),
3189
+ );
3190
+ },
3191
+ });
3192
+ }
3193
+
3194
+ updateShadowViews(
3195
+ views: readonly Omit<ShadowViewUpdateInput, 'sourcePlan' | 'scene'>[],
3196
+ artifact: MaterialShaderArtifact | undefined,
3197
+ skinArtifact: MaterialShaderArtifact | undefined,
3198
+ materialArtifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined,
3199
+ shadowMaterialArtifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined,
3200
+ filtered: FilteredProductionPlan,
3201
+ scene: PersistentGpuDrivenState,
3202
+ shadowCasterDrawKeys: ReadonlySet<string> | undefined,
3203
+ shadowCasterMembership: readonly ShadowCasterMembership[] | undefined,
3204
+ ): Result<void, RhiError> {
3205
+ if (
3206
+ this.shadowViews === undefined ||
3207
+ this.shadowSourcePlan === undefined ||
3208
+ this.shadowScene === undefined
3209
+ ) {
3210
+ return ok(undefined);
3211
+ }
3212
+ const nextProjections = new Map<string, ReadonlyMap<number, GpuDrivenShadowBatchProjection>>();
3213
+ const nextShadowDrawKeysByView = new Map<string, ReadonlySet<string>>();
3214
+ const materialRaster = this.materialRaster;
3215
+ if (materialRaster === undefined) {
3216
+ return err(
3217
+ new RhiError({
3218
+ code: 'internal-error',
3219
+ expected: 'material ABI raster adapter for shadow projection',
3220
+ hint: 'prepare the selected-artifact raster owner before updating shadow views',
3221
+ }),
3222
+ );
3223
+ }
3224
+ let slotsByPrimitive: Map<number, PersistentGpuDrivenState['slots'][number]> | undefined;
3225
+ const preparedByBatch = new Map(filtered.batches.map((entry) => [entry.batch.batchId, entry]));
3226
+ for (const input of views) {
3227
+ const updated = this.shadowViews.update({
3228
+ ...input,
3229
+ sourcePlan: this.shadowSourcePlan,
3230
+ scene: this.shadowScene,
3231
+ });
3232
+ if (!updated.ok) return updated;
3233
+ const identityKey = shadowViewIdentityKey(input.identity);
3234
+ const previousProjections = this.shadowBatchProjections.get(identityKey);
3235
+ const previousClaims = this.shadowDrawKeysByView.get(identityKey);
3236
+ if (
3237
+ updated.value.cache === 'hit' &&
3238
+ previousProjections !== undefined &&
3239
+ previousClaims !== undefined
3240
+ ) {
3241
+ nextShadowDrawKeysByView.set(identityKey, previousClaims);
3242
+ for (const batch of updated.value.plan.batches) {
3243
+ const deformation = batch.candidates[0]?.prepared?.identity.deformation ?? 'rigid';
3244
+ const batchArtifact =
3245
+ preparedByBatch.get(batch.batchId)?.artifact ??
3246
+ artifactForPreparedCandidate(
3247
+ batch.candidates[0] as GpuDrivenBatch['candidates'][number],
3248
+ materialArtifacts,
3249
+ artifact,
3250
+ skinArtifact,
1491
3251
  );
1492
- if (!vertex.ok) return vertex;
1493
- let index: GraphBuffer | undefined;
1494
- const indexHandle = prepared.mesh.indexBuffer;
1495
- if (indexHandle !== null) {
1496
- const imported = graph.importBuffer(
1497
- `gpu-driven.mesh.${prepared.batch.key.assetHandle}.index`,
1498
- {
1499
- size: prepared.mesh.iboBytes,
1500
- usage: GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST,
1501
- },
1502
- () => indexHandle.handle,
1503
- );
1504
- if (!imported.ok) return imported;
1505
- index = imported.value;
1506
- }
1507
- buffers = { vertex: vertex.value, ...(index === undefined ? {} : { index }) };
1508
- meshBuffers.set(prepared.batch.key.assetHandle, buffers);
1509
- accesses.push({ resource: buffers.vertex, usage: 'vertex-read' });
1510
- if (buffers.index !== undefined) {
1511
- accesses.push({ resource: buffers.index, usage: 'index-read' });
1512
- }
3252
+ const shadowArtifact = shadowArtifactForPreparedCandidate(
3253
+ batch.candidates[0] as GpuDrivenBatch['candidates'][number],
3254
+ shadowMaterialArtifacts,
3255
+ batchArtifact,
3256
+ );
3257
+ const raster = deformation === 'skin' ? this.skinRaster : materialRaster;
3258
+ if (shadowArtifact !== undefined && raster !== undefined) {
3259
+ raster.noteProjectionKey(
3260
+ gpuDrivenShadowProjectionKey(identityKey, batch, deformation, shadowArtifact),
3261
+ );
3262
+ }
3263
+ }
3264
+ nextProjections.set(identityKey, previousProjections);
3265
+ continue;
3266
+ }
3267
+ slotsByPrimitive ??= new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
3268
+ const viewClaimedKeys = new Set<string>();
3269
+ for (const batch of updated.value.plan.batches) {
3270
+ for (const candidate of batch.candidates) {
3271
+ const slot = slotsByPrimitive?.get(candidate.primitiveIndex);
3272
+ if (slot === undefined) continue;
3273
+ if (
3274
+ !shadowMembershipIsGpuCompatible(
3275
+ slot,
3276
+ candidate,
3277
+ shadowCasterDrawKeys,
3278
+ shadowCasterMembership,
3279
+ )
3280
+ ) {
3281
+ continue;
3282
+ }
3283
+ for (const key of shadowKeysForCandidate(
3284
+ slot,
3285
+ candidate,
3286
+ shadowCasterDrawKeys,
3287
+ shadowCasterMembership,
3288
+ )) {
3289
+ if (filtered.shadowDrawKeys.has(key)) viewClaimedKeys.add(key);
1513
3290
  }
1514
- const pipeline = raster.pipeline(
1515
- format,
1516
- sampleCount,
1517
- prepared.mesh.layoutProjection.arrayStride,
1518
- prepared.batch.key.topology,
1519
- prepared.batch.key.drawKind === 'indexed' &&
1520
- (prepared.batch.key.topology === 'line-strip' ||
1521
- prepared.batch.key.topology === 'triangle-strip')
1522
- ? prepared.mesh.indexFormat
1523
- : undefined,
1524
- prepared.renderState,
3291
+ }
3292
+ }
3293
+ nextShadowDrawKeysByView.set(identityKey, Object.freeze(viewClaimedKeys));
3294
+ const submission = this.shadowViews.submission(input.identity);
3295
+ const visibleBuffer = submission?.view.visibleBuffer;
3296
+ if (submission === undefined || visibleBuffer === undefined) {
3297
+ return err(
3298
+ new RhiError({
3299
+ code: 'internal-error',
3300
+ expected: 'shadow view visible buffer after update',
3301
+ hint: 'publish the per-view visible window before recording indirect shadow draws',
3302
+ }),
3303
+ );
3304
+ }
3305
+ const projections = new Map<number, GpuDrivenShadowBatchProjection>();
3306
+ for (const batch of submission.plan.batches) {
3307
+ const prepared = preparedByBatch.get(batch.batchId);
3308
+ if (prepared === undefined) {
3309
+ return err(
3310
+ new RhiError({
3311
+ code: 'internal-error',
3312
+ expected: `prepared shadow mesh batch ${batch.batchId}`,
3313
+ hint: 'keep filtered shadow topology and per-view projection on one frame identity',
3314
+ }),
1525
3315
  );
1526
- if (!pipeline.ok) return pipeline;
1527
- const bindings = raster.bindGroup(scene.scene, view, prepared.batch);
1528
- if (!bindings.ok) return bindings;
1529
- this.batchBindGroupCreates += 1;
1530
- projected.push({
1531
- ...prepared,
1532
- ...buffers,
1533
- pipeline: pipeline.value,
1534
- bindings: bindings.value,
1535
- });
1536
3316
  }
1537
- this.indirectDrawCount = projected.length;
1538
- return ok({
1539
- accesses,
1540
- encode: (viewBindGroup, pass, resources: GraphResourceResolver) => {
1541
- // pbr-view-bgl carries the vertex-only Points/Lines UBO at binding
1542
- // 10 as a dynamic uniform buffer. The production view group is
1543
- // shared by the GPU-driven raster pipeline, so even rigid draws
1544
- // must provide the binding-10 offset (zero selects the frame
1545
- // default slot).
1546
- pass.setBindGroup(0, viewBindGroup, [0, 0]);
1547
- let currentPipeline: RenderPipeline | undefined;
1548
- let currentVertex: GraphBuffer | undefined;
1549
- let currentIndex: GraphBuffer | undefined;
1550
- for (const batch of projected) {
1551
- if (currentPipeline !== batch.pipeline) {
1552
- pass.setPipeline(batch.pipeline);
1553
- currentPipeline = batch.pipeline;
1554
- }
1555
- pass.setBindGroup(1, batch.bindings);
1556
- if (currentVertex !== batch.vertex) {
1557
- const vertex = resources.buffer(batch.vertex);
1558
- if (!vertex.ok) throw vertex.error;
1559
- pass.setVertexBuffer(0, vertex.value);
1560
- currentVertex = batch.vertex;
1561
- }
1562
- if (batch.index !== undefined && currentIndex !== batch.index) {
1563
- const index = resources.buffer(batch.index);
1564
- if (!index.ok) throw index.error;
1565
- pass.setIndexBuffer(index.value, batch.mesh.indexFormat);
1566
- currentIndex = batch.index;
1567
- }
1568
- const indirect = resources.buffer(outputs.value.indirect);
1569
- if (!indirect.ok) throw indirect.error;
1570
- const draw = (): void => {
1571
- if (batch.batch.key.drawKind === 'indexed') {
1572
- pass.drawIndexedIndirect(indirect.value, batch.batch.indirectOffset);
1573
- } else {
1574
- pass.drawIndirect(indirect.value, batch.batch.indirectOffset);
1575
- }
1576
- };
1577
- draw();
1578
- }
1579
- },
3317
+ const deformation = batch.candidates[0]?.prepared?.identity.deformation ?? 'rigid';
3318
+ const raster = deformation === 'skin' ? this.skinRaster : materialRaster;
3319
+ const batchArtifact = prepared.artifact;
3320
+ const shadowArtifact = shadowArtifactForPreparedCandidate(
3321
+ batch.candidates[0] as GpuDrivenBatch['candidates'][number],
3322
+ shadowMaterialArtifacts,
3323
+ batchArtifact,
3324
+ );
3325
+ if (raster === undefined || batchArtifact === undefined || shadowArtifact === undefined) {
3326
+ return err(
3327
+ new RhiError({
3328
+ code: 'rhi-not-available',
3329
+ expected: `${deformation === 'skin' ? 'skinned ' : ''}selected shadow MaterialProgramAbi artifact`,
3330
+ hint: 'publish a matching shadow-pass program before entering the capable shadow lane',
3331
+ }),
3332
+ );
3333
+ }
3334
+ const candidates = batch.candidates.flatMap((candidate) => {
3335
+ const slot = slotsByPrimitive?.get(candidate.primitiveIndex);
3336
+ if (slot === undefined) return [];
3337
+ const materialIndex = scene.scene.materialIndexForSlot(
3338
+ candidate.primitiveIndex,
3339
+ batch.key.materialSlot,
3340
+ );
3341
+ return [sceneCandidate(slot, candidate, materialIndex)];
1580
3342
  });
1581
- },
1582
- });
3343
+ const projected = projectStandardPbrScene({ artifact: shadowArtifact, candidates });
3344
+ if (!projected.ok) {
3345
+ return err(
3346
+ new RhiError({
3347
+ code: 'shader-compile-failed',
3348
+ expected: 'receipt-derived shadow scene-index projection',
3349
+ hint: 'publish one validated material receipt per shadow batch',
3350
+ }),
3351
+ );
3352
+ }
3353
+ const projectionKey = gpuDrivenShadowProjectionKey(
3354
+ identityKey,
3355
+ batch,
3356
+ deformation,
3357
+ shadowArtifact,
3358
+ );
3359
+ raster.noteProjectionKey(projectionKey);
3360
+ const meshBindGroup = raster.projectionBindGroup(
3361
+ projected.value.rows,
3362
+ projectionKey,
3363
+ batch.candidates.length,
3364
+ );
3365
+ if (!meshBindGroup.ok) return meshBindGroup;
3366
+ const visibleBindGroup = raster.visibleWindowBindGroupForBuffer(
3367
+ visibleBuffer,
3368
+ batch.visibleBase,
3369
+ batch.visibleCapacity,
3370
+ );
3371
+ if (!visibleBindGroup.ok) return visibleBindGroup;
3372
+ const firstCandidate = batch.candidates[0];
3373
+ const firstSlot =
3374
+ firstCandidate === undefined
3375
+ ? undefined
3376
+ : slotsByPrimitive.get(firstCandidate.primitiveIndex);
3377
+ const material =
3378
+ firstSlot?.snapshot.materials[batch.key.materialSlot] ?? firstSlot?.snapshot.material;
3379
+ if (firstSlot === undefined || material === undefined) {
3380
+ return err(
3381
+ new RhiError({
3382
+ code: 'internal-error',
3383
+ expected: `material snapshot for shadow batch ${batch.batchId}`,
3384
+ hint: 'keep the GPU-driven shadow material projection aligned with the retained slot',
3385
+ }),
3386
+ );
3387
+ }
3388
+ const firstWorldEntity = worldEntityKey(
3389
+ firstSlot.snapshot.worldId,
3390
+ firstSlot.snapshot.entityKey,
3391
+ );
3392
+ const shadowEntry = shadowCasterMembership?.find(
3393
+ (membership) =>
3394
+ membership.worldEntity === firstWorldEntity &&
3395
+ membership.materialHandle === (material.materialHandle ?? -1) &&
3396
+ membership.drawItemIndex === firstCandidate?.drawItemIndex,
3397
+ );
3398
+ projections.set(batch.batchId, {
3399
+ mesh: prepared.mesh,
3400
+ meshBindGroup: meshBindGroup.value,
3401
+ visibleBindGroup: visibleBindGroup.value,
3402
+ deformation,
3403
+ shadowArtifact,
3404
+ ...(shadowEntry?.vertexEntry === undefined
3405
+ ? {}
3406
+ : { shadowVertexEntry: shadowEntry.vertexEntry }),
3407
+ ...(shadowEntry?.fragmentEntry === undefined
3408
+ ? {}
3409
+ : { shadowFragmentEntry: shadowEntry.fragmentEntry }),
3410
+ material,
3411
+ materialEntityKey: worldEntityKey(
3412
+ firstSlot.snapshot.worldId,
3413
+ firstSlot.snapshot.entityKey,
3414
+ ),
3415
+ vertexColorAvailable: prepared.mesh.layoutProjection.attributes.some(
3416
+ (attribute) => attribute.key === 'color',
3417
+ ),
3418
+ });
3419
+ }
3420
+ nextProjections.set(identityKey, projections);
3421
+ }
3422
+ this.shadowViews.retain(views.map((input) => input.identity));
3423
+ this.shadowBatchProjections = nextProjections;
3424
+ this.shadowDrawKeysByView = nextShadowDrawKeysByView;
3425
+ return ok(undefined);
3426
+ }
3427
+
3428
+ private disableShadowViews(): Result<void, RhiError> {
3429
+ this.shadowViews?.retain([]);
3430
+ this.shadowBatchProjections = new Map();
3431
+ this.shadowDrawKeysByView = new Map();
3432
+ return ok(undefined);
1583
3433
  }
1584
3434
 
1585
3435
  dispose(): void {
@@ -1587,20 +3437,141 @@ export class GpuDrivenProduction {
1587
3437
  this.disposed = true;
1588
3438
  this.view?.dispose();
1589
3439
  this.view = undefined;
1590
- this.raster = undefined;
3440
+ this.materialRaster?.dispose();
3441
+ this.materialRaster = undefined;
3442
+ this.committedProjectionSource = undefined;
3443
+ this.skinRaster?.dispose();
3444
+ this.skinRaster = undefined;
1591
3445
  this.filteredCache = undefined;
1592
3446
  this.signatureCache = undefined;
1593
- this.sceneRowsRequired = true;
1594
- }
1595
-
1596
- /** Whether the current production topology can consume GPU Scene row updates. */
1597
- requiresSceneRows(): boolean {
1598
- return this.sceneRowsRequired;
3447
+ this.validatedResidencyCache = undefined;
3448
+ this.cpuValidationTelemetryCache = undefined;
3449
+ this.shadowViews?.dispose();
3450
+ this.shadowViews = undefined;
3451
+ this.shadowSourcePlan = undefined;
3452
+ this.shadowScene = undefined;
3453
+ this.shadowBatchProjections = new Map();
3454
+ this.shadowDrawKeysByView = new Map();
3455
+ this.shadowExpectedDrawKeys = new Set();
3456
+ this.shadowBatchProjectionSource = undefined;
3457
+ this.skinPaletteBuffer = undefined;
3458
+ this.committedPaletteContentRevision = undefined;
3459
+ this.preparationFailure = undefined;
3460
+ this.overflowRecoveryRequired = false;
1599
3461
  }
1600
3462
 
1601
3463
  inspect(): GpuDrivenProductionInspection {
1602
3464
  const view = this.view?.inspect();
3465
+ const channels: GpuDrivenLaneSummary[] = [];
3466
+ const mainTotalDrawCount = this.mainTotalDrawItems;
3467
+ const mainClaimedDrawCount = this.telemetryOverflow
3468
+ ? 0
3469
+ : Math.min(this.gpuOwnedDrawItems, mainTotalDrawCount);
3470
+ const mainResidualDrawCount = Math.max(0, mainTotalDrawCount - mainClaimedDrawCount);
3471
+ const mainFacts = {
3472
+ totalDrawCount: mainTotalDrawCount,
3473
+ claimedDrawCount: mainClaimedDrawCount,
3474
+ residualDrawCount: mainResidualDrawCount,
3475
+ };
3476
+ if (mainClaimedDrawCount > 0 || (this.lane !== 'gpu' && this.lane !== 'blocked')) {
3477
+ channels.push({
3478
+ viewPass: 'main',
3479
+ viewIndex: 0,
3480
+ lane: this.lane,
3481
+ reason: this.laneReason,
3482
+ drawCount: mainClaimedDrawCount,
3483
+ ...mainFacts,
3484
+ });
3485
+ }
3486
+ if (this.cpuSemanticFallbackDrawItems > 0) {
3487
+ channels.push({
3488
+ viewPass: 'main',
3489
+ viewIndex: 0,
3490
+ lane: 'cpu-semantic',
3491
+ reason: 'unsupported',
3492
+ drawCount: this.cpuSemanticFallbackDrawItems,
3493
+ ...mainFacts,
3494
+ });
3495
+ }
3496
+ if (this.cpuDeformationFallbackDrawItems > 0) {
3497
+ const reasons =
3498
+ this.cpuDeformationReasons.size === 0
3499
+ ? ([['resource-not-ready', this.cpuDeformationFallbackDrawItems]] as const)
3500
+ : this.cpuDeformationReasons.entries();
3501
+ for (const [reason, drawCount] of reasons) {
3502
+ channels.push({
3503
+ viewPass: 'main',
3504
+ viewIndex: 0,
3505
+ lane: 'cpu-deformation',
3506
+ reason,
3507
+ drawCount,
3508
+ ...mainFacts,
3509
+ });
3510
+ }
3511
+ }
3512
+ if (this.blockedDrawItems > 0) {
3513
+ channels.push({
3514
+ viewPass: 'main',
3515
+ viewIndex: 0,
3516
+ lane: 'blocked',
3517
+ reason: 'resource-not-ready',
3518
+ drawCount: this.blockedDrawItems,
3519
+ ...mainFacts,
3520
+ ...(this.preparationFailure === undefined ? {} : { failure: this.preparationFailure }),
3521
+ });
3522
+ }
3523
+ for (const entry of this.shadowViews?.inspect() ?? []) {
3524
+ // Retained view resources outlive admission. An idle frame has no
3525
+ // current GPU projection and must not publish last frame's channels.
3526
+ if (this.submissionState === 'idle' || channels.length >= 32) break;
3527
+ const key = shadowViewIdentityKey(entry.identity);
3528
+ const claimed = this.shadowDrawKeysByView.get(key)?.size ?? 0;
3529
+ const total = Math.max(this.shadowExpectedDrawKeys.size, claimed);
3530
+ const effectiveClaimed = this.telemetryOverflow ? 0 : Math.min(claimed, total);
3531
+ const residual = Math.max(0, total - effectiveClaimed);
3532
+ if (total === 0) continue;
3533
+ const projected = this.shadowBatchProjections.has(key) && entry.batchCount > 0;
3534
+ const viewPass =
3535
+ entry.identity.kind === 'directional'
3536
+ ? 'directional-shadow'
3537
+ : entry.identity.kind === 'point'
3538
+ ? 'point-shadow'
3539
+ : 'spot-shadow';
3540
+ if (projected && effectiveClaimed > 0) {
3541
+ channels.push({
3542
+ viewPass,
3543
+ viewIndex: entry.identity.index,
3544
+ ...(entry.identity.face === undefined ? {} : { face: entry.identity.face }),
3545
+ lane: this.telemetryOverflow ? 'blocked' : 'gpu',
3546
+ reason: this.telemetryOverflow ? 'overflow' : 'none',
3547
+ drawCount: effectiveClaimed,
3548
+ totalDrawCount: total,
3549
+ claimedDrawCount: effectiveClaimed,
3550
+ residualDrawCount: residual,
3551
+ });
3552
+ }
3553
+ if (residual > 0) {
3554
+ const failure = shadowOwnershipFailureInspection(viewPass);
3555
+ channels.push({
3556
+ viewPass,
3557
+ viewIndex: entry.identity.index,
3558
+ ...(entry.identity.face === undefined ? {} : { face: entry.identity.face }),
3559
+ lane: 'blocked',
3560
+ reason: this.telemetryOverflow ? 'overflow' : 'shadow-ownership',
3561
+ drawCount: residual,
3562
+ totalDrawCount: total,
3563
+ claimedDrawCount: effectiveClaimed,
3564
+ residualDrawCount: residual,
3565
+ failure,
3566
+ });
3567
+ }
3568
+ }
1603
3569
  return {
3570
+ residencyValidationScans: this.residencyValidationScans,
3571
+ residencyValidationCacheHits: this.residencyValidationCacheHits,
3572
+ cpuValidationScans: this.cpuValidationScans,
3573
+ cpuValidationCacheHits: this.cpuValidationCacheHits,
3574
+ ...this.structureMetrics,
1604
3575
  gpuOwnedSnapshotsMaterialized: this.gpuOwnedSnapshotsMaterialized,
1605
3576
  filteredPlanBuilds: this.filteredPlanBuilds,
1606
3577
  gpuOwnedEntityCount: this.gpuOwnedEntityCount,
@@ -1620,19 +3591,54 @@ export class GpuDrivenProduction {
1620
3591
  : 0,
1621
3592
  batchCount: view?.batchCount ?? 0,
1622
3593
  indirectDrawCount: this.indirectDrawCount,
3594
+ encodedIndirectDraws: this.encodedIndirectDraws,
3595
+ channels: Object.freeze(channels.map((channel) => Object.freeze(channel))),
3596
+ resourceGeneration: view?.resourceGeneration,
3597
+ candidateCapacity: view?.candidateCapacity ?? 0,
3598
+ batchCapacity: view?.batchCapacity ?? 0,
3599
+ indirectCapacity: view?.indirectCapacity ?? 0,
3600
+ overflow: this.telemetryOverflow,
3601
+ submitted: this.submissionState === 'submitted',
3602
+ recoveryCount: this.recoveryCount,
3603
+ retryCount: this.retryCount,
3604
+ lastKnownGoodGeneration: this.lastKnownGoodGeneration,
1623
3605
  };
1624
3606
  }
1625
3607
 
1626
3608
  /** Read renderer-owned LOD counters copied by the last GPU submission. */
1627
3609
  readLodSelection(): Promise<GpuDrivenLodSelectionInspection | undefined> {
1628
3610
  if (!this.telemetryPrepared) return Promise.resolve(undefined);
1629
- return (this.view?.readLodSelection() ?? Promise.resolve(undefined)).then((selection) => {
3611
+ const view = this.view;
3612
+ const filtered = this.filteredCache?.value;
3613
+ const telemetryCandidateCount = this.telemetryCandidateCount;
3614
+ if (view === undefined) return Promise.resolve(undefined);
3615
+ return view.readLodSelection().then((selection) => {
3616
+ // The observer is asynchronous and prepare() may have published a new
3617
+ // filtered plan while mapAsync was pending. Do not attach old counters to
3618
+ // the new world/primitive cache; the next real copy will be observed on
3619
+ // its own generation.
3620
+ if (
3621
+ !this.telemetryPrepared ||
3622
+ this.view !== view ||
3623
+ this.filteredCache?.value !== filtered ||
3624
+ this.telemetryCandidateCount !== telemetryCandidateCount
3625
+ ) {
3626
+ return undefined;
3627
+ }
1630
3628
  if (selection !== undefined) {
1631
3629
  this.telemetryGeometryWork = selection.geometryWork;
1632
3630
  this.telemetryRootGeometryWork = selection.rootGeometryWork;
3631
+ this.telemetryOverflow = selection.overflow;
3632
+ if (selection.overflow) {
3633
+ if (!this.overflowRecoveryRequired) this.retryCount += 1;
3634
+ this.overflowRecoveryRequired = true;
3635
+ this.submissionState = 'aborted';
3636
+ this.lastKnownGoodGeneration = undefined;
3637
+ this.lane = 'blocked';
3638
+ this.laneReason = 'overflow';
3639
+ }
1633
3640
  }
1634
- if (selection === undefined || this.telemetryCandidateCount <= 0) return selection;
1635
- const filtered = this.filteredCache?.value;
3641
+ if (selection === undefined || telemetryCandidateCount <= 0) return selection;
1636
3642
  if (filtered === undefined) return selection;
1637
3643
  const preparedByBatchId = new Map(
1638
3644
  filtered.batches.map((prepared) => [prepared.batch.batchId, prepared] as const),
@@ -1677,8 +3683,8 @@ export class GpuDrivenProduction {
1677
3683
  }
1678
3684
  return {
1679
3685
  ...selection,
1680
- candidateCount: this.telemetryCandidateCount,
1681
- occluded: Math.max(0, this.telemetryCandidateCount - selection.visible),
3686
+ candidateCount: telemetryCandidateCount,
3687
+ occluded: Math.max(0, telemetryCandidateCount - selection.visible),
1682
3688
  worldSelections: Object.freeze(
1683
3689
  [...worldSelections.values()].map((aggregate) =>
1684
3690
  Object.freeze({
@@ -1702,24 +3708,108 @@ export class GpuDrivenProduction {
1702
3708
 
1703
3709
  recordCpuValidation(
1704
3710
  validated: readonly { readonly source: RenderableSnapshot }[],
1705
- gpuOwned: ReadonlySet<number>,
1706
- worldKeys?: readonly number[],
3711
+ gpuOwnedDrawKeys: ReadonlySet<string>,
3712
+ cacheKey?: string,
1707
3713
  ): void {
1708
- this.cpuFallbackDrawItems = validated.length;
3714
+ const cached = this.cpuValidationTelemetryCache;
3715
+ if (
3716
+ cacheKey !== undefined &&
3717
+ cached !== undefined &&
3718
+ cached.cacheKey === cacheKey &&
3719
+ cached.validated === validated &&
3720
+ cached.gpuOwnedDrawKeys === gpuOwnedDrawKeys
3721
+ ) {
3722
+ this.applyCpuValidationTelemetry(cached);
3723
+ this.cpuValidationCacheHits += 1;
3724
+ return;
3725
+ }
3726
+ this.cpuFallbackDrawItems = 0;
3727
+ this.cpuSemanticFallbackDrawItems = 0;
3728
+ this.cpuDeformationFallbackDrawItems = 0;
3729
+ this.cpuDeformationReasons.clear();
3730
+ this.blockedDrawItems = 0;
1709
3731
  let gpuOwnedRows = 0;
3732
+ let totalDrawItems = 0;
3733
+ const addDeformationReason = (
3734
+ reason: 'skin-bounds-missing' | 'skin-address-missing' | 'resource-not-ready',
3735
+ ): void => {
3736
+ this.cpuDeformationReasons.set(reason, (this.cpuDeformationReasons.get(reason) ?? 0) + 1);
3737
+ };
1710
3738
  for (const row of validated) {
1711
- if (
1712
- gpuOwned.has(
1713
- worldEntityKey(
1714
- worldKeys?.[row.source.worldId] ?? row.source.worldId,
1715
- row.source.entityKey,
1716
- ),
1717
- )
1718
- ) {
1719
- gpuOwnedRows += 1;
3739
+ const draws = row.source.gpuDrivenDraws ?? [];
3740
+ if (draws.length === 0) {
3741
+ totalDrawItems += 1;
3742
+ this.cpuFallbackDrawItems += 1;
3743
+ const skinReason = skinLaneReason(row.source);
3744
+ if (skinReason !== undefined) {
3745
+ this.cpuDeformationFallbackDrawItems += 1;
3746
+ addDeformationReason(skinReason);
3747
+ } else {
3748
+ this.cpuSemanticFallbackDrawItems += 1;
3749
+ }
3750
+ continue;
3751
+ }
3752
+ totalDrawItems += draws.length;
3753
+ let gpuOwned = false;
3754
+ let cpuResidual = false;
3755
+ for (const [compactIndex, draw] of draws.entries()) {
3756
+ const material = row.source.materials[draw.materialSlot] ?? row.source.material;
3757
+ const key = gpuDrivenDrawKey(
3758
+ worldEntityKey(row.source.worldId, row.source.entityKey),
3759
+ material.materialHandle ?? -1,
3760
+ gpuDrivenSourceDrawItemIndex(draw, compactIndex),
3761
+ );
3762
+ if (gpuOwnedDrawKeys.has(key)) {
3763
+ gpuOwned = true;
3764
+ continue;
3765
+ }
3766
+ this.cpuFallbackDrawItems += 1;
3767
+ cpuResidual = true;
3768
+ const skinReason = skinLaneReason(row.source);
3769
+ if (skinReason !== undefined) {
3770
+ this.cpuDeformationFallbackDrawItems += 1;
3771
+ addDeformationReason(skinReason);
3772
+ } else if (draw.preparationError?.detail.recovery === 'route-cpu-lane') {
3773
+ // Geometry/UV incompatibility is an explicit semantic CPU lane,
3774
+ // not a failed capable submission.
3775
+ this.cpuSemanticFallbackDrawItems += 1;
3776
+ } else if (draw.preparationError?.detail.owner === 'skin') {
3777
+ this.cpuDeformationFallbackDrawItems += 1;
3778
+ addDeformationReason(
3779
+ draw.preparationError.detail.reason === 'skin-address-missing'
3780
+ ? 'skin-address-missing'
3781
+ : 'resource-not-ready',
3782
+ );
3783
+ } else if (draw.preparationError !== undefined) {
3784
+ this.blockedDrawItems += 1;
3785
+ } else {
3786
+ this.cpuSemanticFallbackDrawItems += 1;
3787
+ }
1720
3788
  }
3789
+ // A row is only CPU-validated when it still has a CPU residual. GPU-only
3790
+ // rows are already admitted by the concrete draw-key set and must not
3791
+ // inflate the per-frame CPU validation budget.
3792
+ if (gpuOwned && cpuResidual) gpuOwnedRows += 1;
1721
3793
  }
3794
+ this.gpuOwnedDrawItems = gpuOwnedDrawKeys.size;
3795
+ this.mainTotalDrawItems = totalDrawItems;
1722
3796
  this.validatedGpuOwnedRows = gpuOwnedRows;
3797
+ this.cpuValidationScans += 1;
3798
+ if (cacheKey !== undefined) {
3799
+ this.cpuValidationTelemetryCache = {
3800
+ cacheKey,
3801
+ validated,
3802
+ gpuOwnedDrawKeys,
3803
+ gpuOwnedDrawCount: this.gpuOwnedDrawItems,
3804
+ totalDrawItems: this.mainTotalDrawItems,
3805
+ gpuOwnedRows: this.validatedGpuOwnedRows,
3806
+ cpuFallbackDrawItems: this.cpuFallbackDrawItems,
3807
+ cpuSemanticFallbackDrawItems: this.cpuSemanticFallbackDrawItems,
3808
+ cpuDeformationFallbackDrawItems: this.cpuDeformationFallbackDrawItems,
3809
+ cpuDeformationReasons: new Map(this.cpuDeformationReasons),
3810
+ blockedDrawItems: this.blockedDrawItems,
3811
+ };
3812
+ }
1723
3813
  }
1724
3814
 
1725
3815
  private sceneIdentity(scene: object): number {