@forgeax/engine-render 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (655) hide show
  1. package/README.md +842 -124
  2. package/dist/assembly/atmosphere-shader-prewarm.d.ts +6 -0
  3. package/dist/assembly/atmosphere-shader-prewarm.d.ts.map +1 -0
  4. package/dist/assembly/dynamic-geometry-host.d.ts +52 -0
  5. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -0
  6. package/dist/assembly/dynamic-geometry-runtime.d.ts +20 -0
  7. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +3 -1
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/gpu-driven-owner.d.ts +10 -0
  11. package/dist/assembly/gpu-driven-owner.d.ts.map +1 -0
  12. package/dist/assembly/host-contract.d.ts +19 -0
  13. package/dist/assembly/host-contract.d.ts.map +1 -1
  14. package/dist/assembly/material/assembly.d.ts +7 -0
  15. package/dist/assembly/material/assembly.d.ts.map +1 -1
  16. package/dist/assembly/material-shader-policy.d.ts +3 -1
  17. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  18. package/dist/assembly/recovery/failure-location.d.ts +12 -0
  19. package/dist/assembly/recovery/failure-location.d.ts.map +1 -0
  20. package/dist/assembly/recovery/renderer-recover.d.ts +6 -4
  21. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  22. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  23. package/dist/assembly/renderer-feature-inspection.d.ts +5 -0
  24. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -0
  25. package/dist/assembly/renderer-frame-transaction.d.ts +19 -0
  26. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  27. package/dist/assembly/renderer-helpers.d.ts +3 -0
  28. package/dist/assembly/renderer-helpers.d.ts.map +1 -1
  29. package/dist/assembly/renderer-inspection.d.ts +4 -0
  30. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  31. package/dist/assembly/shader-prewarm-policy.d.ts +6 -0
  32. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -1
  33. package/dist/assembly/skin-palette-owner.d.ts +8 -0
  34. package/dist/assembly/skin-palette-owner.d.ts.map +1 -0
  35. package/dist/assembly/ssr-shader-prewarm.d.ts +6 -0
  36. package/dist/assembly/ssr-shader-prewarm.d.ts.map +1 -0
  37. package/dist/assembly/webgpu-device-loss.d.ts +15 -0
  38. package/dist/assembly/webgpu-device-loss.d.ts.map +1 -0
  39. package/dist/assembly/webgpu-pbr-ready.d.ts +38 -0
  40. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -0
  41. package/dist/assembly/webgpu-ready-mipmap.d.ts +11 -0
  42. package/dist/assembly/webgpu-ready-mipmap.d.ts.map +1 -0
  43. package/dist/assembly/webgpu-ready.d.ts +7 -3
  44. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  45. package/dist/assembly/webgpu-renderer-bootstrap.d.ts +10 -0
  46. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -0
  47. package/dist/assembly/webgpu-renderer-contract.d.ts +62 -0
  48. package/dist/assembly/webgpu-renderer-contract.d.ts.map +1 -0
  49. package/dist/assembly/webgpu-renderer-guards.d.ts +9 -0
  50. package/dist/assembly/webgpu-renderer-guards.d.ts.map +1 -0
  51. package/dist/assembly/webgpu-renderer-observation.d.ts +6 -0
  52. package/dist/assembly/webgpu-renderer-observation.d.ts.map +1 -0
  53. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts +12 -0
  54. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts.map +1 -0
  55. package/dist/assembly/webgpu-renderer.d.ts +7 -141
  56. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  57. package/dist/assembly/webgpu-vertex-layouts.d.ts +28 -84
  58. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -1
  59. package/dist/authoring.mjs +3 -3
  60. package/dist/{chunk-ADHHVYLW.mjs → chunk-33XZFTQU.mjs} +245 -39
  61. package/dist/chunk-33XZFTQU.mjs.map +1 -0
  62. package/dist/{chunk-GF523LHF.mjs → chunk-BLXGONR2.mjs} +30 -12
  63. package/dist/chunk-BLXGONR2.mjs.map +1 -0
  64. package/dist/{chunk-KM2NOX2I.mjs → chunk-BQVQOQJH.mjs} +958 -254
  65. package/dist/chunk-BQVQOQJH.mjs.map +1 -0
  66. package/dist/{chunk-HKXTW355.mjs → chunk-BTWH7BP4.mjs} +386 -17
  67. package/dist/chunk-BTWH7BP4.mjs.map +1 -0
  68. package/dist/{chunk-TMIASV2N.mjs → chunk-FUODTTIA.mjs} +3934 -676
  69. package/dist/chunk-FUODTTIA.mjs.map +1 -0
  70. package/dist/{chunk-FJ6P52EE.mjs → chunk-ILQ5MEZF.mjs} +3 -17
  71. package/dist/chunk-ILQ5MEZF.mjs.map +1 -0
  72. package/dist/{chunk-E34VL5VI.mjs → chunk-JQTSEI3Z.mjs} +3 -3
  73. package/dist/{chunk-E34VL5VI.mjs.map → chunk-JQTSEI3Z.mjs.map} +1 -1
  74. package/dist/{chunk-GE3SDD72.mjs → chunk-NICXSHAT.mjs} +16 -38
  75. package/dist/chunk-NICXSHAT.mjs.map +1 -0
  76. package/dist/{chunk-ZZ4YQ474.mjs → chunk-NRNXBLIJ.mjs} +577 -7
  77. package/dist/chunk-NRNXBLIJ.mjs.map +1 -0
  78. package/dist/{chunk-JDWARUOI.mjs → chunk-PA5DRJNI.mjs} +17 -3
  79. package/dist/chunk-PA5DRJNI.mjs.map +1 -0
  80. package/dist/{chunk-XDP6XE5V.mjs → chunk-S4DNKQYQ.mjs} +1118 -446
  81. package/dist/chunk-S4DNKQYQ.mjs.map +1 -0
  82. package/dist/components/camera.d.ts +62 -2
  83. package/dist/components/camera.d.ts.map +1 -1
  84. package/dist/components/index.d.ts +1 -0
  85. package/dist/components/index.d.ts.map +1 -1
  86. package/dist/components/instances.d.ts +6 -20
  87. package/dist/components/instances.d.ts.map +1 -1
  88. package/dist/components/scene-instance.d.ts +2 -1
  89. package/dist/components/scene-instance.d.ts.map +1 -1
  90. package/dist/components/screen-space-reflection.d.ts +14 -0
  91. package/dist/components/screen-space-reflection.d.ts.map +1 -0
  92. package/dist/construct-renderer.mjs +13805 -7186
  93. package/dist/construct-renderer.mjs.map +1 -1
  94. package/dist/device/gpu-residency.d.ts +11 -17
  95. package/dist/device/gpu-residency.d.ts.map +1 -1
  96. package/dist/device/mesh-residency-lifetime.d.ts +21 -0
  97. package/dist/device/mesh-residency-lifetime.d.ts.map +1 -0
  98. package/dist/dynamic-geometry.d.ts +125 -0
  99. package/dist/dynamic-geometry.d.ts.map +1 -0
  100. package/dist/environment/background.d.ts +6 -0
  101. package/dist/environment/background.d.ts.map +1 -0
  102. package/dist/errors/gpu-driven.d.ts +6 -3
  103. package/dist/errors/gpu-driven.d.ts.map +1 -1
  104. package/dist/errors/render.d.ts +7 -4
  105. package/dist/errors/render.d.ts.map +1 -1
  106. package/dist/extract/camera.d.ts.map +1 -1
  107. package/dist/extract/gpu-driven.d.ts +33 -3
  108. package/dist/extract/gpu-driven.d.ts.map +1 -1
  109. package/dist/features/host.d.ts +23 -1
  110. package/dist/features/host.d.ts.map +1 -1
  111. package/dist/features/noise-texture.d.ts +17 -0
  112. package/dist/features/noise-texture.d.ts.map +1 -0
  113. package/dist/features/plan.d.ts +83 -2
  114. package/dist/features/plan.d.ts.map +1 -1
  115. package/dist/features/prepared-gpu-work.d.ts +37 -4
  116. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  117. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  118. package/dist/features/prepared-graphics.d.ts +5 -0
  119. package/dist/features/prepared-graphics.d.ts.map +1 -1
  120. package/dist/features/render-graph-compute.d.ts +5 -3
  121. package/dist/features/render-graph-compute.d.ts.map +1 -1
  122. package/dist/features/render-graph-contribution.d.ts +11 -3
  123. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  124. package/dist/features/render-graph-raster.d.ts +19 -3
  125. package/dist/features/render-graph-raster.d.ts.map +1 -1
  126. package/dist/features/types.d.ts +11 -0
  127. package/dist/features/types.d.ts.map +1 -1
  128. package/dist/fullscreen-post-process-pass.d.ts +2 -0
  129. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  130. package/dist/gpu-driven/batch-topology.d.ts +34 -1
  131. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  132. package/dist/gpu-driven/pbr-program.d.ts +10 -0
  133. package/dist/gpu-driven/pbr-program.d.ts.map +1 -0
  134. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  135. package/dist/gpu-driven/production-raster.d.ts +144 -23
  136. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  137. package/dist/gpu-driven/shadow-views.d.ts +126 -0
  138. package/dist/gpu-driven/shadow-views.d.ts.map +1 -0
  139. package/dist/gpu-driven/view-gpu.d.ts +18 -2
  140. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  141. package/dist/gpu-scene-schema.d.ts +2 -8
  142. package/dist/gpu-scene-schema.d.ts.map +1 -1
  143. package/dist/gpu-scene.d.ts +14 -0
  144. package/dist/gpu-scene.d.ts.map +1 -1
  145. package/dist/gpu-texture-usage.d.ts +1 -0
  146. package/dist/gpu-texture-usage.d.ts.map +1 -1
  147. package/dist/hdrp-buffers.d.ts +6 -0
  148. package/dist/hdrp-buffers.d.ts.map +1 -1
  149. package/dist/ibl/frame-resources.d.ts +8 -0
  150. package/dist/ibl/frame-resources.d.ts.map +1 -0
  151. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  152. package/dist/index.d.ts +21 -8
  153. package/dist/index.d.ts.map +1 -1
  154. package/dist/index.mjs +8 -7
  155. package/dist/index.mjs.map +1 -1
  156. package/dist/inspection-types.d.ts +164 -0
  157. package/dist/inspection-types.d.ts.map +1 -1
  158. package/dist/instances-derived-bounds.d.ts +11 -0
  159. package/dist/instances-derived-bounds.d.ts.map +1 -1
  160. package/dist/instances.d.ts +89 -44
  161. package/dist/instances.d.ts.map +1 -1
  162. package/dist/internal.d.ts +3 -0
  163. package/dist/internal.d.ts.map +1 -1
  164. package/dist/internal.mjs +118 -10
  165. package/dist/internal.mjs.map +1 -1
  166. package/dist/lifecycle.d.ts +2 -1
  167. package/dist/lifecycle.d.ts.map +1 -1
  168. package/dist/material-row.d.ts +5 -0
  169. package/dist/material-row.d.ts.map +1 -0
  170. package/dist/materials.d.ts +15 -0
  171. package/dist/materials.d.ts.map +1 -1
  172. package/dist/pbr-pipeline.d.ts +21 -5
  173. package/dist/pbr-pipeline.d.ts.map +1 -1
  174. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  175. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts +12 -0
  176. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts.map +1 -0
  177. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts +70 -0
  178. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts.map +1 -0
  179. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts +75 -0
  180. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts.map +1 -0
  181. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts +111 -0
  182. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts.map +1 -0
  183. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts +16 -0
  184. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts.map +1 -0
  185. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts +14 -0
  186. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts.map +1 -0
  187. package/dist/pipeline/standard-output/auto-exposure/state.d.ts +46 -0
  188. package/dist/pipeline/standard-output/auto-exposure/state.d.ts.map +1 -0
  189. package/dist/pipeline/standard-output/color-transform.d.ts +27 -0
  190. package/dist/pipeline/standard-output/color-transform.d.ts.map +1 -0
  191. package/dist/pipeline/standard-output/graph.d.ts +15 -0
  192. package/dist/pipeline/standard-output/graph.d.ts.map +1 -0
  193. package/dist/pipeline/standard-output/lut-admission.d.ts +29 -0
  194. package/dist/pipeline/standard-output/lut-admission.d.ts.map +1 -0
  195. package/dist/pipeline/standard-output/lut-gpu.d.ts +48 -0
  196. package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -0
  197. package/dist/pipeline/standard-output/lut-state.d.ts +68 -0
  198. package/dist/pipeline/standard-output/lut-state.d.ts.map +1 -0
  199. package/dist/pipeline/standard-output/resources.d.ts +10 -0
  200. package/dist/pipeline/standard-output/resources.d.ts.map +1 -0
  201. package/dist/pipeline/standard-output/types.d.ts +39 -0
  202. package/dist/pipeline/standard-output/types.d.ts.map +1 -0
  203. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  204. package/dist/pipeline/standard-post.d.ts +2 -2
  205. package/dist/pipeline/standard-post.d.ts.map +1 -1
  206. package/dist/pipeline-builder.d.ts +2 -1
  207. package/dist/pipeline-builder.d.ts.map +1 -1
  208. package/dist/pipeline-spec-types.d.ts +4 -0
  209. package/dist/pipeline-spec-types.d.ts.map +1 -1
  210. package/dist/pipeline-spec.d.ts +1 -9
  211. package/dist/pipeline-spec.d.ts.map +1 -1
  212. package/dist/points-lines/standard-owner.d.ts +1 -1
  213. package/dist/points-lines/standard-owner.d.ts.map +1 -1
  214. package/dist/prepare/prepared-graphics-resolver.d.ts +6 -1
  215. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  216. package/dist/record/dynamic-geometry-consumption.d.ts +15 -0
  217. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -0
  218. package/dist/record/frame-lighting.d.ts +3 -3
  219. package/dist/record/frame-lighting.d.ts.map +1 -1
  220. package/dist/record/frame-snapshot.d.ts +106 -5
  221. package/dist/record/frame-snapshot.d.ts.map +1 -1
  222. package/dist/record/frame-targets.d.ts.map +1 -1
  223. package/dist/record/frame.d.ts +39 -4
  224. package/dist/record/frame.d.ts.map +1 -1
  225. package/dist/record/main-pass-geometry.d.ts +7 -8
  226. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  227. package/dist/record/main-pass-material.d.ts +38 -1
  228. package/dist/record/main-pass-material.d.ts.map +1 -1
  229. package/dist/record/main-pass-sprite-draws.d.ts +2 -10
  230. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  231. package/dist/record/main-pass.d.ts +12 -2
  232. package/dist/record/main-pass.d.ts.map +1 -1
  233. package/dist/record/mesh-ssbo.d.ts +15 -6
  234. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  235. package/dist/record/prepared-material-bindings.d.ts +19 -0
  236. package/dist/record/prepared-material-bindings.d.ts.map +1 -0
  237. package/dist/record/recovery-pipeline.d.ts +2 -9
  238. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  239. package/dist/record/render-context.d.ts +76 -26
  240. package/dist/record/render-context.d.ts.map +1 -1
  241. package/dist/record/shadow-pass.d.ts +62 -7
  242. package/dist/record/shadow-pass.d.ts.map +1 -1
  243. package/dist/record/skybox-post-pass.d.ts +1 -1
  244. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  245. package/dist/record/typed-frame-graph.d.ts +35 -7
  246. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  247. package/dist/record/view-ubo.d.ts +1 -0
  248. package/dist/record/view-ubo.d.ts.map +1 -1
  249. package/dist/recovery/render-system-candidate.d.ts +10 -2
  250. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  251. package/dist/recovery/render-system-roots.d.ts +11 -0
  252. package/dist/recovery/render-system-roots.d.ts.map +1 -0
  253. package/dist/recovery/types.d.ts +76 -0
  254. package/dist/recovery/types.d.ts.map +1 -0
  255. package/dist/reflection/record-owner.d.ts +0 -2
  256. package/dist/reflection/record-owner.d.ts.map +1 -1
  257. package/dist/render-contract.d.ts +185 -30
  258. package/dist/render-contract.d.ts.map +1 -1
  259. package/dist/render-graph-primitives.d.ts +1 -0
  260. package/dist/render-graph-primitives.d.ts.map +1 -1
  261. package/dist/render-pipeline.d.ts +64 -5
  262. package/dist/render-pipeline.d.ts.map +1 -1
  263. package/dist/render-system-extract-tail.d.ts +7 -5
  264. package/dist/render-system-extract-tail.d.ts.map +1 -1
  265. package/dist/render-system-extract.d.ts +101 -41
  266. package/dist/render-system-extract.d.ts.map +1 -1
  267. package/dist/render-system-projections.d.ts +22 -0
  268. package/dist/render-system-projections.d.ts.map +1 -0
  269. package/dist/render-system.d.ts +34 -77
  270. package/dist/render-system.d.ts.map +1 -1
  271. package/dist/scene/render-scene-types.d.ts +14 -7
  272. package/dist/scene/render-scene-types.d.ts.map +1 -1
  273. package/dist/scene/render-scene.d.ts +29 -11
  274. package/dist/scene/render-scene.d.ts.map +1 -1
  275. package/dist/scene/shadow-visibility.d.ts +5 -0
  276. package/dist/scene/shadow-visibility.d.ts.map +1 -0
  277. package/dist/ssr/admission.d.ts +70 -1
  278. package/dist/ssr/admission.d.ts.map +1 -1
  279. package/dist/ssr/compose.d.ts +13 -0
  280. package/dist/ssr/compose.d.ts.map +1 -0
  281. package/dist/ssr/errors.d.ts +35 -0
  282. package/dist/ssr/errors.d.ts.map +1 -1
  283. package/dist/ssr/graph.d.ts +60 -0
  284. package/dist/ssr/graph.d.ts.map +1 -0
  285. package/dist/ssr/history.d.ts +97 -0
  286. package/dist/ssr/history.d.ts.map +1 -0
  287. package/dist/ssr/hiz.d.ts +37 -0
  288. package/dist/ssr/hiz.d.ts.map +1 -0
  289. package/dist/ssr/inspection.d.ts +42 -0
  290. package/dist/ssr/inspection.d.ts.map +1 -0
  291. package/dist/ssr/resources.d.ts +58 -0
  292. package/dist/ssr/resources.d.ts.map +1 -0
  293. package/dist/ssr/temporal.d.ts +38 -0
  294. package/dist/ssr/temporal.d.ts.map +1 -0
  295. package/dist/systems/skin-palette-allocator.d.ts +32 -10
  296. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  297. package/dist/systems/skin-palette-types.d.ts +15 -0
  298. package/dist/systems/skin-palette-types.d.ts.map +1 -1
  299. package/dist/systems/transparent-dispatch.d.ts +9 -0
  300. package/dist/systems/transparent-dispatch.d.ts.map +1 -0
  301. package/dist/targets/material-source.d.ts +7 -0
  302. package/dist/targets/material-source.d.ts.map +1 -1
  303. package/dist/temporal/frame-coordinator.d.ts +7 -0
  304. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  305. package/dist/temporal/gpu.d.ts +8 -0
  306. package/dist/temporal/gpu.d.ts.map +1 -1
  307. package/dist/temporal/history.d.ts.map +1 -1
  308. package/dist/temporal/index.mjs +6 -6
  309. package/dist/temporal/inspection.d.ts.map +1 -1
  310. package/dist/temporal/standard-scene-data.d.ts +2 -1
  311. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  312. package/dist/temporal/temporal-view.d.ts +2 -0
  313. package/dist/temporal/temporal-view.d.ts.map +1 -1
  314. package/dist/typed-render-graph-primitives.d.ts +35 -1
  315. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  316. package/dist/typed-shadow-passes.d.ts +3 -1
  317. package/dist/typed-shadow-passes.d.ts.map +1 -1
  318. package/dist/volume/temporal.d.ts +6 -0
  319. package/dist/volume/temporal.d.ts.map +1 -1
  320. package/package.json +21 -20
  321. package/src/__tests__/auto-exposure-gpu-evidence.ts +291 -0
  322. package/src/__tests__/auto-exposure-inspection.unit.test.ts +80 -0
  323. package/src/__tests__/auto-exposure-transaction.integration.test.ts +99 -0
  324. package/src/__tests__/auto-exposure.browser.test.ts +33 -0
  325. package/src/__tests__/auto-exposure.dawn.test.ts +33 -0
  326. package/src/__tests__/detached-inspection.integration.test.ts +5 -0
  327. package/src/__tests__/device-scope-lifecycle.unit.test.ts +18 -0
  328. package/src/__tests__/device-scope-stale-matrix.unit.test.ts +18 -0
  329. package/src/__tests__/docs-gate-green.unit.test.ts +3 -3
  330. package/src/__tests__/dynamic-geometry-host.unit.test.ts +1123 -0
  331. package/src/__tests__/dynamic-geometry.unit.test.ts +233 -0
  332. package/src/__tests__/factory-contract.integration.test.ts +73 -22
  333. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +19 -0
  334. package/src/__tests__/feature-depth-input.dawn.test.ts +121 -0
  335. package/src/__tests__/feature-noise.unit.test.ts +38 -0
  336. package/src/__tests__/fixtures/taa-resolve-pinned-baseline.wgsl +301 -0
  337. package/src/__tests__/frame-camera-selection.unit.test.ts +131 -0
  338. package/src/__tests__/frame-plan-contract.unit.test.ts +24 -0
  339. package/src/__tests__/frame-targets.unit.test.ts +24 -0
  340. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +3 -0
  341. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +79 -0
  342. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +287 -6
  343. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +122 -142
  344. package/src/__tests__/gpu-driven-indirect-raster.browser.test.ts +10 -1
  345. package/src/__tests__/gpu-driven-lod-cache.unit.test.ts +264 -0
  346. package/src/__tests__/gpu-driven-mesh-world-identity.unit.test.ts +349 -0
  347. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +1417 -0
  348. package/src/__tests__/gpu-driven-pbr.integration.test.ts +21 -0
  349. package/src/__tests__/gpu-driven-production-fixture.ts +96 -0
  350. package/src/__tests__/gpu-driven-production.integration.test.ts +1075 -93
  351. package/src/__tests__/gpu-driven-scaling.unit.test.ts +1 -3
  352. package/src/__tests__/gpu-driven-shadow-views.browser.test.ts +63 -0
  353. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +294 -0
  354. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +97 -0
  355. package/src/__tests__/gpu-driven-shadow-views.unit.test.ts +168 -0
  356. package/src/__tests__/gpu-driven-skin-contract.test-d.ts +11 -0
  357. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +72 -20
  358. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +30 -9
  359. package/src/__tests__/gpu-driven-view.unit.test.ts +159 -52
  360. package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -1
  361. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +42 -0
  362. package/src/__tests__/gpu-scene-render-graph-gpu.ts +19 -5
  363. package/src/__tests__/gpu-scene.dawn.test.ts +40 -13
  364. package/src/__tests__/gpu-scene.unit.test.ts +559 -33
  365. package/src/__tests__/instances-culling.integration.test.ts +36 -10
  366. package/src/__tests__/instances-derived-bounds.unit.test.ts +54 -0
  367. package/src/__tests__/instances-store.unit.test.ts +242 -39
  368. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +6 -0
  369. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +13 -16
  370. package/src/__tests__/lut-lkg-transaction.integration.test.ts +122 -0
  371. package/src/__tests__/material-contract-inventory.unit.test.ts +5 -6
  372. package/src/__tests__/material-dispatch-shader-id.unit.test.ts +51 -0
  373. package/src/__tests__/material-lane-selection.unit.test.ts +60 -0
  374. package/src/__tests__/material-numeric-upload.unit.test.ts +53 -0
  375. package/src/__tests__/material-texture-handles.unit.test.ts +18 -0
  376. package/src/__tests__/material-texture-source.unit.test.ts +87 -0
  377. package/src/__tests__/materials-authoring-validation.unit.test.ts +41 -0
  378. package/src/__tests__/materials-standard-contract.unit.test.ts +52 -2
  379. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +4 -0
  380. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +53 -0
  381. package/src/__tests__/normal-fallback.dawn.test.ts +122 -0
  382. package/src/__tests__/particle-material-input-layout.unit.test.ts +104 -0
  383. package/src/__tests__/physical-clearcoat.integration.test.ts +37 -3
  384. package/src/__tests__/pipeline-texture-specialization.unit.test.ts +138 -0
  385. package/src/__tests__/point-shadow-inspection.unit.test.ts +2 -0
  386. package/src/__tests__/prepared-gpu-driven-pbr-contract.test-d.ts +31 -0
  387. package/src/__tests__/prepared-gpu-driven-pbr-contract.unit.test.ts +39 -0
  388. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +35 -4
  389. package/src/__tests__/prepared-graphics-capability.unit.test.ts +21 -0
  390. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +6 -2
  391. package/src/__tests__/profiler-phase-catalog.test.ts +5 -0
  392. package/src/__tests__/public-surface.test-d.ts +3 -0
  393. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +30 -12
  394. package/src/__tests__/reflection-fallback-async-publication.unit.test.ts +104 -0
  395. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +2 -1
  396. package/src/__tests__/render-error-code-owner.test-d.ts +4 -0
  397. package/src/__tests__/render-error-detail-ssot.test-d.ts +22 -0
  398. package/src/__tests__/render-error-exhaustive.test-d.ts +15 -0
  399. package/src/__tests__/render-feature-gpu-work.integration.test.ts +285 -2
  400. package/src/__tests__/render-feature-plan.unit.test.ts +245 -0
  401. package/src/__tests__/render-feature-shadow-order.unit.test.ts +228 -0
  402. package/src/__tests__/render-feature-zero-work.unit.test.ts +35 -1
  403. package/src/__tests__/render-graph-fallback.integration.test.ts +2 -2
  404. package/src/__tests__/render-owner-cohesion.unit.test.ts +55 -0
  405. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +426 -0
  406. package/src/__tests__/render-scene-projection.unit.test.ts +914 -23
  407. package/src/__tests__/render-scene-resource-updates.integration.test.ts +214 -0
  408. package/src/__tests__/render-scene-skin-lifecycle.integration.test.ts +173 -0
  409. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +133 -0
  410. package/src/__tests__/render-target-material-source.test-d.ts +27 -3
  411. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +61 -1
  412. package/src/__tests__/renderer-frame-transaction.integration.test.ts +923 -5
  413. package/src/__tests__/shader-manifest-fixture.ts +39 -0
  414. package/src/__tests__/skin-motion-regression.unit.test.ts +0 -33
  415. package/src/__tests__/skin-palette-persistent.integration.test.ts +28 -0
  416. package/src/__tests__/skin-palette-persistent.unit.test.ts +247 -0
  417. package/src/__tests__/skinned-shadow-caster.test.ts +118 -14
  418. package/src/__tests__/specular-environment-scale.dawn.test.ts +156 -0
  419. package/src/__tests__/ssr-admission-matrix.integration.test.ts +7 -1
  420. package/src/__tests__/ssr-confidence-compose.unit.test.ts +84 -0
  421. package/src/__tests__/ssr-coordinate-oracle.unit.test.ts +28 -0
  422. package/src/__tests__/ssr-format-probe-gating.integration.test.ts +82 -0
  423. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +91 -0
  424. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +112 -0
  425. package/src/__tests__/ssr-gpu-dispatch.ts +946 -0
  426. package/src/__tests__/ssr-graph-topology.unit.test.ts +104 -0
  427. package/src/__tests__/ssr-history-filter.dawn.test.ts +198 -0
  428. package/src/__tests__/ssr-hit-color.dawn.test.ts +273 -0
  429. package/src/__tests__/ssr-hiz-odd-reduction.dawn.test.ts +569 -0
  430. package/src/__tests__/ssr-hiz-rescue.dawn.test.ts +279 -0
  431. package/src/__tests__/ssr-hiz.browser.test.ts +150 -0
  432. package/src/__tests__/ssr-hiz.dawn.test.ts +150 -0
  433. package/src/__tests__/ssr-hiz.unit.test.ts +56 -0
  434. package/src/__tests__/ssr-inspection.unit.test.ts +105 -0
  435. package/src/__tests__/ssr-memory-budget.unit.test.ts +45 -0
  436. package/src/__tests__/ssr-performance-sequence-aggregation.unit.test.ts +15 -0
  437. package/src/__tests__/ssr-public-surface.test-d.ts +37 -0
  438. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +387 -0
  439. package/src/__tests__/ssr-reflection-mip-reducer.dawn.test.ts +309 -0
  440. package/src/__tests__/ssr-reprojection-probe.ts +90 -0
  441. package/src/__tests__/ssr-roughness-mip.dawn.test.ts +163 -0
  442. package/src/__tests__/ssr-shader-prewarm.unit.test.ts +59 -0
  443. package/src/__tests__/ssr-spatial.integration.test.ts +141 -0
  444. package/src/__tests__/ssr-stable-lattice.dawn.test.ts +475 -0
  445. package/src/__tests__/ssr-temporal-history.unit.test.ts +202 -0
  446. package/src/__tests__/ssr-texel-crossing.dawn.test.ts +448 -0
  447. package/src/__tests__/ssr-zero-work.integration.test.ts +87 -0
  448. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +44 -12
  449. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +29 -0
  450. package/src/__tests__/standard-output-chain-contract.unit.test.ts +99 -0
  451. package/src/__tests__/standard-output-chain.integration.test.ts +73 -0
  452. package/src/__tests__/standard-output-gpu-evidence.ts +205 -0
  453. package/src/__tests__/standard-output.browser.test.ts +15 -0
  454. package/src/__tests__/standard-output.dawn.test.ts +15 -0
  455. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +38 -0
  456. package/src/__tests__/standard-pipeline.integration.test.ts +320 -3
  457. package/src/__tests__/standard-profile-oracle.unit.test.ts +22 -0
  458. package/src/__tests__/standard-surface-lighting.unit.test.ts +41 -0
  459. package/src/__tests__/standard-texture-specialization.dawn.test.ts +141 -0
  460. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +6 -3
  461. package/src/__tests__/taa-coverage-edge.dawn.test.ts +193 -0
  462. package/src/__tests__/taa-history-clipping.dawn.test.ts +328 -0
  463. package/src/__tests__/taa-neighborhood-equivalence.dawn.test.ts +778 -0
  464. package/src/__tests__/taa-stability-feedback.browser.test.ts +23 -0
  465. package/src/__tests__/taa-stability-feedback.dawn.test.ts +68 -0
  466. package/src/__tests__/taa-stability-feedback.ts +341 -0
  467. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +49 -7
  468. package/src/__tests__/temporal-reset-matrix.unit.test.ts +9 -0
  469. package/src/__tests__/temporal-transaction.integration.test.ts +28 -0
  470. package/src/__tests__/texture-usage-surface.unit.test.ts +4 -2
  471. package/src/__tests__/typed-pipeline-topology.unit.test.ts +109 -4
  472. package/src/__tests__/typed-scene-pass-material-cache.integration.test.ts +126 -0
  473. package/src/__tests__/vertex-layouts-ssot.unit.test.ts +47 -0
  474. package/src/__tests__/visibility-extract.perf.test.ts +20 -8
  475. package/src/__tests__/visibility-instances.integration.test.ts +19 -7
  476. package/src/__tests__/visibility-producer-matrix.integration.test.ts +20 -9
  477. package/src/assembly/atmosphere-shader-prewarm.ts +38 -0
  478. package/src/assembly/dynamic-geometry-host.ts +887 -0
  479. package/src/assembly/dynamic-geometry-runtime.ts +71 -0
  480. package/src/assembly/factory.ts +4 -0
  481. package/src/assembly/gpu-driven-owner.ts +29 -0
  482. package/src/assembly/host-contract.ts +36 -0
  483. package/src/assembly/material/assembly.ts +38 -0
  484. package/src/assembly/material-shader-policy.ts +71 -28
  485. package/src/assembly/recovery/failure-location.ts +62 -0
  486. package/src/assembly/recovery/renderer-recover.ts +19 -17
  487. package/src/assembly/renderer-facade.ts +29 -0
  488. package/src/assembly/renderer-feature-inspection.ts +13 -0
  489. package/src/assembly/renderer-frame-transaction.ts +63 -0
  490. package/src/assembly/renderer-helpers.ts +26 -0
  491. package/src/assembly/renderer-inspection.ts +6 -0
  492. package/src/assembly/shader-prewarm-policy.ts +32 -0
  493. package/src/assembly/skin-palette-owner.ts +21 -0
  494. package/src/assembly/ssr-shader-prewarm.ts +41 -0
  495. package/src/assembly/webgpu-device-loss.ts +75 -0
  496. package/src/assembly/webgpu-pbr-ready.ts +232 -0
  497. package/src/assembly/webgpu-ready-mipmap.ts +26 -0
  498. package/src/assembly/webgpu-ready.ts +142 -121
  499. package/src/assembly/webgpu-renderer-bootstrap.ts +85 -0
  500. package/src/assembly/webgpu-renderer-contract.ts +69 -0
  501. package/src/assembly/webgpu-renderer-guards.ts +32 -0
  502. package/src/assembly/webgpu-renderer-observation.ts +43 -0
  503. package/src/assembly/webgpu-renderer-recovery-failure.ts +62 -0
  504. package/src/assembly/webgpu-renderer.ts +675 -614
  505. package/src/assembly/webgpu-vertex-layouts.ts +233 -87
  506. package/src/components/__tests__/camera-exposure.contract.unit.test.ts +153 -0
  507. package/src/components/camera.ts +287 -7
  508. package/src/components/index.ts +1 -0
  509. package/src/components/instances.ts +45 -18
  510. package/src/components/scene-instance.ts +2 -1
  511. package/src/components/screen-space-reflection.ts +21 -0
  512. package/src/components/sprite-animation.ts +3 -3
  513. package/src/components/sprite-instances.ts +1 -1
  514. package/src/device/gpu-residency.ts +96 -93
  515. package/src/device/mesh-residency-lifetime.ts +73 -0
  516. package/src/dynamic-geometry.ts +736 -0
  517. package/src/environment/background.ts +263 -0
  518. package/src/errors/gpu-driven.ts +39 -5
  519. package/src/errors/render.ts +27 -3
  520. package/src/extract/camera.ts +26 -1
  521. package/src/extract/gpu-driven.ts +185 -10
  522. package/src/features/host.ts +237 -19
  523. package/src/features/noise-texture.ts +91 -0
  524. package/src/features/plan.ts +553 -24
  525. package/src/features/prepared-gpu-work.ts +631 -49
  526. package/src/features/prepared-graphics-store.ts +31 -0
  527. package/src/features/prepared-graphics.ts +5 -0
  528. package/src/features/render-graph-compute.ts +41 -8
  529. package/src/features/render-graph-contribution.ts +141 -3
  530. package/src/features/render-graph-raster.ts +143 -30
  531. package/src/features/types.ts +11 -0
  532. package/src/fullscreen-post-process-pass.ts +3 -0
  533. package/src/gpu-driven/batch-topology.ts +424 -102
  534. package/src/gpu-driven/pbr-program.ts +31 -0
  535. package/src/gpu-driven/prepared-draw.ts +29 -4
  536. package/src/gpu-driven/production-raster.ts +2267 -694
  537. package/src/gpu-driven/shadow-views.ts +539 -0
  538. package/src/gpu-driven/view-gpu.ts +188 -41
  539. package/src/gpu-scene-schema.ts +52 -11
  540. package/src/gpu-scene.ts +186 -99
  541. package/src/gpu-texture-usage.ts +1 -0
  542. package/src/hdrp-buffers.ts +72 -36
  543. package/src/ibl/frame-resources.ts +38 -0
  544. package/src/ibl/skylight-bind-group.ts +5 -4
  545. package/src/index.ts +125 -1
  546. package/src/inspection-types.ts +191 -0
  547. package/src/instances-derived-bounds.ts +258 -34
  548. package/src/instances.ts +459 -150
  549. package/src/internal.ts +22 -0
  550. package/src/lifecycle.ts +2 -0
  551. package/src/material-row.ts +145 -0
  552. package/src/materials.ts +44 -8
  553. package/src/pbr-pipeline.ts +159 -17
  554. package/src/pipeline/standard-forward-lane.ts +52 -24
  555. package/src/pipeline/standard-output/__tests__/lut-admission.unit.test.ts +103 -0
  556. package/src/pipeline/standard-output/auto-exposure/__tests__/capability.unit.test.ts +40 -0
  557. package/src/pipeline/standard-output/auto-exposure/__tests__/gpu.contract.unit.test.ts +125 -0
  558. package/src/pipeline/standard-output/auto-exposure/__tests__/graph.contract.unit.test.ts +94 -0
  559. package/src/pipeline/standard-output/auto-exposure/__tests__/oracle.test.ts +108 -0
  560. package/src/pipeline/standard-output/auto-exposure/__tests__/preset.test.ts +26 -0
  561. package/src/pipeline/standard-output/auto-exposure/capability.ts +38 -0
  562. package/src/pipeline/standard-output/auto-exposure/gpu.ts +567 -0
  563. package/src/pipeline/standard-output/auto-exposure/graph.ts +221 -0
  564. package/src/pipeline/standard-output/auto-exposure/inspection.ts +231 -0
  565. package/src/pipeline/standard-output/auto-exposure/oracle.ts +291 -0
  566. package/src/pipeline/standard-output/auto-exposure/preset.ts +28 -0
  567. package/src/pipeline/standard-output/auto-exposure/state.ts +156 -0
  568. package/src/pipeline/standard-output/color-transform.ts +124 -0
  569. package/src/pipeline/standard-output/graph.ts +139 -0
  570. package/src/pipeline/standard-output/lut-admission.ts +121 -0
  571. package/src/pipeline/standard-output/lut-gpu.ts +399 -0
  572. package/src/pipeline/standard-output/lut-state.ts +176 -0
  573. package/src/pipeline/standard-output/resources.ts +121 -0
  574. package/src/pipeline/standard-output/types.ts +58 -0
  575. package/src/pipeline/standard-pipeline.ts +192 -59
  576. package/src/pipeline/standard-post.ts +186 -27
  577. package/src/pipeline-builder.ts +16 -6
  578. package/src/pipeline-spec-types.ts +4 -0
  579. package/src/pipeline-spec.ts +37 -29
  580. package/src/points-lines/standard-owner.ts +6 -14
  581. package/src/prepare/prepared-graphics-resolver.ts +9 -2
  582. package/src/record/__tests__/material-slot-plan.unit.test.ts +48 -0
  583. package/src/record/__tests__/mesh-ssbo.unit.test.ts +40 -1
  584. package/src/record/__tests__/recovery-pipeline.unit.test.ts +2 -29
  585. package/src/record/__tests__/sprite-material-abi.unit.test.ts +36 -5
  586. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +45 -0
  587. package/src/record/__tests__/view-ubo-layout.unit.test.ts +5 -0
  588. package/src/record/dynamic-geometry-consumption.ts +59 -0
  589. package/src/record/frame-lighting.ts +12 -11
  590. package/src/record/frame-snapshot.ts +133 -23
  591. package/src/record/frame-targets.ts +9 -0
  592. package/src/record/frame.ts +727 -133
  593. package/src/record/main-pass-geometry.ts +162 -98
  594. package/src/record/main-pass-material.ts +115 -26
  595. package/src/record/main-pass-sprite-draws.ts +18 -107
  596. package/src/record/main-pass.ts +78 -8
  597. package/src/record/material-uniforms.ts +1 -1
  598. package/src/record/mesh-ssbo.ts +59 -15
  599. package/src/record/prepared-material-bindings.ts +234 -0
  600. package/src/record/recovery-pipeline.ts +59 -136
  601. package/src/record/render-context.ts +92 -22
  602. package/src/record/shadow-pass.ts +1231 -209
  603. package/src/record/skybox-post-pass.ts +14 -3
  604. package/src/record/typed-frame-graph.ts +1006 -53
  605. package/src/record/view-ubo.ts +11 -0
  606. package/src/recovery/render-system-candidate.ts +90 -66
  607. package/src/recovery/render-system-roots.ts +113 -0
  608. package/src/recovery/types.ts +77 -0
  609. package/src/reflection/record-owner.ts +26 -54
  610. package/src/render-contract.ts +219 -25
  611. package/src/render-graph-primitives.ts +6 -2
  612. package/src/render-pipeline.ts +70 -1
  613. package/src/render-system-extract-tail.ts +409 -160
  614. package/src/render-system-extract.ts +376 -102
  615. package/src/render-system-projections.ts +241 -0
  616. package/src/render-system.ts +692 -791
  617. package/src/scene/render-scene-types.ts +13 -4
  618. package/src/scene/render-scene.ts +569 -355
  619. package/src/scene/shadow-visibility.ts +21 -0
  620. package/src/ssr/admission.ts +380 -14
  621. package/src/ssr/compose.ts +144 -0
  622. package/src/ssr/errors.ts +48 -0
  623. package/src/ssr/graph.ts +777 -0
  624. package/src/ssr/history.ts +436 -0
  625. package/src/ssr/hiz.ts +124 -0
  626. package/src/ssr/inspection.ts +124 -0
  627. package/src/ssr/resources.ts +202 -0
  628. package/src/ssr/temporal.ts +164 -0
  629. package/src/systems/skin-palette-allocator.ts +495 -148
  630. package/src/systems/skin-palette-types.ts +17 -0
  631. package/src/systems/transparent-dispatch.ts +90 -0
  632. package/src/targets/material-source.ts +34 -0
  633. package/src/temporal/coverage.ts +1 -1
  634. package/src/temporal/frame-coordinator.ts +23 -2
  635. package/src/temporal/gpu.ts +74 -12
  636. package/src/temporal/history.ts +3 -1
  637. package/src/temporal/inspection.ts +2 -1
  638. package/src/temporal/standard-scene-data.ts +3 -1
  639. package/src/temporal/temporal-view.ts +3 -0
  640. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +204 -139
  641. package/src/typed-render-graph-primitives.ts +266 -74
  642. package/src/typed-shadow-passes.ts +122 -12
  643. package/src/volume/temporal.ts +10 -0
  644. package/dist/chunk-ADHHVYLW.mjs.map +0 -1
  645. package/dist/chunk-FJ6P52EE.mjs.map +0 -1
  646. package/dist/chunk-GE3SDD72.mjs.map +0 -1
  647. package/dist/chunk-GF523LHF.mjs.map +0 -1
  648. package/dist/chunk-HKXTW355.mjs.map +0 -1
  649. package/dist/chunk-JDWARUOI.mjs.map +0 -1
  650. package/dist/chunk-KM2NOX2I.mjs.map +0 -1
  651. package/dist/chunk-TMIASV2N.mjs.map +0 -1
  652. package/dist/chunk-XDP6XE5V.mjs.map +0 -1
  653. package/dist/chunk-ZZ4YQ474.mjs.map +0 -1
  654. package/src/__tests__/instances-world-ownership.integration.test.ts +0 -62
  655. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +0 -36
@@ -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,43 @@ 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
+ gpuDrivenShadowDrawKey,
28
+ gpuDrivenSourceDrawItemIndex,
29
+ } from '../extract/gpu-driven';
26
30
  import {
27
31
  GPU_BUFFER_USAGE_COPY_DST,
28
32
  GPU_BUFFER_USAGE_INDEX,
29
33
  GPU_BUFFER_USAGE_STORAGE,
30
- GPU_BUFFER_USAGE_UNIFORM,
31
34
  GPU_BUFFER_USAGE_VERTEX,
32
35
  } from '../gpu-usage';
33
- import { assembleMaterialWithSkylightEntries } from '../ibl/skylight-bind-group';
34
- import type { GpuDrivenProductionInspection } from '../inspection-types';
36
+ import type {
37
+ GpuDrivenLaneReason,
38
+ GpuDrivenLaneSummary,
39
+ GpuDrivenPreparationFailureInspection,
40
+ GpuDrivenProductionInspection,
41
+ GpuDrivenStructureMetrics,
42
+ } from '../inspection-types';
35
43
  import type { PipelineBuilderShaderModuleFactory } from '../pipeline-builder';
44
+ import type { ValidatedRenderable } from '../record/frame-snapshot';
36
45
  import { worldEntityKey } from '../record/frame-snapshot';
37
46
  import { geometryRenderStateForTopology } from '../record/main-pass-material';
38
- import { MESH_SSBO_BYTES } from '../record/mesh-ssbo';
47
+ import { MESH_SSBO_BYTES, packInstanceStorageBuffer } from '../record/mesh-ssbo';
39
48
  import type { PipelineState } from '../record/render-context';
40
49
  import type { CameraSnapshot } from '../render-contract';
41
- import type { RenderPipelineFrame, RenderPipelineGpuDrivenProjection } from '../render-pipeline';
42
- import type { RenderableSnapshot } from '../render-system-extract';
50
+ import type {
51
+ GpuDrivenStandardPbrFrameResources,
52
+ RenderPipelineFrame,
53
+ RenderPipelineGpuDrivenProjection,
54
+ } from '../render-pipeline';
55
+ import type {
56
+ DispatchEntry,
57
+ MaterialSnapshot,
58
+ RenderableSnapshot,
59
+ ShadowCasterMembership,
60
+ } from '../render-system-extract';
43
61
  import type { PersistentGpuDrivenState } from '../scene/render-scene';
44
62
  import { selectLod } from '../scene/visibility/lod-selector';
45
63
  import type { OcclusionFrameProjection } from '../scene/visibility/occlusion-runtime';
@@ -49,20 +67,23 @@ import {
49
67
  projectedHeightForCandidate,
50
68
  type SubmissionPlan,
51
69
  } from './batch-topology';
70
+ import { type GpuDrivenPbrProgram, resolveStandardPbrProgram } from './pbr-program';
52
71
  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: [] });
72
+ type ShadowViewIdentity,
73
+ type ShadowViewProjection,
74
+ ShadowViewStatePool,
75
+ type ShadowViewUpdateInput,
76
+ shadowViewIdentityKey,
77
+ } from './shadow-views';
78
+ import { type GpuDrivenLodSelectionInspection, GpuDrivenView, selectGpuLodLane } from './view-gpu';
62
79
 
63
80
  export interface StandardPbrSceneCandidate {
64
81
  readonly primitiveIndex: number;
65
82
  readonly world: Float32Array;
83
+ /** Optional per-instance local transform selected by BatchTopology. */
84
+ readonly instanceTransform?: Float32Array;
85
+ /** Optional GPU Scene material row carried by the shadow projection. */
86
+ readonly materialIndex?: number;
66
87
  }
67
88
 
68
89
  export interface StandardPbrSceneProjection {
@@ -73,6 +94,17 @@ export interface StandardPbrSceneProjection {
73
94
  readonly rows: Float32Array;
74
95
  }
75
96
 
97
+ export interface GpuDrivenShadowBatchProjection {
98
+ readonly mesh: MeshGpuHandles;
99
+ readonly meshBindGroup: BindGroup;
100
+ readonly visibleBindGroup: BindGroup;
101
+ readonly deformation: 'rigid' | 'skin';
102
+ /** Material snapshot used to assemble the receipt-owned alpha-mask BG. */
103
+ readonly material: MaterialSnapshot;
104
+ /** Composite entity identity used only for video-resource routing. */
105
+ readonly materialEntityKey: number;
106
+ }
107
+
76
108
  /**
77
109
  * Build the producer-owned Standard PBR scene-index projection. The receipt is
78
110
  * the only source of the vertex entry and layout identity; callers cannot
@@ -97,12 +129,16 @@ export function projectStandardPbrScene(input: {
97
129
  for (let index = 0; index < input.candidates.length; index += 1) {
98
130
  const candidate = input.candidates[index];
99
131
  if (candidate === undefined) continue;
132
+ const world =
133
+ candidate.instanceTransform === undefined
134
+ ? candidate.world
135
+ : mat4.multiply(mat4.create(), candidate.world, candidate.instanceTransform);
100
136
  const base = index * 48;
101
137
  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;
138
+ rows[base + value] = world[value] ?? 0;
139
+ rows[base + 28 + value] = world[value] ?? 0;
104
140
  }
105
- const normalMatrix = mat3.normalMatrix(normal, candidate.world);
141
+ const normalMatrix = mat3.normalMatrix(normal, world);
106
142
  for (let column = 0; column < 3; column += 1) {
107
143
  const source = column * 3;
108
144
  const target = base + 16 + column * 4;
@@ -110,6 +146,11 @@ export function projectStandardPbrScene(input: {
110
146
  rows[target + 1] = normalMatrix[source + 1] ?? 0;
111
147
  rows[target + 2] = normalMatrix[source + 2] ?? 0;
112
148
  }
149
+ // The final temporal metadata lane is otherwise unused by the shadow
150
+ // vertex path. Carry the concrete scene-material row there so skinned
151
+ // GPU-driven alpha-mask shadows can keep the palette base in visible.y
152
+ // without introducing a second visible-stream ABI.
153
+ rows[base + 47] = candidate.materialIndex ?? 0;
113
154
  }
114
155
  return {
115
156
  ok: true,
@@ -183,50 +224,6 @@ export function adaptStandardPbrFrameResources(input: {
183
224
  };
184
225
  }
185
226
 
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
227
  interface PreparedBatch {
231
228
  readonly batch: GpuDrivenBatch;
232
229
  readonly mesh: MeshGpuHandles;
@@ -238,13 +235,6 @@ interface PreparedBatch {
238
235
  readonly slotGeneration: number;
239
236
  }
240
237
 
241
- interface ProjectedBatch extends PreparedBatch {
242
- readonly bindings: BindGroup;
243
- readonly pipeline: RenderPipeline;
244
- readonly vertex: GraphBuffer;
245
- readonly index?: GraphBuffer;
246
- }
247
-
248
238
  function bufferBinding(
249
239
  buffer: import('@forgeax/engine-rhi').Buffer,
250
240
  offset?: number,
@@ -282,46 +272,117 @@ function activeFrustum(camera: CameraSnapshot): Float32Array {
282
272
  );
283
273
  }
284
274
 
275
+ function hasFiniteOrderedBounds(bounds: Float32Array | undefined): boolean {
276
+ if (bounds === undefined || bounds.length < 6) return false;
277
+ for (let index = 0; index < 6; index += 1) {
278
+ if (!Number.isFinite(bounds[index])) return false;
279
+ }
280
+ const minX = bounds[0];
281
+ const minY = bounds[1];
282
+ const minZ = bounds[2];
283
+ const maxX = bounds[3];
284
+ const maxY = bounds[4];
285
+ const maxZ = bounds[5];
286
+ return (
287
+ minX !== undefined &&
288
+ minY !== undefined &&
289
+ minZ !== undefined &&
290
+ maxX !== undefined &&
291
+ maxY !== undefined &&
292
+ maxZ !== undefined &&
293
+ minX <= maxX &&
294
+ minY <= maxY &&
295
+ minZ <= maxZ
296
+ );
297
+ }
298
+
299
+ function skinLaneReason(
300
+ snapshot: RenderableSnapshot,
301
+ material: MaterialSnapshot,
302
+ ): 'skin-bounds-missing' | 'skin-address-missing' | undefined {
303
+ const isSkinnedPbr =
304
+ material.materialShaderId === 'forgeax::pbr-skin' ||
305
+ material.materialShaderId === 'forgeax::default-standard-pbr-skin';
306
+ if (!isSkinnedPbr || snapshot.skin === undefined) return undefined;
307
+ if (!hasFiniteOrderedBounds(snapshot.skin.bounds)) return 'skin-bounds-missing';
308
+ if (
309
+ snapshot.skin.storageOrUniform !== 'storage' ||
310
+ !Number.isInteger(snapshot.skin.customDataStart) ||
311
+ snapshot.skin.customDataStart < 0 ||
312
+ snapshot.skin.buffer === undefined
313
+ ) {
314
+ return 'skin-address-missing';
315
+ }
316
+ return undefined;
317
+ }
318
+
285
319
  /**
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.
320
+ * Derived projected heights invalidate the prepared plan only when a
321
+ * candidate has more than its implicit root LOD row. No-LOD plans have no
322
+ * view-dependent selection. Root matrix edits and camera edits feed the same
323
+ * height calculation; retaining the empty key for absent LOD avoids rebuilding
324
+ * their candidate and batch uploads on every camera frame.
289
325
  */
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
- });
326
+ function lodProjectionFingerprint(
327
+ source: SubmissionPlan,
328
+ camera: CameraSnapshot,
329
+ slots: PersistentGpuDrivenState['slots'],
330
+ ): string {
331
+ const primitives = new Set<number>();
332
+ for (const batch of source.batches) {
333
+ for (const candidate of batch.candidates) {
334
+ if ((candidate.lodCoverages?.length ?? 0) > 1) primitives.add(candidate.primitiveIndex);
335
+ }
336
+ }
337
+ if (primitives.size === 0) return '';
338
+ const heights: number[] = [];
339
+ for (const slot of slots) {
340
+ if (primitives.has(slot.slot))
341
+ heights.push(slot.slot, projectedHeightForCandidate(slot, camera));
342
+ }
343
+ return JSON.stringify(heights);
299
344
  }
300
345
 
301
- function productionEligible(snapshot: RenderableSnapshot, batch: GpuDrivenBatch): boolean {
346
+ function preparedPbrCandidateEligible(
347
+ snapshot: RenderableSnapshot,
348
+ batch: GpuDrivenBatch,
349
+ candidate: GpuDrivenBatch['candidates'][number],
350
+ skinArtifact?: MaterialShaderArtifact,
351
+ ): boolean {
302
352
  const material = snapshot.materials[batch.key.materialSlot] ?? snapshot.material;
303
- const shader = material.materialShaderId ?? 'forgeax::default-unlit';
353
+ const prepared = candidate.prepared;
354
+ if (prepared === undefined) return false;
355
+ const standard =
356
+ prepared.identity.material === 'forgeax::default-standard-pbr' ||
357
+ prepared.identity.material === 'forgeax::pbr-skin' ||
358
+ prepared.identity.material === 'forgeax::default-standard-pbr-skin';
359
+ const alphaBlend = material.transparent === true || material.renderState?.blend !== undefined;
360
+ const cpuOnlyMaterialResources =
361
+ (material.textureSources?.size ?? 0) > 0 || (material.videoTextureFields?.size ?? 0) > 0;
362
+ // Transmission has a separate CPU semantic pass today. Keep it out of the
363
+ // GPU-owned candidate set so the residual transmission draw is not swallowed
364
+ // by the generic Standard PBR lane.
365
+ const transmission =
366
+ typeof material.paramSnapshot?.transmission === 'number' &&
367
+ material.paramSnapshot.transmission > 0;
368
+ const skinReady =
369
+ prepared.identity.deformation !== 'skin' ||
370
+ (skinArtifact?.receipt?.skinPaletteAddress !== undefined &&
371
+ snapshot.skin?.storageOrUniform === 'storage' &&
372
+ snapshot.skin.customDataStart >= 0 &&
373
+ Number.isInteger(snapshot.skin.customDataStart) &&
374
+ snapshot.skin.buffer !== undefined &&
375
+ hasFiniteOrderedBounds(snapshot.skin.bounds));
304
376
  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
377
+ standard &&
378
+ !alphaBlend &&
379
+ !cpuOnlyMaterialResources &&
380
+ !transmission &&
381
+ (prepared.identity.deformation === 'rigid' || prepared.identity.deformation === 'skin') &&
382
+ skinReady
312
383
  );
313
384
  }
314
385
 
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
386
  function meshSupportsBatch(mesh: MeshGpuHandles, batch: GpuDrivenBatch): boolean {
326
387
  if (
327
388
  batch.candidates.some((candidate) => {
@@ -341,6 +402,132 @@ function meshSupportsBatch(mesh: MeshGpuHandles, batch: GpuDrivenBatch): boolean
341
402
  : !mesh.indexed;
342
403
  }
343
404
 
405
+ function sceneCandidate(
406
+ slot: PersistentGpuDrivenState['slots'][number],
407
+ candidate: GpuDrivenBatch['candidates'][number],
408
+ materialIndex?: number,
409
+ ): StandardPbrSceneCandidate {
410
+ const transforms = slot.snapshot.instances?.transforms;
411
+ return {
412
+ primitiveIndex: candidate.primitiveIndex,
413
+ world: slot.snapshot.transform.world,
414
+ ...(transforms === undefined
415
+ ? {}
416
+ : {
417
+ instanceTransform: transforms.subarray(
418
+ candidate.instanceOrdinal * 16,
419
+ candidate.instanceOrdinal * 16 + 16,
420
+ ),
421
+ }),
422
+ ...(materialIndex === undefined ? {} : { materialIndex }),
423
+ };
424
+ }
425
+
426
+ function shadowCandidateKey(candidate: GpuDrivenBatch['candidates'][number]): string {
427
+ return `${candidate.primitiveIndex}:${candidate.drawItemIndex}:${candidate.instanceOrdinal}`;
428
+ }
429
+
430
+ function gpuDrivenProjectionKey(
431
+ batch: GpuDrivenBatch,
432
+ deformation: 'rigid' | 'skin',
433
+ artifact: MaterialShaderArtifact,
434
+ ): string {
435
+ return `${batch.batchId}:${batch.generation}:${deformation}:${artifact.layoutIdentity}`;
436
+ }
437
+
438
+ function gpuDrivenShadowProjectionKey(
439
+ viewKey: string,
440
+ batch: GpuDrivenBatch,
441
+ deformation: 'rigid' | 'skin',
442
+ artifact: MaterialShaderArtifact,
443
+ ): string {
444
+ return `shadow:${viewKey}:${gpuDrivenProjectionKey(batch, deformation, artifact)}`;
445
+ }
446
+
447
+ function sourceDrawForCandidate(
448
+ slot: PersistentGpuDrivenState['slots'][number],
449
+ candidate: GpuDrivenBatch['candidates'][number],
450
+ ): NonNullable<RenderableSnapshot['gpuDrivenDraws']>[number] | undefined {
451
+ return slot.snapshot.gpuDrivenDraws?.find(
452
+ (draw, compactIndex) =>
453
+ gpuDrivenSourceDrawItemIndex(draw, compactIndex) === candidate.drawItemIndex,
454
+ );
455
+ }
456
+
457
+ function shadowKeysForCandidate(
458
+ slot: PersistentGpuDrivenState['slots'][number],
459
+ candidate: GpuDrivenBatch['candidates'][number],
460
+ shadowCasterDrawKeys: ReadonlySet<string> | undefined,
461
+ shadowCasterMembership: readonly ShadowCasterMembership[] | undefined,
462
+ ): readonly string[] {
463
+ const draw = sourceDrawForCandidate(slot, candidate);
464
+ if (draw === undefined) return [];
465
+ const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
466
+ const worldEntity = worldEntityKey(slot.snapshot.worldId, slot.snapshot.entityKey);
467
+ const base = gpuDrivenDrawKey(
468
+ worldEntity,
469
+ material.materialHandle ?? -1,
470
+ candidate.drawItemIndex,
471
+ );
472
+ if (shadowCasterMembership !== undefined) {
473
+ const keys = new Set<string>();
474
+ for (const membership of shadowCasterMembership) {
475
+ if (
476
+ membership.worldEntity === worldEntity &&
477
+ membership.materialHandle === (material.materialHandle ?? -1) &&
478
+ membership.drawItemIndex === candidate.drawItemIndex
479
+ ) {
480
+ keys.add(
481
+ gpuDrivenShadowDrawKey(
482
+ membership.worldEntity,
483
+ membership.materialHandle,
484
+ membership.drawItemIndex,
485
+ membership.passIndex,
486
+ ),
487
+ );
488
+ }
489
+ }
490
+ return [...keys];
491
+ }
492
+ if (shadowCasterDrawKeys === undefined) return [];
493
+ return [...shadowCasterDrawKeys].filter((key) => {
494
+ const parts = key.split(':');
495
+ return parts.length === 4 && `${parts[0]}:${parts[1]}:${parts[2]}` === base;
496
+ });
497
+ }
498
+
499
+ function shadowMembershipIsGpuCompatible(
500
+ slot: PersistentGpuDrivenState['slots'][number],
501
+ candidate: GpuDrivenBatch['candidates'][number],
502
+ shadowCasterDrawKeys: ReadonlySet<string> | undefined,
503
+ shadowCasterMembership: readonly ShadowCasterMembership[] | undefined,
504
+ ): boolean {
505
+ const keys = shadowKeysForCandidate(
506
+ slot,
507
+ candidate,
508
+ shadowCasterDrawKeys,
509
+ shadowCasterMembership,
510
+ );
511
+ if (keys.length !== 1) return false;
512
+ if (shadowCasterMembership === undefined) return true;
513
+ const draw = sourceDrawForCandidate(slot, candidate);
514
+ if (draw === undefined) return false;
515
+ const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
516
+ const worldEntity = worldEntityKey(slot.snapshot.worldId, slot.snapshot.entityKey);
517
+ const matching = shadowCasterMembership.filter(
518
+ (membership) =>
519
+ membership.worldEntity === worldEntity &&
520
+ membership.materialHandle === (material.materialHandle ?? -1) &&
521
+ membership.drawItemIndex === candidate.drawItemIndex,
522
+ );
523
+ if (matching.length !== 1) return false;
524
+ const membership = matching[0];
525
+ if (membership?.cpuReason !== undefined) return false;
526
+ if (membership?.renderState !== undefined) return false;
527
+ const shader = membership?.materialShaderId;
528
+ return shader === undefined || shader === 'forgeax::default-shadow-caster';
529
+ }
530
+
344
531
  function selectedLodLevel(candidate: GpuDrivenCandidate): number {
345
532
  const coverages = candidate.lodCoverages;
346
533
  if (coverages === undefined || coverages.length <= 1) return 0;
@@ -368,28 +555,38 @@ function selectedLodRange(
368
555
  );
369
556
  }
370
557
 
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
558
  function admittedRasterBatchCount(plan: SubmissionPlan): number {
377
559
  return plan.batches.reduce((count, batch) => count + (batch.visibleCapacity > 0 ? 1 : 0), 0);
378
560
  }
379
561
 
562
+ function gpuDrawEntityCount(drawKeys: ReadonlySet<string>): number {
563
+ const entities = new Set<string>();
564
+ for (const key of drawKeys) {
565
+ const separator = key.indexOf(':');
566
+ if (separator > 0) entities.add(key.slice(0, separator));
567
+ }
568
+ return entities.size;
569
+ }
570
+
380
571
  function filteredPlan(
381
572
  source: SubmissionPlan,
382
573
  slots: PersistentGpuDrivenState['slots'],
383
- meshes: ReadonlyMap<number, MeshGpuHandles>,
384
- camera: CameraSnapshot,
574
+ meshBySlot: ReadonlyMap<number, MeshGpuHandles>,
575
+ shadowCasterDrawKeys?: ReadonlySet<string>,
576
+ skinArtifact?: MaterialShaderArtifact,
577
+ shadowCasterMembership?: readonly ShadowCasterMembership[],
578
+ camera?: CameraSnapshot,
385
579
  activeEntityKeys?: ReadonlySet<number>,
386
580
  worldKeys?: readonly number[],
387
581
  ): {
388
582
  readonly plan: SubmissionPlan;
389
583
  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;
584
+ /** True only when every source draw item/instance has a main GPU candidate. */
585
+ readonly ownsAllDrawItems: boolean;
586
+ /** Concrete entity/material/draw identities claimed by this projection. */
587
+ readonly drawKeys: ReadonlySet<string>;
588
+ /** Extracted concrete ShadowCaster keys actually claimed by this plan. */
589
+ readonly shadowDrawKeys: ReadonlySet<string>;
393
590
  } {
394
591
  const slotByPrimitive = new Map(slots.map((slot) => [slot.slot, slot] as const));
395
592
  const participatingByPrimitive = new Map<number, Set<GpuDrivenBatch>>();
@@ -403,42 +600,62 @@ function filteredPlan(
403
600
  }
404
601
  }
405
602
  }
406
- const entityKeys = new Set<number>();
603
+ const eligibleMainCandidates = new Set<string>();
604
+ const drawKeys = new Set<string>();
605
+ const eligibleShadowCandidates = new Set<string>();
606
+ const shadowDrawKeys = new Set<string>();
607
+ const shadowSelectionProvided =
608
+ shadowCasterDrawKeys !== undefined || shadowCasterMembership !== undefined;
407
609
  const worldKeyFor = (slot: PersistentGpuDrivenState['slots'][number]): number =>
408
610
  worldKeys?.[slot.snapshot.worldId] ?? slot.snapshot.worldId;
409
611
  const activeKey = (slot: PersistentGpuDrivenState['slots'][number]): number =>
410
612
  worldEntityKey(worldKeyFor(slot), slot.snapshot.entityKey);
411
- const eligiblePrimitives = new Set<number>();
412
- let topologyEligiblePrimitiveCount = 0;
413
613
  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));
614
+ const mesh = meshBySlot.get(slot.slot);
615
+ const participating = participatingByPrimitive.get(slot.slot) ?? new Set<GpuDrivenBatch>();
616
+ for (const batch of participating) {
617
+ for (const candidate of batch.candidates) {
618
+ if (candidate.primitiveIndex !== slot.slot) continue;
619
+ const draw = sourceDrawForCandidate(slot, candidate);
620
+ const mainEligible =
621
+ draw !== undefined &&
622
+ preparedPbrCandidateEligible(slot.snapshot, batch, candidate, skinArtifact) &&
623
+ mesh !== undefined &&
624
+ meshSupportsBatch(mesh, batch);
625
+ if (!shadowSelectionProvided && mainEligible && draw !== undefined) {
626
+ eligibleMainCandidates.add(shadowCandidateKey(candidate));
627
+ const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
628
+ drawKeys.add(
629
+ gpuDrivenDrawKey(
630
+ worldEntityKey(worldKeyFor(slot), slot.snapshot.entityKey),
631
+ material.materialHandle ?? -1,
632
+ candidate.drawItemIndex,
633
+ ),
634
+ );
635
+ }
636
+ if (
637
+ shadowSelectionProvided &&
638
+ mainEligible &&
639
+ shadowMembershipIsGpuCompatible(
640
+ slot,
641
+ candidate,
642
+ shadowCasterDrawKeys,
643
+ shadowCasterMembership,
644
+ )
645
+ ) {
646
+ eligibleShadowCandidates.add(shadowCandidateKey(candidate));
647
+ for (const key of shadowKeysForCandidate(
648
+ slot,
649
+ candidate,
650
+ shadowCasterDrawKeys,
651
+ shadowCasterMembership,
652
+ )) {
653
+ shadowDrawKeys.add(key);
654
+ }
655
+ }
433
656
  }
434
657
  }
435
658
  }
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
659
  const groups = new Map<
443
660
  string,
444
661
  {
@@ -451,15 +668,21 @@ function filteredPlan(
451
668
  for (const batch of source.batches) {
452
669
  const candidates = batch.candidates.filter((candidate) => {
453
670
  const slot = slotByPrimitive.get(candidate.primitiveIndex);
454
- if (slot === undefined || !eligiblePrimitives.has(slot.slot)) return false;
455
- const mesh = meshes.get(slot.snapshot.assetHandle);
671
+ if (slot === undefined) return false;
672
+ if (
673
+ !shadowSelectionProvided
674
+ ? !eligibleMainCandidates.has(shadowCandidateKey(candidate))
675
+ : !eligibleShadowCandidates.has(shadowCandidateKey(candidate))
676
+ )
677
+ return false;
678
+ const mesh = meshBySlot.get(slot.slot);
456
679
  return mesh !== undefined && meshSupportsBatch(mesh, batch);
457
680
  });
458
681
  if (candidates.length === 0) continue;
459
682
  for (const candidate of candidates) {
460
683
  const slot = slotByPrimitive.get(candidate.primitiveIndex);
461
684
  if (slot === undefined) continue;
462
- const mesh = meshes.get(slot.snapshot.assetHandle);
685
+ const mesh = meshBySlot.get(slot.slot);
463
686
  if (mesh === undefined) continue;
464
687
  const physicalLodRanges = mesh.lodRanges
465
688
  ?.slice(1)
@@ -467,7 +690,10 @@ function filteredPlan(
467
690
  const projected = Object.freeze({
468
691
  ...candidate,
469
692
  submitAdmission: activeEntityKeys === undefined || activeEntityKeys.has(activeKey(slot)),
470
- projectedHeight: projectedHeightForCandidate(slot, camera),
693
+ projectedHeight:
694
+ camera === undefined
695
+ ? (candidate.projectedHeight ?? Number.NaN)
696
+ : projectedHeightForCandidate(slot, camera),
471
697
  ...(physicalLodRanges?.every((range) => range !== undefined)
472
698
  ? { lodRanges: physicalLodRanges as NonNullable<typeof candidate.lodRanges> }
473
699
  : {}),
@@ -495,7 +721,6 @@ function filteredPlan(
495
721
  }
496
722
  }
497
723
  }
498
-
499
724
  const batches: PreparedBatch[] = [];
500
725
  let visibleBase = 0;
501
726
  const filteredBatches: GpuDrivenBatch[] = [];
@@ -521,7 +746,19 @@ function filteredPlan(
521
746
  });
522
747
  visibleBase += admittedCount;
523
748
  filteredBatches.push(filtered);
524
- const firstCandidate = group.candidates[0];
749
+ // Keep every selector batch id unique even when no current entity is
750
+ // admitted to raster. The zero-admission batch still feeds LOD telemetry,
751
+ // while the following raster batch must not reuse its indirect slot.
752
+ batchId += 1;
753
+ // A selector batch can retain facet-suppressed candidates for LOD
754
+ // telemetry. Raster ownership, however, must be represented by an
755
+ // admitted candidate; using the first retained row can point material
756
+ // routing at an entity that the main-pass producer intentionally omitted.
757
+ const firstCandidate = group.candidates.find(
758
+ (candidate) =>
759
+ (candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
760
+ .submitAdmission !== false,
761
+ );
525
762
  const firstSlot =
526
763
  firstCandidate === undefined ? undefined : slotByPrimitive.get(firstCandidate.primitiveIndex);
527
764
  if (firstCandidate === undefined || firstSlot === undefined) continue;
@@ -533,23 +770,142 @@ function filteredPlan(
533
770
  primitiveSlot: firstSlot.slot,
534
771
  slotGeneration: firstSlot.generation,
535
772
  });
536
- batchId += 1;
537
773
  }
774
+ const ownershipSlots =
775
+ activeEntityKeys === undefined
776
+ ? slots
777
+ : slots.filter((slot) => activeEntityKeys.has(activeKey(slot)));
778
+ const ownsAllDrawItems =
779
+ !shadowSelectionProvided &&
780
+ ownershipSlots.every((slot) => {
781
+ const draws = slot.snapshot.gpuDrivenDraws ?? [];
782
+ if (draws.length === 0) return false;
783
+ const instanceCount = slot.snapshot.instances?.instanceCount ?? 1;
784
+ const candidates = source.batches.flatMap((batch) =>
785
+ batch.candidates.filter((candidate) => candidate.primitiveIndex === slot.slot),
786
+ );
787
+ return (
788
+ candidates.length === draws.length * instanceCount &&
789
+ candidates.every((candidate) => {
790
+ if (!eligibleMainCandidates.has(shadowCandidateKey(candidate))) return false;
791
+ const draw = sourceDrawForCandidate(slot, candidate);
792
+ if (draw === undefined) return false;
793
+ const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
794
+ return drawKeys.has(
795
+ gpuDrivenDrawKey(
796
+ worldEntityKey(worldKeyFor(slot), slot.snapshot.entityKey),
797
+ material.materialHandle ?? -1,
798
+ candidate.drawItemIndex,
799
+ ),
800
+ );
801
+ })
802
+ );
803
+ });
538
804
  return {
539
805
  plan: Object.freeze({
540
806
  revision: source.revision,
807
+ payloadRevision: source.payloadRevision,
541
808
  batches: Object.freeze(filteredBatches),
542
809
  candidateCount: filteredBatches.reduce((total, batch) => total + batch.candidates.length, 0),
543
810
  visibleCapacity: visibleBase,
544
811
  }),
545
812
  batches: Object.freeze(batches),
546
- entityKeys,
547
- topologyInvariantEmpty: topologyEligiblePrimitiveCount === 0,
813
+ ownsAllDrawItems,
814
+ drawKeys,
815
+ shadowDrawKeys,
548
816
  };
549
817
  }
550
818
 
819
+ function shadowCasterMembershipSignature(
820
+ drawKeys: ReadonlySet<string> | undefined,
821
+ membership: readonly ShadowCasterMembership[] | undefined,
822
+ ): string {
823
+ const keySignature = drawKeys === undefined ? '*' : [...drawKeys].sort().join(',');
824
+ if (membership === undefined) return keySignature;
825
+ const membershipSignature = membership
826
+ .map((entry) =>
827
+ [
828
+ entry.worldEntity,
829
+ entry.renderableIndex,
830
+ entry.drawItemIndex,
831
+ entry.materialHandle,
832
+ entry.passIndex,
833
+ entry.materialShaderId ?? '',
834
+ JSON.stringify(entry.renderState ?? null),
835
+ entry.cpuReason ?? '',
836
+ ].join(':'),
837
+ )
838
+ .sort()
839
+ .join(',');
840
+ return `${keySignature}|membership=${membershipSignature}`;
841
+ }
842
+
551
843
  type FilteredProductionPlan = ReturnType<typeof filteredPlan>;
552
844
 
845
+ function sceneRequiresGpuDrivenArtifact(scene: PersistentGpuDrivenState): boolean {
846
+ return scene.plan.batches.some((batch) =>
847
+ batch.candidates.some((candidate) => {
848
+ const material = candidate.prepared?.identity.material;
849
+ return (
850
+ (material === 'forgeax::default-standard-pbr' ||
851
+ material === 'forgeax::pbr-skin' ||
852
+ material === 'forgeax::default-standard-pbr-skin') &&
853
+ (candidate.prepared?.identity.deformation === 'rigid' ||
854
+ candidate.prepared?.identity.deformation === 'skin')
855
+ );
856
+ }),
857
+ );
858
+ }
859
+
860
+ function nonCpuPreparationFailure(
861
+ scene: PersistentGpuDrivenState,
862
+ ): { readonly error: GpuDrivenPreparationError; readonly drawKeys: readonly string[] } | undefined {
863
+ const drawKeys: string[] = [];
864
+ let first: GpuDrivenPreparationError | undefined;
865
+ for (const slot of scene.slots) {
866
+ const draws = slot.snapshot.gpuDrivenDraws ?? [];
867
+ for (const [compactIndex, draw] of draws.entries()) {
868
+ const error = draw.preparationError;
869
+ if (error === undefined || error.detail.recovery === 'route-cpu-lane') continue;
870
+ const material = slot.snapshot.materials[draw.materialSlot] ?? slot.snapshot.material;
871
+ drawKeys.push(
872
+ gpuDrivenDrawKey(
873
+ worldEntityKey(slot.snapshot.worldId, slot.snapshot.entityKey),
874
+ material.materialHandle ?? -1,
875
+ gpuDrivenSourceDrawItemIndex(draw, compactIndex),
876
+ ),
877
+ );
878
+ first ??= error;
879
+ }
880
+ }
881
+ return first === undefined ? undefined : { error: first, drawKeys };
882
+ }
883
+
884
+ function preparationFailureInspection(
885
+ error: GpuDrivenPreparationError,
886
+ ): GpuDrivenPreparationFailureInspection {
887
+ return Object.freeze({
888
+ code: error.code,
889
+ expected: error.expected,
890
+ hint: error.hint,
891
+ detail: error.detail,
892
+ });
893
+ }
894
+
895
+ function shadowOwnershipFailureInspection(
896
+ viewPass: GpuDrivenLaneSummary['viewPass'],
897
+ ): GpuDrivenPreparationFailureInspection {
898
+ return preparationFailureInspection(
899
+ new GpuDrivenPreparationError('shadow-ownership', {
900
+ reason: 'shadow-ownership-missing',
901
+ owner: 'shadow',
902
+ expected: 'one GPU-compatible owner for every declared ShadowCaster pass',
903
+ actual: `${viewPass} contains a residual ShadowCaster draw item`,
904
+ recovery: 'route-cpu-lane',
905
+ }),
906
+ );
907
+ }
908
+
553
909
  export interface GpuDrivenWorldLodSelectionInspection {
554
910
  readonly worldKey: number;
555
911
  readonly primitiveSlot: number;
@@ -570,89 +926,322 @@ function sameEntityKeySet(
570
926
  return true;
571
927
  }
572
928
 
573
- class GpuDrivenRigidRaster {
929
+ /** The GPU-driven bridge to the existing four-group Standard PBR pipeline. */
930
+ class StandardPbrRasterAdapter {
574
931
  private readonly pipelines = new Map<string, RenderPipeline>();
932
+ private readonly projectionBuffers = new Set<Buffer>();
933
+ private readonly projectionBindings = new Map<
934
+ string,
935
+ {
936
+ readonly buffer: Buffer;
937
+ readonly bindGroup: BindGroup;
938
+ readonly capacityBytes: number;
939
+ rows: Float32Array;
940
+ }
941
+ >();
942
+ /**
943
+ * Projection rows are graph-generation resources. A new topology may stage
944
+ * a replacement before the graph is promoted; keeping staged bindings out
945
+ * of the active map lets an aborted graph leave the last submitted rows
946
+ * intact. Superseded buffers are fence-retired on promotion.
947
+ */
948
+ private readonly stagedProjectionBindings = new Map<
949
+ string,
950
+ {
951
+ readonly buffer: Buffer;
952
+ readonly bindGroup: BindGroup;
953
+ readonly capacityBytes: number;
954
+ rows: Float32Array;
955
+ }
956
+ >();
957
+ private readonly pendingProjectionRetirements = new Set<Buffer>();
958
+ private readonly destroyedProjectionBuffers = new Set<Buffer>();
959
+ private projectionDisposed = false;
960
+ private stagedProjectionKeys = new Set<string>();
961
+ private readonly activeProjectionRowsAtStage = new Map<string, Float32Array>();
962
+ private readonly visibleWindowBindGroups = new Map<Buffer, Map<string, BindGroup>>();
575
963
 
576
964
  private constructor(
577
965
  private readonly device: RhiDevice,
578
- private readonly layout: BindGroupLayout,
579
966
  private readonly pipelineLayout: import('@forgeax/engine-rhi').PipelineLayout,
580
- private readonly module: ShaderModule,
967
+ private readonly meshLayout: BindGroupLayout,
968
+ private readonly instancesBindGroupLayout: BindGroupLayout,
969
+ private readonly programs: ReadonlyMap<string, GpuDrivenPbrProgram>,
970
+ private instancesBindGroup: BindGroup,
971
+ private readonly instanceBuffer: Buffer,
972
+ private readonly instanceBufferSize: number,
973
+ private visibleBuffer: Buffer,
974
+ private readonly artifact: MaterialShaderArtifact,
975
+ private readonly deformation: 'rigid' | 'skin',
976
+ private paletteBuffer: Buffer | undefined,
977
+ private paletteBindingWindowBytes: number | undefined,
581
978
  ) {}
582
979
 
583
980
  static create(input: {
584
981
  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',
982
+ readonly pipelineState: PipelineState;
983
+ readonly artifact: MaterialShaderArtifact;
984
+ readonly visibleBuffer: Buffer;
985
+ readonly deformation?: 'rigid' | 'skin';
986
+ readonly paletteBuffer?: Buffer;
987
+ readonly paletteBindingWindowBytes?: number;
988
+ }): Result<StandardPbrRasterAdapter, RhiError> {
989
+ const deformation = input.deformation ?? 'rigid';
990
+ const pipelineLayout =
991
+ deformation === 'skin'
992
+ ? input.pipelineState.gpuDrivenPbrSkinPipelineLayout
993
+ : input.pipelineState.gpuDrivenPbrPipelineLayout;
994
+ const instancesBindGroupLayout = input.pipelineState.gpuDrivenInstancesBindGroupLayout;
995
+ if (pipelineLayout == null || instancesBindGroupLayout == null) {
996
+ return err(
997
+ new RhiError({
998
+ code: 'rhi-not-available',
999
+ expected: `${deformation === 'skin' ? 'skinned ' : ''}Standard PBR pipeline layout`,
1000
+ hint: 'initialize the producer Standard PBR pipeline before enabling GPU-driven raster',
1001
+ }),
1002
+ );
1003
+ }
1004
+ const programs = input.pipelineState.gpuDrivenPbrPrograms;
1005
+ if (
1006
+ programs === undefined ||
1007
+ resolveStandardPbrProgram(
1008
+ programs,
1009
+ input.artifact.material,
1010
+ input.artifact.receipt?.vertexInputs.some((attribute) => attribute.semantic === 'color'),
1011
+ ) === undefined
1012
+ ) {
1013
+ return err(
1014
+ new RhiError({
1015
+ code: 'rhi-not-available',
1016
+ expected: `producer-owned ${deformation === 'skin' ? 'skinned ' : ''}Standard PBR shader module for the current device generation`,
1017
+ hint: 'publish the validated Standard PBR module from buildReadyWebGPU before enabling GPU-driven raster',
1018
+ }),
1019
+ );
1020
+ }
1021
+ const meshLayout =
1022
+ deformation === 'skin'
1023
+ ? input.pipelineState.pbrSkinMeshBindGroupLayout
1024
+ : input.pipelineState.meshBindGroupLayout;
1025
+ if (meshLayout === null) {
1026
+ return err(
1027
+ new RhiError({
1028
+ code: 'rhi-not-available',
1029
+ expected: `${deformation === 'skin' ? 'skinned ' : ''}Standard PBR mesh bind-group layout`,
1030
+ hint: 'publish the exact mesh-array layout selected by the material receipt',
1031
+ }),
1032
+ );
1033
+ }
1034
+ if (
1035
+ deformation === 'skin' &&
1036
+ (input.paletteBuffer === undefined || input.paletteBindingWindowBytes === undefined)
1037
+ ) {
1038
+ return err(
1039
+ new RhiError({
1040
+ code: 'rhi-not-available',
1041
+ expected: 'persistent skin palette buffer and binding window',
1042
+ hint: 'keep the palette receipt and the GPU-driven skinned projection on one frame identity',
1043
+ }),
1044
+ );
1045
+ }
1046
+ const identity = new Float32Array(16);
1047
+ identity[0] = 1;
1048
+ identity[5] = 1;
1049
+ identity[10] = 1;
1050
+ identity[15] = 1;
1051
+ const instancePayload = packInstanceStorageBuffer(identity);
1052
+ const instanceBuffer = input.device.createBuffer({
1053
+ label: 'standard-pbr-gpu-driven-instance-data',
1054
+ size: instancePayload.byteLength,
1055
+ usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
1056
+ });
1057
+ if (!instanceBuffer.ok) return instanceBuffer;
1058
+ const instanceWrite = input.device.queue.writeBuffer(instanceBuffer.value, 0, instancePayload);
1059
+ if (!instanceWrite.ok) return instanceWrite;
1060
+ const instancesBindGroup = input.device.createBindGroup({
1061
+ label: 'standard-pbr-gpu-driven-visible-instances-bg',
1062
+ layout: instancesBindGroupLayout,
590
1063
  entries: [
591
- { binding: 0, visibility: VERTEX_STAGE, buffer: { type: 'read-only-storage' } },
1064
+ {
1065
+ binding: 0,
1066
+ resource: bufferBinding(instanceBuffer.value, 0, instancePayload.byteLength),
1067
+ },
592
1068
  {
593
1069
  binding: 1,
594
- visibility: VERTEX_STAGE,
595
- buffer: { type: 'read-only-storage' },
1070
+ resource: bufferBinding(input.visibleBuffer),
596
1071
  },
597
1072
  {
598
1073
  binding: 2,
599
- visibility: VERTEX_STAGE | FRAGMENT_STAGE,
600
- buffer: { type: 'read-only-storage' },
1074
+ resource: bufferBinding(input.visibleBuffer),
601
1075
  },
602
- { binding: 3, visibility: VERTEX_STAGE, buffer: { type: 'read-only-storage' } },
603
- { binding: 4, visibility: VERTEX_STAGE, buffer: { type: 'read-only-storage' } },
604
1076
  ],
605
1077
  });
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;
1078
+ if (!instancesBindGroup.ok) return instancesBindGroup;
617
1079
  return ok(
618
- new GpuDrivenRigidRaster(input.device, layout.value, pipelineLayout.value, module.value),
1080
+ new StandardPbrRasterAdapter(
1081
+ input.device,
1082
+ pipelineLayout,
1083
+ meshLayout,
1084
+ instancesBindGroupLayout,
1085
+ programs,
1086
+ instancesBindGroup.value,
1087
+ instanceBuffer.value,
1088
+ instancePayload.byteLength,
1089
+ input.visibleBuffer,
1090
+ input.artifact,
1091
+ deformation,
1092
+ input.paletteBuffer,
1093
+ input.paletteBindingWindowBytes,
1094
+ ),
619
1095
  );
620
1096
  }
621
1097
 
1098
+ ensurePaletteBuffer(
1099
+ paletteBuffer: Buffer | undefined,
1100
+ paletteBindingWindowBytes: number | undefined,
1101
+ ): Result<void, RhiError> {
1102
+ if (this.deformation !== 'skin') return ok(undefined);
1103
+ if (paletteBuffer === undefined || paletteBindingWindowBytes === undefined) {
1104
+ return err(
1105
+ new RhiError({
1106
+ code: 'rhi-not-available',
1107
+ expected: 'persistent skin palette buffer and binding window',
1108
+ hint: 'publish a storage-backed SkinPaletteReceipt before the GPU-driven skinned lane',
1109
+ }),
1110
+ );
1111
+ }
1112
+ this.paletteBuffer = paletteBuffer;
1113
+ this.paletteBindingWindowBytes = paletteBindingWindowBytes;
1114
+ return ok(undefined);
1115
+ }
1116
+
1117
+ ensureVisibleBuffer(visibleBuffer: Buffer): Result<void, RhiError> {
1118
+ if (this.visibleBuffer === visibleBuffer) return ok(undefined);
1119
+ const created = this.device.createBindGroup({
1120
+ label: 'standard-pbr-gpu-driven-visible-instances-bg',
1121
+ layout: this.instancesBindGroupLayout,
1122
+ entries: [
1123
+ {
1124
+ binding: 0,
1125
+ resource: bufferBinding(this.instanceBuffer, 0, this.instanceBufferSize),
1126
+ },
1127
+ { binding: 1, resource: bufferBinding(visibleBuffer) },
1128
+ { binding: 2, resource: bufferBinding(visibleBuffer) },
1129
+ ],
1130
+ });
1131
+ if (!created.ok) return created;
1132
+ this.instancesBindGroup = created.value;
1133
+ this.visibleBuffer = visibleBuffer;
1134
+ return ok(undefined);
1135
+ }
1136
+
1137
+ visibleWindowBindGroup(
1138
+ visibleBase: number,
1139
+ visibleCapacity: number,
1140
+ ): Result<BindGroup, RhiError> {
1141
+ return this.visibleWindowBindGroupForBuffer(this.visibleBuffer, visibleBase, visibleCapacity);
1142
+ }
1143
+
1144
+ visibleWindowBindGroupForBuffer(
1145
+ visibleBuffer: Buffer,
1146
+ visibleBase: number,
1147
+ visibleCapacity: number,
1148
+ ): Result<BindGroup, RhiError> {
1149
+ const offset = visibleBase * 8;
1150
+ const size = visibleCapacity * 8;
1151
+ if (offset % 256 !== 0 || size <= 0) {
1152
+ return err(
1153
+ new RhiError({
1154
+ code: 'rhi-descriptor-invalid',
1155
+ expected: '256-byte-aligned non-empty visible window',
1156
+ hint: `allocate each GPU-driven batch visible window on the portable storage-buffer boundary (base=${visibleBase}, capacity=${visibleCapacity}, offset=${offset}, size=${size})`,
1157
+ }),
1158
+ );
1159
+ }
1160
+ let groups = this.visibleWindowBindGroups.get(visibleBuffer);
1161
+ if (groups === undefined) {
1162
+ groups = new Map();
1163
+ this.visibleWindowBindGroups.set(visibleBuffer, groups);
1164
+ }
1165
+ const key = `${visibleBase}:${visibleCapacity}`;
1166
+ const cached = groups.get(key);
1167
+ if (cached !== undefined) return ok(cached);
1168
+ const created = this.device.createBindGroup({
1169
+ label: `standard-pbr-gpu-driven-visible-window-${visibleBase}`,
1170
+ layout: this.instancesBindGroupLayout,
1171
+ entries: [
1172
+ {
1173
+ binding: 0,
1174
+ resource: bufferBinding(this.instanceBuffer, 0, this.instanceBufferSize),
1175
+ },
1176
+ { binding: 1, resource: bufferBinding(visibleBuffer, offset, size) },
1177
+ { binding: 2, resource: bufferBinding(visibleBuffer, offset, size) },
1178
+ ],
1179
+ });
1180
+ if (!created.ok) return created;
1181
+ groups.set(key, created.value);
1182
+ return ok(created.value);
1183
+ }
1184
+
622
1185
  pipeline(
623
1186
  format: TextureFormat,
624
1187
  sampleCount: 1 | 4,
625
- vertexStride: number,
1188
+ layoutProjection: MeshGpuHandles['layoutProjection'],
1189
+ shaderUvSetCount: number,
626
1190
  topology: GpuDrivenBatch['key']['topology'],
627
1191
  stripIndexFormat: 'uint16' | 'uint32' | undefined,
628
1192
  renderState: MaterialRenderState | undefined,
629
1193
  ): Result<RenderPipeline, RhiError> {
630
- const key = `${format}|${sampleCount}|${vertexStride}|${topology}|${stripIndexFormat ?? ''}|${JSON.stringify(renderState ?? null)}`;
1194
+ const vertexLayout = deriveVertexBufferLayoutFromProjection(layoutProjection, {
1195
+ shaderUvSetCount,
1196
+ })[0];
1197
+ if (vertexLayout === undefined) {
1198
+ return err(
1199
+ new RhiError({
1200
+ code: 'shader-compile-failed',
1201
+ expected: 'non-empty geometry VertexLayoutProjection',
1202
+ hint: 'prepare a receipt-compatible mesh projection before Standard PBR raster',
1203
+ }),
1204
+ );
1205
+ }
1206
+ const program = resolveStandardPbrProgram(
1207
+ this.programs,
1208
+ this.artifact.material,
1209
+ layoutProjection.attributes.some((attribute) => attribute.key === 'color'),
1210
+ );
1211
+ if (program === undefined) {
1212
+ return err(
1213
+ new RhiError({
1214
+ code: 'rhi-not-available',
1215
+ expected: 'producer-owned Standard PBR program matching the geometry vertex layout',
1216
+ hint: 'publish both vertex-color variants for the current device generation',
1217
+ }),
1218
+ );
1219
+ }
1220
+ const key = `${format}|${sampleCount}|${vertexLayout.arrayStride}|${topology}|${stripIndexFormat ?? ''}|${JSON.stringify(renderState ?? null)}|${program.artifact.layoutIdentity}|${layoutProjection.digest}|uv${shaderUvSetCount}`;
631
1221
  const cached = this.pipelines.get(key);
632
1222
  if (cached !== undefined) return ok(cached);
633
- const created = this.device.createRenderPipeline({
634
- label: `gpu-driven-rigid-unlit.${key}`,
1223
+ const pipeline = this.device.createRenderPipeline({
1224
+ label: `standard-pbr-gpu-driven.${key}`,
635
1225
  layout: this.pipelineLayout,
636
1226
  vertex: {
637
- module: this.module,
638
- entryPoint: 'vs_main',
1227
+ module: program.module,
1228
+ entryPoint: program.artifact.receipt?.sceneIndexEntry ?? 'vs_scene_index',
639
1229
  buffers: [
640
1230
  {
641
- arrayStride: vertexStride,
1231
+ arrayStride: vertexLayout.arrayStride,
642
1232
  stepMode: 'vertex',
643
- attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }],
1233
+ attributes: vertexLayout.attributes.map(({ shaderLocation, offset, format }) => ({
1234
+ shaderLocation,
1235
+ offset,
1236
+ format: format as GPUVertexFormat,
1237
+ })),
644
1238
  },
645
1239
  ],
646
1240
  },
647
1241
  fragment: {
648
- module: this.module,
1242
+ module: program.module,
649
1243
  entryPoint: 'fs_main',
650
- targets: [
651
- {
652
- format,
653
- ...(renderState?.blend === undefined ? {} : { blend: renderState.blend }),
654
- },
655
- ],
1244
+ targets: [{ format }],
656
1245
  },
657
1246
  primitive: {
658
1247
  topology,
@@ -664,272 +1253,290 @@ class GpuDrivenRigidRaster {
664
1253
  format: 'depth24plus-stencil8',
665
1254
  depthWriteEnabled: renderState?.depthWriteEnabled ?? true,
666
1255
  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
1256
  },
676
- ...(sampleCount === 4
677
- ? {
678
- multisample: {
679
- count: 4,
680
- alphaToCoverageEnabled: renderState?.alphaToCoverageEnabled ?? false,
681
- },
682
- }
683
- : {}),
1257
+ ...(sampleCount === 4 ? { multisample: { count: 4 } } : {}),
684
1258
  });
685
- if (!created.ok) return created;
686
- this.pipelines.set(key, created.value);
687
- return created;
1259
+ if (!pipeline.ok) return pipeline;
1260
+ this.pipelines.set(key, pipeline.value);
1261
+ return pipeline;
688
1262
  }
689
1263
 
690
- bindGroup(
691
- scene: PersistentGpuDrivenState['scene'],
692
- view: GpuDrivenView,
693
- batch: GpuDrivenBatch,
1264
+ projectionBindGroup(
1265
+ rows: Float32Array,
1266
+ key?: string,
1267
+ capacityRows?: number,
694
1268
  ): 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
- }),
1269
+ if (key !== undefined) {
1270
+ const capacityBytes = Math.max(
1271
+ MESH_SSBO_BYTES,
1272
+ (capacityRows ?? Math.ceil(rows.byteLength / MESH_SSBO_BYTES)) * MESH_SSBO_BYTES,
703
1273
  );
1274
+ const retained = this.stagedProjectionBindings.get(key) ?? this.projectionBindings.get(key);
1275
+ if (retained !== undefined && retained.capacityBytes >= capacityBytes) {
1276
+ const write = this.writeProjectionRows(key, retained, rows);
1277
+ if (!write.ok) return write;
1278
+ return ok(retained.bindGroup);
1279
+ }
1280
+ const created = this.createProjectionBinding(rows, capacityBytes);
1281
+ if (!created.ok) return created;
1282
+ this.stagedProjectionBindings.set(key, created.value);
1283
+ return ok(created.value.bindGroup);
704
1284
  }
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
- });
1285
+ const created = this.createProjectionBinding(rows, Math.max(MESH_SSBO_BYTES, rows.byteLength));
1286
+ if (!created.ok) return created;
1287
+ return ok(created.value.bindGroup);
722
1288
  }
723
- }
724
1289
 
725
- /** The GPU-driven bridge to the existing four-group Standard PBR pipeline. */
726
- class StandardPbrRasterAdapter {
727
- private readonly pipelines = new Map<string, RenderPipeline>();
728
- private readonly projectionBuffers: Buffer[] = [];
1290
+ refreshProjectionRows(key: string, rows: Float32Array): Result<void, RhiError> {
1291
+ const retained = this.stagedProjectionBindings.get(key) ?? this.projectionBindings.get(key);
1292
+ if (retained === undefined) return ok(undefined);
1293
+ if (rows.byteLength > retained.capacityBytes) {
1294
+ const capacityBytes = Math.max(
1295
+ MESH_SSBO_BYTES,
1296
+ Math.ceil(rows.byteLength / MESH_SSBO_BYTES) * MESH_SSBO_BYTES,
1297
+ );
1298
+ const replacement = this.createProjectionBinding(rows, capacityBytes);
1299
+ if (!replacement.ok) return replacement;
1300
+ const previousStaged = this.stagedProjectionBindings.get(key);
1301
+ if (previousStaged !== undefined && previousStaged.buffer !== replacement.value.buffer) {
1302
+ this.retireProjectionBuffer(previousStaged.buffer);
1303
+ }
1304
+ this.stagedProjectionBindings.set(key, replacement.value);
1305
+ return ok(undefined);
1306
+ }
1307
+ return this.writeProjectionRows(key, retained, rows);
1308
+ }
729
1309
 
730
- private constructor(
731
- private readonly device: RhiDevice,
732
- private readonly pipelineLayout: import('@forgeax/engine-rhi').PipelineLayout,
733
- private readonly meshLayout: BindGroupLayout,
734
- private readonly module: ShaderModule,
735
- private readonly materialBindGroup: BindGroup,
736
- private readonly instancesBindGroup: BindGroup,
737
- private readonly artifact: MaterialShaderArtifact,
738
- ) {}
1310
+ /** Stage the projection keys required by the frame being prepared. */
1311
+ stageProjectionKeys(keys: ReadonlySet<string>): void {
1312
+ this.stagedProjectionKeys = new Set(keys);
1313
+ this.activeProjectionRowsAtStage.clear();
1314
+ }
739
1315
 
740
- static create(input: {
741
- readonly device: RhiDevice;
742
- readonly shaderModuleFactory: PipelineBuilderShaderModuleFactory;
743
- readonly pipelineState: PipelineState;
744
- 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) {
749
- return err(
750
- new RhiError({
751
- 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',
754
- }),
755
- );
1316
+ /** Add a shadow projection key discovered while updating a view plan. */
1317
+ noteProjectionKey(key: string): void {
1318
+ this.stagedProjectionKeys.add(key);
1319
+ }
1320
+
1321
+ /** Promote staged projection resources after the command buffer submits. */
1322
+ commitProjectionBindings(): void {
1323
+ const keep = new Set(this.stagedProjectionKeys);
1324
+ for (const [key, staged] of this.stagedProjectionBindings) {
1325
+ if (!keep.has(key)) {
1326
+ this.retireProjectionBuffer(staged.buffer);
1327
+ continue;
1328
+ }
1329
+ const previous = this.projectionBindings.get(key);
1330
+ this.projectionBindings.set(key, staged);
1331
+ if (previous !== undefined && previous.buffer !== staged.buffer) {
1332
+ this.retireProjectionBuffer(previous.buffer);
1333
+ }
756
1334
  }
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,
833
- instancesBindGroup.value,
834
- input.artifact,
835
- ),
836
- );
1335
+ this.stagedProjectionBindings.clear();
1336
+ for (const [key, active] of this.projectionBindings) {
1337
+ if (!keep.has(key)) {
1338
+ this.projectionBindings.delete(key);
1339
+ this.retireProjectionBuffer(active.buffer);
1340
+ }
1341
+ }
1342
+ this.stagedProjectionKeys.clear();
1343
+ this.activeProjectionRowsAtStage.clear();
837
1344
  }
838
1345
 
839
- pipeline(
840
- format: TextureFormat,
841
- sampleCount: 1 | 4,
842
- layoutProjection: MeshGpuHandles['layoutProjection'],
843
- topology: GpuDrivenBatch['key']['topology'],
844
- stripIndexFormat: 'uint16' | 'uint32' | undefined,
845
- renderState: MaterialRenderState | undefined,
846
- ): Result<RenderPipeline, RhiError> {
847
- const projectedLayout = deriveVertexBufferLayoutFromProjection(
848
- layoutProjection,
849
- this.artifact.uvSetCount === undefined
850
- ? 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}`;
856
- const cached = this.pipelines.get(key);
857
- if (cached !== undefined) return ok(cached);
858
- const pipeline = this.device.createRenderPipeline({
859
- label: `standard-pbr-gpu-driven.${key}`,
860
- layout: this.pipelineLayout,
861
- vertex: {
862
- module: this.module,
863
- entryPoint: this.artifact.receipt?.sceneIndexEntry ?? 'vs_scene_index',
864
- buffers: [
865
- {
866
- arrayStride: vertexStride,
867
- stepMode: 'vertex',
868
- attributes: attributes.map(({ shaderLocation, offset, format }) => ({
869
- shaderLocation,
870
- offset,
871
- format: format as GPUVertexFormat,
872
- })),
873
- },
874
- ],
875
- },
876
- fragment: {
877
- module: this.module,
878
- entryPoint: 'fs_main',
879
- targets: [{ format }],
880
- },
881
- primitive: {
882
- topology,
883
- cullMode: renderState?.cullMode ?? 'back',
884
- frontFace: renderState?.frontFace ?? 'ccw',
885
- ...(stripIndexFormat === undefined ? {} : { stripIndexFormat }),
886
- },
887
- depthStencil: {
888
- format: 'depth24plus-stencil8',
889
- depthWriteEnabled: renderState?.depthWriteEnabled ?? true,
890
- depthCompare: renderState?.depthCompare ?? 'less',
891
- },
892
- ...(sampleCount === 4 ? { multisample: { count: 4 } } : {}),
893
- });
894
- if (!pipeline.ok) return pipeline;
895
- this.pipelines.set(key, pipeline.value);
896
- return pipeline;
1346
+ /** Discard resources created for an aborted graph while retaining LKG rows. */
1347
+ abortProjectionBindings(): void {
1348
+ for (const [key, rows] of this.activeProjectionRowsAtStage) {
1349
+ const active = this.projectionBindings.get(key);
1350
+ if (active === undefined) continue;
1351
+ const restored = this.device.queue.writeBuffer(active.buffer, 0, rows);
1352
+ if (restored.ok) active.rows = new Float32Array(rows);
1353
+ }
1354
+ for (const staged of this.stagedProjectionBindings.values()) {
1355
+ this.retireProjectionBuffer(staged.buffer);
1356
+ }
1357
+ this.stagedProjectionBindings.clear();
1358
+ this.stagedProjectionKeys.clear();
1359
+ this.activeProjectionRowsAtStage.clear();
897
1360
  }
898
1361
 
899
- projectionBindGroup(rows: Float32Array): Result<BindGroup, RhiError> {
1362
+ private createProjectionBinding(
1363
+ rows: Float32Array,
1364
+ capacityBytes: number,
1365
+ ): Result<
1366
+ {
1367
+ readonly buffer: Buffer;
1368
+ readonly bindGroup: BindGroup;
1369
+ readonly capacityBytes: number;
1370
+ rows: Float32Array;
1371
+ },
1372
+ RhiError
1373
+ > {
900
1374
  const buffer = this.device.createBuffer({
901
1375
  label: 'standard-pbr-gpu-driven-scene-meshes',
902
- size: Math.max(MESH_SSBO_BYTES, rows.byteLength),
1376
+ size: capacityBytes,
903
1377
  usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
904
1378
  });
905
1379
  if (!buffer.ok) return buffer;
906
1380
  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({
1381
+ if (!write.ok) {
1382
+ this.destroyProjectionBuffer(buffer.value);
1383
+ return write;
1384
+ }
1385
+ this.projectionBuffers.add(buffer.value);
1386
+ const entries = [
1387
+ { binding: 0, resource: bufferBinding(buffer.value, 0, capacityBytes) },
1388
+ ...(this.deformation === 'skin' &&
1389
+ this.paletteBuffer !== undefined &&
1390
+ this.paletteBindingWindowBytes !== undefined
1391
+ ? [
1392
+ {
1393
+ binding: 1,
1394
+ // The scene-index second lane is a global palette row. Bind the
1395
+ // complete arena so rows beyond the legacy 255-joint window
1396
+ // remain addressable; the shader performs the row selection.
1397
+ resource: bufferBinding(this.paletteBuffer),
1398
+ },
1399
+ {
1400
+ binding: 2,
1401
+ // The dedicated GPU-driven skin layout keeps the current /
1402
+ // previous palette pair used by the direct skin layout. The
1403
+ // scene-index shader currently reads the current arena, but
1404
+ // the shared layout still requires a complete binding shape.
1405
+ resource: bufferBinding(this.paletteBuffer),
1406
+ },
1407
+ ]
1408
+ : []),
1409
+ ];
1410
+ const bindGroup = this.device.createBindGroup({
910
1411
  label: 'standard-pbr-gpu-driven-scene-mesh-bg',
911
1412
  layout: this.meshLayout,
912
- entries: [{ binding: 0, resource: bufferBinding(buffer.value, 0, rows.byteLength) }],
1413
+ entries,
1414
+ });
1415
+ if (!bindGroup.ok) {
1416
+ this.destroyProjectionBuffer(buffer.value);
1417
+ return bindGroup;
1418
+ }
1419
+ return ok({
1420
+ buffer: buffer.value,
1421
+ bindGroup: bindGroup.value,
1422
+ capacityBytes,
1423
+ rows: new Float32Array(rows),
913
1424
  });
914
1425
  }
915
1426
 
1427
+ private writeProjectionRows(
1428
+ key: string,
1429
+ binding: {
1430
+ readonly buffer: Buffer;
1431
+ readonly bindGroup: BindGroup;
1432
+ readonly capacityBytes: number;
1433
+ rows: Float32Array;
1434
+ },
1435
+ rows: Float32Array,
1436
+ ): Result<void, RhiError> {
1437
+ // Active bindings are the last-known-good graph resources. Keep one CPU
1438
+ // snapshot so an aborted candidate can restore their exact contents
1439
+ // without allocating a second GPU buffer on every stable frame.
1440
+ if (
1441
+ this.stagedProjectionBindings.get(key) === undefined &&
1442
+ !this.activeProjectionRowsAtStage.has(key)
1443
+ ) {
1444
+ this.activeProjectionRowsAtStage.set(key, new Float32Array(binding.rows));
1445
+ }
1446
+ const write = this.device.queue.writeBuffer(binding.buffer, 0, rows);
1447
+ if (!write.ok) return write;
1448
+ binding.rows = new Float32Array(rows);
1449
+ return ok(undefined);
1450
+ }
1451
+
916
1452
  dispose(): void {
917
- for (const buffer of this.projectionBuffers) this.device.destroyBuffer(buffer);
918
- this.projectionBuffers.length = 0;
1453
+ this.projectionDisposed = true;
1454
+ for (const binding of this.projectionBindings.values()) {
1455
+ this.destroyProjectionBuffer(binding.buffer);
1456
+ }
1457
+ for (const binding of this.stagedProjectionBindings.values()) {
1458
+ this.destroyProjectionBuffer(binding.buffer);
1459
+ }
1460
+ for (const buffer of this.projectionBuffers) this.destroyProjectionBuffer(buffer);
1461
+ for (const buffer of this.pendingProjectionRetirements) {
1462
+ this.destroyProjectionBuffer(buffer);
1463
+ }
1464
+ this.projectionBindings.clear();
1465
+ this.stagedProjectionBindings.clear();
1466
+ this.activeProjectionRowsAtStage.clear();
1467
+ this.projectionBuffers.clear();
1468
+ this.pendingProjectionRetirements.clear();
1469
+ this.device.destroyBuffer(this.instanceBuffer);
1470
+ }
1471
+
1472
+ private retireProjectionBuffer(buffer: Buffer): void {
1473
+ if (this.destroyedProjectionBuffers.has(buffer)) return;
1474
+ this.projectionBuffers.delete(buffer);
1475
+ if (this.projectionDisposed) {
1476
+ this.destroyProjectionBuffer(buffer);
1477
+ return;
1478
+ }
1479
+ if (!this.pendingProjectionRetirements.add(buffer)) return;
1480
+ void this.device.queue.onSubmittedWorkDone().then(
1481
+ () => {
1482
+ this.pendingProjectionRetirements.delete(buffer);
1483
+ this.destroyProjectionBuffer(buffer);
1484
+ },
1485
+ () => {
1486
+ this.pendingProjectionRetirements.delete(buffer);
1487
+ this.destroyProjectionBuffer(buffer);
1488
+ },
1489
+ );
919
1490
  }
920
1491
 
921
- get frameGroups(): readonly [BindGroup, BindGroup] {
922
- return [this.materialBindGroup, this.instancesBindGroup];
1492
+ private destroyProjectionBuffer(buffer: Buffer): void {
1493
+ if (this.destroyedProjectionBuffers.has(buffer)) return;
1494
+ this.destroyedProjectionBuffers.add(buffer);
1495
+ this.projectionBuffers.delete(buffer);
1496
+ this.device.destroyBuffer(buffer);
1497
+ }
1498
+
1499
+ get instancesFrameGroup(): BindGroup {
1500
+ return this.instancesBindGroup;
1501
+ }
1502
+
1503
+ get kind(): 'rigid' | 'skin' {
1504
+ return this.deformation;
923
1505
  }
924
1506
  }
925
1507
 
926
1508
  export interface PreparedGpuDrivenFrame {
927
1509
  readonly topologySignature: string;
928
- readonly entityKeys: ReadonlySet<number>;
929
- readonly ownsAllRenderables: boolean;
1510
+ /** True when every source draw item/instance is claimed by the GPU lane. */
1511
+ readonly ownsAllDrawItems: boolean;
1512
+ /** True only when the extracted ShadowCaster channel is fully represented. */
1513
+ readonly ownsAllShadowCasters?: boolean;
1514
+ /** Concrete main-pass draw identities claimed by the GPU projection. */
1515
+ readonly drawKeys?: ReadonlySet<string>;
1516
+ readonly standardPbrFrameResources: GpuDrivenStandardPbrFrameResources;
930
1517
  readonly occlusion?: OcclusionFrameProjection;
931
1518
  /** @internal Commits a replacement generation only after graph promotion. */
932
1519
  readonly _commitResourceReplacement: () => void;
1520
+ /** @internal Retires superseded view buffers when graph compilation promotes them. */
1521
+ readonly _commitGpuResourceReplacement?: () => void;
1522
+ /** @internal Discards staged shadow cache publication after a failed frame. */
1523
+ readonly _abortResourceReplacement?: () => void;
1524
+ /** Concrete ShadowCaster keys claimed by the GPU subset for this frame. */
1525
+ readonly shadowDrawKeys?: ReadonlySet<string>;
1526
+ /** View-scoped ShadowCaster claims consumed by each shadow record pass. */
1527
+ readonly shadowDrawKeysByView?: ReadonlyMap<string, ReadonlySet<string>>;
1528
+ readonly projectShadow?: (
1529
+ graph: RenderGraphBuilder<RenderPipelineFrame>,
1530
+ identity: ShadowViewIdentity,
1531
+ ) => Result<ShadowViewProjection | undefined, RenderGraphError>;
1532
+ readonly updateShadowViews?: (
1533
+ views: readonly Omit<ShadowViewUpdateInput, 'sourcePlan' | 'scene'>[],
1534
+ ) => Result<void, RhiError>;
1535
+ readonly shadowViewPool?: ShadowViewStatePool;
1536
+ readonly shadowBatchProjections?: ReadonlyMap<
1537
+ string,
1538
+ ReadonlyMap<number, GpuDrivenShadowBatchProjection>
1539
+ >;
933
1540
  project(
934
1541
  graph: RenderGraphBuilder<RenderPipelineFrame>,
935
1542
  format: TextureFormat,
@@ -937,6 +1544,70 @@ export interface PreparedGpuDrivenFrame {
937
1544
  ): Result<RenderPipelineGpuDrivenProjection, RenderGraphError | RhiError>;
938
1545
  }
939
1546
 
1547
+ /**
1548
+ * Renderer-owned identity token for the validation that resolves renderable
1549
+ * assets to GPU meshes. The token is deliberately narrower than a frame
1550
+ * number: unchanged persistent scene snapshots can reuse the resolved rows,
1551
+ * while a mesh/device/pipeline generation change forces a fresh producer read.
1552
+ */
1553
+ export interface GpuDrivenResidencyValidationInput {
1554
+ /** Opaque renderer-computed world/source identity and generation fence. */
1555
+ readonly cacheKey: string;
1556
+ readonly renderables: readonly RenderableSnapshot[];
1557
+ readonly transparentDispatch: readonly DispatchEntry[];
1558
+ readonly pipelineState: PipelineState;
1559
+ readonly pipelineHandle: number;
1560
+ readonly meshResidencyEpoch: number;
1561
+ readonly deviceGeneration: number;
1562
+ readonly assetCatalogEpoch: number;
1563
+ }
1564
+
1565
+ interface GpuDrivenResidencyValidationCache extends GpuDrivenResidencyValidationInput {
1566
+ readonly value: readonly ValidatedRenderable[];
1567
+ readonly dispatchRenderableIndices: readonly (number | undefined)[];
1568
+ readonly renderableIdentities: readonly RenderableSnapshot[];
1569
+ readonly dispatchIdentities: readonly DispatchEntry[];
1570
+ }
1571
+
1572
+ interface GpuDrivenCpuValidationTelemetryCache {
1573
+ readonly cacheKey: string;
1574
+ readonly validated: readonly { readonly source: RenderableSnapshot }[];
1575
+ readonly gpuOwnedDrawKeys: ReadonlySet<string>;
1576
+ readonly gpuOwnedDrawCount: number;
1577
+ readonly totalDrawItems: number;
1578
+ readonly gpuOwnedRows: number;
1579
+ readonly cpuFallbackDrawItems: number;
1580
+ readonly cpuSemanticFallbackDrawItems: number;
1581
+ readonly cpuDeformationFallbackDrawItems: number;
1582
+ readonly cpuDeformationReasons: ReadonlyMap<
1583
+ 'skin-bounds-missing' | 'skin-address-missing' | 'resource-not-ready',
1584
+ number
1585
+ >;
1586
+ readonly blockedDrawItems: number;
1587
+ }
1588
+
1589
+ function sameIdentitySequence<T extends object>(
1590
+ expected: readonly T[],
1591
+ actual: readonly T[],
1592
+ ): boolean {
1593
+ if (expected.length !== actual.length) return false;
1594
+ for (let index = 0; index < expected.length; index += 1) {
1595
+ if (expected[index] !== actual[index]) return false;
1596
+ }
1597
+ return true;
1598
+ }
1599
+
1600
+ function sameDispatchRenderableIndices(
1601
+ expected: readonly (number | undefined)[],
1602
+ actual: readonly DispatchEntry[],
1603
+ ): boolean {
1604
+ if (expected.length !== actual.length) return false;
1605
+ for (let index = 0; index < expected.length; index += 1) {
1606
+ if (expected[index] !== actual[index]?.renderableIndex) return false;
1607
+ }
1608
+ return true;
1609
+ }
1610
+
940
1611
  export interface RecoveryCapabilityFacts {
941
1612
  readonly generation: number;
942
1613
  readonly shaderCompilation: boolean;
@@ -994,17 +1665,34 @@ export function classifyRecoveryCapability(
994
1665
  export class GpuDrivenProduction {
995
1666
  private disposed = false;
996
1667
  private view: GpuDrivenView | undefined;
997
- private raster: GpuDrivenRigidRaster | undefined;
998
1668
  private standardRaster: StandardPbrRasterAdapter | undefined;
1669
+ private skinRaster: StandardPbrRasterAdapter | undefined;
999
1670
  private readonly sceneIdentities = new WeakMap<object, number>();
1000
1671
  private nextSceneIdentity = 1;
1001
1672
  private filteredCache:
1002
1673
  | {
1003
1674
  readonly source: SubmissionPlan;
1675
+ readonly scene: PersistentGpuDrivenState['scene'];
1676
+ readonly slots: PersistentGpuDrivenState['slots'];
1677
+ readonly worldKeys: PersistentGpuDrivenState['worldKeys'];
1004
1678
  readonly meshResidencyEpoch: number;
1005
1679
  readonly lodProjectionFingerprint: string;
1006
1680
  readonly activeEntityRevision?: number;
1007
1681
  readonly activeEntityKeys?: ReadonlySet<number>;
1682
+ readonly skinArtifact?: MaterialShaderArtifact;
1683
+ readonly value: FilteredProductionPlan;
1684
+ }
1685
+ | undefined;
1686
+ private shadowFilteredCache:
1687
+ | {
1688
+ readonly source: SubmissionPlan;
1689
+ readonly scene: PersistentGpuDrivenState['scene'];
1690
+ readonly slots: PersistentGpuDrivenState['slots'];
1691
+ readonly worldKeys: PersistentGpuDrivenState['worldKeys'];
1692
+ readonly lodProjectionFingerprint: string;
1693
+ readonly meshResidencyEpoch: number;
1694
+ readonly membership: string;
1695
+ readonly skinArtifact?: MaterialShaderArtifact;
1008
1696
  readonly value: FilteredProductionPlan;
1009
1697
  }
1010
1698
  | undefined;
@@ -1014,13 +1702,62 @@ export class GpuDrivenProduction {
1014
1702
  private batchBindGroupCreates = 0;
1015
1703
  private validatedGpuOwnedRows = 0;
1016
1704
  private cpuFallbackDrawItems = 0;
1705
+ private cpuSemanticFallbackDrawItems = 0;
1706
+ private cpuDeformationFallbackDrawItems = 0;
1707
+ private readonly cpuDeformationReasons = new Map<
1708
+ 'skin-bounds-missing' | 'skin-address-missing' | 'resource-not-ready',
1709
+ number
1710
+ >();
1711
+ private blockedDrawItems = 0;
1712
+ private preparationFailure: GpuDrivenPreparationFailureInspection | undefined;
1713
+ private encodedIndirectDraws = 0;
1017
1714
  private telemetryPrepared = false;
1018
1715
  private telemetryCandidateCount = 0;
1019
1716
  private telemetryGeometryWork = 0;
1020
1717
  private telemetryRootGeometryWork = 0;
1718
+ private telemetryOverflow = false;
1021
1719
  private indirectDrawCount = 0;
1022
- private sceneRowsRequired = true;
1720
+ private gpuOwnedDrawItems = 0;
1721
+ private mainTotalDrawItems = 0;
1722
+ private lane: 'gpu' | 'cpu-semantic' | 'cpu-deformation' | 'blocked' = 'cpu-semantic';
1723
+ private laneReason: GpuDrivenLaneReason = 'capability';
1724
+ private submissionState: 'idle' | 'pending' | 'submitted' | 'aborted' = 'idle';
1725
+ private retryCount = 0;
1726
+ private lastKnownGoodGeneration: number | undefined;
1727
+ private overflowRecoveryRequired = false;
1728
+ private shadowViews: ShadowViewStatePool | undefined;
1729
+ private shadowSourcePlan: SubmissionPlan | undefined;
1730
+ private shadowScene: PersistentGpuDrivenState['scene'] | undefined;
1731
+ private shadowBatchProjections = new Map<
1732
+ string,
1733
+ ReadonlyMap<number, GpuDrivenShadowBatchProjection>
1734
+ >();
1735
+ private shadowDrawKeysByView = new Map<string, ReadonlySet<string>>();
1736
+ private shadowExpectedDrawKeys = new Set<string>();
1737
+ private shadowBatchProjectionSource: SubmissionPlan | undefined;
1738
+ private skinPaletteBuffer: Buffer | undefined;
1739
+ private committedPaletteContentRevision: number | undefined;
1023
1740
  private standardPbrProjection: StandardPbrSceneProjection | undefined;
1741
+ private committedProjectionSource:
1742
+ | {
1743
+ readonly filtered: FilteredProductionPlan;
1744
+ readonly scene: PersistentGpuDrivenState['scene'];
1745
+ readonly contentRevision: number;
1746
+ readonly artifact: MaterialShaderArtifact;
1747
+ readonly skinArtifact: MaterialShaderArtifact | undefined;
1748
+ }
1749
+ | undefined;
1750
+ private validatedResidencyCache: GpuDrivenResidencyValidationCache | undefined;
1751
+ private residencyValidationCacheHits = 0;
1752
+ private residencyValidationScans = 0;
1753
+ private cpuValidationTelemetryCache: GpuDrivenCpuValidationTelemetryCache | undefined;
1754
+ private cpuValidationCacheHits = 0;
1755
+ private cpuValidationScans = 0;
1756
+ private structureMetrics: GpuDrivenStructureMetrics = {
1757
+ worldEntitiesScanned: 0,
1758
+ sceneTableUploadBytes: 0,
1759
+ paletteUploadBytes: 0,
1760
+ };
1024
1761
  private signatureCache:
1025
1762
  | {
1026
1763
  readonly filtered: FilteredProductionPlan;
@@ -1034,8 +1771,67 @@ export class GpuDrivenProduction {
1034
1771
  constructor(
1035
1772
  private readonly device: RhiDevice,
1036
1773
  private readonly shaderModuleFactory: PipelineBuilderShaderModuleFactory,
1774
+ private readonly recoveryCount = 0,
1037
1775
  ) {}
1038
1776
 
1777
+ /**
1778
+ * Reuse the last producer-resolved residency rows when all source identity
1779
+ * and generation fences still match. Renderables and dispatch entries are
1780
+ * checked element-by-element as well as by their stable object identities so
1781
+ * an accidental in-place mutation cannot reuse stale renderable indices.
1782
+ */
1783
+ reuseResidencyValidation(
1784
+ input: GpuDrivenResidencyValidationInput,
1785
+ ): readonly ValidatedRenderable[] | undefined {
1786
+ const cached = this.validatedResidencyCache;
1787
+ if (
1788
+ cached === undefined ||
1789
+ cached.cacheKey !== input.cacheKey ||
1790
+ cached.pipelineState !== input.pipelineState ||
1791
+ cached.pipelineState.device !== this.device ||
1792
+ cached.pipelineHandle !== input.pipelineHandle ||
1793
+ cached.meshResidencyEpoch !== input.meshResidencyEpoch ||
1794
+ cached.deviceGeneration !== input.deviceGeneration ||
1795
+ cached.assetCatalogEpoch !== input.assetCatalogEpoch ||
1796
+ !sameIdentitySequence(cached.renderableIdentities, input.renderables) ||
1797
+ !sameIdentitySequence(cached.dispatchIdentities, input.transparentDispatch) ||
1798
+ !sameDispatchRenderableIndices(cached.dispatchRenderableIndices, input.transparentDispatch)
1799
+ ) {
1800
+ return undefined;
1801
+ }
1802
+ this.residencyValidationCacheHits += 1;
1803
+ return cached.value;
1804
+ }
1805
+
1806
+ /** Publish the producer-owned rows after a completed residency validation. */
1807
+ rememberResidencyValidation(
1808
+ input: GpuDrivenResidencyValidationInput,
1809
+ value: readonly ValidatedRenderable[],
1810
+ ): void {
1811
+ this.validatedResidencyCache = {
1812
+ ...input,
1813
+ value,
1814
+ renderableIdentities: input.renderables,
1815
+ dispatchIdentities: input.transparentDispatch,
1816
+ dispatchRenderableIndices: input.transparentDispatch.map((entry) => entry.renderableIndex),
1817
+ };
1818
+ this.residencyValidationScans += 1;
1819
+ }
1820
+
1821
+ private applyCpuValidationTelemetry(cache: GpuDrivenCpuValidationTelemetryCache): void {
1822
+ this.gpuOwnedDrawItems = cache.gpuOwnedDrawCount;
1823
+ this.mainTotalDrawItems = cache.totalDrawItems;
1824
+ this.validatedGpuOwnedRows = cache.gpuOwnedRows;
1825
+ this.cpuFallbackDrawItems = cache.cpuFallbackDrawItems;
1826
+ this.cpuSemanticFallbackDrawItems = cache.cpuSemanticFallbackDrawItems;
1827
+ this.cpuDeformationFallbackDrawItems = cache.cpuDeformationFallbackDrawItems;
1828
+ this.cpuDeformationReasons.clear();
1829
+ for (const [reason, count] of cache.cpuDeformationReasons) {
1830
+ this.cpuDeformationReasons.set(reason, count);
1831
+ }
1832
+ this.blockedDrawItems = cache.blockedDrawItems;
1833
+ }
1834
+
1039
1835
  static forDevice(
1040
1836
  device: RhiDevice,
1041
1837
  shaderModuleFactory: PipelineBuilderShaderModuleFactory,
@@ -1043,34 +1839,6 @@ export class GpuDrivenProduction {
1043
1839
  return new GpuDrivenProduction(device, shaderModuleFactory);
1044
1840
  }
1045
1841
 
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
1842
  createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown> {
1075
1843
  return {
1076
1844
  kind: 'scene-table',
@@ -1088,10 +1856,20 @@ export class GpuDrivenProduction {
1088
1856
  prepare(input: {
1089
1857
  readonly scene: PersistentGpuDrivenState | undefined;
1090
1858
  readonly camera: CameraSnapshot;
1091
- readonly meshes: ReadonlyMap<number, MeshGpuHandles>;
1859
+ /** GPU meshes projected by retained RenderScene slot, never raw asset handle. */
1860
+ readonly meshBySlot: ReadonlyMap<number, MeshGpuHandles>;
1092
1861
  readonly viewBindGroupLayout: BindGroupLayout;
1093
1862
  readonly meshResidencyEpoch: number;
1094
1863
  readonly hdrp: boolean;
1864
+ /** Cooked producer artifact selected from the frame material catalog. */
1865
+ readonly standardPbrArtifact?: MaterialShaderArtifact;
1866
+ /** Cooked producer artifact for the GPU skinning variant. */
1867
+ readonly standardPbrSkinArtifact?: MaterialShaderArtifact;
1868
+ readonly standardPbrPipelineState?: PipelineState;
1869
+ /** Extracted ShadowCaster ownership keyed by concrete draw item and pass. */
1870
+ readonly shadowCasterDrawKeys?: ReadonlySet<string>;
1871
+ /** Structured ShadowCaster ownership facts for exact pass admission. */
1872
+ readonly shadowCasterMembership?: readonly ShadowCasterMembership[];
1095
1873
  /** Entity keys admitted by the persistent visibility facet for this view. */
1096
1874
  readonly activeEntityKeys?: ReadonlySet<number>;
1097
1875
  /** Collision-free renderer-owned revision for the active visibility set. */
@@ -1099,20 +1877,37 @@ export class GpuDrivenProduction {
1099
1877
  /** Total extracted candidates, including facet-suppressed rows. */
1100
1878
  readonly telemetryCandidateCount?: number;
1101
1879
  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> {
1880
+ /** Per-frame deltas from the existing RenderScene/GPU Scene/palette owners. */
1881
+ readonly structureMetrics?: GpuDrivenStructureMetrics;
1882
+ }): Result<PreparedGpuDrivenFrame | undefined, RhiError | GpuDrivenPreparationError> {
1106
1883
  this.gpuOwnedSnapshotsMaterialized = 0;
1107
1884
  this.filteredPlanBuilds = 0;
1108
1885
  this.batchBindGroupCreates = 0;
1109
1886
  this.validatedGpuOwnedRows = 0;
1110
1887
  this.cpuFallbackDrawItems = 0;
1888
+ this.cpuSemanticFallbackDrawItems = 0;
1889
+ this.cpuDeformationFallbackDrawItems = 0;
1890
+ this.cpuDeformationReasons.clear();
1891
+ this.blockedDrawItems = 0;
1892
+ this.preparationFailure = undefined;
1893
+ this.encodedIndirectDraws = 0;
1111
1894
  this.telemetryPrepared = false;
1112
1895
  this.telemetryCandidateCount = input.telemetryCandidateCount ?? 0;
1113
1896
  this.telemetryGeometryWork = 0;
1114
1897
  this.telemetryRootGeometryWork = 0;
1898
+ this.telemetryOverflow = false;
1115
1899
  this.indirectDrawCount = 0;
1900
+ this.gpuOwnedDrawItems = 0;
1901
+ this.mainTotalDrawItems = 0;
1902
+ this.structureMetrics = input.structureMetrics ?? {
1903
+ worldEntitiesScanned: 0,
1904
+ sceneTableUploadBytes: 0,
1905
+ paletteUploadBytes: 0,
1906
+ };
1907
+ this.lane = 'cpu-semantic';
1908
+ this.laneReason = input.hdrp ? 'unsupported' : 'capability';
1909
+ this.submissionState = 'idle';
1910
+ this.shadowExpectedDrawKeys = new Set();
1116
1911
  const capability = classifyRecoveryCapability({
1117
1912
  generation: 0,
1118
1913
  shaderCompilation: true,
@@ -1122,26 +1917,79 @@ export class GpuDrivenProduction {
1122
1917
  multisample: true,
1123
1918
  });
1124
1919
  if (
1125
- input.hdrp ||
1126
1920
  input.scene === undefined ||
1127
- capability.status === 'disabled' ||
1128
- selectGpuLodLane(this.device.caps) === 'cpu'
1921
+ input.hdrp ||
1922
+ selectGpuLodLane(this.device.caps) === 'cpu' ||
1923
+ capability.status === 'disabled'
1129
1924
  ) {
1130
1925
  this.gpuOwnedEntityCount = 0;
1131
- this.sceneRowsRequired = false;
1132
1926
  return ok(undefined);
1133
1927
  }
1928
+ if (this.overflowRecoveryRequired && this.view !== undefined) {
1929
+ const recovered = this.view._recoverFromOverflow();
1930
+ if (!recovered.ok) {
1931
+ this.gpuOwnedEntityCount = 0;
1932
+ this.lane = 'blocked';
1933
+ this.laneReason = 'capacity';
1934
+ return recovered;
1935
+ }
1936
+ this.overflowRecoveryRequired = false;
1937
+ }
1134
1938
  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;
1939
+ const preparationFailure = nonCpuPreparationFailure(scene);
1940
+ if (preparationFailure !== undefined) {
1941
+ this.gpuOwnedEntityCount = 0;
1942
+ // A capable candidate that cannot be prepared is a stopped promotion,
1943
+ // not a CPU-semantic lane. Keep the concrete rejected draw count in the
1944
+ // detached channel so an inspector can repair the producer and retry the
1945
+ // same view-pass without guessing from an error string.
1946
+ this.blockedDrawItems = preparationFailure.drawKeys.length;
1947
+ this.mainTotalDrawItems = Math.max(
1948
+ this.mainTotalDrawItems,
1949
+ scene.plan.batches.reduce((total, batch) => total + batch.candidates.length, 0),
1950
+ );
1951
+ this.lane = 'blocked';
1952
+ this.laneReason = 'resource-not-ready';
1953
+ this.preparationFailure = preparationFailureInspection(preparationFailure.error);
1954
+ return err(preparationFailure.error);
1955
+ }
1956
+ const standardArtifact = input.standardPbrArtifact;
1957
+ const skinArtifact = input.standardPbrSkinArtifact;
1958
+ const standardPipelineState = input.standardPbrPipelineState;
1959
+ if (standardArtifact === undefined || standardPipelineState === undefined) {
1960
+ this.gpuOwnedEntityCount = 0;
1961
+ this.laneReason = 'resource-not-ready';
1962
+ if (sceneRequiresGpuDrivenArtifact(input.scene)) {
1963
+ this.blockedDrawItems = input.scene.plan.batches.reduce(
1964
+ (total, batch) => total + batch.candidates.length,
1965
+ 0,
1966
+ );
1967
+ this.mainTotalDrawItems = Math.max(this.mainTotalDrawItems, this.blockedDrawItems);
1968
+ this.lane = 'blocked';
1969
+ const failure =
1970
+ standardArtifact === undefined
1971
+ ? new GpuDrivenPreparationError('missing-material-receipt', {
1972
+ reason: 'material-receipt-missing',
1973
+ owner: 'material',
1974
+ expected: 'producer-owned Standard PBR GPU-driven artifact',
1975
+ })
1976
+ : new GpuDrivenPreparationError('resource-not-ready', {
1977
+ reason: 'material-resource-missing',
1978
+ owner: 'material',
1979
+ expected: 'producer-owned Standard PBR GPU-driven pipeline state',
1980
+ });
1981
+ this.preparationFailure = preparationFailureInspection(failure);
1982
+ return err(failure);
1983
+ }
1984
+ return ok(undefined);
1985
+ }
1986
+ const projectionFingerprint =
1987
+ this.filteredCache?.source === scene.plan &&
1988
+ this.filteredCache.lodProjectionFingerprint === ''
1989
+ ? ''
1990
+ : lodProjectionFingerprint(scene.plan, input.camera, scene.slots);
1143
1991
  const cachedVisibilityMatches =
1144
- cachedPlanIsCameraInvariant || this.filteredCache === undefined
1992
+ this.filteredCache === undefined
1145
1993
  ? true
1146
1994
  : input.activeEntityRevision !== undefined &&
1147
1995
  this.filteredCache.activeEntityRevision !== undefined
@@ -1151,22 +1999,30 @@ export class GpuDrivenProduction {
1151
1999
  if (
1152
2000
  filtered === undefined ||
1153
2001
  this.filteredCache?.source !== scene.plan ||
1154
- (!cachedPlanIsTopologyInvariant &&
1155
- this.filteredCache.meshResidencyEpoch !== input.meshResidencyEpoch) ||
1156
- (!cachedPlanIsCameraInvariant &&
1157
- this.filteredCache.lodProjectionFingerprint !== projectionFingerprint) ||
1158
- !cachedVisibilityMatches
2002
+ this.filteredCache?.scene !== scene.scene ||
2003
+ this.filteredCache?.slots !== scene.slots ||
2004
+ this.filteredCache?.worldKeys !== scene.worldKeys ||
2005
+ this.filteredCache.meshResidencyEpoch !== input.meshResidencyEpoch ||
2006
+ this.filteredCache.lodProjectionFingerprint !== projectionFingerprint ||
2007
+ !cachedVisibilityMatches ||
2008
+ this.filteredCache.skinArtifact !== skinArtifact
1159
2009
  ) {
1160
2010
  filtered = filteredPlan(
1161
2011
  scene.plan,
1162
2012
  scene.slots,
1163
- input.meshes,
2013
+ input.meshBySlot,
2014
+ undefined,
2015
+ skinArtifact,
2016
+ undefined,
1164
2017
  input.camera,
1165
2018
  input.activeEntityKeys,
1166
2019
  scene.worldKeys,
1167
2020
  );
1168
2021
  this.filteredCache = {
1169
2022
  source: scene.plan,
2023
+ scene: scene.scene,
2024
+ slots: scene.slots,
2025
+ worldKeys: scene.worldKeys,
1170
2026
  meshResidencyEpoch: input.meshResidencyEpoch,
1171
2027
  lodProjectionFingerprint: projectionFingerprint,
1172
2028
  ...(input.activeEntityRevision === undefined
@@ -1175,36 +2031,110 @@ export class GpuDrivenProduction {
1175
2031
  ...(input.activeEntityKeys === undefined
1176
2032
  ? {}
1177
2033
  : { activeEntityKeys: input.activeEntityKeys }),
2034
+ ...(skinArtifact === undefined ? {} : { skinArtifact }),
1178
2035
  value: filtered,
1179
2036
  };
1180
2037
  this.gpuOwnedSnapshotsMaterialized = scene.slots.length;
1181
2038
  this.filteredPlanBuilds = 1;
1182
2039
  }
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.
2040
+ this.gpuOwnedEntityCount = gpuDrawEntityCount(filtered.drawKeys);
1189
2041
  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) {
2042
+ if (filtered.batches.length === 0) {
2043
+ this.laneReason = 'unsupported';
2044
+ return ok(undefined);
2045
+ }
2046
+ this.lane = 'gpu';
2047
+ this.laneReason = 'none';
2048
+ this.submissionState = 'pending';
2049
+ const shadowMembership = shadowCasterMembershipSignature(
2050
+ input.shadowCasterDrawKeys,
2051
+ input.shadowCasterMembership,
2052
+ );
2053
+ let shadowFiltered = this.shadowFilteredCache?.value;
2054
+ if (
2055
+ shadowFiltered === undefined ||
2056
+ this.shadowFilteredCache?.source !== scene.plan ||
2057
+ this.shadowFilteredCache.scene !== scene.scene ||
2058
+ this.shadowFilteredCache.slots !== scene.slots ||
2059
+ this.shadowFilteredCache.worldKeys !== scene.worldKeys ||
2060
+ this.shadowFilteredCache.lodProjectionFingerprint !== projectionFingerprint ||
2061
+ this.shadowFilteredCache.meshResidencyEpoch !== input.meshResidencyEpoch ||
2062
+ this.shadowFilteredCache.membership !== shadowMembership ||
2063
+ this.shadowFilteredCache.skinArtifact !== skinArtifact
2064
+ ) {
2065
+ const projectedShadowFiltered = filteredPlan(
2066
+ scene.plan,
2067
+ scene.slots,
2068
+ input.meshBySlot,
2069
+ input.shadowCasterDrawKeys,
2070
+ skinArtifact,
2071
+ input.shadowCasterMembership,
2072
+ input.camera,
2073
+ undefined,
2074
+ scene.worldKeys,
2075
+ );
2076
+ shadowFiltered = projectedShadowFiltered;
2077
+ this.shadowFilteredCache = {
2078
+ source: scene.plan,
2079
+ scene: scene.scene,
2080
+ slots: scene.slots,
2081
+ worldKeys: scene.worldKeys,
2082
+ lodProjectionFingerprint: projectionFingerprint,
2083
+ meshResidencyEpoch: input.meshResidencyEpoch,
2084
+ membership: shadowMembership,
2085
+ ...(skinArtifact === undefined ? {} : { skinArtifact }),
2086
+ value: shadowFiltered,
2087
+ };
2088
+ }
2089
+ this.shadowSourcePlan = shadowFiltered.plan;
2090
+ this.shadowScene = scene.scene;
2091
+ const shadowCasterKeys =
2092
+ input.shadowCasterMembership === undefined
2093
+ ? input.shadowCasterDrawKeys
2094
+ : new Set(
2095
+ input.shadowCasterMembership.map((membership) =>
2096
+ gpuDrivenShadowDrawKey(
2097
+ membership.worldEntity,
2098
+ membership.materialHandle,
2099
+ membership.drawItemIndex,
2100
+ membership.passIndex,
2101
+ ),
2102
+ ),
2103
+ );
2104
+ this.shadowExpectedDrawKeys = new Set(shadowCasterKeys ?? []);
2105
+ const ownsAllShadowCasters =
2106
+ shadowCasterKeys === undefined ||
2107
+ [...shadowCasterKeys].every((key) => shadowFiltered.shadowDrawKeys.has(key));
2108
+ if (this.shadowViews === undefined) {
2109
+ const created = ShadowViewStatePool.create({
2110
+ device: this.device,
2111
+ shaderModuleFactory: this.shaderModuleFactory,
2112
+ });
2113
+ if (!created.ok) return created;
2114
+ this.shadowViews = created.value;
2115
+ }
2116
+ const projectionSource = {
2117
+ filtered,
2118
+ scene: scene.scene,
2119
+ contentRevision: scene.scene.contentRevision,
2120
+ artifact: standardArtifact,
2121
+ skinArtifact,
2122
+ };
2123
+ const previousSource = this.committedProjectionSource;
2124
+ const reuseProjectionRows =
2125
+ previousSource?.filtered === filtered &&
2126
+ previousSource.scene === scene.scene &&
2127
+ previousSource.contentRevision === scene.scene.contentRevision &&
2128
+ previousSource.artifact === standardArtifact &&
2129
+ previousSource.skinArtifact === skinArtifact;
2130
+ if (!reuseProjectionRows) {
1194
2131
  const slotsByPrimitive = new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
1195
2132
  const projection = projectStandardPbrScene({
1196
- artifact: input.standardPbrArtifact,
2133
+ artifact: standardArtifact,
1197
2134
  candidates: filtered.batches.flatMap(({ batch }) =>
1198
2135
  batch.candidates.flatMap((candidate) => {
1199
2136
  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
- ];
2137
+ return slot === undefined ? [] : [sceneCandidate(slot, candidate)];
1208
2138
  }),
1209
2139
  ),
1210
2140
  });
@@ -1218,16 +2148,6 @@ export class GpuDrivenProduction {
1218
2148
  );
1219
2149
  }
1220
2150
  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
2151
  }
1232
2152
  if (this.view === undefined) {
1233
2153
  const created = GpuDrivenView.create({
@@ -1237,22 +2157,177 @@ export class GpuDrivenProduction {
1237
2157
  if (!created.ok) return created;
1238
2158
  this.view = created.value;
1239
2159
  }
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
2160
  const updated = this.view.update(filtered.plan, scene.scene, activeFrustum(input.camera));
1250
2161
  if (!updated.ok) return updated;
1251
2162
  this.telemetryPrepared = true;
1252
2163
  const view = this.view;
1253
- const raster = this.raster;
2164
+ const visibleBuffer = view.visibleBuffer;
2165
+ if (visibleBuffer === undefined) {
2166
+ return err(
2167
+ new RhiError({
2168
+ code: 'internal-error',
2169
+ expected: 'GpuDrivenView visible buffer after update',
2170
+ hint: 'keep the visible-index binding request on the updated GPU scene view',
2171
+ }),
2172
+ );
2173
+ }
2174
+ if (this.standardRaster === undefined) {
2175
+ const created = StandardPbrRasterAdapter.create({
2176
+ device: this.device,
2177
+ pipelineState: standardPipelineState,
2178
+ artifact: standardArtifact,
2179
+ visibleBuffer,
2180
+ });
2181
+ if (!created.ok) return created;
2182
+ this.standardRaster = created.value;
2183
+ } else {
2184
+ const rebound = this.standardRaster.ensureVisibleBuffer(visibleBuffer);
2185
+ if (!rebound.ok) return rebound;
2186
+ }
1254
2187
  const standardRaster = this.standardRaster;
1255
- const standardArtifact = input.standardPbrArtifact;
2188
+ if (standardRaster === undefined) {
2189
+ return err(
2190
+ new RhiError({
2191
+ code: 'internal-error',
2192
+ expected: 'Standard PBR raster adapter after prepared artifact admission',
2193
+ hint: 'keep Standard PBR artifact and PipelineState on one frame identity',
2194
+ }),
2195
+ );
2196
+ }
2197
+ const skinBatch = filtered.batches.find(
2198
+ ({ batch }) => batch.candidates[0]?.prepared?.identity.deformation === 'skin',
2199
+ );
2200
+ if (skinBatch !== undefined) {
2201
+ const skinSnapshot = scene.slots.find(
2202
+ (slot) => slot.slot === skinBatch.batch.candidates[0]?.primitiveIndex,
2203
+ )?.snapshot.skin;
2204
+ const skinAllocator = standardPipelineState.skinPaletteAllocator;
2205
+ if (
2206
+ skinArtifact === undefined ||
2207
+ skinSnapshot === undefined ||
2208
+ skinAllocator === null ||
2209
+ skinAllocator === undefined
2210
+ ) {
2211
+ return err(
2212
+ new RhiError({
2213
+ code: 'rhi-not-available',
2214
+ expected: 'skinned Standard PBR artifact and persistent palette owner',
2215
+ hint: 'keep missing skin producer resources on the CPU deformation path',
2216
+ }),
2217
+ );
2218
+ }
2219
+ if (this.skinRaster === undefined) {
2220
+ const created = StandardPbrRasterAdapter.create({
2221
+ device: this.device,
2222
+ pipelineState: standardPipelineState,
2223
+ artifact: skinArtifact,
2224
+ visibleBuffer,
2225
+ deformation: 'skin',
2226
+ paletteBuffer: skinSnapshot.buffer,
2227
+ paletteBindingWindowBytes: skinAllocator.bindingWindowBytes,
2228
+ });
2229
+ if (!created.ok) return created;
2230
+ this.skinRaster = created.value;
2231
+ } else {
2232
+ const rebound = this.skinRaster.ensureVisibleBuffer(visibleBuffer);
2233
+ if (!rebound.ok) return rebound;
2234
+ const palette = this.skinRaster.ensurePaletteBuffer(
2235
+ skinSnapshot.buffer,
2236
+ skinAllocator.bindingWindowBytes,
2237
+ );
2238
+ if (!palette.ok) return palette;
2239
+ }
2240
+ if (this.skinPaletteBuffer !== skinSnapshot.buffer) {
2241
+ this.skinPaletteBuffer = skinSnapshot.buffer;
2242
+ this.shadowBatchProjections.clear();
2243
+ this.shadowBatchProjectionSource = undefined;
2244
+ this.shadowViews?.invalidate();
2245
+ }
2246
+ }
2247
+ const skinRaster = this.skinRaster;
2248
+ const shadowViewPool = this.shadowViews;
2249
+ if (shadowViewPool === undefined) {
2250
+ return err(
2251
+ new RhiError({
2252
+ code: 'internal-error',
2253
+ expected: 'ShadowViewStatePool after GPU-driven preparation',
2254
+ hint: 'publish view-keyed shadow state with the prepared frame',
2255
+ }),
2256
+ );
2257
+ }
2258
+ // A pose can change while conservative bounds, allocation and scene rows
2259
+ // remain unchanged. Read the palette owner, never optional telemetry.
2260
+ const paletteContentRevision = standardPipelineState.skinPaletteAllocator?.contentRevision;
2261
+ if (paletteContentRevision !== this.committedPaletteContentRevision)
2262
+ shadowViewPool.invalidate();
2263
+ const abortProvisionalResources = (): void => {
2264
+ standardRaster.abortProjectionBindings();
2265
+ skinRaster?.abortProjectionBindings();
2266
+ shadowViewPool._abortResourceReplacement();
2267
+ };
2268
+ // The render graph is cached by topology, while transforms are frame
2269
+ // data. Refresh retained scene-row buffers here so a cached graph reads
2270
+ // the current world matrices without forcing a topology rebuild.
2271
+ let slotsByPrimitive: Map<number, PersistentGpuDrivenState['slots'][number]> | undefined;
2272
+ const projectionSlot = (index: number) => {
2273
+ slotsByPrimitive ??= new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
2274
+ return slotsByPrimitive.get(index);
2275
+ };
2276
+ // Start the provisional resource set before any retained row is updated;
2277
+ // an aborted graph can then restore the active rows from its snapshot.
2278
+ standardRaster.stageProjectionKeys(new Set());
2279
+ skinRaster?.stageProjectionKeys(new Set());
2280
+ for (const prepared of filtered.batches) {
2281
+ if (reuseProjectionRows) {
2282
+ const deformation = prepared.batch.candidates[0]?.prepared?.identity.deformation ?? 'rigid';
2283
+ const raster = deformation === 'skin' ? skinRaster : standardRaster;
2284
+ const artifact = deformation === 'skin' ? skinArtifact : standardArtifact;
2285
+ if (raster !== undefined && artifact !== undefined)
2286
+ raster.noteProjectionKey(gpuDrivenProjectionKey(prepared.batch, deformation, artifact));
2287
+ continue;
2288
+ }
2289
+ const admittedCandidates = prepared.batch.candidates.filter(
2290
+ (candidate) =>
2291
+ (candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
2292
+ .submitAdmission !== false,
2293
+ );
2294
+ if (admittedCandidates.length === 0) continue;
2295
+ const deformation = admittedCandidates[0]?.prepared?.identity.deformation ?? 'rigid';
2296
+ const raster = deformation === 'skin' ? skinRaster : standardRaster;
2297
+ const artifact = deformation === 'skin' ? skinArtifact : standardArtifact;
2298
+ if (raster === undefined || artifact === undefined) continue;
2299
+ const projectionKey = gpuDrivenProjectionKey(prepared.batch, deformation, artifact);
2300
+ raster.noteProjectionKey(projectionKey);
2301
+ const candidates = admittedCandidates.flatMap((candidate) => {
2302
+ const slot = projectionSlot(candidate.primitiveIndex);
2303
+ return slot === undefined ? [] : [sceneCandidate(slot, candidate)];
2304
+ });
2305
+ const projection = projectStandardPbrScene({ artifact, candidates });
2306
+ if (!projection.ok) {
2307
+ abortProvisionalResources();
2308
+ return err(
2309
+ new RhiError({
2310
+ code: 'shader-compile-failed',
2311
+ expected: 'receipt-derived Standard PBR scene projection',
2312
+ hint: 'keep the retained projection rows on the current material receipt',
2313
+ }),
2314
+ );
2315
+ }
2316
+ const refreshed = raster.refreshProjectionRows(projectionKey, projection.value.rows);
2317
+ if (!refreshed.ok) {
2318
+ abortProvisionalResources();
2319
+ return refreshed;
2320
+ }
2321
+ }
2322
+ // Bindings created by the graph projection below are provisional until
2323
+ // the frame submits. A successful commit promotes exactly these keys and
2324
+ // fence-retires any superseded batch generation; an abort keeps the last
2325
+ // known-good active set and drops only staged bindings. Shadow view update
2326
+ // adds its keyed projections to the same staged set.
2327
+ if (this.shadowBatchProjectionSource !== shadowFiltered.plan) {
2328
+ this.shadowBatchProjections.clear();
2329
+ this.shadowBatchProjectionSource = shadowFiltered.plan;
2330
+ }
1256
2331
  const sceneIdentity = this.sceneIdentity(scene.scene);
1257
2332
  const viewInspection = view.inspect();
1258
2333
  let signature = this.signatureCache?.value;
@@ -1272,13 +2347,24 @@ export class GpuDrivenProduction {
1272
2347
  batches: filtered.batches.map(({ batch, mesh }) => [
1273
2348
  batch.batchId,
1274
2349
  batch.generation,
1275
- batch.visibleCapacity,
1276
2350
  batch.visibleBase,
2351
+ batch.visibleCapacity,
1277
2352
  batch.candidates.map((candidate) => [
1278
2353
  candidate.primitiveIndex,
1279
2354
  candidate.generation,
1280
2355
  candidate.instanceOrdinal,
1281
2356
  ]),
2357
+ batch.candidates
2358
+ .filter(
2359
+ (candidate) =>
2360
+ (candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
2361
+ .submitAdmission !== false,
2362
+ )
2363
+ .map((candidate) => [
2364
+ candidate.primitiveIndex,
2365
+ candidate.generation,
2366
+ candidate.instanceOrdinal,
2367
+ ]),
1282
2368
  mesh.vboBytes,
1283
2369
  mesh.iboBytes,
1284
2370
  mesh.indexFormat,
@@ -1286,7 +2372,6 @@ export class GpuDrivenProduction {
1286
2372
  ]),
1287
2373
  capacities: [
1288
2374
  viewInspection.candidateCapacity,
1289
- viewInspection.visibleBufferCapacity,
1290
2375
  viewInspection.batchCapacity,
1291
2376
  viewInspection.indirectCapacity,
1292
2377
  ],
@@ -1299,31 +2384,96 @@ export class GpuDrivenProduction {
1299
2384
  value: signature,
1300
2385
  };
1301
2386
  }
2387
+ const shadowTopologySignature = () =>
2388
+ this.shadowViews
2389
+ ?.inspect()
2390
+ .map(
2391
+ (entry) =>
2392
+ `${shadowViewIdentityKey(entry.identity)}:${entry.cache}:${entry.generation}:${entry.sourceRevision}:${entry.resourceGeneration}`,
2393
+ )
2394
+ .join('|') ?? '';
2395
+ const production = this;
1302
2396
  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,
2397
+ get topologySignature() {
2398
+ const shadow = shadowTopologySignature();
2399
+ return shadow.length === 0 ? signature : `${signature}|shadow=${shadow}`;
2400
+ },
2401
+ ownsAllDrawItems: filtered.ownsAllDrawItems,
1309
2402
  ...(input.occlusion === undefined ? {} : { occlusion: input.occlusion }),
1310
- _commitResourceReplacement: () => view._commitResourceReplacement(),
2403
+ ownsAllShadowCasters,
2404
+ drawKeys: filtered.drawKeys,
2405
+ shadowDrawKeys: shadowFiltered.shadowDrawKeys,
2406
+ get shadowDrawKeysByView() {
2407
+ return new Map(production.shadowDrawKeysByView);
2408
+ },
2409
+ standardPbrFrameResources: {
2410
+ materialBindGroups: [],
2411
+ materialSlotIndicesByEntity: new Map(),
2412
+ instancesBindGroup: standardRaster.instancesFrameGroup,
2413
+ materialStride: 512,
2414
+ sceneMaterialBuffer: scene.scene.materialBuffer,
2415
+ },
2416
+ _commitGpuResourceReplacement: () => {
2417
+ view._commitResourceReplacement();
2418
+ },
2419
+ _commitResourceReplacement: () => {
2420
+ view._commitResourceReplacement();
2421
+ shadowViewPool._commitResourceReplacement();
2422
+ standardRaster.commitProjectionBindings();
2423
+ skinRaster?.commitProjectionBindings();
2424
+ this.committedProjectionSource = projectionSource;
2425
+ this.committedPaletteContentRevision = paletteContentRevision;
2426
+ this.submissionState = 'submitted';
2427
+ this.lastKnownGoodGeneration = view.inspect().resourceGeneration;
2428
+ },
2429
+ _abortResourceReplacement: () => {
2430
+ shadowViewPool._abortResourceReplacement();
2431
+ standardRaster.abortProjectionBindings();
2432
+ skinRaster?.abortProjectionBindings();
2433
+ this.committedProjectionSource = undefined;
2434
+ this.submissionState = 'aborted';
2435
+ this.retryCount += 1;
2436
+ },
2437
+ projectShadow: (graph, identity) => {
2438
+ if (!shadowViewPool.isActive(identity) || shadowViewPool.submission(identity) === undefined)
2439
+ return ok(undefined);
2440
+ return shadowViewPool.project(graph, identity);
2441
+ },
2442
+ updateShadowViews: (views) =>
2443
+ shadowFiltered.plan.batches.length > 0
2444
+ ? this.updateShadowViews(
2445
+ views,
2446
+ standardArtifact,
2447
+ skinArtifact,
2448
+ shadowFiltered,
2449
+ scene,
2450
+ input.shadowCasterDrawKeys,
2451
+ input.shadowCasterMembership,
2452
+ )
2453
+ : this.disableShadowViews(),
2454
+ shadowViewPool,
2455
+ get shadowBatchProjections() {
2456
+ return new Map(
2457
+ [...production.shadowBatchProjections].map(
2458
+ ([key, projections]) => [key, new Map(projections)] as const,
2459
+ ),
2460
+ );
2461
+ },
1311
2462
  project: (graph, format, sampleCount) => {
1312
2463
  this.indirectDrawCount = admittedRasterBatchCount(filtered.plan);
1313
2464
  const outputs = view.addPasses(graph);
1314
2465
  if (!outputs.ok) return outputs;
1315
- if (
1316
- usesStandardPbrRaster &&
1317
- standardRaster !== undefined &&
1318
- standardArtifact !== undefined
1319
- ) {
2466
+ if (standardRaster !== undefined && standardArtifact !== undefined) {
1320
2467
  const standardProjected: Array<{
1321
2468
  readonly batch: GpuDrivenBatch;
1322
2469
  readonly mesh: MeshGpuHandles;
2470
+ readonly materialEntityKey: number;
1323
2471
  readonly vertex: GraphBuffer;
1324
2472
  readonly index?: GraphBuffer;
1325
2473
  readonly pipeline: RenderPipeline;
1326
2474
  readonly meshBindGroup: BindGroup;
2475
+ readonly raster: StandardPbrRasterAdapter;
2476
+ readonly deformation: 'rigid' | 'skin';
1327
2477
  }> = [];
1328
2478
  const standardAccesses: GraphAccess[] = [
1329
2479
  { resource: outputs.value.primitive, usage: 'storage-read' },
@@ -1333,25 +2483,41 @@ export class GpuDrivenProduction {
1333
2483
  { resource: outputs.value.transform, usage: 'storage-read' },
1334
2484
  { resource: outputs.value.indirect, usage: 'indirect-read' },
1335
2485
  ];
1336
- const slotsByPrimitive = new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
1337
2486
  const meshBuffers = new Map<
1338
- number,
2487
+ MeshGpuHandles,
1339
2488
  { readonly vertex: GraphBuffer; readonly index?: GraphBuffer }
1340
2489
  >();
1341
2490
  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
- ];
2491
+ if (prepared.batch.visibleCapacity === 0) continue;
2492
+ const admittedCandidates = prepared.batch.candidates.filter(
2493
+ (candidate) =>
2494
+ (candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
2495
+ .submitAdmission !== false,
2496
+ );
2497
+ if (admittedCandidates.length === 0) continue;
2498
+ const deformation = admittedCandidates[0]?.prepared?.identity.deformation ?? 'rigid';
2499
+ const raster = deformation === 'skin' ? skinRaster : standardRaster;
2500
+ const artifact = deformation === 'skin' ? skinArtifact : standardArtifact;
2501
+ if (raster === undefined || artifact === undefined) {
2502
+ return err(
2503
+ new RhiError({
2504
+ code: 'rhi-not-available',
2505
+ expected: `${deformation === 'skin' ? 'skinned ' : ''}Standard PBR GPU-driven raster owner`,
2506
+ hint: 'keep an unsupported deformation on the CPU fallback lane',
2507
+ }),
2508
+ );
2509
+ }
2510
+ // The visible stream is compacted after the persistent visibility
2511
+ // facet applies its active-entity admission. Project the mesh rows
2512
+ // from that same admitted sequence so visible.x remains the
2513
+ // batch-local row identity even when a suppressed candidate sits
2514
+ // between two submitted candidates.
2515
+ const candidates = admittedCandidates.flatMap((candidate) => {
2516
+ const slot = projectionSlot(candidate.primitiveIndex);
2517
+ return slot === undefined ? [] : [sceneCandidate(slot, candidate)];
1352
2518
  });
1353
2519
  const projection = projectStandardPbrScene({
1354
- artifact: standardArtifact,
2520
+ artifact,
1355
2521
  candidates,
1356
2522
  });
1357
2523
  if (!projection.ok) {
@@ -1363,12 +2529,26 @@ export class GpuDrivenProduction {
1363
2529
  }),
1364
2530
  );
1365
2531
  }
1366
- const meshBindGroup = standardRaster.projectionBindGroup(projection.value.rows);
2532
+ const meshBindGroup = raster.projectionBindGroup(
2533
+ projection.value.rows,
2534
+ gpuDrivenProjectionKey(prepared.batch, deformation, artifact),
2535
+ prepared.batch.candidates.length,
2536
+ );
1367
2537
  if (!meshBindGroup.ok) return meshBindGroup;
1368
- let buffers = meshBuffers.get(prepared.batch.key.assetHandle);
2538
+ const materialSlot = projectionSlot(admittedCandidates[0]?.primitiveIndex ?? -1);
2539
+ if (materialSlot === undefined) {
2540
+ return err(
2541
+ new RhiError({
2542
+ code: 'internal-error',
2543
+ expected: 'batch representative entity for global material slot projection',
2544
+ hint: 'preserve the retained scene identity when projecting the GPU batch',
2545
+ }),
2546
+ );
2547
+ }
2548
+ let buffers = meshBuffers.get(prepared.mesh);
1369
2549
  if (buffers === undefined) {
1370
2550
  const vertex = graph.importBuffer(
1371
- `gpu-driven.standard.${prepared.batch.key.assetHandle}.vertex`,
2551
+ `gpu-driven.standard.${prepared.worldKey}.${prepared.batch.key.assetHandle}.vertex`,
1372
2552
  {
1373
2553
  size: prepared.mesh.vboBytes,
1374
2554
  usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
@@ -1380,7 +2560,7 @@ export class GpuDrivenProduction {
1380
2560
  if (prepared.mesh.indexBuffer !== null) {
1381
2561
  const indexHandle = prepared.mesh.indexBuffer;
1382
2562
  const imported = graph.importBuffer(
1383
- `gpu-driven.standard.${prepared.batch.key.assetHandle}.index`,
2563
+ `gpu-driven.standard.${prepared.worldKey}.${prepared.batch.key.assetHandle}.index`,
1384
2564
  {
1385
2565
  size: prepared.mesh.iboBytes,
1386
2566
  usage: GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST,
@@ -1391,16 +2571,17 @@ export class GpuDrivenProduction {
1391
2571
  index = imported.value;
1392
2572
  }
1393
2573
  buffers = { vertex: vertex.value, ...(index === undefined ? {} : { index }) };
1394
- meshBuffers.set(prepared.batch.key.assetHandle, buffers);
2574
+ meshBuffers.set(prepared.mesh, buffers);
1395
2575
  standardAccesses.push({ resource: buffers.vertex, usage: 'vertex-read' });
1396
2576
  if (buffers.index !== undefined) {
1397
2577
  standardAccesses.push({ resource: buffers.index, usage: 'index-read' });
1398
2578
  }
1399
2579
  }
1400
- const pipeline = standardRaster.pipeline(
2580
+ const pipeline = raster.pipeline(
1401
2581
  format,
1402
2582
  sampleCount,
1403
2583
  prepared.mesh.layoutProjection,
2584
+ standardPipelineState.standardPbrShaderUvSetCount,
1404
2585
  prepared.batch.key.topology,
1405
2586
  prepared.batch.key.drawKind === 'indexed' &&
1406
2587
  (prepared.batch.key.topology === 'line-strip' ||
@@ -1413,31 +2594,89 @@ export class GpuDrivenProduction {
1413
2594
  standardProjected.push({
1414
2595
  batch: prepared.batch,
1415
2596
  mesh: prepared.mesh,
2597
+ materialEntityKey: worldEntityKey(
2598
+ materialSlot.snapshot.worldId,
2599
+ materialSlot.snapshot.entityKey,
2600
+ ),
1416
2601
  ...buffers,
1417
2602
  pipeline: pipeline.value,
1418
2603
  meshBindGroup: meshBindGroup.value,
2604
+ raster,
2605
+ deformation,
1419
2606
  });
1420
2607
  }
1421
- const [materialBindGroup, instancesBindGroup] = standardRaster.frameGroups;
2608
+ if (
2609
+ standardProjected.length === 0 ||
2610
+ standardProjected.length !== filtered.batches.length
2611
+ ) {
2612
+ return err(
2613
+ new RhiError({
2614
+ code: 'internal-error',
2615
+ expected: 'one actual Standard PBR raster dispatch for each eligible batch',
2616
+ hint: 'repair the receipt-backed batch projection before encoding the frame',
2617
+ }),
2618
+ );
2619
+ }
2620
+ const rasterDispatches = Object.freeze(standardProjected);
1422
2621
  return ok({
1423
2622
  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.
2623
+ encode: (
2624
+ viewBindGroup,
2625
+ pass,
2626
+ resources: GraphResourceResolver,
2627
+ frameResources: GpuDrivenStandardPbrFrameResources,
2628
+ ) => {
2629
+ // The shared Standard view BGL carries two dynamic buffers:
2630
+ // the frame view UBO and the per-frame shadow/light table. GPU
2631
+ // indirect draws use the same zero offsets as direct draws;
2632
+ // supplying only one offset makes Dawn reject the command
2633
+ // buffer before the indirect work can reach the rasterizer.
1429
2634
  pass.setBindGroup(0, viewBindGroup, [0, 0]);
1430
- pass.setBindGroup(1, materialBindGroup, [0]);
1431
- pass.setBindGroup(3, instancesBindGroup);
1432
2635
  let currentPipeline: RenderPipeline | undefined;
1433
2636
  let currentVertex: GraphBuffer | undefined;
1434
2637
  let currentIndex: GraphBuffer | undefined;
1435
- for (const batch of standardProjected) {
2638
+ for (const batch of rasterDispatches) {
1436
2639
  if (currentPipeline !== batch.pipeline) {
1437
2640
  pass.setPipeline(batch.pipeline);
1438
2641
  currentPipeline = batch.pipeline;
1439
2642
  }
1440
- pass.setBindGroup(2, batch.meshBindGroup, [0]);
2643
+ const globalMaterialSlots = frameResources.materialSlotIndicesByEntity.get(
2644
+ batch.materialEntityKey,
2645
+ );
2646
+ const globalMaterialSlot =
2647
+ globalMaterialSlots?.[batch.batch.key.materialSlot] ??
2648
+ (frameResources.materialSlotIndicesByEntity.size === 0
2649
+ ? batch.batch.key.materialSlot
2650
+ : undefined);
2651
+ if (globalMaterialSlot === undefined) {
2652
+ throw new RhiError({
2653
+ code: 'rhi-not-available',
2654
+ expected: `frame-global material slot for entity ${batch.materialEntityKey}`,
2655
+ hint: 'populate materialSlotIndicesByEntity from the main-pass material producer',
2656
+ });
2657
+ }
2658
+ const materialBindGroup = frameResources.materialBindGroups[globalMaterialSlot];
2659
+ if (materialBindGroup === undefined) {
2660
+ throw new RhiError({
2661
+ code: 'rhi-not-available',
2662
+ expected: `material bind group for global slot ${globalMaterialSlot}`,
2663
+ hint: 'keep GPU-driven material selection on the main-pass producer contract',
2664
+ });
2665
+ }
2666
+ pass.setBindGroup(1, materialBindGroup, [
2667
+ globalMaterialSlot * frameResources.materialStride,
2668
+ ]);
2669
+ pass.setBindGroup(
2670
+ 2,
2671
+ batch.meshBindGroup,
2672
+ batch.deformation === 'skin' ? [0, 0, 0] : [0],
2673
+ );
2674
+ const visibleWindow = batch.raster.visibleWindowBindGroup(
2675
+ batch.batch.visibleBase,
2676
+ batch.batch.visibleCapacity,
2677
+ );
2678
+ if (!visibleWindow.ok) throw visibleWindow.error;
2679
+ pass.setBindGroup(3, visibleWindow.value);
1441
2680
  if (currentVertex !== batch.vertex) {
1442
2681
  const vertex = resources.buffer(batch.vertex);
1443
2682
  if (!vertex.ok) throw vertex.error;
@@ -1457,129 +2696,222 @@ export class GpuDrivenProduction {
1457
2696
  } else {
1458
2697
  pass.drawIndirect(indirect.value, batch.batch.indirectOffset);
1459
2698
  }
2699
+ this.encodedIndirectDraws += 1;
1460
2700
  }
1461
2701
  },
1462
2702
  });
1463
2703
  }
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,
2704
+ return err(
2705
+ new RhiError({
2706
+ code: 'internal-error',
2707
+ expected: 'Standard PBR raster projection branch',
2708
+ hint: 'preserve the artifact-backed Standard PBR branch as the only raster owner',
2709
+ }),
2710
+ );
2711
+ },
2712
+ });
2713
+ }
2714
+
2715
+ updateShadowViews(
2716
+ views: readonly Omit<ShadowViewUpdateInput, 'sourcePlan' | 'scene'>[],
2717
+ artifact: MaterialShaderArtifact,
2718
+ skinArtifact: MaterialShaderArtifact | undefined,
2719
+ filtered: FilteredProductionPlan,
2720
+ scene: PersistentGpuDrivenState,
2721
+ shadowCasterDrawKeys: ReadonlySet<string> | undefined,
2722
+ shadowCasterMembership: readonly ShadowCasterMembership[] | undefined,
2723
+ ): Result<void, RhiError> {
2724
+ if (
2725
+ this.shadowViews === undefined ||
2726
+ this.shadowSourcePlan === undefined ||
2727
+ this.shadowScene === undefined
2728
+ ) {
2729
+ return ok(undefined);
2730
+ }
2731
+ const nextProjections = new Map<string, ReadonlyMap<number, GpuDrivenShadowBatchProjection>>();
2732
+ const nextShadowDrawKeysByView = new Map<string, ReadonlySet<string>>();
2733
+ const standardRaster = this.standardRaster;
2734
+ if (standardRaster === undefined) {
2735
+ return err(
2736
+ new RhiError({
2737
+ code: 'internal-error',
2738
+ expected: 'Standard PBR raster adapter for shadow projection',
2739
+ hint: 'prepare the artifact-backed raster owner before updating shadow views',
2740
+ }),
2741
+ );
2742
+ }
2743
+ let slotsByPrimitive: Map<number, PersistentGpuDrivenState['slots'][number]> | undefined;
2744
+ const preparedByBatch = new Map(filtered.batches.map((entry) => [entry.batch.batchId, entry]));
2745
+ for (const input of views) {
2746
+ const updated = this.shadowViews.update({
2747
+ ...input,
2748
+ sourcePlan: this.shadowSourcePlan,
2749
+ scene: this.shadowScene,
2750
+ });
2751
+ if (!updated.ok) return updated;
2752
+ const identityKey = shadowViewIdentityKey(input.identity);
2753
+ const previousProjections = this.shadowBatchProjections.get(identityKey);
2754
+ const previousClaims = this.shadowDrawKeysByView.get(identityKey);
2755
+ if (
2756
+ updated.value.cache === 'hit' &&
2757
+ previousProjections !== undefined &&
2758
+ previousClaims !== undefined
2759
+ ) {
2760
+ nextShadowDrawKeysByView.set(identityKey, previousClaims);
2761
+ for (const batch of updated.value.plan.batches) {
2762
+ const deformation = batch.candidates[0]?.prepared?.identity.deformation ?? 'rigid';
2763
+ const batchArtifact = deformation === 'skin' ? skinArtifact : artifact;
2764
+ const raster = deformation === 'skin' ? this.skinRaster : standardRaster;
2765
+ if (batchArtifact !== undefined && raster !== undefined) {
2766
+ raster.noteProjectionKey(
2767
+ gpuDrivenShadowProjectionKey(identityKey, batch, deformation, batchArtifact),
1491
2768
  );
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
- }
1513
2769
  }
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,
2770
+ }
2771
+ nextProjections.set(identityKey, previousProjections);
2772
+ continue;
2773
+ }
2774
+ slotsByPrimitive ??= new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
2775
+ const viewClaimedKeys = new Set<string>();
2776
+ for (const batch of updated.value.plan.batches) {
2777
+ for (const candidate of batch.candidates) {
2778
+ const slot = slotsByPrimitive?.get(candidate.primitiveIndex);
2779
+ if (slot === undefined) continue;
2780
+ if (
2781
+ !shadowMembershipIsGpuCompatible(
2782
+ slot,
2783
+ candidate,
2784
+ shadowCasterDrawKeys,
2785
+ shadowCasterMembership,
2786
+ )
2787
+ ) {
2788
+ continue;
2789
+ }
2790
+ for (const key of shadowKeysForCandidate(
2791
+ slot,
2792
+ candidate,
2793
+ shadowCasterDrawKeys,
2794
+ shadowCasterMembership,
2795
+ )) {
2796
+ if (filtered.shadowDrawKeys.has(key)) viewClaimedKeys.add(key);
2797
+ }
2798
+ }
2799
+ }
2800
+ nextShadowDrawKeysByView.set(identityKey, Object.freeze(viewClaimedKeys));
2801
+ const submission = this.shadowViews.submission(input.identity);
2802
+ const visibleBuffer = submission?.view.visibleBuffer;
2803
+ if (submission === undefined || visibleBuffer === undefined) {
2804
+ return err(
2805
+ new RhiError({
2806
+ code: 'internal-error',
2807
+ expected: 'shadow view visible buffer after update',
2808
+ hint: 'publish the per-view visible window before recording indirect shadow draws',
2809
+ }),
2810
+ );
2811
+ }
2812
+ const projections = new Map<number, GpuDrivenShadowBatchProjection>();
2813
+ for (const batch of submission.plan.batches) {
2814
+ const prepared = preparedByBatch.get(batch.batchId);
2815
+ if (prepared === undefined) {
2816
+ return err(
2817
+ new RhiError({
2818
+ code: 'internal-error',
2819
+ expected: `prepared shadow mesh batch ${batch.batchId}`,
2820
+ hint: 'keep filtered shadow topology and per-view projection on one frame identity',
2821
+ }),
1525
2822
  );
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
2823
  }
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
- },
2824
+ const deformation = batch.candidates[0]?.prepared?.identity.deformation ?? 'rigid';
2825
+ const raster = deformation === 'skin' ? this.skinRaster : standardRaster;
2826
+ const batchArtifact = deformation === 'skin' ? skinArtifact : artifact;
2827
+ if (raster === undefined || batchArtifact === undefined) {
2828
+ return err(
2829
+ new RhiError({
2830
+ code: 'rhi-not-available',
2831
+ expected: `${deformation === 'skin' ? 'skinned ' : ''}Standard PBR shadow projection owner`,
2832
+ hint: 'keep a shadow batch on the CPU caster path until its material receipt is ready',
2833
+ }),
2834
+ );
2835
+ }
2836
+ const candidates = batch.candidates.flatMap((candidate) => {
2837
+ const slot = slotsByPrimitive?.get(candidate.primitiveIndex);
2838
+ if (slot === undefined) return [];
2839
+ const materialIndex = scene.scene.materialIndexForSlot(
2840
+ candidate.primitiveIndex,
2841
+ batch.key.materialSlot,
2842
+ );
2843
+ return [sceneCandidate(slot, candidate, materialIndex)];
1580
2844
  });
1581
- },
1582
- });
2845
+ const projected = projectStandardPbrScene({ artifact: batchArtifact, candidates });
2846
+ if (!projected.ok) {
2847
+ return err(
2848
+ new RhiError({
2849
+ code: 'shader-compile-failed',
2850
+ expected: 'receipt-derived shadow scene-index projection',
2851
+ hint: 'publish one validated Standard PBR receipt per shadow batch',
2852
+ }),
2853
+ );
2854
+ }
2855
+ const projectionKey = gpuDrivenShadowProjectionKey(
2856
+ identityKey,
2857
+ batch,
2858
+ deformation,
2859
+ batchArtifact,
2860
+ );
2861
+ raster.noteProjectionKey(projectionKey);
2862
+ const meshBindGroup = raster.projectionBindGroup(
2863
+ projected.value.rows,
2864
+ projectionKey,
2865
+ batch.candidates.length,
2866
+ );
2867
+ if (!meshBindGroup.ok) return meshBindGroup;
2868
+ const visibleBindGroup = raster.visibleWindowBindGroupForBuffer(
2869
+ visibleBuffer,
2870
+ batch.visibleBase,
2871
+ batch.visibleCapacity,
2872
+ );
2873
+ if (!visibleBindGroup.ok) return visibleBindGroup;
2874
+ const firstCandidate = batch.candidates[0];
2875
+ const firstSlot =
2876
+ firstCandidate === undefined
2877
+ ? undefined
2878
+ : slotsByPrimitive.get(firstCandidate.primitiveIndex);
2879
+ const material =
2880
+ firstSlot?.snapshot.materials[batch.key.materialSlot] ?? firstSlot?.snapshot.material;
2881
+ if (firstSlot === undefined || material === undefined) {
2882
+ return err(
2883
+ new RhiError({
2884
+ code: 'internal-error',
2885
+ expected: `material snapshot for shadow batch ${batch.batchId}`,
2886
+ hint: 'keep the GPU-driven shadow material projection aligned with the retained slot',
2887
+ }),
2888
+ );
2889
+ }
2890
+ projections.set(batch.batchId, {
2891
+ mesh: prepared.mesh,
2892
+ meshBindGroup: meshBindGroup.value,
2893
+ visibleBindGroup: visibleBindGroup.value,
2894
+ deformation,
2895
+ material,
2896
+ materialEntityKey: worldEntityKey(
2897
+ firstSlot.snapshot.worldId,
2898
+ firstSlot.snapshot.entityKey,
2899
+ ),
2900
+ });
2901
+ }
2902
+ nextProjections.set(identityKey, projections);
2903
+ }
2904
+ this.shadowViews.retain(views.map((input) => input.identity));
2905
+ this.shadowBatchProjections = nextProjections;
2906
+ this.shadowDrawKeysByView = nextShadowDrawKeysByView;
2907
+ return ok(undefined);
2908
+ }
2909
+
2910
+ private disableShadowViews(): Result<void, RhiError> {
2911
+ this.shadowViews?.retain([]);
2912
+ this.shadowBatchProjections = new Map();
2913
+ this.shadowDrawKeysByView = new Map();
2914
+ return ok(undefined);
1583
2915
  }
1584
2916
 
1585
2917
  dispose(): void {
@@ -1587,20 +2919,141 @@ export class GpuDrivenProduction {
1587
2919
  this.disposed = true;
1588
2920
  this.view?.dispose();
1589
2921
  this.view = undefined;
1590
- this.raster = undefined;
2922
+ this.standardRaster?.dispose();
2923
+ this.standardRaster = undefined;
2924
+ this.committedProjectionSource = undefined;
2925
+ this.skinRaster?.dispose();
2926
+ this.skinRaster = undefined;
1591
2927
  this.filteredCache = undefined;
1592
2928
  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;
2929
+ this.validatedResidencyCache = undefined;
2930
+ this.cpuValidationTelemetryCache = undefined;
2931
+ this.shadowViews?.dispose();
2932
+ this.shadowViews = undefined;
2933
+ this.shadowSourcePlan = undefined;
2934
+ this.shadowScene = undefined;
2935
+ this.shadowBatchProjections = new Map();
2936
+ this.shadowDrawKeysByView = new Map();
2937
+ this.shadowExpectedDrawKeys = new Set();
2938
+ this.shadowBatchProjectionSource = undefined;
2939
+ this.skinPaletteBuffer = undefined;
2940
+ this.committedPaletteContentRevision = undefined;
2941
+ this.preparationFailure = undefined;
2942
+ this.overflowRecoveryRequired = false;
1599
2943
  }
1600
2944
 
1601
2945
  inspect(): GpuDrivenProductionInspection {
1602
2946
  const view = this.view?.inspect();
2947
+ const channels: GpuDrivenLaneSummary[] = [];
2948
+ const mainTotalDrawCount = this.mainTotalDrawItems;
2949
+ const mainClaimedDrawCount = this.telemetryOverflow
2950
+ ? 0
2951
+ : Math.min(this.gpuOwnedDrawItems, mainTotalDrawCount);
2952
+ const mainResidualDrawCount = Math.max(0, mainTotalDrawCount - mainClaimedDrawCount);
2953
+ const mainFacts = {
2954
+ totalDrawCount: mainTotalDrawCount,
2955
+ claimedDrawCount: mainClaimedDrawCount,
2956
+ residualDrawCount: mainResidualDrawCount,
2957
+ };
2958
+ if (mainClaimedDrawCount > 0 || (this.lane !== 'gpu' && this.lane !== 'blocked')) {
2959
+ channels.push({
2960
+ viewPass: 'main',
2961
+ viewIndex: 0,
2962
+ lane: this.lane,
2963
+ reason: this.laneReason,
2964
+ drawCount: mainClaimedDrawCount,
2965
+ ...mainFacts,
2966
+ });
2967
+ }
2968
+ if (this.cpuSemanticFallbackDrawItems > 0) {
2969
+ channels.push({
2970
+ viewPass: 'main',
2971
+ viewIndex: 0,
2972
+ lane: 'cpu-semantic',
2973
+ reason: 'unsupported',
2974
+ drawCount: this.cpuSemanticFallbackDrawItems,
2975
+ ...mainFacts,
2976
+ });
2977
+ }
2978
+ if (this.cpuDeformationFallbackDrawItems > 0) {
2979
+ const reasons =
2980
+ this.cpuDeformationReasons.size === 0
2981
+ ? ([['resource-not-ready', this.cpuDeformationFallbackDrawItems]] as const)
2982
+ : this.cpuDeformationReasons.entries();
2983
+ for (const [reason, drawCount] of reasons) {
2984
+ channels.push({
2985
+ viewPass: 'main',
2986
+ viewIndex: 0,
2987
+ lane: 'cpu-deformation',
2988
+ reason,
2989
+ drawCount,
2990
+ ...mainFacts,
2991
+ });
2992
+ }
2993
+ }
2994
+ if (this.blockedDrawItems > 0) {
2995
+ channels.push({
2996
+ viewPass: 'main',
2997
+ viewIndex: 0,
2998
+ lane: 'blocked',
2999
+ reason: 'resource-not-ready',
3000
+ drawCount: this.blockedDrawItems,
3001
+ ...mainFacts,
3002
+ ...(this.preparationFailure === undefined ? {} : { failure: this.preparationFailure }),
3003
+ });
3004
+ }
3005
+ for (const entry of this.shadowViews?.inspect() ?? []) {
3006
+ // Retained view resources outlive admission. An idle frame has no
3007
+ // current GPU projection and must not publish last frame's channels.
3008
+ if (this.submissionState === 'idle' || channels.length >= 32) break;
3009
+ const key = shadowViewIdentityKey(entry.identity);
3010
+ const claimed = this.shadowDrawKeysByView.get(key)?.size ?? 0;
3011
+ const total = Math.max(this.shadowExpectedDrawKeys.size, claimed);
3012
+ const effectiveClaimed = this.telemetryOverflow ? 0 : Math.min(claimed, total);
3013
+ const residual = Math.max(0, total - effectiveClaimed);
3014
+ if (total === 0) continue;
3015
+ const projected = this.shadowBatchProjections.has(key) && entry.batchCount > 0;
3016
+ const viewPass =
3017
+ entry.identity.kind === 'directional'
3018
+ ? 'directional-shadow'
3019
+ : entry.identity.kind === 'point'
3020
+ ? 'point-shadow'
3021
+ : 'spot-shadow';
3022
+ if (projected && effectiveClaimed > 0) {
3023
+ channels.push({
3024
+ viewPass,
3025
+ viewIndex: entry.identity.index,
3026
+ ...(entry.identity.face === undefined ? {} : { face: entry.identity.face }),
3027
+ lane: this.telemetryOverflow ? 'blocked' : 'gpu',
3028
+ reason: this.telemetryOverflow ? 'overflow' : 'none',
3029
+ drawCount: effectiveClaimed,
3030
+ totalDrawCount: total,
3031
+ claimedDrawCount: effectiveClaimed,
3032
+ residualDrawCount: residual,
3033
+ });
3034
+ }
3035
+ if (residual > 0) {
3036
+ const failure = shadowOwnershipFailureInspection(viewPass);
3037
+ channels.push({
3038
+ viewPass,
3039
+ viewIndex: entry.identity.index,
3040
+ ...(entry.identity.face === undefined ? {} : { face: entry.identity.face }),
3041
+ lane: 'blocked',
3042
+ reason: this.telemetryOverflow ? 'overflow' : 'shadow-ownership',
3043
+ drawCount: residual,
3044
+ totalDrawCount: total,
3045
+ claimedDrawCount: effectiveClaimed,
3046
+ residualDrawCount: residual,
3047
+ failure,
3048
+ });
3049
+ }
3050
+ }
1603
3051
  return {
3052
+ residencyValidationScans: this.residencyValidationScans,
3053
+ residencyValidationCacheHits: this.residencyValidationCacheHits,
3054
+ cpuValidationScans: this.cpuValidationScans,
3055
+ cpuValidationCacheHits: this.cpuValidationCacheHits,
3056
+ ...this.structureMetrics,
1604
3057
  gpuOwnedSnapshotsMaterialized: this.gpuOwnedSnapshotsMaterialized,
1605
3058
  filteredPlanBuilds: this.filteredPlanBuilds,
1606
3059
  gpuOwnedEntityCount: this.gpuOwnedEntityCount,
@@ -1620,19 +3073,54 @@ export class GpuDrivenProduction {
1620
3073
  : 0,
1621
3074
  batchCount: view?.batchCount ?? 0,
1622
3075
  indirectDrawCount: this.indirectDrawCount,
3076
+ encodedIndirectDraws: this.encodedIndirectDraws,
3077
+ channels: Object.freeze(channels.map((channel) => Object.freeze(channel))),
3078
+ resourceGeneration: view?.resourceGeneration,
3079
+ candidateCapacity: view?.candidateCapacity ?? 0,
3080
+ batchCapacity: view?.batchCapacity ?? 0,
3081
+ indirectCapacity: view?.indirectCapacity ?? 0,
3082
+ overflow: this.telemetryOverflow,
3083
+ submitted: this.submissionState === 'submitted',
3084
+ recoveryCount: this.recoveryCount,
3085
+ retryCount: this.retryCount,
3086
+ lastKnownGoodGeneration: this.lastKnownGoodGeneration,
1623
3087
  };
1624
3088
  }
1625
3089
 
1626
3090
  /** Read renderer-owned LOD counters copied by the last GPU submission. */
1627
3091
  readLodSelection(): Promise<GpuDrivenLodSelectionInspection | undefined> {
1628
3092
  if (!this.telemetryPrepared) return Promise.resolve(undefined);
1629
- return (this.view?.readLodSelection() ?? Promise.resolve(undefined)).then((selection) => {
3093
+ const view = this.view;
3094
+ const filtered = this.filteredCache?.value;
3095
+ const telemetryCandidateCount = this.telemetryCandidateCount;
3096
+ if (view === undefined) return Promise.resolve(undefined);
3097
+ return view.readLodSelection().then((selection) => {
3098
+ // The observer is asynchronous and prepare() may have published a new
3099
+ // filtered plan while mapAsync was pending. Do not attach old counters to
3100
+ // the new world/primitive cache; the next real copy will be observed on
3101
+ // its own generation.
3102
+ if (
3103
+ !this.telemetryPrepared ||
3104
+ this.view !== view ||
3105
+ this.filteredCache?.value !== filtered ||
3106
+ this.telemetryCandidateCount !== telemetryCandidateCount
3107
+ ) {
3108
+ return undefined;
3109
+ }
1630
3110
  if (selection !== undefined) {
1631
3111
  this.telemetryGeometryWork = selection.geometryWork;
1632
3112
  this.telemetryRootGeometryWork = selection.rootGeometryWork;
3113
+ this.telemetryOverflow = selection.overflow;
3114
+ if (selection.overflow) {
3115
+ if (!this.overflowRecoveryRequired) this.retryCount += 1;
3116
+ this.overflowRecoveryRequired = true;
3117
+ this.submissionState = 'aborted';
3118
+ this.lastKnownGoodGeneration = undefined;
3119
+ this.lane = 'blocked';
3120
+ this.laneReason = 'overflow';
3121
+ }
1633
3122
  }
1634
- if (selection === undefined || this.telemetryCandidateCount <= 0) return selection;
1635
- const filtered = this.filteredCache?.value;
3123
+ if (selection === undefined || telemetryCandidateCount <= 0) return selection;
1636
3124
  if (filtered === undefined) return selection;
1637
3125
  const preparedByBatchId = new Map(
1638
3126
  filtered.batches.map((prepared) => [prepared.batch.batchId, prepared] as const),
@@ -1677,8 +3165,8 @@ export class GpuDrivenProduction {
1677
3165
  }
1678
3166
  return {
1679
3167
  ...selection,
1680
- candidateCount: this.telemetryCandidateCount,
1681
- occluded: Math.max(0, this.telemetryCandidateCount - selection.visible),
3168
+ candidateCount: telemetryCandidateCount,
3169
+ occluded: Math.max(0, telemetryCandidateCount - selection.visible),
1682
3170
  worldSelections: Object.freeze(
1683
3171
  [...worldSelections.values()].map((aggregate) =>
1684
3172
  Object.freeze({
@@ -1702,24 +3190,109 @@ export class GpuDrivenProduction {
1702
3190
 
1703
3191
  recordCpuValidation(
1704
3192
  validated: readonly { readonly source: RenderableSnapshot }[],
1705
- gpuOwned: ReadonlySet<number>,
1706
- worldKeys?: readonly number[],
3193
+ gpuOwnedDrawKeys: ReadonlySet<string>,
3194
+ cacheKey?: string,
1707
3195
  ): void {
1708
- this.cpuFallbackDrawItems = validated.length;
3196
+ const cached = this.cpuValidationTelemetryCache;
3197
+ if (
3198
+ cacheKey !== undefined &&
3199
+ cached !== undefined &&
3200
+ cached.cacheKey === cacheKey &&
3201
+ cached.validated === validated &&
3202
+ cached.gpuOwnedDrawKeys === gpuOwnedDrawKeys
3203
+ ) {
3204
+ this.applyCpuValidationTelemetry(cached);
3205
+ this.cpuValidationCacheHits += 1;
3206
+ return;
3207
+ }
3208
+ this.cpuFallbackDrawItems = 0;
3209
+ this.cpuSemanticFallbackDrawItems = 0;
3210
+ this.cpuDeformationFallbackDrawItems = 0;
3211
+ this.cpuDeformationReasons.clear();
3212
+ this.blockedDrawItems = 0;
1709
3213
  let gpuOwnedRows = 0;
3214
+ let totalDrawItems = 0;
3215
+ const addDeformationReason = (
3216
+ reason: 'skin-bounds-missing' | 'skin-address-missing' | 'resource-not-ready',
3217
+ ): void => {
3218
+ this.cpuDeformationReasons.set(reason, (this.cpuDeformationReasons.get(reason) ?? 0) + 1);
3219
+ };
1710
3220
  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;
3221
+ const draws = row.source.gpuDrivenDraws ?? [];
3222
+ if (draws.length === 0) {
3223
+ totalDrawItems += 1;
3224
+ this.cpuFallbackDrawItems += 1;
3225
+ const material = row.source.material;
3226
+ const skinReason = skinLaneReason(row.source, material);
3227
+ if (skinReason !== undefined) {
3228
+ this.cpuDeformationFallbackDrawItems += 1;
3229
+ addDeformationReason(skinReason);
3230
+ } else {
3231
+ this.cpuSemanticFallbackDrawItems += 1;
3232
+ }
3233
+ continue;
1720
3234
  }
3235
+ totalDrawItems += draws.length;
3236
+ let gpuOwned = false;
3237
+ let cpuResidual = false;
3238
+ for (const [compactIndex, draw] of draws.entries()) {
3239
+ const material = row.source.materials[draw.materialSlot] ?? row.source.material;
3240
+ const key = gpuDrivenDrawKey(
3241
+ worldEntityKey(row.source.worldId, row.source.entityKey),
3242
+ material.materialHandle ?? -1,
3243
+ gpuDrivenSourceDrawItemIndex(draw, compactIndex),
3244
+ );
3245
+ if (gpuOwnedDrawKeys.has(key)) {
3246
+ gpuOwned = true;
3247
+ continue;
3248
+ }
3249
+ this.cpuFallbackDrawItems += 1;
3250
+ cpuResidual = true;
3251
+ const skinReason = skinLaneReason(row.source, material);
3252
+ if (skinReason !== undefined) {
3253
+ this.cpuDeformationFallbackDrawItems += 1;
3254
+ addDeformationReason(skinReason);
3255
+ } else if (draw.preparationError?.detail.recovery === 'route-cpu-lane') {
3256
+ // Geometry/UV incompatibility is an explicit semantic CPU lane,
3257
+ // not a failed capable submission.
3258
+ this.cpuSemanticFallbackDrawItems += 1;
3259
+ } else if (draw.preparationError?.detail.owner === 'skin') {
3260
+ this.cpuDeformationFallbackDrawItems += 1;
3261
+ addDeformationReason(
3262
+ draw.preparationError.detail.reason === 'skin-address-missing'
3263
+ ? 'skin-address-missing'
3264
+ : 'resource-not-ready',
3265
+ );
3266
+ } else if (draw.preparationError !== undefined) {
3267
+ this.blockedDrawItems += 1;
3268
+ } else {
3269
+ this.cpuSemanticFallbackDrawItems += 1;
3270
+ }
3271
+ }
3272
+ // A row is only CPU-validated when it still has a CPU residual. GPU-only
3273
+ // rows are already admitted by the concrete draw-key set and must not
3274
+ // inflate the per-frame CPU validation budget.
3275
+ if (gpuOwned && cpuResidual) gpuOwnedRows += 1;
1721
3276
  }
3277
+ this.gpuOwnedDrawItems = gpuOwnedDrawKeys.size;
3278
+ this.mainTotalDrawItems = totalDrawItems;
1722
3279
  this.validatedGpuOwnedRows = gpuOwnedRows;
3280
+ this.cpuValidationScans += 1;
3281
+ if (cacheKey !== undefined) {
3282
+ this.cpuValidationTelemetryCache = {
3283
+ cacheKey,
3284
+ validated,
3285
+ gpuOwnedDrawKeys,
3286
+ gpuOwnedDrawCount: this.gpuOwnedDrawItems,
3287
+ totalDrawItems: this.mainTotalDrawItems,
3288
+ gpuOwnedRows: this.validatedGpuOwnedRows,
3289
+ cpuFallbackDrawItems: this.cpuFallbackDrawItems,
3290
+ cpuSemanticFallbackDrawItems: this.cpuSemanticFallbackDrawItems,
3291
+ cpuDeformationFallbackDrawItems: this.cpuDeformationFallbackDrawItems,
3292
+ cpuDeformationReasons: new Map(this.cpuDeformationReasons),
3293
+ blockedDrawItems: this.blockedDrawItems,
3294
+ };
3295
+ }
1723
3296
  }
1724
3297
 
1725
3298
  private sceneIdentity(scene: object): number {