@forgeax/engine-render 0.1.33 → 0.1.35

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 (624) hide show
  1. package/README.md +1062 -127
  2. package/dist/assembly/backend-contract.d.ts +2 -0
  3. package/dist/assembly/backend-contract.d.ts.map +1 -1
  4. package/dist/assembly/barrel-distortion-inspection.d.ts +3 -0
  5. package/dist/assembly/barrel-distortion-inspection.d.ts.map +1 -0
  6. package/dist/assembly/canvas-draw-guard.d.ts +3 -0
  7. package/dist/assembly/canvas-draw-guard.d.ts.map +1 -0
  8. package/dist/assembly/cloud-shadow-entry.d.ts +4 -0
  9. package/dist/assembly/cloud-shadow-entry.d.ts.map +1 -0
  10. package/dist/assembly/device-feature-admission.d.ts +2 -2
  11. package/dist/assembly/dynamic-geometry-host.d.ts +4 -1
  12. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  13. package/dist/assembly/dynamic-geometry-runtime.d.ts +1 -1
  14. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -1
  15. package/dist/assembly/factory.d.ts +1 -1
  16. package/dist/assembly/factory.d.ts.map +1 -1
  17. package/dist/assembly/host-contract.d.ts +6 -5
  18. package/dist/assembly/host-contract.d.ts.map +1 -1
  19. package/dist/assembly/material/assembly.d.ts.map +1 -1
  20. package/dist/assembly/material/pipeline-helpers.d.ts +5 -0
  21. package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -1
  22. package/dist/assembly/material-shader-policy.d.ts +16 -2
  23. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  24. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -1
  25. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
  26. package/dist/assembly/recovery/renderer-recover.d.ts +2 -0
  27. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  28. package/dist/assembly/render-feature-post-processes.d.ts +6 -0
  29. package/dist/assembly/render-feature-post-processes.d.ts.map +1 -0
  30. package/dist/assembly/render-target-host.d.ts +5 -0
  31. package/dist/assembly/render-target-host.d.ts.map +1 -1
  32. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  33. package/dist/assembly/renderer-feature-inspection.d.ts +1 -1
  34. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -1
  35. package/dist/assembly/renderer-frame-transaction.d.ts +12 -0
  36. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  37. package/dist/assembly/renderer-inspection.d.ts +11 -0
  38. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  39. package/dist/assembly/webgpu-pbr-ready.d.ts +1 -0
  40. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -1
  41. package/dist/assembly/webgpu-ready-per-pass.d.ts +39 -0
  42. package/dist/assembly/webgpu-ready-per-pass.d.ts.map +1 -0
  43. package/dist/assembly/webgpu-ready.d.ts +1 -1
  44. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  45. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -1
  46. package/dist/assembly/webgpu-renderer-observation-owner.d.ts +35 -0
  47. package/dist/assembly/webgpu-renderer-observation-owner.d.ts.map +1 -0
  48. package/dist/assembly/webgpu-renderer.d.ts +2 -1
  49. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  50. package/dist/authoring.mjs +2 -4
  51. package/dist/authoring.mjs.map +1 -1
  52. package/dist/barrel-distortion.d.ts +47 -0
  53. package/dist/barrel-distortion.d.ts.map +1 -0
  54. package/dist/chunk-3XUBWRY2.mjs +1842 -0
  55. package/dist/chunk-3XUBWRY2.mjs.map +1 -0
  56. package/dist/{chunk-OO3DRYK7.mjs → chunk-G6HDNGY7.mjs} +2400 -1767
  57. package/dist/chunk-G6HDNGY7.mjs.map +1 -0
  58. package/dist/{chunk-ZDQWGQW7.mjs → chunk-LM5IE7GF.mjs} +4 -4
  59. package/dist/{chunk-ZDQWGQW7.mjs.map → chunk-LM5IE7GF.mjs.map} +1 -1
  60. package/dist/{chunk-VOZWDM3L.mjs → chunk-LNIUIKQ7.mjs} +1514 -498
  61. package/dist/chunk-LNIUIKQ7.mjs.map +1 -0
  62. package/dist/chunk-MXTWKZO4.mjs +3268 -0
  63. package/dist/chunk-MXTWKZO4.mjs.map +1 -0
  64. package/dist/chunk-MZ4C7EHF.mjs +6942 -0
  65. package/dist/chunk-MZ4C7EHF.mjs.map +1 -0
  66. package/dist/chunk-RIPAEGI6.mjs +48 -0
  67. package/dist/chunk-RIPAEGI6.mjs.map +1 -0
  68. package/dist/{chunk-LEAXOMYJ.mjs → chunk-SGPUJW3F.mjs} +117 -5
  69. package/dist/chunk-SGPUJW3F.mjs.map +1 -0
  70. package/dist/{chunk-PA5DRJNI.mjs → chunk-X2KA6WHM.mjs} +29 -4
  71. package/dist/chunk-X2KA6WHM.mjs.map +1 -0
  72. package/dist/{chunk-BLXGONR2.mjs → chunk-XSFB7XXQ.mjs} +51 -4
  73. package/dist/chunk-XSFB7XXQ.mjs.map +1 -0
  74. package/dist/cloud/density.d.ts +55 -0
  75. package/dist/cloud/density.d.ts.map +1 -0
  76. package/dist/cloud/extract.d.ts +25 -0
  77. package/dist/cloud/extract.d.ts.map +1 -0
  78. package/dist/cloud/feature.d.ts +57 -0
  79. package/dist/cloud/feature.d.ts.map +1 -0
  80. package/dist/cloud/frame-context.d.ts +8 -0
  81. package/dist/cloud/frame-context.d.ts.map +1 -0
  82. package/dist/cloud/inspection.d.ts +59 -0
  83. package/dist/cloud/inspection.d.ts.map +1 -0
  84. package/dist/cloud/optics.d.ts +56 -0
  85. package/dist/cloud/optics.d.ts.map +1 -0
  86. package/dist/cloud/parameters.d.ts +73 -0
  87. package/dist/cloud/parameters.d.ts.map +1 -0
  88. package/dist/cloud/resources.d.ts +60 -0
  89. package/dist/cloud/resources.d.ts.map +1 -0
  90. package/dist/cloud/shadow.d.ts +42 -0
  91. package/dist/cloud/shadow.d.ts.map +1 -0
  92. package/dist/cloud/temporal.d.ts +74 -0
  93. package/dist/cloud/temporal.d.ts.map +1 -0
  94. package/dist/components/barrel-distortion.d.ts +13 -0
  95. package/dist/components/barrel-distortion.d.ts.map +1 -0
  96. package/dist/components/cloud-layer.d.ts +29 -0
  97. package/dist/components/cloud-layer.d.ts.map +1 -0
  98. package/dist/components/index.d.ts +2 -0
  99. package/dist/components/index.d.ts.map +1 -1
  100. package/dist/components/instances.d.ts +20 -6
  101. package/dist/components/instances.d.ts.map +1 -1
  102. package/dist/construct-renderer.d.ts +1 -1
  103. package/dist/construct-renderer.d.ts.map +1 -1
  104. package/dist/construct-renderer.mjs +8406 -5785
  105. package/dist/construct-renderer.mjs.map +1 -1
  106. package/dist/device/gpu-residency.d.ts +19 -19
  107. package/dist/device/gpu-residency.d.ts.map +1 -1
  108. package/dist/dynamic-geometry.d.ts +5 -1
  109. package/dist/dynamic-geometry.d.ts.map +1 -1
  110. package/dist/errors/cloud.d.ts +59 -0
  111. package/dist/errors/cloud.d.ts.map +1 -0
  112. package/dist/errors/render.d.ts +20 -5
  113. package/dist/errors/render.d.ts.map +1 -1
  114. package/dist/examples/single-layer-medium-surface.d.ts +47 -0
  115. package/dist/examples/single-layer-medium-surface.d.ts.map +1 -0
  116. package/dist/extract/camera.d.ts.map +1 -1
  117. package/dist/extract/contracts.d.ts +5 -1
  118. package/dist/extract/contracts.d.ts.map +1 -1
  119. package/dist/extract/material-context.d.ts +7 -0
  120. package/dist/extract/material-context.d.ts.map +1 -0
  121. package/dist/features/barrel-distortion.d.ts +29 -0
  122. package/dist/features/barrel-distortion.d.ts.map +1 -0
  123. package/dist/features/error-reporting.d.ts +6 -2
  124. package/dist/features/error-reporting.d.ts.map +1 -1
  125. package/dist/features/host.d.ts +15 -0
  126. package/dist/features/host.d.ts.map +1 -1
  127. package/dist/features/plan.d.ts +10 -0
  128. package/dist/features/plan.d.ts.map +1 -1
  129. package/dist/features/render-graph-contribution.d.ts +5 -0
  130. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  131. package/dist/features/render-graph-raster.d.ts +1 -0
  132. package/dist/features/render-graph-raster.d.ts.map +1 -1
  133. package/dist/features/targets.d.ts +2 -0
  134. package/dist/features/targets.d.ts.map +1 -1
  135. package/dist/features/types.d.ts +48 -0
  136. package/dist/features/types.d.ts.map +1 -1
  137. package/dist/fullscreen-post-process-pass.d.ts +15 -7
  138. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  139. package/dist/glyph-text-layout-system.d.ts +1 -1
  140. package/dist/glyph-text-layout-system.d.ts.map +1 -1
  141. package/dist/gpu-dirty-ranges.d.ts +11 -0
  142. package/dist/gpu-dirty-ranges.d.ts.map +1 -0
  143. package/dist/gpu-driven/batch-topology.d.ts +17 -0
  144. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  145. package/dist/gpu-driven/material-artifacts.d.ts +6 -0
  146. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
  147. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  148. package/dist/gpu-driven/production-raster-material.d.ts +178 -0
  149. package/dist/gpu-driven/production-raster-material.d.ts.map +1 -0
  150. package/dist/gpu-driven/production-raster.d.ts +47 -3
  151. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  152. package/dist/gpu-driven/resource-allocation.d.ts +31 -0
  153. package/dist/gpu-driven/resource-allocation.d.ts.map +1 -0
  154. package/dist/gpu-driven/shadow-ownership.d.ts +14 -0
  155. package/dist/gpu-driven/shadow-ownership.d.ts.map +1 -0
  156. package/dist/gpu-driven/view-gpu.d.ts +26 -2
  157. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  158. package/dist/gpu-scene.d.ts +5 -0
  159. package/dist/gpu-scene.d.ts.map +1 -1
  160. package/dist/hdrp-buffers.d.ts +2 -2
  161. package/dist/hdrp-buffers.d.ts.map +1 -1
  162. package/dist/ibl/frame-resources.d.ts.map +1 -1
  163. package/dist/ibl/skylight-bind-group.d.ts +14 -5
  164. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  165. package/dist/index.d.ts +30 -7
  166. package/dist/index.d.ts.map +1 -1
  167. package/dist/index.mjs +780 -9
  168. package/dist/index.mjs.map +1 -1
  169. package/dist/inspection-types.d.ts +135 -1
  170. package/dist/inspection-types.d.ts.map +1 -1
  171. package/dist/instance-buffer-cache.d.ts +4 -4
  172. package/dist/instance-buffer-cache.d.ts.map +1 -1
  173. package/dist/instances.d.ts +47 -100
  174. package/dist/instances.d.ts.map +1 -1
  175. package/dist/internal.mjs +7 -8
  176. package/dist/internal.mjs.map +1 -1
  177. package/dist/pbr-pipeline.d.ts +22 -4
  178. package/dist/pbr-pipeline.d.ts.map +1 -1
  179. package/dist/pipeline/single-layer-medium-passes.d.ts +73 -0
  180. package/dist/pipeline/single-layer-medium-passes.d.ts.map +1 -0
  181. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  182. package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
  183. package/dist/pipeline/standard-output/lut-gpu.d.ts +2 -2
  184. package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -1
  185. package/dist/pipeline/standard-output/resources.d.ts +2 -2
  186. package/dist/pipeline/standard-output/resources.d.ts.map +1 -1
  187. package/dist/pipeline/standard-output/types.d.ts +4 -2
  188. package/dist/pipeline/standard-output/types.d.ts.map +1 -1
  189. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  190. package/dist/pipeline/standard-post.d.ts.map +1 -1
  191. package/dist/pipeline-builder.d.ts +1 -1
  192. package/dist/pipeline-builder.d.ts.map +1 -1
  193. package/dist/plugin.d.ts.map +1 -1
  194. package/dist/prepare/prepared-graphics-resolver.d.ts +2 -0
  195. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  196. package/dist/publication/contract.d.ts +77 -0
  197. package/dist/publication/contract.d.ts.map +1 -0
  198. package/dist/publication/dependencies.d.ts +7 -0
  199. package/dist/publication/dependencies.d.ts.map +1 -0
  200. package/dist/publication/prepare-geometry.d.ts +9 -0
  201. package/dist/publication/prepare-geometry.d.ts.map +1 -0
  202. package/dist/publication/programs.d.ts +5 -0
  203. package/dist/publication/programs.d.ts.map +1 -0
  204. package/dist/publication/publisher.d.ts +33 -0
  205. package/dist/publication/publisher.d.ts.map +1 -0
  206. package/dist/publication/receiver.d.ts +29 -0
  207. package/dist/publication/receiver.d.ts.map +1 -0
  208. package/dist/publication/resource-scope.d.ts +18 -0
  209. package/dist/publication/resource-scope.d.ts.map +1 -0
  210. package/dist/publication/targets.d.ts +33 -0
  211. package/dist/publication/targets.d.ts.map +1 -0
  212. package/dist/publication/video.d.ts +5 -0
  213. package/dist/publication/video.d.ts.map +1 -0
  214. package/dist/record/barrel-distortion-frame.d.ts +15 -0
  215. package/dist/record/barrel-distortion-frame.d.ts.map +1 -0
  216. package/dist/record/dynamic-geometry-consumption.d.ts +10 -2
  217. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -1
  218. package/dist/record/fold-buckets.d.ts +95 -0
  219. package/dist/record/fold-buckets.d.ts.map +1 -0
  220. package/dist/record/fold-instance-buffer.d.ts +6 -0
  221. package/dist/record/fold-instance-buffer.d.ts.map +1 -0
  222. package/dist/record/frame-lighting.d.ts +1 -0
  223. package/dist/record/frame-lighting.d.ts.map +1 -1
  224. package/dist/record/frame-snapshot.d.ts +43 -9
  225. package/dist/record/frame-snapshot.d.ts.map +1 -1
  226. package/dist/record/frame.d.ts +15 -65
  227. package/dist/record/frame.d.ts.map +1 -1
  228. package/dist/record/gpu-draw-receipts.d.ts +11 -0
  229. package/dist/record/gpu-draw-receipts.d.ts.map +1 -0
  230. package/dist/record/gpu-timing.d.ts +8 -0
  231. package/dist/record/gpu-timing.d.ts.map +1 -1
  232. package/dist/record/helpers.d.ts +2 -2
  233. package/dist/record/helpers.d.ts.map +1 -1
  234. package/dist/record/main-pass-geometry.d.ts +22 -6
  235. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  236. package/dist/record/main-pass-material.d.ts +14 -10
  237. package/dist/record/main-pass-material.d.ts.map +1 -1
  238. package/dist/record/main-pass-sprite-draws.d.ts +3 -16
  239. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  240. package/dist/record/main-pass.d.ts +13 -2
  241. package/dist/record/main-pass.d.ts.map +1 -1
  242. package/dist/record/motion-blur-receipt.d.ts +11 -0
  243. package/dist/record/motion-blur-receipt.d.ts.map +1 -0
  244. package/dist/record/prepared-material-bindings.d.ts +3 -3
  245. package/dist/record/prepared-material-bindings.d.ts.map +1 -1
  246. package/dist/record/probe-blend-buffer.d.ts +16 -0
  247. package/dist/record/probe-blend-buffer.d.ts.map +1 -0
  248. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  249. package/dist/record/render-context.d.ts +24 -5
  250. package/dist/record/render-context.d.ts.map +1 -1
  251. package/dist/record/shadow-pass.d.ts +2 -0
  252. package/dist/record/shadow-pass.d.ts.map +1 -1
  253. package/dist/record/skybox-post-pass.d.ts +1 -1
  254. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  255. package/dist/record/sprite-instance-buffer.d.ts +31 -0
  256. package/dist/record/sprite-instance-buffer.d.ts.map +1 -0
  257. package/dist/record/target-capture-graph.d.ts +19 -0
  258. package/dist/record/target-capture-graph.d.ts.map +1 -0
  259. package/dist/record/typed-frame-graph.d.ts +24 -15
  260. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  261. package/dist/record/view-ubo.d.ts +11 -3
  262. package/dist/record/view-ubo.d.ts.map +1 -1
  263. package/dist/record/volume-params.d.ts +9 -0
  264. package/dist/record/volume-params.d.ts.map +1 -0
  265. package/dist/recovery/render-system-candidate.d.ts +14 -4
  266. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  267. package/dist/reflection/record-owner.d.ts.map +1 -1
  268. package/dist/render-contract.d.ts +46 -5
  269. package/dist/render-contract.d.ts.map +1 -1
  270. package/dist/render-graph-primitives.d.ts.map +1 -1
  271. package/dist/render-pipeline.d.ts +103 -2
  272. package/dist/render-pipeline.d.ts.map +1 -1
  273. package/dist/render-system-extract-tail.d.ts +3 -2
  274. package/dist/render-system-extract-tail.d.ts.map +1 -1
  275. package/dist/render-system-extract.d.ts +18 -9
  276. package/dist/render-system-extract.d.ts.map +1 -1
  277. package/dist/render-system-presentation.d.ts +23 -0
  278. package/dist/render-system-presentation.d.ts.map +1 -0
  279. package/dist/render-system-producer-roots.d.ts +10 -0
  280. package/dist/render-system-producer-roots.d.ts.map +1 -0
  281. package/dist/render-system-projections.d.ts +3 -2
  282. package/dist/render-system-projections.d.ts.map +1 -1
  283. package/dist/render-system-residency.d.ts +12 -0
  284. package/dist/render-system-residency.d.ts.map +1 -0
  285. package/dist/render-system.d.ts +22 -21
  286. package/dist/render-system.d.ts.map +1 -1
  287. package/dist/scene/probe-blend.d.ts +27 -0
  288. package/dist/scene/probe-blend.d.ts.map +1 -1
  289. package/dist/scene/render-scene.d.ts +18 -7
  290. package/dist/scene/render-scene.d.ts.map +1 -1
  291. package/dist/scene/render-source.d.ts +42 -0
  292. package/dist/scene/render-source.d.ts.map +1 -0
  293. package/dist/scene/source-systems.d.ts +6 -0
  294. package/dist/scene/source-systems.d.ts.map +1 -0
  295. package/dist/ssao-buffers.d.ts +9 -0
  296. package/dist/ssao-buffers.d.ts.map +1 -1
  297. package/dist/surface/dynamic-input.d.ts +133 -0
  298. package/dist/surface/dynamic-input.d.ts.map +1 -0
  299. package/dist/surface/gpu-driven.d.ts +41 -0
  300. package/dist/surface/gpu-driven.d.ts.map +1 -0
  301. package/dist/surface/submission-observation.d.ts +136 -0
  302. package/dist/surface/submission-observation.d.ts.map +1 -0
  303. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  304. package/dist/systems/skin-palette-types.d.ts +10 -0
  305. package/dist/systems/skin-palette-types.d.ts.map +1 -1
  306. package/dist/systems/transparent-dispatch.d.ts +2 -2
  307. package/dist/systems/transparent-dispatch.d.ts.map +1 -1
  308. package/dist/systems/transparent-sort-config.d.ts +2 -1
  309. package/dist/systems/transparent-sort-config.d.ts.map +1 -1
  310. package/dist/temporal/gpu.d.ts +13 -1
  311. package/dist/temporal/gpu.d.ts.map +1 -1
  312. package/dist/temporal/index.mjs +6 -7
  313. package/dist/temporal/inspection.d.ts.map +1 -1
  314. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  315. package/dist/temporal/target-coverage-attachment.d.ts.map +1 -1
  316. package/dist/transmission/single-layer-medium.d.ts +89 -0
  317. package/dist/transmission/single-layer-medium.d.ts.map +1 -0
  318. package/dist/typed-render-graph-primitives.d.ts +14 -2
  319. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  320. package/dist/volume/component.d.ts +9 -4
  321. package/dist/volume/component.d.ts.map +1 -1
  322. package/dist/volume/extract.d.ts +1 -1
  323. package/dist/volume/extract.d.ts.map +1 -1
  324. package/dist/volume/inspection.d.ts +2 -0
  325. package/dist/volume/inspection.d.ts.map +1 -1
  326. package/dist/volume/passes.d.ts +4 -1
  327. package/dist/volume/passes.d.ts.map +1 -1
  328. package/dist/volume/resources.d.ts.map +1 -1
  329. package/package.json +22 -21
  330. package/src/__tests__/atmosphere-daylight.dawn.test.ts +72 -0
  331. package/src/__tests__/auto-exposure-transaction.integration.test.ts +4 -9
  332. package/src/__tests__/barrel-distortion-lkg.integration.test.ts +353 -0
  333. package/src/__tests__/barrel-distortion.dawn.test.ts +41 -0
  334. package/src/__tests__/barrel-distortion.unit.test.ts +364 -0
  335. package/src/__tests__/construct-renderer-identity.unit.test.ts +49 -0
  336. package/src/__tests__/detached-recovery-graph.unit.test.ts +277 -0
  337. package/src/__tests__/device-feature-admission.unit.test.ts +3 -2
  338. package/src/__tests__/device-loss-fanout.unit.test.ts +36 -1
  339. package/src/__tests__/docs-gate-green.unit.test.ts +112 -1
  340. package/src/__tests__/factory-contract.integration.test.ts +295 -45
  341. package/src/__tests__/feature-depth-input.dawn.test.ts +129 -2
  342. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +14 -3
  343. package/src/__tests__/gpu-dirty-ranges.unit.test.ts +22 -0
  344. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +16 -3
  345. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +12 -0
  346. package/src/__tests__/gpu-driven-material-artifacts.unit.test.ts +143 -0
  347. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +5 -6
  348. package/src/__tests__/gpu-driven-production.integration.test.ts +482 -3
  349. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +6 -5
  350. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +94 -6
  351. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +18 -2
  352. package/src/__tests__/gpu-driven-view.browser.test.ts +5 -8
  353. package/src/__tests__/gpu-driven-view.dawn.test.ts +6 -2
  354. package/src/__tests__/gpu-driven-view.unit.test.ts +1 -1
  355. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +1 -0
  356. package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -4
  357. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +1 -0
  358. package/src/__tests__/gpu-scene.unit.test.ts +119 -9
  359. package/src/__tests__/instance-allocation-failure.unit.test.ts +57 -0
  360. package/src/__tests__/instances-culling.integration.test.ts +10 -36
  361. package/src/__tests__/instances-store.unit.test.ts +97 -305
  362. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  363. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +4 -1
  364. package/src/__tests__/main-pass-material-medium-selection.unit.test.ts +145 -0
  365. package/src/__tests__/material-shader-prewarm.unit.test.ts +59 -0
  366. package/src/__tests__/material-snapshot-mutation.integration.test.ts +39 -1
  367. package/src/__tests__/materials-standard-contract.unit.test.ts +0 -1
  368. package/src/__tests__/morph-culling-reentry.dawn.test.ts +5 -5
  369. package/src/__tests__/morph-visual-evidence.integration.test.ts +8 -7
  370. package/src/__tests__/motion-blur-compute.dawn.test.ts +13 -7
  371. package/src/__tests__/motion-blur.pixel.integration.test.ts +2 -2
  372. package/src/__tests__/nineslice-handle-surface.unit.test.ts +4 -4
  373. package/src/__tests__/pbr-view-layout.unit.test.ts +9 -0
  374. package/src/__tests__/physical-clearcoat.integration.test.ts +66 -8
  375. package/src/__tests__/physical-material-budget.unit.test.ts +40 -0
  376. package/src/__tests__/prepared-graphics-capability.unit.test.ts +66 -2
  377. package/src/__tests__/probe-bindings.unit.test.ts +582 -1
  378. package/src/__tests__/probe-scene-dirty.integration.test.ts +28 -1
  379. package/src/__tests__/receipt-observation-retirement.unit.test.ts +65 -0
  380. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +69 -0
  381. package/src/__tests__/render-error-code-owner.test-d.ts +8 -0
  382. package/src/__tests__/render-feature-diagnostics.unit.test.ts +15 -2
  383. package/src/__tests__/render-feature-error-reporting.unit.test.ts +21 -1
  384. package/src/__tests__/render-feature-prepared-diagnostics.unit.test.ts +10 -1
  385. package/src/__tests__/render-feature-retry.unit.test.ts +12 -1
  386. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +145 -19
  387. package/src/__tests__/render-scene-projection.unit.test.ts +36 -11
  388. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +20 -20
  389. package/src/__tests__/render-target-probe-soak.perf.test.ts +2 -2
  390. package/src/__tests__/render-temporal-environment-consumer.browser.test.ts +3 -3
  391. package/src/__tests__/render-temporal-environment-consumer.dawn.test.ts +5 -2
  392. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +42 -8
  393. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +16 -0
  394. package/src/__tests__/renderer-frame-transaction.integration.test.ts +26 -10
  395. package/src/__tests__/resource-allocation.unit.test.ts +123 -0
  396. package/src/__tests__/single-layer-medium-passes.unit.test.ts +275 -0
  397. package/src/__tests__/skin-motion-regression.unit.test.ts +70 -1
  398. package/src/__tests__/skin-palette-recycling.dawn.test.ts +97 -0
  399. package/src/__tests__/skinned-shadow-caster.test.ts +15 -0
  400. package/src/__tests__/ssr-carrier-public-contract.unit.test.ts +21 -0
  401. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +4 -4
  402. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +2 -2
  403. package/src/__tests__/ssr-gpu-dispatch.ts +1 -1
  404. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +6 -2
  405. package/src/__tests__/standard-output-chain-contract.unit.test.ts +28 -0
  406. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +46 -1
  407. package/src/__tests__/standard-pipeline.integration.test.ts +182 -1
  408. package/src/__tests__/surface-dynamic-buffer-lifecycle.unit.test.ts +506 -0
  409. package/src/__tests__/surface-submission-observation.unit.test.ts +812 -0
  410. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +32 -0
  411. package/src/__tests__/temporal-performance.integration.test.ts +3 -3
  412. package/src/__tests__/visibility-instances.integration.test.ts +7 -19
  413. package/src/__tests__/visibility-producer-matrix.integration.test.ts +9 -20
  414. package/src/__tests__/volume-optics-oracle.unit.test.ts +4 -2
  415. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +30 -20
  416. package/src/__tests__/volumetric-fog-errors.unit.test.ts +4 -3
  417. package/src/__tests__/volumetric-fog-multiple.unit.test.ts +55 -0
  418. package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +1 -1
  419. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +113 -9
  420. package/src/__tests__/volumetric-fog-topology.unit.test.ts +5 -3
  421. package/src/assembly/backend-contract.ts +30 -0
  422. package/src/assembly/barrel-distortion-inspection.ts +22 -0
  423. package/src/assembly/canvas-draw-guard.ts +11 -0
  424. package/src/assembly/cloud-shadow-entry.ts +22 -0
  425. package/src/assembly/device-feature-admission.ts +2 -2
  426. package/src/assembly/dynamic-geometry-host.ts +65 -37
  427. package/src/assembly/dynamic-geometry-runtime.ts +7 -1
  428. package/src/assembly/factory.ts +2 -30
  429. package/src/assembly/host-contract.ts +8 -5
  430. package/src/assembly/material/assembly.ts +25 -13
  431. package/src/assembly/material/pipeline-helpers.ts +53 -12
  432. package/src/assembly/material-shader-policy.ts +55 -0
  433. package/src/assembly/material-shader-prewarm.ts +21 -10
  434. package/src/assembly/recovery/device-loss-fanout.ts +13 -0
  435. package/src/assembly/recovery/renderer-recover.ts +50 -18
  436. package/src/assembly/render-feature-post-processes.ts +54 -0
  437. package/src/assembly/render-target-host.ts +57 -47
  438. package/src/assembly/renderer-facade.ts +7 -2
  439. package/src/assembly/renderer-feature-inspection.ts +8 -2
  440. package/src/assembly/renderer-frame-transaction.ts +23 -0
  441. package/src/assembly/renderer-inspection.ts +28 -0
  442. package/src/assembly/skin-palette-owner.ts +1 -1
  443. package/src/assembly/temporal-post-process-prewarm.ts +3 -3
  444. package/src/assembly/webgpu-pbr-ready.ts +9 -1
  445. package/src/assembly/webgpu-ready-per-pass.ts +104 -0
  446. package/src/assembly/webgpu-ready.ts +116 -62
  447. package/src/assembly/webgpu-renderer-bootstrap.ts +0 -12
  448. package/src/assembly/webgpu-renderer-observation-owner.ts +222 -0
  449. package/src/assembly/webgpu-renderer.ts +303 -313
  450. package/src/assembly/webgpu-vertex-layouts.ts +1 -1
  451. package/src/barrel-distortion.ts +247 -0
  452. package/src/cloud/__tests__/cloud-density-parity.dawn.test.ts +173 -0
  453. package/src/cloud/__tests__/cloud-layer-framebuffer-y.dawn.test.ts +265 -0
  454. package/src/cloud/__tests__/cloud-layer.browser.test.ts +432 -0
  455. package/src/cloud/__tests__/cloud-layer.unit.test.ts +634 -0
  456. package/src/cloud/density.ts +600 -0
  457. package/src/cloud/extract.ts +110 -0
  458. package/src/cloud/feature.ts +1810 -0
  459. package/src/cloud/frame-context.ts +42 -0
  460. package/src/cloud/inspection.ts +130 -0
  461. package/src/cloud/optics.ts +285 -0
  462. package/src/cloud/parameters.ts +255 -0
  463. package/src/cloud/resources.ts +122 -0
  464. package/src/cloud/shadow.ts +167 -0
  465. package/src/cloud/temporal.ts +226 -0
  466. package/src/components/barrel-distortion.ts +46 -0
  467. package/src/components/cloud-layer.ts +47 -0
  468. package/src/components/index.ts +2 -0
  469. package/src/components/instances.ts +18 -45
  470. package/src/components/sprite-instances.ts +1 -1
  471. package/src/construct-renderer.ts +1 -1
  472. package/src/device/gpu-residency.ts +26 -25
  473. package/src/dynamic-geometry.ts +12 -1
  474. package/src/errors/cloud.ts +103 -0
  475. package/src/errors/render.ts +47 -8
  476. package/src/examples/single-layer-medium-surface.ts +59 -0
  477. package/src/extract/camera.ts +15 -1
  478. package/src/extract/contracts.ts +5 -0
  479. package/src/extract/material-context.ts +21 -0
  480. package/src/features/barrel-distortion.ts +172 -0
  481. package/src/features/error-reporting.ts +26 -6
  482. package/src/features/host.ts +128 -19
  483. package/src/features/plan.ts +10 -0
  484. package/src/features/render-graph-contribution.ts +9 -0
  485. package/src/features/render-graph-raster.ts +45 -3
  486. package/src/features/targets.ts +66 -0
  487. package/src/features/types.ts +51 -0
  488. package/src/fullscreen-post-process-pass.ts +41 -9
  489. package/src/glyph-text-layout-system.ts +2 -2
  490. package/src/gpu-dirty-ranges.ts +48 -0
  491. package/src/gpu-driven/batch-topology.ts +53 -0
  492. package/src/gpu-driven/material-artifacts.ts +33 -25
  493. package/src/gpu-driven/prepared-draw.ts +12 -4
  494. package/src/gpu-driven/production-raster-material.ts +1894 -0
  495. package/src/gpu-driven/production-raster.ts +825 -784
  496. package/src/gpu-driven/resource-allocation.ts +142 -0
  497. package/src/gpu-driven/shadow-ownership.ts +71 -0
  498. package/src/gpu-driven/view-gpu.ts +164 -10
  499. package/src/gpu-scene.ts +75 -84
  500. package/src/hdrp-buffers.ts +4 -3
  501. package/src/ibl/frame-resources.ts +0 -1
  502. package/src/ibl/skylight-bind-group.ts +62 -32
  503. package/src/index.ts +205 -9
  504. package/src/inspection-types.ts +162 -0
  505. package/src/instance-buffer-cache.ts +4 -4
  506. package/src/instances.ts +152 -569
  507. package/src/pbr-pipeline.ts +230 -30
  508. package/src/pipeline/single-layer-medium-passes.ts +267 -0
  509. package/src/pipeline/standard-forward-lane.ts +313 -57
  510. package/src/pipeline/standard-output/graph.ts +8 -2
  511. package/src/pipeline/standard-output/lut-gpu.ts +3 -3
  512. package/src/pipeline/standard-output/resources.ts +2 -2
  513. package/src/pipeline/standard-output/types.ts +12 -1
  514. package/src/pipeline/standard-pipeline.ts +291 -18
  515. package/src/pipeline/standard-post.ts +90 -24
  516. package/src/pipeline-builder.ts +1 -1
  517. package/src/plugin.ts +4 -0
  518. package/src/points-lines/__tests__/points-lines.bench.ts +17 -8
  519. package/src/prepare/prepared-graphics-resolver.ts +2 -0
  520. package/src/publication/__tests__/derived-geometry.integration.test.ts +177 -0
  521. package/src/publication/__tests__/features.integration.test.ts +122 -0
  522. package/src/publication/__tests__/programs.unit.test.ts +25 -0
  523. package/src/publication/__tests__/publication.integration.test.ts +370 -0
  524. package/src/publication/__tests__/targets.integration.test.ts +120 -0
  525. package/src/publication/__tests__/video.browser.test.ts +50 -0
  526. package/src/publication/contract.ts +104 -0
  527. package/src/publication/dependencies.ts +54 -0
  528. package/src/publication/prepare-geometry.ts +94 -0
  529. package/src/publication/programs.ts +45 -0
  530. package/src/publication/publisher.ts +618 -0
  531. package/src/publication/receiver.ts +297 -0
  532. package/src/publication/resource-scope.ts +31 -0
  533. package/src/publication/targets.ts +182 -0
  534. package/src/publication/video.ts +33 -0
  535. package/src/record/__tests__/main-pass-geometry.unit.test.ts +30 -0
  536. package/src/record/__tests__/material-bg-cache.unit.test.ts +15 -1
  537. package/src/record/__tests__/sprite-material-abi.unit.test.ts +9 -11
  538. package/src/record/__tests__/temporal-payload.unit.test.ts +1 -1
  539. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +11 -0
  540. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -0
  541. package/src/record/__tests__/video-texture-view.unit.test.ts +9 -9
  542. package/src/record/__tests__/view-ubo-layout.unit.test.ts +4 -3
  543. package/src/record/barrel-distortion-frame.ts +110 -0
  544. package/src/record/dynamic-geometry-consumption.ts +39 -9
  545. package/src/record/fold-buckets.ts +855 -0
  546. package/src/record/fold-instance-buffer.ts +51 -0
  547. package/src/record/frame-lighting.ts +36 -2
  548. package/src/record/frame-snapshot.ts +45 -9
  549. package/src/record/frame.ts +258 -1105
  550. package/src/record/gpu-draw-receipts.ts +38 -0
  551. package/src/record/gpu-timing.ts +48 -5
  552. package/src/record/helpers.ts +2 -2
  553. package/src/record/main-pass-geometry.ts +272 -84
  554. package/src/record/main-pass-material.ts +102 -36
  555. package/src/record/main-pass-sprite-draws.ts +13 -217
  556. package/src/record/main-pass.ts +164 -27
  557. package/src/record/motion-blur-receipt.ts +91 -0
  558. package/src/record/prepared-material-bindings.ts +11 -6
  559. package/src/record/probe-blend-buffer.ts +180 -0
  560. package/src/record/recovery-pipeline.ts +35 -3
  561. package/src/record/render-context.ts +41 -5
  562. package/src/record/shadow-pass.ts +30 -4
  563. package/src/record/skybox-post-pass.ts +28 -4
  564. package/src/record/sprite-instance-buffer.ts +155 -0
  565. package/src/record/target-capture-graph.ts +258 -0
  566. package/src/record/typed-frame-graph.ts +776 -340
  567. package/src/record/view-ubo.ts +27 -4
  568. package/src/record/volume-params.ts +225 -0
  569. package/src/recovery/render-system-candidate.ts +243 -50
  570. package/src/reflection/record-owner.ts +7 -3
  571. package/src/render-contract.ts +54 -5
  572. package/src/render-graph-primitives.ts +7 -1
  573. package/src/render-pipeline.ts +130 -0
  574. package/src/render-system-extract-tail.ts +90 -78
  575. package/src/render-system-extract.ts +76 -76
  576. package/src/render-system-presentation.ts +56 -0
  577. package/src/render-system-producer-roots.ts +96 -0
  578. package/src/render-system-projections.ts +3 -2
  579. package/src/render-system-residency.ts +159 -0
  580. package/src/render-system.ts +421 -408
  581. package/src/scene/probe-blend.ts +96 -4
  582. package/src/scene/render-scene.ts +416 -337
  583. package/src/scene/render-source.ts +90 -0
  584. package/src/scene/source-systems.ts +33 -0
  585. package/src/ssao-buffers.ts +95 -2
  586. package/src/surface/__tests__/dynamic-input.unit.test.ts +374 -0
  587. package/src/surface/__tests__/gpu-driven.unit.test.ts +87 -0
  588. package/src/surface/dynamic-input.ts +658 -0
  589. package/src/surface/gpu-driven.ts +147 -0
  590. package/src/surface/submission-observation.ts +517 -0
  591. package/src/systems/skin-palette-allocator.ts +5 -1
  592. package/src/systems/skin-palette-types.ts +11 -0
  593. package/src/systems/transparent-dispatch.ts +2 -2
  594. package/src/systems/transparent-sort-config.ts +3 -1
  595. package/src/temporal/gpu.ts +101 -17
  596. package/src/temporal/inspection.ts +7 -1
  597. package/src/temporal/standard-scene-data.ts +5 -0
  598. package/src/temporal/target-coverage-attachment.ts +4 -0
  599. package/src/transmission/__tests__/single-layer-medium.unit.test.ts +149 -0
  600. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +12 -11
  601. package/src/transmission/single-layer-medium.ts +322 -0
  602. package/src/typed-render-graph-primitives.ts +70 -2
  603. package/src/volume/component.ts +10 -9
  604. package/src/volume/extract.ts +11 -5
  605. package/src/volume/inspection.ts +3 -0
  606. package/src/volume/passes.ts +87 -4
  607. package/src/volume/resources.ts +2 -1
  608. package/dist/chunk-3RTCJTH4.mjs +0 -1610
  609. package/dist/chunk-3RTCJTH4.mjs.map +0 -1
  610. package/dist/chunk-BLXGONR2.mjs.map +0 -1
  611. package/dist/chunk-ILQ5MEZF.mjs +0 -32
  612. package/dist/chunk-ILQ5MEZF.mjs.map +0 -1
  613. package/dist/chunk-LEAXOMYJ.mjs.map +0 -1
  614. package/dist/chunk-NB2S7ZRX.mjs +0 -1783
  615. package/dist/chunk-NB2S7ZRX.mjs.map +0 -1
  616. package/dist/chunk-NNQL7CB3.mjs +0 -1713
  617. package/dist/chunk-NNQL7CB3.mjs.map +0 -1
  618. package/dist/chunk-OO3DRYK7.mjs.map +0 -1
  619. package/dist/chunk-PA5DRJNI.mjs.map +0 -1
  620. package/dist/chunk-PLAXOOX5.mjs +0 -1907
  621. package/dist/chunk-PLAXOOX5.mjs.map +0 -1
  622. package/dist/chunk-RXZ3RGOU.mjs +0 -53
  623. package/dist/chunk-RXZ3RGOU.mjs.map +0 -1
  624. package/dist/chunk-VOZWDM3L.mjs.map +0 -1
@@ -1,4 +1,3 @@
1
- import { deriveVertexBufferLayoutFromProjection } from '@forgeax/engine-geometry';
2
1
  import { frustum, mat3, mat4 } from '@forgeax/engine-math';
3
2
  import type {
4
3
  GraphAccess,
@@ -28,11 +27,11 @@ import {
28
27
  gpuDrivenShadowDrawKey,
29
28
  gpuDrivenSourceDrawItemIndex,
30
29
  } from '../extract/gpu-driven';
31
- import { GPU_SCENE_LAYOUTS } from '../gpu-scene-schema';
32
30
  import {
33
31
  GPU_BUFFER_USAGE_COPY_DST,
34
32
  GPU_BUFFER_USAGE_INDEX,
35
33
  GPU_BUFFER_USAGE_STORAGE,
34
+ GPU_BUFFER_USAGE_UNIFORM,
36
35
  GPU_BUFFER_USAGE_VERTEX,
37
36
  } from '../gpu-usage';
38
37
  import type {
@@ -42,12 +41,15 @@ import type {
42
41
  GpuDrivenProductionInspection,
43
42
  GpuDrivenStructureMetrics,
44
43
  } from '../inspection-types';
45
- import { isStandardPbrMaterialShader, resolvePipelineGroup2Contract } from '../pbr-pipeline';
44
+ import {
45
+ isCanonicalStandardPbrMaterialShader,
46
+ isStandardPbrMaterialShader,
47
+ resolvePipelineGroup2Contract,
48
+ } from '../pbr-pipeline';
46
49
  import type { PipelineBuilderShaderModuleFactory } from '../pipeline-builder';
47
50
  import type { ValidatedRenderable } from '../record/frame-snapshot';
48
- import { worldEntityKey } from '../record/frame-snapshot';
51
+ import { getOpaqueResourceIdentity, worldEntityKey } from '../record/frame-snapshot';
49
52
  import { geometryRenderStateForTopology } from '../record/main-pass-material';
50
- import { MESH_SSBO_BYTES, packInstanceStorageBuffer } from '../record/mesh-ssbo';
51
53
  import {
52
54
  MATERIAL_PER_ENTITY_STRIDE,
53
55
  type PipelineState,
@@ -55,7 +57,6 @@ import {
55
57
  } from '../record/render-context';
56
58
  import type { CameraSnapshot } from '../render-contract';
57
59
  import type {
58
- GpuDrivenSceneMeshBinding,
59
60
  GpuDrivenStandardPbrFrameResources,
60
61
  RenderPipelineFrame,
61
62
  RenderPipelineGpuDrivenProjection,
@@ -69,14 +70,27 @@ import type {
69
70
  import type { PersistentGpuDrivenState } from '../scene/render-scene';
70
71
  import { selectLod } from '../scene/visibility/lod-selector';
71
72
  import type { OcclusionFrameProjection } from '../scene/visibility/occlusion-runtime';
73
+ import type {
74
+ DynamicInputRange,
75
+ ReadonlyDynamicInputPage,
76
+ SurfaceDynamicInputFrame,
77
+ } from '../surface/dynamic-input';
78
+ import {
79
+ admitSingleLayerMediumSubmission,
80
+ type SurfaceGpuSubmissionAdmission,
81
+ } from '../surface/gpu-driven';
82
+ import type { SurfaceSubmissionCandidate } from '../surface/submission-observation';
72
83
  import {
73
84
  type GpuDrivenBatch,
74
85
  type GpuDrivenCandidate,
86
+ inspectResourceClassSplits,
75
87
  projectedHeightForCandidate,
76
88
  type SubmissionPlan,
77
89
  } from './batch-topology';
90
+ import { requiresProbeBlendRecord } from './material-artifacts';
78
91
  import { materialBindingKey } from './material-bindings';
79
- import { type GpuDrivenPbrProgram, resolveStandardPbrProgram } from './pbr-program';
92
+ import { MaterialAbiRasterAdapter } from './production-raster-material';
93
+ import { combineGpuResourceAllocationInspections } from './resource-allocation';
80
94
  import {
81
95
  type ShadowViewIdentity,
82
96
  type ShadowViewProjection,
@@ -100,6 +114,39 @@ export interface StandardPbrSceneCandidate {
100
114
  readonly materialIndex?: number;
101
115
  }
102
116
 
117
+ /** Keep visible.x and the selected group(2) mesh page in one address domain. */
118
+ export function resolveGpuDrivenMeshGroup(input: {
119
+ readonly clustered: boolean;
120
+ readonly deformation: 'rigid' | 'skin';
121
+ readonly meshBindGroup: BindGroup;
122
+ readonly meshProjectionBuffer: Buffer;
123
+ readonly meshProjectionBytes: number;
124
+ readonly frameResources: Pick<
125
+ GpuDrivenStandardPbrFrameResources,
126
+ 'clusterBindGroup' | 'clusterBindGroupForMesh' | 'clusterBindGroupForSkin'
127
+ >;
128
+ readonly skinPaletteBinding?:
129
+ | { readonly buffer: Buffer; readonly bindingWindowBytes: number }
130
+ | undefined;
131
+ }): BindGroup | undefined {
132
+ if (!input.clustered) return input.meshBindGroup;
133
+ if (input.deformation === 'skin') {
134
+ const palette = input.skinPaletteBinding;
135
+ return palette === undefined
136
+ ? undefined
137
+ : input.frameResources.clusterBindGroupForSkin?.(
138
+ input.meshProjectionBuffer,
139
+ input.meshProjectionBytes,
140
+ palette.buffer,
141
+ palette.bindingWindowBytes,
142
+ );
143
+ }
144
+ return input.frameResources.clusterBindGroupForMesh?.(
145
+ input.meshProjectionBuffer,
146
+ input.meshProjectionBytes,
147
+ );
148
+ }
149
+
103
150
  export interface StandardPbrSceneProjection {
104
151
  readonly entryPoint: string;
105
152
  readonly layoutIdentity: string;
@@ -258,21 +305,34 @@ interface PreparedBatch {
258
305
  readonly slotGeneration: number;
259
306
  }
260
307
 
261
- function bufferBinding(
262
- buffer: import('@forgeax/engine-rhi').Buffer,
263
- offset?: number,
264
- size?: number,
265
- ) {
266
- return {
267
- kind: 'buffer' as const,
268
- value: {
269
- buffer,
270
- ...(offset === undefined ? {} : { offset }),
271
- ...(size === undefined ? {} : { size }),
272
- },
308
+ /**
309
+ * Renderer-owned join between the scene-index candidate rows and a Surface
310
+ * producer range. The user-facing range keeps its formal instance address;
311
+ * the frame row is assigned after the RenderScene plan has been filtered and
312
+ * its visible bases are known.
313
+ */
314
+ interface SurfaceFrameRange {
315
+ readonly frameIndex: number;
316
+ readonly range?: DynamicInputRange;
317
+ readonly directMember: {
318
+ readonly worldIdentity: string;
319
+ readonly worldId: number;
320
+ readonly entityKey: number;
321
+ readonly drawItemIndex: number;
322
+ readonly instanceOrdinal: number;
273
323
  };
274
324
  }
275
325
 
326
+ interface SurfaceFrameProjection {
327
+ readonly ranges: readonly SurfaceFrameRange[];
328
+ readonly consumptionRanges: readonly DynamicInputRange[];
329
+ readonly capacity: number;
330
+ }
331
+
332
+ function surfaceDynamicInputError(expected: string, hint: string): RhiError {
333
+ return new RhiError({ code: 'rhi-descriptor-invalid', expected, hint });
334
+ }
335
+
276
336
  function activeFrustum(camera: CameraSnapshot): Float32Array {
277
337
  const projection = mat4.create();
278
338
  if (camera.projection === 'orthographic') {
@@ -930,6 +990,136 @@ function shadowCasterMembershipSignature(
930
990
 
931
991
  type FilteredProductionPlan = ReturnType<typeof filteredPlan>;
932
992
 
993
+ function isSingleLayerMediumArtifact(artifact: MaterialShaderArtifact): boolean {
994
+ return artifact.receipt?.surface?.model === 'single-layer-medium';
995
+ }
996
+
997
+ function isSingleLayerMediumBatch(batch: PreparedBatch): boolean {
998
+ return isSingleLayerMediumArtifact(batch.artifact);
999
+ }
1000
+
1001
+ /**
1002
+ * Join Surface ranges to the renderer-owned candidate rows in the visible
1003
+ * stream. The GPU culler may assign atomic compact slots in any order, but
1004
+ * each member preserves the candidate's batch-local projected row in
1005
+ * `visibleItems.x`; frame rows use that same identity rather than the slot.
1006
+ * The producer's `instanceIndex` remains in the range and is copied into the
1007
+ * frame record for the Surface ABI.
1008
+ */
1009
+ function buildSurfaceFrameRanges(
1010
+ filtered: FilteredProductionPlan,
1011
+ ranges: readonly DynamicInputRange[],
1012
+ scene: PersistentGpuDrivenState,
1013
+ ): Result<readonly SurfaceFrameRange[], RhiError> {
1014
+ const slotsByPrimitive = new Map(scene.slots.map((slot) => [slot.slot, slot]));
1015
+ const surfaceMembers: Array<{
1016
+ readonly frameIndex: number;
1017
+ readonly identity: string;
1018
+ readonly directMember: SurfaceFrameRange['directMember'];
1019
+ }> = [];
1020
+ for (const prepared of filtered.batches) {
1021
+ if (!isSingleLayerMediumBatch(prepared)) continue;
1022
+ let visibleMemberIndex = 0;
1023
+ for (const candidate of prepared.batch.candidates) {
1024
+ if (
1025
+ (candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
1026
+ .submitAdmission === false
1027
+ ) {
1028
+ continue;
1029
+ }
1030
+ const slot = slotsByPrimitive.get(candidate.primitiveIndex);
1031
+ const worldIdentity = slot === undefined ? undefined : scene.worldIdentities?.[slot.worldId];
1032
+ if (slot === undefined || worldIdentity === undefined) {
1033
+ return err(
1034
+ new RhiError({
1035
+ code: 'rhi-descriptor-invalid',
1036
+ expected:
1037
+ 'every admitted medium draw member has a renderer slot and public World identity',
1038
+ hint: `rebuild the persistent scene projection before publishing Surface ranges (primitive=${candidate.primitiveIndex})`,
1039
+ }),
1040
+ );
1041
+ }
1042
+ surfaceMembers.push({
1043
+ frameIndex: prepared.batch.visibleBase + visibleMemberIndex,
1044
+ directMember: {
1045
+ worldIdentity,
1046
+ worldId: slot.worldId,
1047
+ entityKey: slot.entityKey,
1048
+ drawItemIndex: candidate.drawItemIndex,
1049
+ instanceOrdinal: candidate.instanceOrdinal,
1050
+ },
1051
+ identity: JSON.stringify([
1052
+ worldIdentity,
1053
+ slot.entityKey,
1054
+ candidate.drawItemIndex,
1055
+ candidate.instanceOrdinal,
1056
+ ]),
1057
+ });
1058
+ visibleMemberIndex += 1;
1059
+ }
1060
+ }
1061
+ if (ranges.length === 0) {
1062
+ return ok(
1063
+ Object.freeze(
1064
+ surfaceMembers.map(({ frameIndex, directMember }) => ({ frameIndex, directMember })),
1065
+ ),
1066
+ );
1067
+ }
1068
+ const rangeByIdentity = new Map<string, DynamicInputRange>();
1069
+ for (const range of ranges) {
1070
+ const identity = JSON.stringify([
1071
+ range.member.worldIdentity,
1072
+ range.member.entityKey,
1073
+ range.member.drawItemIndex,
1074
+ range.member.instanceOrdinal,
1075
+ ]);
1076
+ if (rangeByIdentity.has(identity)) {
1077
+ return err(
1078
+ new RhiError({
1079
+ code: 'rhi-descriptor-invalid',
1080
+ expected: 'at most one Surface dynamic range for each stable draw member',
1081
+ hint: `remove the duplicate producer range for member ${identity}`,
1082
+ }),
1083
+ );
1084
+ }
1085
+ rangeByIdentity.set(identity, range);
1086
+ }
1087
+ if (ranges.length !== surfaceMembers.length) {
1088
+ return err(
1089
+ new RhiError({
1090
+ code: 'rhi-descriptor-invalid',
1091
+ expected: 'one stable-identity Surface dynamic range for every admitted medium draw member',
1092
+ hint: `publish one range per admitted Surface member (ranges=${ranges.length}, members=${surfaceMembers.length})`,
1093
+ }),
1094
+ );
1095
+ }
1096
+ const frameRanges: SurfaceFrameRange[] = [];
1097
+ for (const member of surfaceMembers) {
1098
+ const range = rangeByIdentity.get(member.identity);
1099
+ if (range === undefined) {
1100
+ return err(
1101
+ new RhiError({
1102
+ code: 'rhi-descriptor-invalid',
1103
+ expected: 'every admitted medium draw member resolves one stable-identity Surface range',
1104
+ hint: `publish the missing producer range for member ${member.identity}`,
1105
+ }),
1106
+ );
1107
+ }
1108
+ rangeByIdentity.delete(member.identity);
1109
+ frameRanges.push({ frameIndex: member.frameIndex, range, directMember: member.directMember });
1110
+ }
1111
+ if (rangeByIdentity.size !== 0) {
1112
+ return err(
1113
+ new RhiError({
1114
+ code: 'rhi-descriptor-invalid',
1115
+ expected: 'every Surface range identifies an admitted medium draw member',
1116
+ hint: `remove ranges for non-admitted members: ${[...rangeByIdentity.keys()].join(', ')}`,
1117
+ }),
1118
+ );
1119
+ }
1120
+ return ok(Object.freeze(frameRanges));
1121
+ }
1122
+
933
1123
  function sceneRequiresGpuDrivenArtifact(scene: PersistentGpuDrivenState): boolean {
934
1124
  return scene.plan.batches.some((batch) =>
935
1125
  batch.candidates.some((candidate) => {
@@ -1139,736 +1329,11 @@ function materialArtifactSignature(
1139
1329
  }
1140
1330
 
1141
1331
  /** The GPU-driven bridge to the existing four-group Standard PBR pipeline. */
1142
- class MaterialAbiRasterAdapter {
1143
- private readonly pipelines = new Map<string, RenderPipeline>();
1144
- private readonly projectionBuffers = new Set<Buffer>();
1145
- private readonly projectionBindings = new Map<
1146
- string,
1147
- {
1148
- readonly buffer: Buffer;
1149
- readonly bindGroup: BindGroup;
1150
- readonly capacityBytes: number;
1151
- rows: Float32Array;
1152
- }
1153
- >();
1154
- /**
1155
- * Projection rows are graph-generation resources. A new topology may stage
1156
- * a replacement before the graph is promoted; keeping staged bindings out
1157
- * of the active map lets an aborted graph leave the last submitted rows
1158
- * intact. Superseded buffers are fence-retired on promotion.
1159
- */
1160
- private readonly stagedProjectionBindings = new Map<
1161
- string,
1162
- {
1163
- readonly buffer: Buffer;
1164
- readonly bindGroup: BindGroup;
1165
- readonly capacityBytes: number;
1166
- rows: Float32Array;
1167
- }
1168
- >();
1169
- private readonly pendingProjectionRetirements = new Set<Buffer>();
1170
- private readonly destroyedProjectionBuffers = new Set<Buffer>();
1171
- private projectionDisposed = false;
1172
- private stagedProjectionKeys = new Set<string>();
1173
- private readonly activeProjectionRowsAtStage = new Map<string, Float32Array>();
1174
- private readonly visibleWindowBindGroups = new Map<Buffer, Map<string, BindGroup>>();
1175
-
1176
- private constructor(
1177
- private readonly device: RhiDevice,
1178
- private readonly pipelineLayout: import('@forgeax/engine-rhi').PipelineLayout,
1179
- private readonly meshLayout: BindGroupLayout,
1180
- private readonly instancesBindGroupLayout: BindGroupLayout,
1181
- private readonly programs: ReadonlyMap<string, GpuDrivenPbrProgram> | undefined,
1182
- private instancesBindGroup: BindGroup,
1183
- private readonly instanceBuffer: Buffer,
1184
- private readonly instanceBufferSize: number,
1185
- private visibleBuffer: Buffer,
1186
- private readonly artifact: MaterialShaderArtifact,
1187
- private readonly deformation: 'rigid' | 'skin',
1188
- private readonly pipelineFactory: MaterialPipelineFactory | undefined,
1189
- private paletteBuffer: Buffer | undefined,
1190
- private paletteBindingWindowBytes: number | undefined,
1191
- ) {}
1192
-
1193
- static create(input: {
1194
- readonly device: RhiDevice;
1195
- readonly pipelineState: PipelineState;
1196
- readonly artifact: MaterialShaderArtifact;
1197
- readonly visibleBuffer: Buffer;
1198
- readonly deformation?: 'rigid' | 'skin';
1199
- readonly paletteBuffer?: Buffer;
1200
- readonly paletteBindingWindowBytes?: number;
1201
- /** Renderer-owned factory for the selected artifact's actual PSO. */
1202
- readonly pipelineFactory?: MaterialPipelineFactory;
1203
- }): Result<MaterialAbiRasterAdapter, RhiError | GpuDrivenPreparationError> {
1204
- const deformation = input.deformation ?? 'rigid';
1205
- const receipt = input.artifact.receipt;
1206
- if (receipt === undefined) {
1207
- return err(
1208
- new RhiError({
1209
- code: 'rhi-not-available',
1210
- expected: 'published MaterialProgramAbi receipt for the selected material artifact',
1211
- hint: 'recook and publish the material before creating a GPU-driven raster owner',
1212
- }),
1213
- );
1214
- }
1215
- const materialRowMatchesSceneTable =
1216
- receipt.materialRow.byteLength === GPU_SCENE_LAYOUTS.material.stride &&
1217
- new Set(receipt.materialRow.fields).size === receipt.materialRow.fields.length;
1218
- if (!materialRowMatchesSceneTable) {
1219
- return err(
1220
- new GpuDrivenPreparationError('material-abi-not-executable', {
1221
- reason: 'material-abi-not-executable',
1222
- owner: 'material',
1223
- expected: `scene-index material row ${GPU_SCENE_LAYOUTS.material.stride} bytes with unique producer fields`,
1224
- actual: `row ${receipt.materialRow.byteLength} bytes with fields ${receipt.materialRow.fields.join(',')}`,
1225
- }),
1226
- );
1227
- }
1228
- if (
1229
- receipt.reflection.layoutIdentity !== input.artifact.layoutIdentity ||
1230
- receipt.directEntry.length === 0 ||
1231
- receipt.sceneIndexEntry.length === 0 ||
1232
- receipt.resourceSlots.some((slot) => slot.group !== 1)
1233
- ) {
1234
- return err(
1235
- new RhiError({
1236
- code: 'rhi-descriptor-invalid',
1237
- expected: `complete selected MaterialProgramAbi reflection, scene row stride ${GPU_SCENE_LAYOUTS.material.stride}, and resource slots`,
1238
- hint: 'repair the producer publication or use the owner for its non-scene material layout',
1239
- }),
1240
- );
1241
- }
1242
- // Scene-index shaders declare the dedicated slot-3 ABI. Keep the legacy
1243
- // fields as a compatibility fallback for synthetic fixtures; the real
1244
- // factory publishes the dedicated handles whenever the capable storage
1245
- // lane is enabled, so production draws never bind the direct-draw BGL.
1246
- const pipelineLayout =
1247
- deformation === 'skin'
1248
- ? input.pipelineState.gpuDrivenPbrSkinPipelineLayout
1249
- : input.pipelineState.gpuDrivenPbrPipelineLayout;
1250
- const instancesBindGroupLayout = input.pipelineState.gpuDrivenInstancesBindGroupLayout;
1251
- if (pipelineLayout == null || instancesBindGroupLayout == null) {
1252
- return err(
1253
- new RhiError({
1254
- code: 'rhi-not-available',
1255
- expected: `${deformation === 'skin' ? 'skinned ' : ''}published material pipeline layout`,
1256
- hint: 'initialize the producer material pipeline before enabling GPU-driven raster',
1257
- }),
1258
- );
1259
- }
1260
- const programs = input.pipelineState.gpuDrivenPbrPrograms;
1261
- if (
1262
- input.pipelineFactory === undefined &&
1263
- (programs === undefined ||
1264
- resolveStandardPbrProgram(
1265
- programs,
1266
- input.artifact.material,
1267
- input.artifact.receipt?.vertexInputs.some((attribute) => attribute.semantic === 'color'),
1268
- ) === undefined)
1269
- ) {
1270
- return err(
1271
- new RhiError({
1272
- code: 'rhi-not-available',
1273
- expected: `producer-owned ${deformation === 'skin' ? 'skinned ' : ''}material shader module for the current device generation`,
1274
- hint: 'publish the selected material module from buildReadyWebGPU before enabling GPU-driven raster',
1275
- }),
1276
- );
1277
- }
1278
- const meshLayout =
1279
- deformation === 'skin'
1280
- ? input.pipelineState.pbrSkinMeshBindGroupLayout
1281
- : input.pipelineState.meshBindGroupLayout;
1282
- if (meshLayout === null) {
1283
- return err(
1284
- new RhiError({
1285
- code: 'rhi-not-available',
1286
- expected: `${deformation === 'skin' ? 'skinned ' : ''}material mesh bind-group layout`,
1287
- hint: 'publish the exact mesh-array layout selected by the material receipt',
1288
- }),
1289
- );
1290
- }
1291
- if (
1292
- deformation === 'skin' &&
1293
- (input.paletteBuffer === undefined || input.paletteBindingWindowBytes === undefined)
1294
- ) {
1295
- return err(
1296
- new RhiError({
1297
- code: 'rhi-not-available',
1298
- expected: 'persistent skin palette buffer and binding window',
1299
- hint: 'keep the palette receipt and the GPU-driven skinned projection on one frame identity',
1300
- }),
1301
- );
1302
- }
1303
- const identity = new Float32Array(16);
1304
- identity[0] = 1;
1305
- identity[5] = 1;
1306
- identity[10] = 1;
1307
- identity[15] = 1;
1308
- const instancePayload = packInstanceStorageBuffer(identity);
1309
- const instanceBuffer = input.device.createBuffer({
1310
- label: 'standard-pbr-gpu-driven-instance-data',
1311
- size: instancePayload.byteLength,
1312
- usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
1313
- });
1314
- if (!instanceBuffer.ok) return instanceBuffer;
1315
- const instanceWrite = input.device.queue.writeBuffer(instanceBuffer.value, 0, instancePayload);
1316
- if (!instanceWrite.ok) return instanceWrite;
1317
- const instancesBindGroup = input.device.createBindGroup({
1318
- label: 'standard-pbr-gpu-driven-visible-instances-bg',
1319
- layout: instancesBindGroupLayout,
1320
- entries: [
1321
- {
1322
- binding: 0,
1323
- resource: bufferBinding(instanceBuffer.value, 0, instancePayload.byteLength),
1324
- },
1325
- {
1326
- binding: 1,
1327
- resource: bufferBinding(input.visibleBuffer),
1328
- },
1329
- {
1330
- binding: 2,
1331
- resource: bufferBinding(input.visibleBuffer),
1332
- },
1333
- ],
1334
- });
1335
- if (!instancesBindGroup.ok) return instancesBindGroup;
1336
- return ok(
1337
- new MaterialAbiRasterAdapter(
1338
- input.device,
1339
- pipelineLayout,
1340
- meshLayout,
1341
- instancesBindGroupLayout,
1342
- programs,
1343
- instancesBindGroup.value,
1344
- instanceBuffer.value,
1345
- instancePayload.byteLength,
1346
- input.visibleBuffer,
1347
- input.artifact,
1348
- deformation,
1349
- input.pipelineFactory,
1350
- input.paletteBuffer,
1351
- input.paletteBindingWindowBytes,
1352
- ),
1353
- );
1354
- }
1355
-
1356
- ensurePaletteBuffer(
1357
- paletteBuffer: Buffer | undefined,
1358
- paletteBindingWindowBytes: number | undefined,
1359
- ): Result<void, RhiError> {
1360
- if (this.deformation !== 'skin') return ok(undefined);
1361
- if (paletteBuffer === undefined || paletteBindingWindowBytes === undefined) {
1362
- return err(
1363
- new RhiError({
1364
- code: 'rhi-not-available',
1365
- expected: 'persistent skin palette buffer and binding window',
1366
- hint: 'publish a storage-backed SkinPaletteReceipt before the GPU-driven skinned lane',
1367
- }),
1368
- );
1369
- }
1370
- this.paletteBuffer = paletteBuffer;
1371
- this.paletteBindingWindowBytes = paletteBindingWindowBytes;
1372
- return ok(undefined);
1373
- }
1374
-
1375
- ensureVisibleBuffer(visibleBuffer: Buffer): Result<void, RhiError> {
1376
- if (this.visibleBuffer === visibleBuffer) return ok(undefined);
1377
- const created = this.device.createBindGroup({
1378
- label: 'standard-pbr-gpu-driven-visible-instances-bg',
1379
- layout: this.instancesBindGroupLayout,
1380
- entries: [
1381
- {
1382
- binding: 0,
1383
- resource: bufferBinding(this.instanceBuffer, 0, this.instanceBufferSize),
1384
- },
1385
- { binding: 1, resource: bufferBinding(visibleBuffer) },
1386
- { binding: 2, resource: bufferBinding(visibleBuffer) },
1387
- ],
1388
- });
1389
- if (!created.ok) return created;
1390
- this.instancesBindGroup = created.value;
1391
- this.visibleBuffer = visibleBuffer;
1392
- return ok(undefined);
1393
- }
1394
-
1395
- visibleWindowBindGroup(
1396
- visibleBase: number,
1397
- visibleCapacity: number,
1398
- ): Result<BindGroup, RhiError> {
1399
- return this.visibleWindowBindGroupForBuffer(this.visibleBuffer, visibleBase, visibleCapacity);
1400
- }
1401
-
1402
- visibleWindowBindGroupForBuffer(
1403
- visibleBuffer: Buffer,
1404
- visibleBase: number,
1405
- visibleCapacity: number,
1406
- ): Result<BindGroup, RhiError> {
1407
- const offset = visibleBase * 8;
1408
- const size = visibleCapacity * 8;
1409
- if (offset % 256 !== 0 || size <= 0) {
1410
- return err(
1411
- new RhiError({
1412
- code: 'rhi-descriptor-invalid',
1413
- expected: '256-byte-aligned non-empty visible window',
1414
- hint: `allocate each GPU-driven batch visible window on the portable storage-buffer boundary (base=${visibleBase}, capacity=${visibleCapacity}, offset=${offset}, size=${size})`,
1415
- }),
1416
- );
1417
- }
1418
- let groups = this.visibleWindowBindGroups.get(visibleBuffer);
1419
- if (groups === undefined) {
1420
- groups = new Map();
1421
- this.visibleWindowBindGroups.set(visibleBuffer, groups);
1422
- }
1423
- const key = `${visibleBase}:${visibleCapacity}`;
1424
- const cached = groups.get(key);
1425
- if (cached !== undefined) return ok(cached);
1426
- const created = this.device.createBindGroup({
1427
- label: `standard-pbr-gpu-driven-visible-window-${visibleBase}`,
1428
- layout: this.instancesBindGroupLayout,
1429
- entries: [
1430
- {
1431
- binding: 0,
1432
- resource: bufferBinding(this.instanceBuffer, 0, this.instanceBufferSize),
1433
- },
1434
- { binding: 1, resource: bufferBinding(visibleBuffer, offset, size) },
1435
- { binding: 2, resource: bufferBinding(visibleBuffer, offset, size) },
1436
- ],
1437
- });
1438
- if (!created.ok) return created;
1439
- groups.set(key, created.value);
1440
- return ok(created.value);
1441
- }
1442
-
1443
- pipeline(
1444
- format: TextureFormat,
1445
- sampleCount: 1 | 4,
1446
- layoutProjection: MeshGpuHandles['layoutProjection'],
1447
- shaderUvSetCount: number,
1448
- topology: GpuDrivenBatch['key']['topology'],
1449
- stripIndexFormat: 'uint16' | 'uint32' | undefined,
1450
- renderState: MaterialRenderState | undefined,
1451
- selectedArtifact: MaterialShaderArtifact = this.artifact,
1452
- additionalColorFormats: readonly TextureFormat[] = [],
1453
- ): Result<RenderPipeline, RhiError> {
1454
- const vertexLayout = deriveVertexBufferLayoutFromProjection(layoutProjection, {
1455
- shaderUvSetCount,
1456
- })[0];
1457
- if (vertexLayout === undefined) {
1458
- return err(
1459
- new RhiError({
1460
- code: 'shader-compile-failed',
1461
- expected: 'non-empty geometry VertexLayoutProjection',
1462
- hint: 'prepare a receipt-compatible mesh projection before Standard PBR raster',
1463
- }),
1464
- );
1465
- }
1466
- const receipt = selectedArtifact.receipt;
1467
- if (receipt === undefined) {
1468
- return err(
1469
- new RhiError({
1470
- code: 'rhi-not-available',
1471
- expected: 'selected MaterialProgramAbi receipt for GPU-driven raster',
1472
- hint: 'keep the draw on the producer recovery path until its ABI is published',
1473
- }),
1474
- );
1475
- }
1476
- const program =
1477
- this.programs === undefined
1478
- ? undefined
1479
- : resolveStandardPbrProgram(
1480
- this.programs,
1481
- this.artifact.material,
1482
- layoutProjection.attributes.some((attribute) => attribute.key === 'color'),
1483
- );
1484
- if (this.pipelineFactory === undefined && program === undefined) {
1485
- return err(
1486
- new RhiError({
1487
- code: 'rhi-not-available',
1488
- expected: 'producer-owned Standard PBR program matching the geometry vertex layout',
1489
- hint: 'publish both vertex-color variants for the current device generation',
1490
- }),
1491
- );
1492
- }
1493
- const key = `${format}:${additionalColorFormats.join(',')}|${sampleCount}|${vertexLayout.arrayStride}|${topology}|${stripIndexFormat ?? ''}|${JSON.stringify(renderState ?? null)}|${selectedArtifact.material}|${selectedArtifact.layoutIdentity}|${receipt.receiptIdentity}|${receipt.generation}|${selectedArtifact.variantSet ?? ''}|${materialArtifactProgramIdentity(selectedArtifact)}|${selectedArtifact.vertexEntry ?? receipt.sceneIndexEntry}|${selectedArtifact.fragmentEntry ?? ''}|${layoutProjection.digest}|uv${shaderUvSetCount}`;
1494
- const cached = this.pipelines.get(key);
1495
- if (cached !== undefined) return ok(cached);
1496
- if (this.pipelineFactory !== undefined) {
1497
- const pipelineEntry = this.pipelineFactory(
1498
- selectedArtifact.specializationKey ?? selectedArtifact.material,
1499
- false,
1500
- renderState,
1501
- topology,
1502
- stripIndexFormat,
1503
- selectedArtifact.variantSet,
1504
- 'forward',
1505
- undefined,
1506
- sampleCount,
1507
- format,
1508
- selectedArtifact.uvSetCount ?? shaderUvSetCount,
1509
- 'depth24plus-stencil8',
1510
- undefined,
1511
- layoutProjection,
1512
- // GPU-driven receipts are already producer-validated. Use the
1513
- // synchronous shader-module adapter for this submission seam so a
1514
- // newly loaded authored material cannot lose its first draw to the
1515
- // ordinary async pre-bake warm-up window.
1516
- 'immediate',
1517
- this.deformation === 'skin' ? 'gpu-driven-skin' : 'gpu-driven-pbr',
1518
- receipt.sceneIndexEntry,
1519
- additionalColorFormats,
1520
- selectedArtifact.vertexEntry,
1521
- selectedArtifact.fragmentEntry,
1522
- );
1523
- if (pipelineEntry === null) {
1524
- return err(
1525
- new RhiError({
1526
- code: 'rhi-not-available',
1527
- expected: `producer-owned scene-index pipeline for material '${selectedArtifact.material}'`,
1528
- hint: 'publish a matching program, entry, layout and resource contract before GPU admission',
1529
- }),
1530
- );
1531
- }
1532
- const expectedGroup2Contract = resolvePipelineGroup2Contract(selectedArtifact.wgsl);
1533
- if (pipelineEntry.group2Contract !== expectedGroup2Contract) {
1534
- return err(
1535
- new RhiError({
1536
- code: 'rhi-descriptor-invalid',
1537
- expected: `GPU-driven ${this.deformation === 'skin' ? 'skinned ' : ''}material pipeline group(2) ${expectedGroup2Contract} contract`,
1538
- hint: 'custom attachment or incompatible group(2) layouts remain on their specialized lane',
1539
- }),
1540
- );
1541
- }
1542
- this.pipelines.set(key, pipelineEntry.pipeline);
1543
- return ok(pipelineEntry.pipeline);
1544
- }
1545
- if (
1546
- program === undefined ||
1547
- selectedArtifact.material !== this.artifact.material ||
1548
- selectedArtifact.layoutIdentity !== this.artifact.layoutIdentity ||
1549
- selectedArtifact.receipt?.receiptIdentity !== this.artifact.receipt?.receiptIdentity
1550
- ) {
1551
- return err(
1552
- new RhiError({
1553
- code: 'rhi-not-available',
1554
- expected: 'selected material pipeline factory for the requested ABI artifact',
1555
- hint: 'do not pair a receipt-backed custom or color variant with the legacy fixed module',
1556
- }),
1557
- );
1558
- }
1559
- const pipeline = this.device.createRenderPipeline({
1560
- label: `standard-pbr-gpu-driven.${key}`,
1561
- layout: this.pipelineLayout,
1562
- vertex: {
1563
- module: program.module,
1564
- entryPoint: selectedArtifact.vertexEntry ?? receipt.sceneIndexEntry,
1565
- buffers: [
1566
- {
1567
- arrayStride: vertexLayout.arrayStride,
1568
- stepMode: 'vertex',
1569
- attributes: vertexLayout.attributes.map(({ shaderLocation, offset, format }) => ({
1570
- shaderLocation,
1571
- offset,
1572
- format: format as GPUVertexFormat,
1573
- })),
1574
- },
1575
- ],
1576
- },
1577
- fragment: {
1578
- module: program.module,
1579
- entryPoint: 'fs_main',
1580
- targets: [format, ...additionalColorFormats].map((format) => ({ format })),
1581
- },
1582
- primitive: {
1583
- topology,
1584
- cullMode: renderState?.cullMode ?? 'back',
1585
- frontFace: renderState?.frontFace ?? 'ccw',
1586
- ...(stripIndexFormat === undefined ? {} : { stripIndexFormat }),
1587
- },
1588
- depthStencil: {
1589
- format: 'depth24plus-stencil8',
1590
- depthWriteEnabled: renderState?.depthWriteEnabled ?? true,
1591
- depthCompare: renderState?.depthCompare ?? 'less',
1592
- },
1593
- ...(sampleCount === 4 ? { multisample: { count: 4 } } : {}),
1594
- });
1595
- if (!pipeline.ok) return pipeline;
1596
- this.pipelines.set(key, pipeline.value);
1597
- return pipeline;
1598
- }
1599
-
1600
- /**
1601
- * Drop only program-derived caches when a publication switches. Resource
1602
- * buffers stay alive until their normal fence retirement, while the next
1603
- * draw must obtain a PSO for the new artifact rather than reuse the old one.
1604
- */
1605
- invalidatePipelineCache(): void {
1606
- this.pipelines.clear();
1607
- }
1608
-
1609
- projectionBindGroup(
1610
- rows: Float32Array,
1611
- key?: string,
1612
- capacityRows?: number,
1613
- ): Result<BindGroup, RhiError> {
1614
- if (key !== undefined) {
1615
- const capacityBytes = Math.max(
1616
- MESH_SSBO_BYTES,
1617
- (capacityRows ?? Math.ceil(rows.byteLength / MESH_SSBO_BYTES)) * MESH_SSBO_BYTES,
1618
- );
1619
- const retained = this.stagedProjectionBindings.get(key) ?? this.projectionBindings.get(key);
1620
- if (retained !== undefined && retained.capacityBytes >= capacityBytes) {
1621
- const write = this.writeProjectionRows(key, retained, rows);
1622
- if (!write.ok) return write;
1623
- return ok(retained.bindGroup);
1624
- }
1625
- const created = this.createProjectionBinding(rows, capacityBytes);
1626
- if (!created.ok) return created;
1627
- this.stagedProjectionBindings.set(key, created.value);
1628
- return ok(created.value.bindGroup);
1629
- }
1630
- const created = this.createProjectionBinding(rows, Math.max(MESH_SSBO_BYTES, rows.byteLength));
1631
- if (!created.ok) return created;
1632
- return ok(created.value.bindGroup);
1633
- }
1634
-
1635
- refreshProjectionRows(key: string, rows: Float32Array): Result<void, RhiError> {
1636
- const retained = this.stagedProjectionBindings.get(key) ?? this.projectionBindings.get(key);
1637
- if (retained === undefined) return ok(undefined);
1638
- if (rows.byteLength > retained.capacityBytes) {
1639
- const capacityBytes = Math.max(
1640
- MESH_SSBO_BYTES,
1641
- Math.ceil(rows.byteLength / MESH_SSBO_BYTES) * MESH_SSBO_BYTES,
1642
- );
1643
- const replacement = this.createProjectionBinding(rows, capacityBytes);
1644
- if (!replacement.ok) return replacement;
1645
- const previousStaged = this.stagedProjectionBindings.get(key);
1646
- if (previousStaged !== undefined && previousStaged.buffer !== replacement.value.buffer) {
1647
- this.retireProjectionBuffer(previousStaged.buffer);
1648
- }
1649
- this.stagedProjectionBindings.set(key, replacement.value);
1650
- return ok(undefined);
1651
- }
1652
- return this.writeProjectionRows(key, retained, rows);
1653
- }
1654
-
1655
- sceneMeshBinding(key: string): GpuDrivenSceneMeshBinding {
1656
- const binding = this.stagedProjectionBindings.get(key) ?? this.projectionBindings.get(key);
1657
- if (binding === undefined)
1658
- throw new RhiError({
1659
- code: 'rhi-not-available',
1660
- expected: 'retained scene mesh projection',
1661
- hint: 'prepare this batch projection before binding its lighting resources',
1662
- });
1663
- return {
1664
- meshBuffer: binding.buffer,
1665
- meshBytes: binding.capacityBytes,
1666
- ...(this.deformation === 'skin' && this.paletteBuffer !== undefined
1667
- ? { paletteBuffer: this.paletteBuffer }
1668
- : {}),
1669
- };
1670
- }
1671
-
1672
- /** Stage the projection keys required by the frame being prepared. */
1673
- stageProjectionKeys(keys: ReadonlySet<string>): void {
1674
- this.stagedProjectionKeys = new Set(keys);
1675
- this.activeProjectionRowsAtStage.clear();
1676
- }
1677
-
1678
- /** Add a shadow projection key discovered while updating a view plan. */
1679
- noteProjectionKey(key: string): void {
1680
- this.stagedProjectionKeys.add(key);
1681
- }
1682
-
1683
- /** Promote staged projection resources after the command buffer submits. */
1684
- commitProjectionBindings(): void {
1685
- const keep = new Set(this.stagedProjectionKeys);
1686
- for (const [key, staged] of this.stagedProjectionBindings) {
1687
- if (!keep.has(key)) {
1688
- this.retireProjectionBuffer(staged.buffer);
1689
- continue;
1690
- }
1691
- const previous = this.projectionBindings.get(key);
1692
- this.projectionBindings.set(key, staged);
1693
- if (previous !== undefined && previous.buffer !== staged.buffer) {
1694
- this.retireProjectionBuffer(previous.buffer);
1695
- }
1696
- }
1697
- this.stagedProjectionBindings.clear();
1698
- for (const [key, active] of this.projectionBindings) {
1699
- if (!keep.has(key)) {
1700
- this.projectionBindings.delete(key);
1701
- this.retireProjectionBuffer(active.buffer);
1702
- }
1703
- }
1704
- this.stagedProjectionKeys.clear();
1705
- this.activeProjectionRowsAtStage.clear();
1706
- }
1707
-
1708
- /** Discard resources created for an aborted graph while retaining LKG rows. */
1709
- abortProjectionBindings(): void {
1710
- for (const [key, rows] of this.activeProjectionRowsAtStage) {
1711
- const active = this.projectionBindings.get(key);
1712
- if (active === undefined) continue;
1713
- const restored = this.device.queue.writeBuffer(active.buffer, 0, rows);
1714
- if (restored.ok) active.rows = new Float32Array(rows);
1715
- }
1716
- for (const staged of this.stagedProjectionBindings.values()) {
1717
- this.retireProjectionBuffer(staged.buffer);
1718
- }
1719
- this.stagedProjectionBindings.clear();
1720
- this.stagedProjectionKeys.clear();
1721
- this.activeProjectionRowsAtStage.clear();
1722
- }
1723
-
1724
- private createProjectionBinding(
1725
- rows: Float32Array,
1726
- capacityBytes: number,
1727
- ): Result<
1728
- {
1729
- readonly buffer: Buffer;
1730
- readonly bindGroup: BindGroup;
1731
- readonly capacityBytes: number;
1732
- rows: Float32Array;
1733
- },
1734
- RhiError
1735
- > {
1736
- const buffer = this.device.createBuffer({
1737
- label: 'standard-pbr-gpu-driven-scene-meshes',
1738
- size: capacityBytes,
1739
- usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
1740
- });
1741
- if (!buffer.ok) return buffer;
1742
- const write = this.device.queue.writeBuffer(buffer.value, 0, rows);
1743
- if (!write.ok) {
1744
- this.destroyProjectionBuffer(buffer.value);
1745
- return write;
1746
- }
1747
- this.projectionBuffers.add(buffer.value);
1748
- const entries = [
1749
- { binding: 0, resource: bufferBinding(buffer.value, 0, capacityBytes) },
1750
- ...(this.deformation === 'skin' &&
1751
- this.paletteBuffer !== undefined &&
1752
- this.paletteBindingWindowBytes !== undefined
1753
- ? [
1754
- {
1755
- binding: 1,
1756
- // The scene-index second lane is a global palette row. Bind the
1757
- // complete arena so rows beyond the legacy 255-joint window
1758
- // remain addressable; the shader performs the row selection.
1759
- resource: bufferBinding(this.paletteBuffer),
1760
- },
1761
- {
1762
- binding: 2,
1763
- // The dedicated GPU-driven skin layout keeps the current /
1764
- // previous palette pair used by the direct skin layout. The
1765
- // scene-index shader currently reads the current arena, but
1766
- // the shared layout still requires a complete binding shape.
1767
- resource: bufferBinding(this.paletteBuffer),
1768
- },
1769
- ]
1770
- : []),
1771
- ];
1772
- const bindGroup = this.device.createBindGroup({
1773
- label: 'standard-pbr-gpu-driven-scene-mesh-bg',
1774
- layout: this.meshLayout,
1775
- entries,
1776
- });
1777
- if (!bindGroup.ok) {
1778
- this.destroyProjectionBuffer(buffer.value);
1779
- return bindGroup;
1780
- }
1781
- return ok({
1782
- buffer: buffer.value,
1783
- bindGroup: bindGroup.value,
1784
- capacityBytes,
1785
- rows: new Float32Array(rows),
1786
- });
1787
- }
1788
-
1789
- private writeProjectionRows(
1790
- key: string,
1791
- binding: {
1792
- readonly buffer: Buffer;
1793
- readonly bindGroup: BindGroup;
1794
- readonly capacityBytes: number;
1795
- rows: Float32Array;
1796
- },
1797
- rows: Float32Array,
1798
- ): Result<void, RhiError> {
1799
- // Active bindings are the last-known-good graph resources. Keep one CPU
1800
- // snapshot so an aborted candidate can restore their exact contents
1801
- // without allocating a second GPU buffer on every stable frame.
1802
- if (
1803
- this.stagedProjectionBindings.get(key) === undefined &&
1804
- !this.activeProjectionRowsAtStage.has(key)
1805
- ) {
1806
- this.activeProjectionRowsAtStage.set(key, new Float32Array(binding.rows));
1807
- }
1808
- const write = this.device.queue.writeBuffer(binding.buffer, 0, rows);
1809
- if (!write.ok) return write;
1810
- binding.rows = new Float32Array(rows);
1811
- return ok(undefined);
1812
- }
1813
-
1814
- dispose(): void {
1815
- this.projectionDisposed = true;
1816
- for (const binding of this.projectionBindings.values()) {
1817
- this.destroyProjectionBuffer(binding.buffer);
1818
- }
1819
- for (const binding of this.stagedProjectionBindings.values()) {
1820
- this.destroyProjectionBuffer(binding.buffer);
1821
- }
1822
- for (const buffer of this.projectionBuffers) this.destroyProjectionBuffer(buffer);
1823
- for (const buffer of this.pendingProjectionRetirements) {
1824
- this.destroyProjectionBuffer(buffer);
1825
- }
1826
- this.projectionBindings.clear();
1827
- this.stagedProjectionBindings.clear();
1828
- this.activeProjectionRowsAtStage.clear();
1829
- this.projectionBuffers.clear();
1830
- this.pendingProjectionRetirements.clear();
1831
- this.device.destroyBuffer(this.instanceBuffer);
1832
- }
1833
-
1834
- private retireProjectionBuffer(buffer: Buffer): void {
1835
- if (this.destroyedProjectionBuffers.has(buffer)) return;
1836
- this.projectionBuffers.delete(buffer);
1837
- if (this.projectionDisposed) {
1838
- this.destroyProjectionBuffer(buffer);
1839
- return;
1840
- }
1841
- if (!this.pendingProjectionRetirements.add(buffer)) return;
1842
- void this.device.queue.onSubmittedWorkDone().then(
1843
- () => {
1844
- this.pendingProjectionRetirements.delete(buffer);
1845
- this.destroyProjectionBuffer(buffer);
1846
- },
1847
- () => {
1848
- this.pendingProjectionRetirements.delete(buffer);
1849
- this.destroyProjectionBuffer(buffer);
1850
- },
1851
- );
1852
- }
1853
-
1854
- private destroyProjectionBuffer(buffer: Buffer): void {
1855
- if (this.destroyedProjectionBuffers.has(buffer)) return;
1856
- this.destroyedProjectionBuffers.add(buffer);
1857
- this.projectionBuffers.delete(buffer);
1858
- this.device.destroyBuffer(buffer);
1859
- }
1860
-
1861
- get instancesFrameGroup(): BindGroup {
1862
- return this.instancesBindGroup;
1863
- }
1864
-
1865
- get kind(): 'rigid' | 'skin' {
1866
- return this.deformation;
1867
- }
1868
- }
1869
1332
 
1870
1333
  export interface PreparedGpuDrivenFrame {
1871
1334
  readonly topologySignature: string;
1335
+ /** Composition-local world indices mapped to the stable identities used by draw keys. */
1336
+ readonly worldKeys: readonly number[];
1872
1337
  /** True when every source draw item/instance is claimed by the GPU lane. */
1873
1338
  readonly ownsAllDrawItems: boolean;
1874
1339
  /** True only when the extracted ShadowCaster channel is fully represented. */
@@ -1876,6 +1341,8 @@ export interface PreparedGpuDrivenFrame {
1876
1341
  /** Concrete main-pass draw identities claimed by the GPU projection. */
1877
1342
  readonly drawKeys?: ReadonlySet<string>;
1878
1343
  readonly standardPbrFrameResources: GpuDrivenStandardPbrFrameResources;
1344
+ /** Published Surface admission and its pass lane decision. */
1345
+ readonly surfaceSubmission?: SurfaceGpuSubmissionAdmission;
1879
1346
  readonly occlusion?: OcclusionFrameProjection;
1880
1347
  /** @internal Commits a replacement generation only after graph promotion. */
1881
1348
  readonly _commitResourceReplacement: () => void;
@@ -1883,6 +1350,8 @@ export interface PreparedGpuDrivenFrame {
1883
1350
  readonly _commitGpuResourceReplacement?: () => void;
1884
1351
  /** @internal Discards staged shadow cache publication after a failed frame. */
1885
1352
  readonly _abortResourceReplacement?: () => void;
1353
+ /** @internal Marks validated dynamic ranges consumed only after queue submit. */
1354
+ readonly _consumeSurfaceDynamicInput?: (frameNumber: number) => Result<void, RhiError>;
1886
1355
  /** Concrete ShadowCaster keys claimed by the GPU subset for this frame. */
1887
1356
  readonly shadowDrawKeys?: ReadonlySet<string>;
1888
1357
  /** View-scoped ShadowCaster claims consumed by each shadow record pass. */
@@ -1903,6 +1372,7 @@ export interface PreparedGpuDrivenFrame {
1903
1372
  graph: RenderGraphBuilder<RenderPipelineFrame>,
1904
1373
  format: TextureFormat,
1905
1374
  sampleCount: 1 | 4,
1375
+ surfaceSubmissionObservation?: () => SurfaceSubmissionCandidate | undefined,
1906
1376
  additionalColorFormats?: readonly TextureFormat[],
1907
1377
  ): Result<RenderPipelineGpuDrivenProjection, RenderGraphError | RhiError>;
1908
1378
  }
@@ -1936,6 +1406,7 @@ interface GpuDrivenCpuValidationTelemetryCache {
1936
1406
  readonly cacheKey: string;
1937
1407
  readonly validated: readonly { readonly source: RenderableSnapshot }[];
1938
1408
  readonly gpuOwnedDrawKeys: ReadonlySet<string>;
1409
+ readonly worldKeys: readonly number[] | undefined;
1939
1410
  readonly gpuOwnedDrawCount: number;
1940
1411
  readonly totalDrawItems: number;
1941
1412
  readonly gpuOwnedRows: number;
@@ -2047,6 +1518,26 @@ export class GpuDrivenProduction {
2047
1518
  readonly value: FilteredProductionPlan;
2048
1519
  }
2049
1520
  | undefined;
1521
+ private surfaceFrameRangeCache:
1522
+ | {
1523
+ readonly filtered: FilteredProductionPlan;
1524
+ readonly slots: PersistentGpuDrivenState['slots'];
1525
+ readonly page: ReadonlyDynamicInputPage | undefined;
1526
+ readonly projectionRevision: number;
1527
+ readonly value: SurfaceFrameProjection;
1528
+ }
1529
+ | undefined;
1530
+ private consumedSurfaceInput:
1531
+ | {
1532
+ readonly page: ReadonlyDynamicInputPage;
1533
+ readonly projectionRevision: number;
1534
+ readonly contentRevision: number;
1535
+ readonly bufferGeneration: number;
1536
+ readonly deviceGeneration: number;
1537
+ }
1538
+ | undefined;
1539
+ private surfaceFrameRangeBuilds = 0;
1540
+ private surfaceFrameRangeMemberScans = 0;
2050
1541
  private shadowFilteredCache:
2051
1542
  | {
2052
1543
  readonly source: SubmissionPlan;
@@ -2099,6 +1590,8 @@ export class GpuDrivenProduction {
2099
1590
  >();
2100
1591
  private shadowDrawKeysByView = new Map<string, ReadonlySet<string>>();
2101
1592
  private shadowExpectedDrawKeys = new Set<string>();
1593
+ private surfaceSubmission: SurfaceGpuSubmissionAdmission | undefined;
1594
+ private surfaceArtifactInspection: GpuDrivenProductionInspection['surfaceArtifact'];
2102
1595
  private shadowBatchProjectionSource: SubmissionPlan | undefined;
2103
1596
  private skinPaletteBuffer: Buffer | undefined;
2104
1597
  private committedPaletteContentRevision: number | undefined;
@@ -2138,6 +1631,110 @@ export class GpuDrivenProduction {
2138
1631
  private readonly recoveryCount = 0,
2139
1632
  ) {}
2140
1633
 
1634
+ private resolveSurfaceFrameRanges(
1635
+ filtered: FilteredProductionPlan,
1636
+ frame: SurfaceDynamicInputFrame | undefined,
1637
+ scene: PersistentGpuDrivenState,
1638
+ ): Result<SurfaceFrameProjection, RhiError> {
1639
+ const ranges = frame?.ranges ?? [];
1640
+ const projectionRevision = frame?.projectionRevision ?? 0;
1641
+ if (!Number.isSafeInteger(projectionRevision) || projectionRevision < 0) {
1642
+ return err(
1643
+ surfaceDynamicInputError(
1644
+ 'the Surface projection revision is a non-negative safe integer',
1645
+ 'advance the producer-owned projection revision only when membership or addresses change',
1646
+ ),
1647
+ );
1648
+ }
1649
+ const cached = this.surfaceFrameRangeCache;
1650
+ if (
1651
+ cached?.filtered === filtered &&
1652
+ cached.slots === scene.slots &&
1653
+ cached.page === frame?.page &&
1654
+ cached.projectionRevision === projectionRevision
1655
+ ) {
1656
+ return ok(cached.value);
1657
+ }
1658
+ this.surfaceFrameRangeBuilds += 1;
1659
+ for (const prepared of filtered.batches) {
1660
+ if (!isSingleLayerMediumBatch(prepared)) continue;
1661
+ for (const candidate of prepared.batch.candidates) {
1662
+ if (
1663
+ (candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
1664
+ .submitAdmission !== false
1665
+ ) {
1666
+ this.surfaceFrameRangeMemberScans += 1;
1667
+ }
1668
+ }
1669
+ }
1670
+ const built = buildSurfaceFrameRanges(filtered, ranges, scene);
1671
+ if (!built.ok) return built;
1672
+ let capacity = 1;
1673
+ const consumptionRanges: DynamicInputRange[] = [];
1674
+ for (const entry of built.value) {
1675
+ capacity = Math.max(capacity, entry.frameIndex + 1);
1676
+ if (entry.range !== undefined) consumptionRanges.push(entry.range);
1677
+ }
1678
+ const value = {
1679
+ ranges: built.value,
1680
+ consumptionRanges: Object.freeze(consumptionRanges),
1681
+ capacity,
1682
+ };
1683
+ return ok(value);
1684
+ }
1685
+
1686
+ private rememberSurfaceFrameProjection(
1687
+ filtered: FilteredProductionPlan,
1688
+ frame: SurfaceDynamicInputFrame | undefined,
1689
+ scene: PersistentGpuDrivenState,
1690
+ value: SurfaceFrameProjection,
1691
+ ): void {
1692
+ this.surfaceFrameRangeCache = {
1693
+ filtered,
1694
+ slots: scene.slots,
1695
+ page: frame?.page,
1696
+ projectionRevision: frame?.projectionRevision ?? 0,
1697
+ value,
1698
+ };
1699
+ }
1700
+
1701
+ private consumeSurfaceDynamicInput(
1702
+ page: ReadonlyDynamicInputPage,
1703
+ ranges: readonly DynamicInputRange[],
1704
+ projectionRevision: number,
1705
+ frameNumber: number,
1706
+ ): Result<void, RhiError> {
1707
+ const consumed = this.consumedSurfaceInput;
1708
+ if (
1709
+ consumed?.page === page &&
1710
+ consumed.projectionRevision === projectionRevision &&
1711
+ consumed.contentRevision === page.contentRevision &&
1712
+ consumed.bufferGeneration === page.bufferGeneration &&
1713
+ consumed.deviceGeneration === page.deviceGeneration
1714
+ ) {
1715
+ return ok(undefined);
1716
+ }
1717
+ for (const range of ranges) {
1718
+ const receipt = page.consume(range, frameNumber);
1719
+ if (!receipt.ok) {
1720
+ return err(
1721
+ surfaceDynamicInputError(
1722
+ 'each submitted Surface dynamic range was uploaded before graph consumption',
1723
+ receipt.error.hint,
1724
+ ),
1725
+ );
1726
+ }
1727
+ }
1728
+ this.consumedSurfaceInput = {
1729
+ page,
1730
+ projectionRevision,
1731
+ contentRevision: page.contentRevision,
1732
+ bufferGeneration: page.bufferGeneration,
1733
+ deviceGeneration: page.deviceGeneration,
1734
+ };
1735
+ return ok(undefined);
1736
+ }
1737
+
2141
1738
  /**
2142
1739
  * Reuse the last producer-resolved residency rows when all source identity
2143
1740
  * and generation fences still match. Renderables and dispatch entries are
@@ -2224,6 +1821,10 @@ export class GpuDrivenProduction {
2224
1821
  readonly meshBySlot: ReadonlyMap<number, MeshGpuHandles>;
2225
1822
  readonly viewBindGroupLayout: BindGroupLayout;
2226
1823
  readonly meshResidencyEpoch: number;
1824
+ /** Optional for legacy Standard callers; omitted means non-HDRP. */
1825
+ readonly hdrp?: boolean;
1826
+ /** Explicit Standard topology; cluster resources are bound by main-pass. */
1827
+ readonly clustered?: boolean;
2227
1828
  readonly materialBindingClasses?: ReadonlyMap<string, string>;
2228
1829
  /** Cooked producer artifact selected from the frame material catalog. */
2229
1830
  readonly materialArtifact?: MaterialShaderArtifact;
@@ -2237,6 +1838,8 @@ export class GpuDrivenProduction {
2237
1838
  readonly materialArtifacts?: ReadonlyMap<string, MaterialShaderArtifact>;
2238
1839
  /** Per-draw shadow-pass artifacts keyed by the same prepared identity. */
2239
1840
  readonly shadowMaterialArtifacts?: ReadonlyMap<string, MaterialShaderArtifact>;
1841
+ /** Retained ProbeBlend record page; slot N is addressed at aligned lane N+1. */
1842
+ readonly probeBlendRecordBuffer?: Buffer;
2240
1843
  readonly materialPipelineState?: PipelineState;
2241
1844
  readonly standardPbrPipelineState?: PipelineState;
2242
1845
  /** Existing renderer owner that builds the selected artifact PSO/BGL. */
@@ -2256,7 +1859,14 @@ export class GpuDrivenProduction {
2256
1859
  readonly occlusion?: OcclusionFrameProjection;
2257
1860
  /** Per-frame deltas from the existing RenderScene/GPU Scene/palette owners. */
2258
1861
  readonly structureMetrics?: GpuDrivenStructureMetrics;
1862
+ /** World/App frame-time snapshot consumed by authored Surface code. */
1863
+ readonly frameTime?: number;
1864
+ /** Current RHI device generation used for Surface admission. */
1865
+ readonly deviceGeneration?: number;
1866
+ /** Renderer-owned generic read-only Surface page and instance ranges. */
1867
+ readonly surfaceDynamicInput?: SurfaceDynamicInputFrame;
2259
1868
  }): Result<PreparedGpuDrivenFrame | undefined, RhiError | GpuDrivenPreparationError> {
1869
+ this.surfaceArtifactInspection = undefined;
2260
1870
  this.gpuOwnedSnapshotsMaterialized = 0;
2261
1871
  this.filteredPlanBuilds = 0;
2262
1872
  this.batchBindGroupCreates = 0;
@@ -2285,6 +1895,7 @@ export class GpuDrivenProduction {
2285
1895
  this.laneReason = 'capability';
2286
1896
  this.submissionState = 'idle';
2287
1897
  this.shadowExpectedDrawKeys = new Set();
1898
+ this.surfaceSubmission = undefined;
2288
1899
  const capability = classifyRecoveryCapability({
2289
1900
  generation: 0,
2290
1901
  shaderCompilation: true,
@@ -2295,6 +1906,7 @@ export class GpuDrivenProduction {
2295
1906
  });
2296
1907
  if (
2297
1908
  input.scene === undefined ||
1909
+ (input.hdrp === true && input.clustered !== true) ||
2298
1910
  selectGpuLodLane(this.device.caps) === 'cpu' ||
2299
1911
  capability.status === 'disabled'
2300
1912
  ) {
@@ -2332,6 +1944,57 @@ export class GpuDrivenProduction {
2332
1944
  const materialArtifact = input.materialArtifact ?? input.standardPbrArtifact;
2333
1945
  const skinArtifact = input.materialSkinArtifact ?? input.standardPbrSkinArtifact;
2334
1946
  const materialPipelineState = input.materialPipelineState ?? input.standardPbrPipelineState;
1947
+ const surfaceArtifact = [
1948
+ materialArtifact,
1949
+ skinArtifact,
1950
+ ...(input.materialArtifacts === undefined ? [] : input.materialArtifacts.values()),
1951
+ ].find((artifact) => artifact?.receipt?.surface?.model === 'single-layer-medium');
1952
+ this.surfaceArtifactInspection =
1953
+ surfaceArtifact?.receipt === undefined
1954
+ ? undefined
1955
+ : Object.freeze({
1956
+ material: surfaceArtifact.material,
1957
+ specializationKey: surfaceArtifact.specializationKey,
1958
+ variantSet: surfaceArtifact.variantSet,
1959
+ layoutIdentity: surfaceArtifact.layoutIdentity,
1960
+ receiptIdentity: surfaceArtifact.receipt.receiptIdentity,
1961
+ receiptGeneration: surfaceArtifact.receipt.generation,
1962
+ directEntry: surfaceArtifact.receipt.directEntry,
1963
+ sceneIndexEntry: surfaceArtifact.receipt.sceneIndexEntry,
1964
+ });
1965
+ const surfaceDynamicInputLayout = surfaceArtifact?.receipt?.surface?.dynamicInput?.layout;
1966
+ if (surfaceArtifact?.receipt?.surface !== undefined) {
1967
+ const deviceGeneration = input.deviceGeneration ?? 1;
1968
+ const admitted = admitSingleLayerMediumSubmission({
1969
+ abi: surfaceArtifact.receipt,
1970
+ caps: {
1971
+ compute: this.device.caps.compute,
1972
+ storageBuffer: this.device.caps.storageBuffer,
1973
+ indirectDrawing: this.device.caps.indirectDrawing,
1974
+ },
1975
+ sceneIndexReady: surfaceArtifact.receipt.sceneIndexEntry.length > 0,
1976
+ resourcesReady:
1977
+ surfaceArtifact.receipt.reflection.layoutIdentity === surfaceArtifact.layoutIdentity,
1978
+ dynamicInputReady:
1979
+ surfaceArtifact.receipt.surface.dynamicInput === undefined ||
1980
+ input.surfaceDynamicInput !== undefined,
1981
+ deviceGeneration,
1982
+ preparedDeviceGeneration: deviceGeneration,
1983
+ });
1984
+ if (!admitted.ok) {
1985
+ this.lane = 'blocked';
1986
+ this.laneReason = 'resource-not-ready';
1987
+ const failure = new GpuDrivenPreparationError('resource-not-ready', {
1988
+ reason: 'material-resource-missing',
1989
+ owner: 'material',
1990
+ expected: admitted.error.expected,
1991
+ actual: JSON.stringify(admitted.error.actual),
1992
+ });
1993
+ this.preparationFailure = preparationFailureInspection(failure);
1994
+ return err(failure);
1995
+ }
1996
+ this.surfaceSubmission = admitted.value;
1997
+ }
2335
1998
  const artifactSignature = `${materialArtifactSignature(
2336
1999
  input.materialArtifacts,
2337
2000
  materialArtifact,
@@ -2590,6 +2253,20 @@ export class GpuDrivenProduction {
2590
2253
  }),
2591
2254
  );
2592
2255
  }
2256
+ const probeBlendRequested = filtered.batches.some(({ artifact }) =>
2257
+ requiresProbeBlendRecord(artifact),
2258
+ );
2259
+ if (probeBlendRequested && input.probeBlendRecordBuffer === undefined) {
2260
+ return err(
2261
+ new GpuDrivenPreparationError('resource-not-ready', {
2262
+ reason: 'material-resource-missing',
2263
+ owner: 'material',
2264
+ expected: 'the retained ProbeBlend record page for every scene-index probe draw',
2265
+ actual: 'the prepared frame did not publish its ProbeBlend record page',
2266
+ recovery: 'retry-same-draw',
2267
+ }),
2268
+ );
2269
+ }
2593
2270
  const committedArtifactChanged =
2594
2271
  this.committedProjectionSource !== undefined &&
2595
2272
  this.committedProjectionSource.materialArtifactSignature !== artifactSignature;
@@ -2615,10 +2292,15 @@ export class GpuDrivenProduction {
2615
2292
  ...(input.materialPipelineFactory === undefined
2616
2293
  ? {}
2617
2294
  : { pipelineFactory: input.materialPipelineFactory }),
2295
+ ...(input.probeBlendRecordBuffer === undefined
2296
+ ? {}
2297
+ : { probeBlendRecordBuffer: input.probeBlendRecordBuffer }),
2618
2298
  });
2619
2299
  if (!created.ok) return created;
2620
2300
  this.materialRaster = created.value;
2621
2301
  } else {
2302
+ const probe = this.materialRaster.ensureProbeBlendRecordBuffer(input.probeBlendRecordBuffer);
2303
+ if (!probe.ok) return probe;
2622
2304
  const rebound = this.materialRaster.ensureVisibleBuffer(visibleBuffer);
2623
2305
  if (!rebound.ok) return rebound;
2624
2306
  }
@@ -2664,6 +2346,10 @@ export class GpuDrivenProduction {
2664
2346
  deformation: 'skin',
2665
2347
  paletteBuffer: skinSnapshot.buffer,
2666
2348
  paletteBindingWindowBytes: skinAllocator.bindingWindowBytes,
2349
+ sharedSurfaceFrame: materialRaster.surfaceDynamicSharedFrameState,
2350
+ ...(input.probeBlendRecordBuffer === undefined
2351
+ ? {}
2352
+ : { probeBlendRecordBuffer: input.probeBlendRecordBuffer }),
2667
2353
  ...(input.materialPipelineFactory === undefined
2668
2354
  ? {}
2669
2355
  : { pipelineFactory: input.materialPipelineFactory }),
@@ -2671,6 +2357,8 @@ export class GpuDrivenProduction {
2671
2357
  if (!created.ok) return created;
2672
2358
  this.skinRaster = created.value;
2673
2359
  } else {
2360
+ const probe = this.skinRaster.ensureProbeBlendRecordBuffer(input.probeBlendRecordBuffer);
2361
+ if (!probe.ok) return probe;
2674
2362
  const rebound = this.skinRaster.ensureVisibleBuffer(visibleBuffer);
2675
2363
  if (!rebound.ok) return rebound;
2676
2364
  const palette = this.skinRaster.ensurePaletteBuffer(
@@ -2687,6 +2375,52 @@ export class GpuDrivenProduction {
2687
2375
  }
2688
2376
  }
2689
2377
  const skinRaster = this.skinRaster;
2378
+ if (this.surfaceSubmission !== undefined) {
2379
+ const surfaceFrameRangesResult = this.resolveSurfaceFrameRanges(
2380
+ filtered,
2381
+ input.surfaceDynamicInput,
2382
+ input.scene,
2383
+ );
2384
+ if (!surfaceFrameRangesResult.ok) return surfaceFrameRangesResult;
2385
+ const surfaceFrameProjection = surfaceFrameRangesResult.value;
2386
+ const surfaceFrameRanges = surfaceFrameProjection.ranges;
2387
+ const surfaceFrameCapacity = Math.max(
2388
+ filtered.plan.visibleCapacity,
2389
+ surfaceFrameProjection.capacity,
2390
+ );
2391
+ const surfaceInput = materialRaster.ensureSurfaceDynamicInput(
2392
+ input.surfaceDynamicInput,
2393
+ surfaceFrameCapacity,
2394
+ input.surfaceDynamicInput?.frameTime ?? input.frameTime ?? 0,
2395
+ surfaceDynamicInputLayout,
2396
+ input.deviceGeneration ?? 1,
2397
+ surfaceFrameRanges,
2398
+ );
2399
+ if (!surfaceInput.ok) return surfaceInput;
2400
+ if (skinRaster !== undefined) {
2401
+ const skinSurfaceInput = skinRaster.ensureSurfaceDynamicInput(
2402
+ input.surfaceDynamicInput,
2403
+ surfaceFrameCapacity,
2404
+ input.surfaceDynamicInput?.frameTime ?? input.frameTime ?? 0,
2405
+ surfaceDynamicInputLayout,
2406
+ input.deviceGeneration ?? 1,
2407
+ surfaceFrameRanges,
2408
+ );
2409
+ if (!skinSurfaceInput.ok) return skinSurfaceInput;
2410
+ }
2411
+ const committedSurfaceInput = materialRaster.commitSurfaceDynamicInput();
2412
+ if (!committedSurfaceInput.ok) return committedSurfaceInput;
2413
+ if (skinRaster !== undefined) {
2414
+ const committedSkinSurfaceInput = skinRaster.commitSurfaceDynamicInput();
2415
+ if (!committedSkinSurfaceInput.ok) return committedSkinSurfaceInput;
2416
+ }
2417
+ this.rememberSurfaceFrameProjection(
2418
+ filtered,
2419
+ input.surfaceDynamicInput,
2420
+ input.scene,
2421
+ surfaceFrameProjection,
2422
+ );
2423
+ }
2690
2424
  const shadowViewPool = this.shadowViews;
2691
2425
  if (shadowViewPool === undefined) {
2692
2426
  return err(
@@ -2842,25 +2576,83 @@ export class GpuDrivenProduction {
2842
2576
  )
2843
2577
  .join('|') ?? '';
2844
2578
  const production = this;
2579
+ const surfaceDynamicInputForCommit =
2580
+ this.surfaceSubmission !== undefined && input.surfaceDynamicInput !== undefined
2581
+ ? {
2582
+ page: input.surfaceDynamicInput.page,
2583
+ ranges: this.surfaceFrameRangeCache?.value.consumptionRanges ?? [],
2584
+ projectionRevision: input.surfaceDynamicInput.projectionRevision,
2585
+ }
2586
+ : undefined;
2845
2587
  return ok({
2846
2588
  get topologySignature() {
2847
2589
  const shadow = shadowTopologySignature();
2848
2590
  return shadow.length === 0 ? signature : `${signature}|shadow=${shadow}`;
2849
2591
  },
2850
2592
  ownsAllDrawItems: filtered.ownsAllDrawItems,
2593
+ worldKeys: scene.worldKeys ?? [],
2851
2594
  ...(input.occlusion === undefined ? {} : { occlusion: input.occlusion }),
2852
2595
  ownsAllShadowCasters,
2853
2596
  drawKeys: filtered.drawKeys,
2854
2597
  shadowDrawKeys: shadowFiltered.shadowDrawKeys,
2598
+ ...(this.surfaceSubmission === undefined
2599
+ ? {}
2600
+ : { surfaceSubmission: this.surfaceSubmission }),
2855
2601
  get shadowDrawKeysByView() {
2856
2602
  return new Map(production.shadowDrawKeysByView);
2857
2603
  },
2858
2604
  standardPbrFrameResources: {
2605
+ resourceGeneration: viewInspection.resourceGeneration,
2859
2606
  materialBindGroups: [],
2860
2607
  materialSlotIndicesByEntity: new Map(),
2861
2608
  instancesBindGroup: materialRaster.instancesFrameGroup,
2862
2609
  materialStride: MATERIAL_PER_ENTITY_STRIDE,
2863
2610
  sceneMaterialBuffer: scene.scene.materialBuffer,
2611
+ ...(this.surfaceSubmission === undefined
2612
+ ? {}
2613
+ : {
2614
+ surfaceDynamicInput: {
2615
+ page: materialRaster.surfaceDynamicInputBuffer,
2616
+ frame: materialRaster.surfaceDynamicFrameBuffer,
2617
+ sharedFrame: materialRaster.surfaceDynamicSharedFrameBuffer,
2618
+ pageBytes: materialRaster.surfaceDynamicInputBufferSize,
2619
+ frameBytes: materialRaster.surfaceDynamicFrameBufferSize,
2620
+ sharedFrameBytes: materialRaster.surfaceDynamicSharedFrameBufferSize,
2621
+ },
2622
+ surfaceDirectInstances: (directInput) => {
2623
+ const resolved = materialRaster.directSurfaceInstances(directInput);
2624
+ if (!resolved.ok) return resolved;
2625
+ const firstInstanceOrdinal = directInput.firstInstanceOrdinal ?? 0;
2626
+ const memberIds: string[] = [];
2627
+ for (let index = 0; index < directInput.instanceCount; index += 1) {
2628
+ const instanceOrdinal = firstInstanceOrdinal + index;
2629
+ const frameRange = this.surfaceFrameRangeCache?.value.ranges.find(
2630
+ ({ directMember }) =>
2631
+ directMember.worldId === directInput.worldId &&
2632
+ directMember.entityKey === directInput.entityKey &&
2633
+ directMember.drawItemIndex === directInput.drawItemIndex &&
2634
+ directMember.instanceOrdinal === instanceOrdinal,
2635
+ );
2636
+ if (frameRange === undefined) {
2637
+ return err(
2638
+ surfaceDynamicInputError(
2639
+ 'every direct Surface command resolves the retained public member identity',
2640
+ `rebuild the Surface projection for world=${directInput.worldId}, entity=${directInput.entityKey}, draw=${directInput.drawItemIndex}, instance=${instanceOrdinal}`,
2641
+ ),
2642
+ );
2643
+ }
2644
+ memberIds.push(
2645
+ JSON.stringify([
2646
+ frameRange.directMember.worldIdentity,
2647
+ frameRange.directMember.entityKey,
2648
+ frameRange.directMember.drawItemIndex,
2649
+ frameRange.directMember.instanceOrdinal,
2650
+ ]),
2651
+ );
2652
+ }
2653
+ return ok({ ...resolved.value, memberIds: Object.freeze(memberIds) });
2654
+ },
2655
+ }),
2864
2656
  },
2865
2657
  _commitGpuResourceReplacement: () => {
2866
2658
  view._commitResourceReplacement();
@@ -2876,6 +2668,8 @@ export class GpuDrivenProduction {
2876
2668
  this.lastKnownGoodGeneration = view.inspect().resourceGeneration;
2877
2669
  },
2878
2670
  _abortResourceReplacement: () => {
2671
+ materialRaster.abortSurfaceDynamicInput();
2672
+ skinRaster?.abortSurfaceDynamicInput();
2879
2673
  shadowViewPool._abortResourceReplacement();
2880
2674
  materialRaster.abortProjectionBindings();
2881
2675
  skinRaster?.abortProjectionBindings();
@@ -2883,6 +2677,18 @@ export class GpuDrivenProduction {
2883
2677
  this.submissionState = 'aborted';
2884
2678
  this.retryCount += 1;
2885
2679
  },
2680
+ ...(surfaceDynamicInputForCommit === undefined ||
2681
+ surfaceDynamicInputForCommit.ranges.length === 0
2682
+ ? {}
2683
+ : {
2684
+ _consumeSurfaceDynamicInput: (frameNumber: number) =>
2685
+ this.consumeSurfaceDynamicInput(
2686
+ surfaceDynamicInputForCommit.page,
2687
+ surfaceDynamicInputForCommit.ranges,
2688
+ surfaceDynamicInputForCommit.projectionRevision,
2689
+ frameNumber,
2690
+ ),
2691
+ }),
2886
2692
  projectShadow: (graph, identity) => {
2887
2693
  if (!shadowViewPool.isActive(identity) || shadowViewPool.submission(identity) === undefined)
2888
2694
  return ok(undefined);
@@ -2910,25 +2716,75 @@ export class GpuDrivenProduction {
2910
2716
  ),
2911
2717
  );
2912
2718
  },
2913
- project: (graph, format, sampleCount, additionalColorFormats) => {
2719
+ project: (
2720
+ graph,
2721
+ format,
2722
+ sampleCount,
2723
+ surfaceSubmissionObservation,
2724
+ additionalColorFormats,
2725
+ ) => {
2914
2726
  this.indirectDrawCount = admittedRasterBatchCount(filtered.plan);
2915
- const outputs = view.addPasses(graph);
2727
+ const outputs = view.addPasses(graph, 'gpu-driven', true, surfaceSubmissionObservation);
2916
2728
  if (!outputs.ok) return outputs;
2917
2729
  if (materialRaster !== undefined && filtered.batches.length > 0) {
2730
+ let surfacePage: GraphBuffer | undefined;
2731
+ let surfaceFrame: GraphBuffer | undefined;
2732
+ let surfaceSharedFrame: GraphBuffer | undefined;
2733
+ if (this.surfaceSubmission !== undefined) {
2734
+ const importedPage = graph.importBuffer(
2735
+ 'gpu-driven.surface.dynamic-input',
2736
+ {
2737
+ size: materialRaster.surfaceDynamicInputBufferSize,
2738
+ usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
2739
+ },
2740
+ () => materialRaster.surfaceDynamicInputBuffer,
2741
+ );
2742
+ if (!importedPage.ok) return importedPage;
2743
+ const importedFrame = graph.importBuffer(
2744
+ 'gpu-driven.surface.frame-input',
2745
+ {
2746
+ size: materialRaster.surfaceDynamicFrameBufferSize,
2747
+ usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
2748
+ },
2749
+ () => materialRaster.surfaceDynamicFrameBuffer,
2750
+ );
2751
+ if (!importedFrame.ok) return importedFrame;
2752
+ const importedSharedFrame = graph.importBuffer(
2753
+ 'gpu-driven.surface.shared-frame-input',
2754
+ {
2755
+ size: materialRaster.surfaceDynamicSharedFrameBufferSize,
2756
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
2757
+ },
2758
+ () => materialRaster.surfaceDynamicSharedFrameBuffer,
2759
+ );
2760
+ if (!importedSharedFrame.ok) return importedSharedFrame;
2761
+ surfacePage = importedPage.value;
2762
+ surfaceFrame = importedFrame.value;
2763
+ surfaceSharedFrame = importedSharedFrame.value;
2764
+ }
2918
2765
  const standardProjected: Array<{
2919
2766
  readonly batch: GpuDrivenBatch;
2767
+ readonly artifact: MaterialShaderArtifact;
2920
2768
  readonly mesh: MeshGpuHandles;
2921
2769
  readonly materialEntityKey: number;
2922
2770
  readonly vertex: GraphBuffer;
2923
2771
  readonly index?: GraphBuffer;
2924
- readonly pipelineFor: (
2925
- formats: readonly TextureFormat[],
2926
- ) => Result<RenderPipeline, RhiError>;
2927
2772
  readonly meshBindGroup: BindGroup;
2773
+ readonly meshProjectionBuffer: Buffer;
2774
+ readonly meshProjectionBytes: number;
2928
2775
  readonly raster: MaterialAbiRasterAdapter;
2929
2776
  readonly deformation: 'rigid' | 'skin';
2930
2777
  readonly clustered: boolean;
2931
2778
  readonly projectionKey: string;
2779
+ readonly pipelineFor: (
2780
+ formats: readonly TextureFormat[],
2781
+ ) => Result<RenderPipeline, RhiError>;
2782
+ readonly nearestPipelineFor?: (
2783
+ formats: readonly TextureFormat[],
2784
+ ) => Result<RenderPipeline, RhiError>;
2785
+ readonly colorPipelineFor?: (
2786
+ formats: readonly TextureFormat[],
2787
+ ) => Result<RenderPipeline, RhiError>;
2932
2788
  }> = [];
2933
2789
  const standardAccesses: GraphAccess[] = [
2934
2790
  { resource: outputs.value.primitive, usage: 'storage-read' },
@@ -2937,6 +2793,15 @@ export class GpuDrivenProduction {
2937
2793
  { resource: outputs.value.visible, usage: 'storage-read' },
2938
2794
  { resource: outputs.value.transform, usage: 'storage-read' },
2939
2795
  { resource: outputs.value.indirect, usage: 'indirect-read' },
2796
+ ...(surfacePage === undefined
2797
+ ? []
2798
+ : [{ resource: surfacePage, usage: 'storage-read' as const }]),
2799
+ ...(surfaceFrame === undefined
2800
+ ? []
2801
+ : [{ resource: surfaceFrame, usage: 'storage-read' as const }]),
2802
+ ...(surfaceSharedFrame === undefined
2803
+ ? []
2804
+ : [{ resource: surfaceSharedFrame, usage: 'uniform-read' as const }]),
2940
2805
  ];
2941
2806
  const meshBuffers = new Map<
2942
2807
  MeshGpuHandles,
@@ -2963,10 +2828,10 @@ export class GpuDrivenProduction {
2963
2828
  );
2964
2829
  }
2965
2830
  // The visible stream is compacted after the persistent visibility
2966
- // facet applies its active-entity admission. Project the mesh rows
2967
- // from that same admitted sequence so visible.x remains the
2968
- // batch-local row identity even when a suppressed candidate sits
2969
- // between two submitted candidates.
2831
+ // facet applies its active-entity admission. Project mesh rows and
2832
+ // frame rows from the same admitted sequence; visible.x is the
2833
+ // batch-local candidate identity, while the atomic slot is only
2834
+ // the draw order chosen by the GPU culler.
2970
2835
  const candidates = admittedCandidates.flatMap((candidate) => {
2971
2836
  const slot = projectionSlot(candidate.primitiveIndex);
2972
2837
  return slot === undefined ? [] : [sceneCandidate(slot, candidate)];
@@ -2984,12 +2849,23 @@ export class GpuDrivenProduction {
2984
2849
  }),
2985
2850
  );
2986
2851
  }
2852
+ const projectionKey = gpuDrivenProjectionKey(prepared.batch, deformation, artifact);
2987
2853
  const meshBindGroup = raster.projectionBindGroup(
2988
2854
  projection.value.rows,
2989
- gpuDrivenProjectionKey(prepared.batch, deformation, artifact),
2855
+ projectionKey,
2990
2856
  prepared.batch.candidates.length,
2991
2857
  );
2992
2858
  if (!meshBindGroup.ok) return meshBindGroup;
2859
+ const meshProjection = raster.projectionBuffer(projectionKey);
2860
+ if (meshProjection === undefined) {
2861
+ return err(
2862
+ new RhiError({
2863
+ code: 'internal-error',
2864
+ expected: 'retained batch-local mesh projection buffer',
2865
+ hint: 'keep the projected BindGroup and its buffer on one retained identity',
2866
+ }),
2867
+ );
2868
+ }
2993
2869
  const materialSlot = projectionSlot(admittedCandidates[0]?.primitiveIndex ?? -1);
2994
2870
  if (materialSlot === undefined) {
2995
2871
  return err(
@@ -3032,8 +2908,18 @@ export class GpuDrivenProduction {
3032
2908
  standardAccesses.push({ resource: buffers.index, usage: 'index-read' });
3033
2909
  }
3034
2910
  }
2911
+ const shaderUvSetCount = isCanonicalStandardPbrMaterialShader(artifact.material)
2912
+ ? materialPipelineState.standardPbrShaderUvSetCount
2913
+ : (artifact.uvSetCount ?? materialPipelineState.standardPbrShaderUvSetCount);
2914
+ const stripIndexFormat =
2915
+ prepared.batch.key.drawKind === 'indexed' &&
2916
+ (prepared.batch.key.topology === 'line-strip' ||
2917
+ prepared.batch.key.topology === 'triangle-strip')
2918
+ ? prepared.mesh.indexFormat
2919
+ : undefined;
3035
2920
  const pipelineFor = (
3036
2921
  formats: readonly TextureFormat[],
2922
+ selectedArtifact: MaterialShaderArtifact = artifact,
3037
2923
  ): Result<RenderPipeline, RhiError> => {
3038
2924
  return raster.pipeline(
3039
2925
  formats[0] ?? format,
@@ -3043,22 +2929,31 @@ export class GpuDrivenProduction {
3043
2929
  // Standard count is only a fallback for legacy synthetic
3044
2930
  // fixtures; using it for a custom program would add aliases
3045
2931
  // the shader never declares (or omit a declared custom set).
3046
- artifact.uvSetCount ?? materialPipelineState.standardPbrShaderUvSetCount,
2932
+ shaderUvSetCount,
3047
2933
  prepared.batch.key.topology,
3048
- prepared.batch.key.drawKind === 'indexed' &&
3049
- (prepared.batch.key.topology === 'line-strip' ||
3050
- prepared.batch.key.topology === 'triangle-strip')
3051
- ? prepared.mesh.indexFormat
3052
- : undefined,
2934
+ stripIndexFormat,
3053
2935
  prepared.renderState,
3054
- artifact,
2936
+ selectedArtifact,
3055
2937
  formats.slice(1),
3056
2938
  );
3057
2939
  };
3058
2940
  const initialPipeline = pipelineFor([format, ...(additionalColorFormats ?? [])]);
3059
2941
  if (!initialPipeline.ok) return initialPipeline;
2942
+ let nearestPipelineFor:
2943
+ | ((formats: readonly TextureFormat[]) => Result<RenderPipeline, RhiError>)
2944
+ | undefined;
2945
+ let colorPipelineFor:
2946
+ | ((formats: readonly TextureFormat[]) => Result<RenderPipeline, RhiError>)
2947
+ | undefined;
2948
+ if (isSingleLayerMediumArtifact(artifact)) {
2949
+ nearestPipelineFor = (formats) =>
2950
+ pipelineFor(formats, { ...artifact, fragmentEntry: 'fs_nearest_layer' });
2951
+ colorPipelineFor = (formats) =>
2952
+ pipelineFor(formats, { ...artifact, fragmentEntry: 'fs_color' });
2953
+ }
3060
2954
  standardProjected.push({
3061
2955
  batch: prepared.batch,
2956
+ artifact,
3062
2957
  mesh: prepared.mesh,
3063
2958
  materialEntityKey: worldEntityKey(
3064
2959
  materialSlot.snapshot.worldId,
@@ -3066,7 +2961,11 @@ export class GpuDrivenProduction {
3066
2961
  ),
3067
2962
  ...buffers,
3068
2963
  pipelineFor,
2964
+ ...(nearestPipelineFor === undefined ? {} : { nearestPipelineFor }),
2965
+ ...(colorPipelineFor === undefined ? {} : { colorPipelineFor }),
3069
2966
  meshBindGroup: meshBindGroup.value,
2967
+ meshProjectionBuffer: meshProjection.buffer,
2968
+ meshProjectionBytes: meshProjection.bindingBytes,
3070
2969
  raster,
3071
2970
  deformation,
3072
2971
  clustered: ['cluster', 'skin-cluster'].includes(
@@ -3095,6 +2994,8 @@ export class GpuDrivenProduction {
3095
2994
  pass,
3096
2995
  resources: GraphResourceResolver,
3097
2996
  frameResources: GpuDrivenStandardPbrFrameResources,
2997
+ filter,
2998
+ fragmentEntryPoint,
3098
2999
  ) => {
3099
3000
  // The shared Standard view BGL carries two dynamic buffers:
3100
3001
  // the frame view UBO and the per-frame shadow/light table. GPU
@@ -3106,6 +3007,18 @@ export class GpuDrivenProduction {
3106
3007
  let currentVertex: GraphBuffer | undefined;
3107
3008
  let currentIndex: GraphBuffer | undefined;
3108
3009
  for (const batch of rasterDispatches) {
3010
+ const isMedium = isSingleLayerMediumArtifact(batch.artifact);
3011
+ if (
3012
+ (filter === 'single-layer-medium' && !isMedium) ||
3013
+ ((filter === 'opaque' ||
3014
+ filter === 'deferred-opaque' ||
3015
+ filter === 'forward-only-opaque') &&
3016
+ isMedium) ||
3017
+ (filter === 'deferred-opaque' && batch.batch.key.materialPass !== 'deferred') ||
3018
+ (filter === 'forward-only-opaque' && batch.batch.key.materialPass === 'deferred')
3019
+ ) {
3020
+ continue;
3021
+ }
3109
3022
  const globalMaterialSlots = frameResources.materialSlotIndicesByEntity.get(
3110
3023
  batch.materialEntityKey,
3111
3024
  );
@@ -3126,14 +3039,19 @@ export class GpuDrivenProduction {
3126
3039
  !frameResources.selectedMaterialSlots.has(globalMaterialSlot)
3127
3040
  )
3128
3041
  continue;
3129
- const selectedPipeline = batch.pipelineFor(
3042
+ const pipelineFor =
3043
+ fragmentEntryPoint === 'fs_nearest_layer'
3044
+ ? (batch.nearestPipelineFor ?? batch.pipelineFor)
3045
+ : fragmentEntryPoint === 'fs_color'
3046
+ ? (batch.colorPipelineFor ?? batch.pipelineFor)
3047
+ : batch.pipelineFor;
3048
+ const selectedPipeline = pipelineFor(
3130
3049
  frameResources.colorFormats ?? [format, ...(additionalColorFormats ?? [])],
3131
3050
  );
3132
3051
  if (!selectedPipeline.ok) throw selectedPipeline.error;
3133
- const pipeline = selectedPipeline.value;
3134
- if (currentPipeline !== pipeline) {
3135
- pass.setPipeline(pipeline);
3136
- currentPipeline = pipeline;
3052
+ if (currentPipeline !== selectedPipeline.value) {
3053
+ pass.setPipeline(selectedPipeline.value);
3054
+ currentPipeline = selectedPipeline.value;
3137
3055
  }
3138
3056
  const materialBindGroup = frameResources.materialBindGroups[globalMaterialSlot];
3139
3057
  if (materialBindGroup === undefined) {
@@ -3146,21 +3064,30 @@ export class GpuDrivenProduction {
3146
3064
  pass.setBindGroup(1, materialBindGroup, [
3147
3065
  globalMaterialSlot * frameResources.materialStride,
3148
3066
  ]);
3149
- const meshBindGroup = batch.clustered
3150
- ? frameResources.resolveClusteredMeshBindGroup?.(
3151
- batch.raster.sceneMeshBinding(batch.projectionKey),
3152
- )
3153
- : batch.meshBindGroup;
3154
- if (meshBindGroup === undefined)
3067
+ const group2 = resolveGpuDrivenMeshGroup({
3068
+ clustered: batch.clustered,
3069
+ deformation: batch.deformation,
3070
+ meshBindGroup: batch.meshBindGroup,
3071
+ meshProjectionBuffer: batch.meshProjectionBuffer,
3072
+ meshProjectionBytes: batch.meshProjectionBytes,
3073
+ frameResources,
3074
+ skinPaletteBinding: batch.raster.skinPaletteBinding(),
3075
+ });
3076
+ if (group2 === undefined) {
3155
3077
  throw new RhiError({
3156
3078
  code: 'rhi-not-available',
3157
- expected: 'current scene-index lighting binding',
3158
- hint: 'resolve clustered surface resources during the material pass',
3079
+ expected: 'clustered Standard group(2) BindGroup for GPU-driven batch',
3080
+ hint: 'publish the frame-owned cluster lighting resources before indirect encode',
3159
3081
  });
3160
- pass.setBindGroup(2, meshBindGroup, batch.deformation === 'skin' ? [0, 0, 0] : [0]);
3082
+ }
3083
+ pass.setBindGroup(2, group2, batch.deformation === 'skin' ? [0, 0, 0] : [0]);
3161
3084
  const visibleWindow = batch.raster.visibleWindowBindGroup(
3162
3085
  batch.batch.visibleBase,
3163
3086
  batch.batch.visibleCapacity,
3087
+ isSingleLayerMediumArtifact(batch.artifact) ? batch.batch.visibleBase : undefined,
3088
+ isSingleLayerMediumArtifact(batch.artifact)
3089
+ ? batch.batch.visibleCapacity
3090
+ : undefined,
3164
3091
  );
3165
3092
  if (!visibleWindow.ok) throw visibleWindow.error;
3166
3093
  pass.setBindGroup(3, visibleWindow.value);
@@ -3183,6 +3110,29 @@ export class GpuDrivenProduction {
3183
3110
  } else {
3184
3111
  pass.drawIndirect(indirect.value, batch.batch.indirectOffset);
3185
3112
  }
3113
+ const surfacePass =
3114
+ isMedium && fragmentEntryPoint === 'fs_nearest_layer'
3115
+ ? 'nearest-layer'
3116
+ : isMedium && fragmentEntryPoint === 'fs_color'
3117
+ ? 'color'
3118
+ : undefined;
3119
+ if (surfacePass !== undefined) {
3120
+ frameResources.surfaceSubmissionObservation?.record(surfacePass, {
3121
+ kind:
3122
+ batch.batch.key.drawKind === 'indexed'
3123
+ ? 'draw-indexed-indirect'
3124
+ : 'draw-indirect',
3125
+ indirectBufferIdentity: getOpaqueResourceIdentity(indirect.value as object),
3126
+ indirectOffset: batch.batch.indirectOffset,
3127
+ pipelineIdentity: getOpaqueResourceIdentity(selectedPipeline as object),
3128
+ ...(batch.artifact.receipt === undefined
3129
+ ? {}
3130
+ : {
3131
+ receiptIdentity: batch.artifact.receipt.receiptIdentity,
3132
+ receiptGeneration: batch.artifact.receipt.generation,
3133
+ }),
3134
+ });
3135
+ }
3186
3136
  this.encodedIndirectDraws += 1;
3187
3137
  }
3188
3138
  },
@@ -3371,7 +3321,7 @@ export class GpuDrivenProduction {
3371
3321
  batch.candidates.length,
3372
3322
  );
3373
3323
  if (!meshBindGroup.ok) return meshBindGroup;
3374
- const visibleBindGroup = raster.visibleWindowBindGroupForBuffer(
3324
+ const visibleBindGroup = raster.shadowVisibleWindowBindGroupForBuffer(
3375
3325
  visibleBuffer,
3376
3326
  batch.visibleBase,
3377
3327
  batch.visibleCapacity,
@@ -3451,6 +3401,8 @@ export class GpuDrivenProduction {
3451
3401
  this.skinRaster?.dispose();
3452
3402
  this.skinRaster = undefined;
3453
3403
  this.filteredCache = undefined;
3404
+ this.surfaceFrameRangeCache = undefined;
3405
+ this.consumedSurfaceInput = undefined;
3454
3406
  this.signatureCache = undefined;
3455
3407
  this.validatedResidencyCache = undefined;
3456
3408
  this.cpuValidationTelemetryCache = undefined;
@@ -3465,11 +3417,22 @@ export class GpuDrivenProduction {
3465
3417
  this.skinPaletteBuffer = undefined;
3466
3418
  this.committedPaletteContentRevision = undefined;
3467
3419
  this.preparationFailure = undefined;
3420
+ this.surfaceArtifactInspection = undefined;
3468
3421
  this.overflowRecoveryRequired = false;
3469
3422
  }
3470
3423
 
3471
3424
  inspect(): GpuDrivenProductionInspection {
3472
3425
  const view = this.view?.inspect();
3426
+ const filtered = this.filteredCache?.value;
3427
+ const resourceAllocation = combineGpuResourceAllocationInspections([
3428
+ ...(view === undefined ? [] : [view.resourceAllocation]),
3429
+ ...(this.materialRaster === undefined ? [] : [this.materialRaster.resourceAllocation]),
3430
+ ...(this.skinRaster === undefined ? [] : [this.skinRaster.resourceAllocation]),
3431
+ ]);
3432
+ const resourceClasses =
3433
+ filtered === undefined
3434
+ ? undefined
3435
+ : inspectResourceClassSplits({ batches: filtered.batches.map(({ batch }) => batch) });
3473
3436
  const channels: GpuDrivenLaneSummary[] = [];
3474
3437
  const mainTotalDrawCount = this.mainTotalDrawItems;
3475
3438
  const mainClaimedDrawCount = this.telemetryOverflow
@@ -3575,11 +3538,23 @@ export class GpuDrivenProduction {
3575
3538
  }
3576
3539
  }
3577
3540
  return {
3541
+ ...(this.surfaceArtifactInspection === undefined
3542
+ ? {}
3543
+ : { surfaceArtifact: this.surfaceArtifactInspection }),
3578
3544
  residencyValidationScans: this.residencyValidationScans,
3579
3545
  residencyValidationCacheHits: this.residencyValidationCacheHits,
3580
3546
  cpuValidationScans: this.cpuValidationScans,
3581
3547
  cpuValidationCacheHits: this.cpuValidationCacheHits,
3582
3548
  ...this.structureMetrics,
3549
+ surfaceFrameRangeBuilds: this.surfaceFrameRangeBuilds,
3550
+ surfaceFrameMemberScans:
3551
+ this.surfaceFrameRangeMemberScans +
3552
+ (this.materialRaster?.surfaceDynamicFrameMemberScans ?? 0) +
3553
+ (this.skinRaster?.surfaceDynamicFrameMemberScans ?? 0),
3554
+ surfaceFrameRowAllocations:
3555
+ (this.materialRaster?.surfaceDynamicFrameRowAllocations ?? 0) +
3556
+ (this.skinRaster?.surfaceDynamicFrameRowAllocations ?? 0),
3557
+ surfaceFrameTimeWrites: this.materialRaster?.surfaceDynamicFrameTimeWrites ?? 0,
3583
3558
  gpuOwnedSnapshotsMaterialized: this.gpuOwnedSnapshotsMaterialized,
3584
3559
  filteredPlanBuilds: this.filteredPlanBuilds,
3585
3560
  gpuOwnedEntityCount: this.gpuOwnedEntityCount,
@@ -3610,6 +3585,23 @@ export class GpuDrivenProduction {
3610
3585
  recoveryCount: this.recoveryCount,
3611
3586
  retryCount: this.retryCount,
3612
3587
  lastKnownGoodGeneration: this.lastKnownGoodGeneration,
3588
+ ...(resourceAllocation === undefined ? {} : { resourceAllocation }),
3589
+ resourceAllocationOwners: Object.freeze({
3590
+ ...(view === undefined ? {} : { gpuDrivenView: view.resourceAllocation }),
3591
+ ...(this.materialRaster === undefined
3592
+ ? {}
3593
+ : { materialRaster: this.materialRaster.resourceAllocation }),
3594
+ ...(this.skinRaster === undefined
3595
+ ? {}
3596
+ : { skinRaster: this.skinRaster.resourceAllocation }),
3597
+ }),
3598
+ ...(resourceClasses === undefined
3599
+ ? {}
3600
+ : {
3601
+ resourceClassCount: resourceClasses.resourceClassCount,
3602
+ resourceClassSplits: resourceClasses.resourceClassSplits,
3603
+ resourceClassSplitReasons: resourceClasses.resourceClassSplitReasons,
3604
+ }),
3613
3605
  };
3614
3606
  }
3615
3607
 
@@ -3653,6 +3645,7 @@ export class GpuDrivenProduction {
3653
3645
  readonly worldKey: number;
3654
3646
  readonly primitiveSlot: number;
3655
3647
  readonly slotGeneration: number;
3648
+ readonly artifact?: MaterialShaderArtifact;
3656
3649
  };
3657
3650
  const preparedByBatchId = new Map<number, LodBatchAttribution>(
3658
3651
  filtered.batches.map((prepared) => [
@@ -3662,9 +3655,12 @@ export class GpuDrivenProduction {
3662
3655
  worldKey: prepared.worldKey,
3663
3656
  primitiveSlot: prepared.primitiveSlot,
3664
3657
  slotGeneration: prepared.slotGeneration,
3658
+ artifact: prepared.artifact,
3665
3659
  },
3666
3660
  ]),
3667
3661
  );
3662
+ const projectedCandidateCount =
3663
+ telemetryCandidateCount > 0 ? telemetryCandidateCount : selection.candidateCount;
3668
3664
  // Selector-only batches remain in filtered.plan so their GPU counters
3669
3665
  // can describe candidates suppressed from the raster admission lane.
3670
3666
  // They have no PreparedBatch entry because no draw is encoded, but they
@@ -3710,7 +3706,10 @@ export class GpuDrivenProduction {
3710
3706
  let lodVisible = 0;
3711
3707
  for (const batchSelection of selection.batches) {
3712
3708
  const prepared = preparedByBatchId.get(batchSelection.batchId);
3713
- if (prepared === undefined) return undefined;
3709
+ // Selector-only or otherwise non-raster batches remain in the GPU
3710
+ // telemetry plan. They have no material producer row and therefore do
3711
+ // not invalidate readback evidence for the prepared Surface batches.
3712
+ if (prepared === undefined) continue;
3714
3713
  // The GPU selector also visits ordinary GPU-driven meshes so one
3715
3714
  // compact compute path can populate their indirect args. Those rows
3716
3715
  // are level-0 by construction, but they are not LOD candidates and
@@ -3753,14 +3752,50 @@ export class GpuDrivenProduction {
3753
3752
  }
3754
3753
  return {
3755
3754
  ...selection,
3756
- candidateCount: lodCandidateCount,
3757
- visible: lodVisible,
3758
- occluded: Math.max(0, lodCandidateCount - lodVisible),
3755
+ candidateCount: lodCandidateCount > 0 ? lodCandidateCount : projectedCandidateCount,
3756
+ visible: lodCandidateCount > 0 ? lodVisible : selection.visible,
3757
+ occluded: Math.max(
3758
+ 0,
3759
+ (lodCandidateCount > 0 ? lodCandidateCount : projectedCandidateCount) -
3760
+ (lodCandidateCount > 0 ? lodVisible : selection.visible),
3761
+ ),
3759
3762
  lodHistogram: Object.freeze(
3760
3763
  [...lodHistogram.entries()]
3761
3764
  .sort(([left], [right]) => left - right)
3762
3765
  .map(([level, count]) => Object.freeze({ level, count })),
3763
3766
  ),
3767
+ surfaceActualMemberIds: Object.freeze(
3768
+ selection.actualMembers.flatMap((member) => {
3769
+ const prepared = preparedByBatchId.get(member.batchId);
3770
+ if (
3771
+ prepared === undefined ||
3772
+ prepared.artifact === undefined ||
3773
+ !isSingleLayerMediumArtifact(prepared.artifact)
3774
+ ) {
3775
+ return [];
3776
+ }
3777
+ const slot = this.filteredCache?.slots.find(
3778
+ (candidate) => candidate.slot === member.primitiveIndex,
3779
+ );
3780
+ if (slot === undefined) return [];
3781
+ const frameRange = this.surfaceFrameRangeCache?.value.ranges.find(
3782
+ ({ directMember }) =>
3783
+ directMember.worldId === slot.worldId &&
3784
+ directMember.entityKey === slot.entityKey &&
3785
+ directMember.drawItemIndex === member.drawItemIndex &&
3786
+ directMember.instanceOrdinal === member.instanceOrdinal,
3787
+ );
3788
+ if (frameRange === undefined) return [];
3789
+ return [
3790
+ JSON.stringify([
3791
+ frameRange.directMember.worldIdentity,
3792
+ frameRange.directMember.entityKey,
3793
+ frameRange.directMember.drawItemIndex,
3794
+ frameRange.directMember.instanceOrdinal,
3795
+ ]),
3796
+ ];
3797
+ }),
3798
+ ),
3764
3799
  worldSelections: Object.freeze(
3765
3800
  [...worldSelections.values()].map((aggregate) =>
3766
3801
  Object.freeze({
@@ -3786,6 +3821,7 @@ export class GpuDrivenProduction {
3786
3821
  validated: readonly { readonly source: RenderableSnapshot }[],
3787
3822
  gpuOwnedDrawKeys: ReadonlySet<string>,
3788
3823
  cacheKey?: string,
3824
+ worldKeys?: readonly number[],
3789
3825
  ): void {
3790
3826
  const cached = this.cpuValidationTelemetryCache;
3791
3827
  if (
@@ -3793,7 +3829,8 @@ export class GpuDrivenProduction {
3793
3829
  cached !== undefined &&
3794
3830
  cached.cacheKey === cacheKey &&
3795
3831
  cached.validated === validated &&
3796
- cached.gpuOwnedDrawKeys === gpuOwnedDrawKeys
3832
+ cached.gpuOwnedDrawKeys === gpuOwnedDrawKeys &&
3833
+ cached.worldKeys === worldKeys
3797
3834
  ) {
3798
3835
  this.applyCpuValidationTelemetry(cached);
3799
3836
  this.cpuValidationCacheHits += 1;
@@ -3831,7 +3868,10 @@ export class GpuDrivenProduction {
3831
3868
  for (const [compactIndex, draw] of draws.entries()) {
3832
3869
  const material = row.source.materials[draw.materialSlot] ?? row.source.material;
3833
3870
  const key = gpuDrivenDrawKey(
3834
- worldEntityKey(row.source.worldId, row.source.entityKey),
3871
+ worldEntityKey(
3872
+ worldKeys?.[row.source.worldId] ?? row.source.worldId,
3873
+ row.source.entityKey,
3874
+ ),
3835
3875
  material.materialHandle ?? -1,
3836
3876
  gpuDrivenSourceDrawItemIndex(draw, compactIndex),
3837
3877
  );
@@ -3876,6 +3916,7 @@ export class GpuDrivenProduction {
3876
3916
  cacheKey,
3877
3917
  validated,
3878
3918
  gpuOwnedDrawKeys,
3919
+ worldKeys,
3879
3920
  gpuOwnedDrawCount: this.gpuOwnedDrawItems,
3880
3921
  totalDrawItems: this.mainTotalDrawItems,
3881
3922
  gpuOwnedRows: this.validatedGpuOwnedRows,