@forgeax/engine-render 0.1.32 → 0.1.34

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 +1374 -142
  2. package/dist/assembly/analytic-fog-registration.d.ts +3 -0
  3. package/dist/assembly/analytic-fog-registration.d.ts.map +1 -0
  4. package/dist/assembly/backend-contract.d.ts +2 -0
  5. package/dist/assembly/backend-contract.d.ts.map +1 -1
  6. package/dist/assembly/barrel-distortion-inspection.d.ts +3 -0
  7. package/dist/assembly/barrel-distortion-inspection.d.ts.map +1 -0
  8. package/dist/assembly/canvas-draw-guard.d.ts +3 -0
  9. package/dist/assembly/canvas-draw-guard.d.ts.map +1 -0
  10. package/dist/assembly/cloud-shadow-entry.d.ts +4 -0
  11. package/dist/assembly/cloud-shadow-entry.d.ts.map +1 -0
  12. package/dist/assembly/dynamic-geometry-host.d.ts +4 -1
  13. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  14. package/dist/assembly/dynamic-geometry-runtime.d.ts +1 -1
  15. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -1
  16. package/dist/assembly/factory.d.ts +1 -1
  17. package/dist/assembly/factory.d.ts.map +1 -1
  18. package/dist/assembly/fxaa-registration.d.ts +4 -0
  19. package/dist/assembly/fxaa-registration.d.ts.map +1 -0
  20. package/dist/assembly/host-contract.d.ts +3 -3
  21. package/dist/assembly/host-contract.d.ts.map +1 -1
  22. package/dist/assembly/lod-observation.d.ts +4 -0
  23. package/dist/assembly/lod-observation.d.ts.map +1 -0
  24. package/dist/assembly/material/assembly.d.ts.map +1 -1
  25. package/dist/assembly/material/pipeline-helpers.d.ts +5 -0
  26. package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -1
  27. package/dist/assembly/material-shader-policy.d.ts +16 -2
  28. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  29. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -1
  30. package/dist/assembly/motion-blur-registration.d.ts +3 -0
  31. package/dist/assembly/motion-blur-registration.d.ts.map +1 -0
  32. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
  33. package/dist/assembly/recovery/renderer-recover.d.ts +2 -0
  34. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  35. package/dist/assembly/render-feature-post-processes.d.ts +6 -0
  36. package/dist/assembly/render-feature-post-processes.d.ts.map +1 -0
  37. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  38. package/dist/assembly/renderer-feature-inspection.d.ts +1 -1
  39. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -1
  40. package/dist/assembly/renderer-frame-transaction.d.ts +12 -0
  41. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  42. package/dist/assembly/renderer-inspection.d.ts +11 -0
  43. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  44. package/dist/assembly/temporal-post-process-prewarm.d.ts +13 -0
  45. package/dist/assembly/temporal-post-process-prewarm.d.ts.map +1 -0
  46. package/dist/assembly/webgpu-pbr-ready.d.ts +1 -0
  47. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -1
  48. package/dist/assembly/webgpu-ready-contract.d.ts +4 -3
  49. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -1
  50. package/dist/assembly/webgpu-ready-per-pass.d.ts +39 -0
  51. package/dist/assembly/webgpu-ready-per-pass.d.ts.map +1 -0
  52. package/dist/assembly/webgpu-ready.d.ts +2 -2
  53. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  54. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -1
  55. package/dist/assembly/webgpu-renderer-observation-owner.d.ts +35 -0
  56. package/dist/assembly/webgpu-renderer-observation-owner.d.ts.map +1 -0
  57. package/dist/assembly/webgpu-renderer.d.ts +1 -1
  58. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  59. package/dist/authoring.mjs +1 -1
  60. package/dist/barrel-distortion.d.ts +47 -0
  61. package/dist/barrel-distortion.d.ts.map +1 -0
  62. package/dist/bloom-admission.d.ts +13 -0
  63. package/dist/bloom-admission.d.ts.map +1 -0
  64. package/dist/{chunk-PA5DRJNI.mjs → chunk-ELJFJ2BU.mjs} +2 -2
  65. package/dist/chunk-ELJFJ2BU.mjs.map +1 -0
  66. package/dist/{chunk-JCIRYQ4V.mjs → chunk-HEB564WF.mjs} +10 -4
  67. package/dist/chunk-HEB564WF.mjs.map +1 -0
  68. package/dist/{chunk-JRIPCMYC.mjs → chunk-MKDIT2EM.mjs} +1584 -375
  69. package/dist/chunk-MKDIT2EM.mjs.map +1 -0
  70. package/dist/{chunk-S665NJNF.mjs → chunk-OCDLEI6I.mjs} +3 -3
  71. package/dist/{chunk-S665NJNF.mjs.map → chunk-OCDLEI6I.mjs.map} +1 -1
  72. package/dist/chunk-ODZWR6JQ.mjs +4927 -0
  73. package/dist/chunk-ODZWR6JQ.mjs.map +1 -0
  74. package/dist/{chunk-DRR7D3KX.mjs → chunk-SHFUFJ5N.mjs} +192 -18
  75. package/dist/chunk-SHFUFJ5N.mjs.map +1 -0
  76. package/dist/{chunk-4NNZQCL3.mjs → chunk-UI5SFODX.mjs} +223 -277
  77. package/dist/chunk-UI5SFODX.mjs.map +1 -0
  78. package/dist/{chunk-GWCEMDVR.mjs → chunk-UINHLCZA.mjs} +787 -80
  79. package/dist/chunk-UINHLCZA.mjs.map +1 -0
  80. package/dist/{chunk-6TFBXLRJ.mjs → chunk-WEX45HKH.mjs} +222 -337
  81. package/dist/chunk-WEX45HKH.mjs.map +1 -0
  82. package/dist/cloud/density.d.ts +55 -0
  83. package/dist/cloud/density.d.ts.map +1 -0
  84. package/dist/cloud/extract.d.ts +25 -0
  85. package/dist/cloud/extract.d.ts.map +1 -0
  86. package/dist/cloud/feature.d.ts +57 -0
  87. package/dist/cloud/feature.d.ts.map +1 -0
  88. package/dist/cloud/frame-context.d.ts +8 -0
  89. package/dist/cloud/frame-context.d.ts.map +1 -0
  90. package/dist/cloud/inspection.d.ts +59 -0
  91. package/dist/cloud/inspection.d.ts.map +1 -0
  92. package/dist/cloud/optics.d.ts +56 -0
  93. package/dist/cloud/optics.d.ts.map +1 -0
  94. package/dist/cloud/parameters.d.ts +73 -0
  95. package/dist/cloud/parameters.d.ts.map +1 -0
  96. package/dist/cloud/resources.d.ts +60 -0
  97. package/dist/cloud/resources.d.ts.map +1 -0
  98. package/dist/cloud/shadow.d.ts +42 -0
  99. package/dist/cloud/shadow.d.ts.map +1 -0
  100. package/dist/cloud/temporal.d.ts +74 -0
  101. package/dist/cloud/temporal.d.ts.map +1 -0
  102. package/dist/components/atmosphere.d.ts +2 -0
  103. package/dist/components/atmosphere.d.ts.map +1 -1
  104. package/dist/components/barrel-distortion.d.ts +13 -0
  105. package/dist/components/barrel-distortion.d.ts.map +1 -0
  106. package/dist/components/camera.d.ts +20 -4
  107. package/dist/components/camera.d.ts.map +1 -1
  108. package/dist/components/cloud-layer.d.ts +29 -0
  109. package/dist/components/cloud-layer.d.ts.map +1 -0
  110. package/dist/components/index.d.ts +2 -0
  111. package/dist/components/index.d.ts.map +1 -1
  112. package/dist/components/instances.d.ts +20 -6
  113. package/dist/components/instances.d.ts.map +1 -1
  114. package/dist/components/mesh-filter.d.ts +6 -5
  115. package/dist/components/mesh-filter.d.ts.map +1 -1
  116. package/dist/components/motion-blur.d.ts +1 -0
  117. package/dist/components/motion-blur.d.ts.map +1 -1
  118. package/dist/construct-renderer.d.ts +1 -1
  119. package/dist/construct-renderer.d.ts.map +1 -1
  120. package/dist/construct-renderer.mjs +9751 -3528
  121. package/dist/construct-renderer.mjs.map +1 -1
  122. package/dist/device/gpu-residency.d.ts +6 -0
  123. package/dist/device/gpu-residency.d.ts.map +1 -1
  124. package/dist/dynamic-geometry.d.ts +5 -1
  125. package/dist/dynamic-geometry.d.ts.map +1 -1
  126. package/dist/environment/analytic-fog.d.ts +6 -0
  127. package/dist/environment/analytic-fog.d.ts.map +1 -0
  128. package/dist/errors/cloud.d.ts +59 -0
  129. package/dist/errors/cloud.d.ts.map +1 -0
  130. package/dist/errors/render.d.ts +23 -5
  131. package/dist/errors/render.d.ts.map +1 -1
  132. package/dist/examples/single-layer-medium-surface.d.ts +47 -0
  133. package/dist/examples/single-layer-medium-surface.d.ts.map +1 -0
  134. package/dist/extract/camera.d.ts.map +1 -1
  135. package/dist/extract/contracts.d.ts +12 -1
  136. package/dist/extract/contracts.d.ts.map +1 -1
  137. package/dist/extract/environment.d.ts +2 -0
  138. package/dist/extract/environment.d.ts.map +1 -1
  139. package/dist/features/barrel-distortion.d.ts +29 -0
  140. package/dist/features/barrel-distortion.d.ts.map +1 -0
  141. package/dist/features/error-reporting.d.ts +17 -0
  142. package/dist/features/error-reporting.d.ts.map +1 -0
  143. package/dist/features/host.d.ts +15 -0
  144. package/dist/features/host.d.ts.map +1 -1
  145. package/dist/features/motion-blur/motion-blur-feature.d.ts +20 -2
  146. package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -1
  147. package/dist/features/motion-blur/motion-blur-params.d.ts +21 -0
  148. package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -1
  149. package/dist/features/motion-blur/motion-blur-runtime.d.ts +54 -0
  150. package/dist/features/motion-blur/motion-blur-runtime.d.ts.map +1 -0
  151. package/dist/features/plan.d.ts +15 -4
  152. package/dist/features/plan.d.ts.map +1 -1
  153. package/dist/features/prepared-gpu-work.d.ts +9 -5
  154. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  155. package/dist/features/render-graph-compute.d.ts.map +1 -1
  156. package/dist/features/render-graph-contribution.d.ts +5 -0
  157. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  158. package/dist/features/render-graph-raster.d.ts +1 -0
  159. package/dist/features/render-graph-raster.d.ts.map +1 -1
  160. package/dist/features/targets.d.ts +2 -0
  161. package/dist/features/targets.d.ts.map +1 -1
  162. package/dist/features/types.d.ts +44 -0
  163. package/dist/features/types.d.ts.map +1 -1
  164. package/dist/fullscreen-post-process-pass.d.ts +15 -7
  165. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  166. package/dist/gpu-dirty-ranges.d.ts +11 -0
  167. package/dist/gpu-dirty-ranges.d.ts.map +1 -0
  168. package/dist/gpu-driven/batch-topology.d.ts +17 -0
  169. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  170. package/dist/gpu-driven/material-artifacts.d.ts +6 -0
  171. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
  172. package/dist/gpu-driven/prepared-draw.d.ts +7 -0
  173. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  174. package/dist/gpu-driven/production-raster-material.d.ts +178 -0
  175. package/dist/gpu-driven/production-raster-material.d.ts.map +1 -0
  176. package/dist/gpu-driven/production-raster.d.ts +50 -4
  177. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  178. package/dist/gpu-driven/resource-allocation.d.ts +31 -0
  179. package/dist/gpu-driven/resource-allocation.d.ts.map +1 -0
  180. package/dist/gpu-driven/shadow-ownership.d.ts +14 -0
  181. package/dist/gpu-driven/shadow-ownership.d.ts.map +1 -0
  182. package/dist/gpu-driven/view-gpu.d.ts +35 -2
  183. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  184. package/dist/gpu-scene.d.ts +11 -1
  185. package/dist/gpu-scene.d.ts.map +1 -1
  186. package/dist/hdrp-buffers.d.ts +2 -2
  187. package/dist/hdrp-buffers.d.ts.map +1 -1
  188. package/dist/ibl/skylight-bind-group.d.ts +10 -1
  189. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  190. package/dist/index.d.ts +31 -8
  191. package/dist/index.d.ts.map +1 -1
  192. package/dist/index.mjs +258 -8
  193. package/dist/index.mjs.map +1 -1
  194. package/dist/inspection-types.d.ts +178 -3
  195. package/dist/inspection-types.d.ts.map +1 -1
  196. package/dist/instance-buffer-cache.d.ts +4 -4
  197. package/dist/instance-buffer-cache.d.ts.map +1 -1
  198. package/dist/instances.d.ts +49 -94
  199. package/dist/instances.d.ts.map +1 -1
  200. package/dist/internal.mjs +7 -7
  201. package/dist/pbr-pipeline.d.ts +19 -1
  202. package/dist/pbr-pipeline.d.ts.map +1 -1
  203. package/dist/pipeline/single-layer-medium-passes.d.ts +73 -0
  204. package/dist/pipeline/single-layer-medium-passes.d.ts.map +1 -0
  205. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  206. package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
  207. package/dist/pipeline/standard-output/types.d.ts +4 -2
  208. package/dist/pipeline/standard-output/types.d.ts.map +1 -1
  209. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  210. package/dist/pipeline/standard-post.d.ts +28 -19
  211. package/dist/pipeline/standard-post.d.ts.map +1 -1
  212. package/dist/pipeline-builder.d.ts +1 -1
  213. package/dist/pipeline-builder.d.ts.map +1 -1
  214. package/dist/pipeline-spec.d.ts +2 -2
  215. package/dist/plugin.d.ts.map +1 -1
  216. package/dist/prepare/prepared-graphics-resolver.d.ts +2 -0
  217. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  218. package/dist/record/barrel-distortion-frame.d.ts +15 -0
  219. package/dist/record/barrel-distortion-frame.d.ts.map +1 -0
  220. package/dist/record/dynamic-geometry-consumption.d.ts +10 -2
  221. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -1
  222. package/dist/record/fold-buckets.d.ts +95 -0
  223. package/dist/record/fold-buckets.d.ts.map +1 -0
  224. package/dist/record/fold-instance-buffer.d.ts +6 -0
  225. package/dist/record/fold-instance-buffer.d.ts.map +1 -0
  226. package/dist/record/frame-lighting.d.ts +1 -0
  227. package/dist/record/frame-lighting.d.ts.map +1 -1
  228. package/dist/record/frame-snapshot.d.ts +40 -3
  229. package/dist/record/frame-snapshot.d.ts.map +1 -1
  230. package/dist/record/frame-targets.d.ts.map +1 -1
  231. package/dist/record/frame.d.ts +14 -62
  232. package/dist/record/frame.d.ts.map +1 -1
  233. package/dist/record/gpu-draw-receipts.d.ts +11 -0
  234. package/dist/record/gpu-draw-receipts.d.ts.map +1 -0
  235. package/dist/record/gpu-timing.d.ts +8 -0
  236. package/dist/record/gpu-timing.d.ts.map +1 -1
  237. package/dist/record/main-pass-geometry.d.ts +21 -5
  238. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  239. package/dist/record/main-pass-material.d.ts +7 -1
  240. package/dist/record/main-pass-material.d.ts.map +1 -1
  241. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  242. package/dist/record/main-pass.d.ts +13 -2
  243. package/dist/record/main-pass.d.ts.map +1 -1
  244. package/dist/record/mesh-ssbo.d.ts +6 -5
  245. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  246. package/dist/record/motion-blur-receipt.d.ts +11 -0
  247. package/dist/record/motion-blur-receipt.d.ts.map +1 -0
  248. package/dist/record/probe-blend-buffer.d.ts +16 -0
  249. package/dist/record/probe-blend-buffer.d.ts.map +1 -0
  250. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  251. package/dist/record/render-context.d.ts +34 -31
  252. package/dist/record/render-context.d.ts.map +1 -1
  253. package/dist/record/shadow-pass.d.ts +2 -0
  254. package/dist/record/shadow-pass.d.ts.map +1 -1
  255. package/dist/record/skybox-post-pass.d.ts +6 -4
  256. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  257. package/dist/record/typed-frame-graph.d.ts +22 -2
  258. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  259. package/dist/record/view-ubo.d.ts +11 -3
  260. package/dist/record/view-ubo.d.ts.map +1 -1
  261. package/dist/record/volume-params.d.ts +9 -0
  262. package/dist/record/volume-params.d.ts.map +1 -0
  263. package/dist/recovery/render-system-candidate.d.ts +10 -0
  264. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  265. package/dist/reflection/record-owner.d.ts.map +1 -1
  266. package/dist/render-contract.d.ts +66 -8
  267. package/dist/render-contract.d.ts.map +1 -1
  268. package/dist/render-graph-primitives.d.ts.map +1 -1
  269. package/dist/render-pipeline.d.ts +106 -3
  270. package/dist/render-pipeline.d.ts.map +1 -1
  271. package/dist/render-system-extract-tail.d.ts +3 -2
  272. package/dist/render-system-extract-tail.d.ts.map +1 -1
  273. package/dist/render-system-extract.d.ts +24 -10
  274. package/dist/render-system-extract.d.ts.map +1 -1
  275. package/dist/render-system-presentation.d.ts +23 -0
  276. package/dist/render-system-presentation.d.ts.map +1 -0
  277. package/dist/render-system-producer-roots.d.ts +10 -0
  278. package/dist/render-system-producer-roots.d.ts.map +1 -0
  279. package/dist/render-system-residency.d.ts +12 -0
  280. package/dist/render-system-residency.d.ts.map +1 -0
  281. package/dist/render-system.d.ts +26 -15
  282. package/dist/render-system.d.ts.map +1 -1
  283. package/dist/scene/probe-blend.d.ts +27 -0
  284. package/dist/scene/probe-blend.d.ts.map +1 -1
  285. package/dist/scene/render-scene.d.ts +14 -5
  286. package/dist/scene/render-scene.d.ts.map +1 -1
  287. package/dist/ssao-buffers.d.ts +9 -0
  288. package/dist/ssao-buffers.d.ts.map +1 -1
  289. package/dist/surface/dynamic-input.d.ts +133 -0
  290. package/dist/surface/dynamic-input.d.ts.map +1 -0
  291. package/dist/surface/gpu-driven.d.ts +41 -0
  292. package/dist/surface/gpu-driven.d.ts.map +1 -0
  293. package/dist/surface/submission-observation.d.ts +136 -0
  294. package/dist/surface/submission-observation.d.ts.map +1 -0
  295. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  296. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  297. package/dist/temporal/frame.d.ts +7 -0
  298. package/dist/temporal/frame.d.ts.map +1 -1
  299. package/dist/temporal/gpu.d.ts +13 -1
  300. package/dist/temporal/gpu.d.ts.map +1 -1
  301. package/dist/temporal/index.mjs +5 -5
  302. package/dist/temporal/inspection.d.ts.map +1 -1
  303. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  304. package/dist/temporal/target-coverage-attachment.d.ts.map +1 -1
  305. package/dist/temporal/view.d.ts +6 -1
  306. package/dist/temporal/view.d.ts.map +1 -1
  307. package/dist/transmission/single-layer-medium.d.ts +89 -0
  308. package/dist/transmission/single-layer-medium.d.ts.map +1 -0
  309. package/dist/typed-render-graph-primitives.d.ts +24 -9
  310. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  311. package/dist/volume/component.d.ts +9 -5
  312. package/dist/volume/component.d.ts.map +1 -1
  313. package/dist/volume/extract.d.ts +1 -1
  314. package/dist/volume/extract.d.ts.map +1 -1
  315. package/dist/volume/inspection.d.ts +2 -0
  316. package/dist/volume/inspection.d.ts.map +1 -1
  317. package/dist/volume/passes.d.ts +4 -1
  318. package/dist/volume/passes.d.ts.map +1 -1
  319. package/dist/volume/resources.d.ts.map +1 -1
  320. package/dist/volume/temporal.d.ts +3 -1
  321. package/dist/volume/temporal.d.ts.map +1 -1
  322. package/package.json +22 -21
  323. package/src/__tests__/analytic-fog-graph.unit.test.ts +199 -0
  324. package/src/__tests__/analytic-fog-optics.unit.test.ts +38 -0
  325. package/src/__tests__/atmosphere-daylight.dawn.test.ts +72 -0
  326. package/src/__tests__/barrel-distortion-lkg.integration.test.ts +353 -0
  327. package/src/__tests__/barrel-distortion.dawn.test.ts +41 -0
  328. package/src/__tests__/barrel-distortion.unit.test.ts +364 -0
  329. package/src/__tests__/bloom-exact-zero.unit.test.ts +112 -18
  330. package/src/__tests__/bloom-lifecycle.integration.test.ts +17 -4
  331. package/src/__tests__/construct-renderer-identity.unit.test.ts +49 -0
  332. package/src/__tests__/detached-recovery-graph.unit.test.ts +277 -0
  333. package/src/__tests__/device-loss-fanout.unit.test.ts +36 -1
  334. package/src/__tests__/docs-gate-green.unit.test.ts +112 -1
  335. package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +20 -0
  336. package/src/__tests__/environment-fog-lanes.integration.test.ts +14 -0
  337. package/src/__tests__/environment-selection.unit.test.ts +12 -0
  338. package/src/__tests__/factory-contract.integration.test.ts +626 -113
  339. package/src/__tests__/feature-depth-input.dawn.test.ts +129 -2
  340. package/src/__tests__/frame-camera-selection.unit.test.ts +39 -1
  341. package/src/__tests__/frame-plan-contract.unit.test.ts +2 -0
  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-indirect-raster-evidence.ts +2 -1
  347. package/src/__tests__/gpu-driven-material-artifacts.unit.test.ts +143 -0
  348. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +2 -2
  349. package/src/__tests__/gpu-driven-production.integration.test.ts +482 -3
  350. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +7 -6
  351. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +1 -1
  352. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +103 -4
  353. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +18 -2
  354. package/src/__tests__/gpu-driven-view.browser.test.ts +5 -8
  355. package/src/__tests__/gpu-driven-view.dawn.test.ts +6 -2
  356. package/src/__tests__/gpu-driven-view.unit.test.ts +1 -1
  357. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +1 -0
  358. package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -4
  359. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +1 -0
  360. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +12 -0
  361. package/src/__tests__/gpu-scene.unit.test.ts +210 -9
  362. package/src/__tests__/instance-allocation-failure.unit.test.ts +57 -0
  363. package/src/__tests__/instances-culling.integration.test.ts +10 -36
  364. package/src/__tests__/instances-store.unit.test.ts +105 -238
  365. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  366. package/src/__tests__/main-pass-material-medium-selection.unit.test.ts +146 -0
  367. package/src/__tests__/material-shader-prewarm.unit.test.ts +59 -0
  368. package/src/__tests__/material-snapshot-mutation.integration.test.ts +39 -1
  369. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +31 -0
  370. package/src/__tests__/morph-culling-reentry.dawn.test.ts +5 -5
  371. package/src/__tests__/morph-visual-evidence.integration.test.ts +8 -7
  372. package/src/__tests__/motion-blur-compute.dawn.test.ts +589 -0
  373. package/src/__tests__/motion-blur.pixel.integration.test.ts +2 -2
  374. package/src/__tests__/nineslice-handle-surface.unit.test.ts +4 -4
  375. package/src/__tests__/pbr-view-layout.unit.test.ts +9 -0
  376. package/src/__tests__/physical-clearcoat.integration.test.ts +58 -0
  377. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +21 -0
  378. package/src/__tests__/prepared-graphics-capability.unit.test.ts +66 -2
  379. package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +86 -0
  380. package/src/__tests__/probe-bindings.unit.test.ts +582 -1
  381. package/src/__tests__/probe-scene-dirty.integration.test.ts +28 -1
  382. package/src/__tests__/public-surface.test-d.ts +8 -0
  383. package/src/__tests__/receipt-observation-retirement.unit.test.ts +65 -0
  384. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +69 -0
  385. package/src/__tests__/render-error-code-owner.test-d.ts +9 -0
  386. package/src/__tests__/render-feature-diagnostics.unit.test.ts +15 -2
  387. package/src/__tests__/render-feature-error-reporting.unit.test.ts +44 -0
  388. package/src/__tests__/render-feature-prepared-diagnostics.unit.test.ts +10 -1
  389. package/src/__tests__/render-feature-retry.unit.test.ts +12 -1
  390. package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +23 -0
  391. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +145 -19
  392. package/src/__tests__/render-scene-projection.unit.test.ts +36 -11
  393. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +37 -6
  394. package/src/__tests__/render-target-probe-soak.perf.test.ts +2 -2
  395. package/src/__tests__/render-temporal-environment-consumer.browser.test.ts +3 -3
  396. package/src/__tests__/render-temporal-environment-consumer.dawn.test.ts +5 -2
  397. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +41 -7
  398. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +16 -0
  399. package/src/__tests__/renderer-frame-transaction.integration.test.ts +33 -11
  400. package/src/__tests__/resource-allocation.unit.test.ts +123 -0
  401. package/src/__tests__/shader-manifest-fixture.ts +4 -4
  402. package/src/__tests__/single-layer-medium-passes.unit.test.ts +275 -0
  403. package/src/__tests__/skin-motion-regression.unit.test.ts +70 -1
  404. package/src/__tests__/skin-palette-recycling.dawn.test.ts +97 -0
  405. package/src/__tests__/skinned-shadow-caster.test.ts +17 -0
  406. package/src/__tests__/ssr-carrier-public-contract.unit.test.ts +21 -0
  407. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +4 -4
  408. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +2 -2
  409. package/src/__tests__/ssr-gpu-dispatch.ts +1 -1
  410. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +6 -2
  411. package/src/__tests__/standard-lighting-write.unit.test.ts +2 -1
  412. package/src/__tests__/standard-output-chain-contract.unit.test.ts +28 -0
  413. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +46 -1
  414. package/src/__tests__/standard-pipeline.dawn.test.ts +1 -1
  415. package/src/__tests__/standard-pipeline.integration.test.ts +200 -9
  416. package/src/__tests__/surface-dynamic-buffer-lifecycle.unit.test.ts +506 -0
  417. package/src/__tests__/surface-submission-observation.unit.test.ts +812 -0
  418. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +32 -0
  419. package/src/__tests__/temporal-performance.integration.test.ts +3 -3
  420. package/src/__tests__/temporal-reset-matrix.unit.test.ts +10 -0
  421. package/src/__tests__/temporal-view.unit.test.ts +2 -1
  422. package/src/__tests__/typed-pipeline-topology.unit.test.ts +20 -4
  423. package/src/__tests__/visibility-instances.integration.test.ts +7 -19
  424. package/src/__tests__/visibility-producer-matrix.integration.test.ts +9 -20
  425. package/src/__tests__/volume-optics-oracle.unit.test.ts +7 -3
  426. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +189 -15
  427. package/src/__tests__/volumetric-fog-errors.unit.test.ts +4 -3
  428. package/src/__tests__/volumetric-fog-multiple.unit.test.ts +55 -0
  429. package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +1 -1
  430. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +558 -68
  431. package/src/__tests__/volumetric-fog-temporal.unit.test.ts +27 -0
  432. package/src/__tests__/volumetric-fog-topology.unit.test.ts +23 -5
  433. package/src/assembly/analytic-fog-registration.ts +14 -0
  434. package/src/assembly/backend-contract.ts +30 -0
  435. package/src/assembly/barrel-distortion-inspection.ts +22 -0
  436. package/src/assembly/canvas-draw-guard.ts +11 -0
  437. package/src/assembly/cloud-shadow-entry.ts +22 -0
  438. package/src/assembly/dynamic-geometry-host.ts +65 -37
  439. package/src/assembly/dynamic-geometry-runtime.ts +7 -1
  440. package/src/assembly/factory.ts +2 -30
  441. package/src/assembly/fxaa-registration.ts +17 -0
  442. package/src/assembly/host-contract.ts +3 -3
  443. package/src/assembly/lod-observation.ts +12 -0
  444. package/src/assembly/material/assembly.ts +25 -13
  445. package/src/assembly/material/pipeline-helpers.ts +53 -12
  446. package/src/assembly/material-shader-policy.ts +55 -0
  447. package/src/assembly/material-shader-prewarm.ts +21 -10
  448. package/src/assembly/motion-blur-registration.ts +17 -0
  449. package/src/assembly/recovery/device-loss-fanout.ts +13 -0
  450. package/src/assembly/recovery/renderer-recover.ts +41 -18
  451. package/src/assembly/render-feature-post-processes.ts +54 -0
  452. package/src/assembly/renderer-facade.ts +6 -1
  453. package/src/assembly/renderer-feature-inspection.ts +8 -2
  454. package/src/assembly/renderer-frame-transaction.ts +23 -0
  455. package/src/assembly/renderer-inspection.ts +28 -0
  456. package/src/assembly/skin-palette-owner.ts +1 -1
  457. package/src/assembly/temporal-post-process-prewarm.ts +40 -0
  458. package/src/assembly/webgpu-pbr-ready.ts +9 -1
  459. package/src/assembly/webgpu-ready-contract.ts +8 -3
  460. package/src/assembly/webgpu-ready-per-pass.ts +104 -0
  461. package/src/assembly/webgpu-ready.ts +296 -306
  462. package/src/assembly/webgpu-renderer-bootstrap.ts +0 -12
  463. package/src/assembly/webgpu-renderer-observation-owner.ts +222 -0
  464. package/src/assembly/webgpu-renderer.ts +228 -281
  465. package/src/assembly/webgpu-vertex-layouts.ts +1 -1
  466. package/src/barrel-distortion.ts +247 -0
  467. package/src/bloom-admission.ts +21 -0
  468. package/src/cloud/__tests__/cloud-density-parity.dawn.test.ts +173 -0
  469. package/src/cloud/__tests__/cloud-layer-framebuffer-y.dawn.test.ts +265 -0
  470. package/src/cloud/__tests__/cloud-layer.browser.test.ts +432 -0
  471. package/src/cloud/__tests__/cloud-layer.unit.test.ts +634 -0
  472. package/src/cloud/density.ts +600 -0
  473. package/src/cloud/extract.ts +110 -0
  474. package/src/cloud/feature.ts +1810 -0
  475. package/src/cloud/frame-context.ts +42 -0
  476. package/src/cloud/inspection.ts +130 -0
  477. package/src/cloud/optics.ts +285 -0
  478. package/src/cloud/parameters.ts +255 -0
  479. package/src/cloud/resources.ts +122 -0
  480. package/src/cloud/shadow.ts +167 -0
  481. package/src/cloud/temporal.ts +226 -0
  482. package/src/components/__tests__/motion-blur.unit.test.ts +1 -0
  483. package/src/components/atmosphere.ts +4 -0
  484. package/src/components/barrel-distortion.ts +46 -0
  485. package/src/components/camera.ts +66 -10
  486. package/src/components/cloud-layer.ts +47 -0
  487. package/src/components/index.ts +2 -0
  488. package/src/components/instances.ts +18 -45
  489. package/src/components/mesh-filter.ts +11 -9
  490. package/src/components/motion-blur.ts +1 -0
  491. package/src/components/sprite-instances.ts +1 -1
  492. package/src/construct-renderer.ts +1 -1
  493. package/src/device/gpu-residency.ts +56 -12
  494. package/src/dynamic-geometry.ts +12 -1
  495. package/src/environment/analytic-fog.ts +44 -0
  496. package/src/environment/background.ts +2 -2
  497. package/src/errors/cloud.ts +103 -0
  498. package/src/errors/render.ts +58 -8
  499. package/src/examples/single-layer-medium-surface.ts +59 -0
  500. package/src/extract/camera.ts +35 -8
  501. package/src/extract/contracts.ts +12 -0
  502. package/src/extract/environment.ts +4 -0
  503. package/src/features/barrel-distortion.ts +172 -0
  504. package/src/features/error-reporting.ts +98 -0
  505. package/src/features/host.ts +138 -24
  506. package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +111 -1
  507. package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +58 -3
  508. package/src/features/motion-blur/motion-blur-feature.ts +1519 -15
  509. package/src/features/motion-blur/motion-blur-params.ts +70 -2
  510. package/src/features/motion-blur/motion-blur-runtime.ts +176 -0
  511. package/src/features/plan.ts +33 -8
  512. package/src/features/prepared-gpu-work.ts +86 -35
  513. package/src/features/render-graph-compute.ts +13 -5
  514. package/src/features/render-graph-contribution.ts +11 -0
  515. package/src/features/render-graph-raster.ts +45 -3
  516. package/src/features/targets.ts +66 -0
  517. package/src/features/types.ts +47 -0
  518. package/src/fullscreen-post-process-pass.ts +41 -9
  519. package/src/gpu-dirty-ranges.ts +48 -0
  520. package/src/gpu-driven/batch-topology.ts +53 -0
  521. package/src/gpu-driven/material-artifacts.ts +35 -32
  522. package/src/gpu-driven/prepared-draw.ts +27 -14
  523. package/src/gpu-driven/production-raster-material.ts +1894 -0
  524. package/src/gpu-driven/production-raster.ts +904 -787
  525. package/src/gpu-driven/resource-allocation.ts +142 -0
  526. package/src/gpu-driven/shadow-ownership.ts +71 -0
  527. package/src/gpu-driven/view-gpu.ts +181 -10
  528. package/src/gpu-scene.ts +259 -101
  529. package/src/hdrp-buffers.ts +4 -3
  530. package/src/ibl/skylight-bind-group.ts +49 -7
  531. package/src/index.ts +228 -9
  532. package/src/inspection-types.ts +222 -2
  533. package/src/instance-buffer-cache.ts +4 -4
  534. package/src/instances.ts +151 -492
  535. package/src/pbr-pipeline.ts +219 -13
  536. package/src/pipeline/single-layer-medium-passes.ts +267 -0
  537. package/src/pipeline/standard-forward-lane.ts +313 -57
  538. package/src/pipeline/standard-output/graph.ts +8 -2
  539. package/src/pipeline/standard-output/types.ts +12 -1
  540. package/src/pipeline/standard-pipeline.ts +291 -18
  541. package/src/pipeline/standard-post.ts +397 -102
  542. package/src/pipeline-builder.ts +1 -1
  543. package/src/pipeline-spec.ts +5 -5
  544. package/src/plugin.ts +4 -0
  545. package/src/points-lines/__tests__/points-lines.bench.ts +17 -8
  546. package/src/points-lines/__tests__/view-ubo.unit.test.ts +2 -1
  547. package/src/prepare/prepared-graphics-resolver.ts +2 -0
  548. package/src/record/__tests__/main-pass-geometry.unit.test.ts +30 -0
  549. package/src/record/__tests__/sprite-material-abi.unit.test.ts +1 -1
  550. package/src/record/__tests__/temporal-payload.unit.test.ts +3 -2
  551. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +11 -0
  552. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -0
  553. package/src/record/__tests__/view-ubo-layout.unit.test.ts +4 -3
  554. package/src/record/barrel-distortion-frame.ts +110 -0
  555. package/src/record/dynamic-geometry-consumption.ts +39 -9
  556. package/src/record/fold-buckets.ts +852 -0
  557. package/src/record/fold-instance-buffer.ts +51 -0
  558. package/src/record/frame-lighting.ts +36 -2
  559. package/src/record/frame-snapshot.ts +44 -4
  560. package/src/record/frame-targets.ts +10 -6
  561. package/src/record/frame.ts +373 -1099
  562. package/src/record/gpu-draw-receipts.ts +38 -0
  563. package/src/record/gpu-timing.ts +48 -5
  564. package/src/record/main-pass-geometry.ts +281 -83
  565. package/src/record/main-pass-material.ts +66 -4
  566. package/src/record/main-pass-sprite-draws.ts +13 -66
  567. package/src/record/main-pass.ts +156 -20
  568. package/src/record/mesh-ssbo.ts +43 -6
  569. package/src/record/motion-blur-receipt.ts +91 -0
  570. package/src/record/probe-blend-buffer.ts +180 -0
  571. package/src/record/recovery-pipeline.ts +35 -3
  572. package/src/record/render-context.ts +51 -31
  573. package/src/record/shadow-pass.ts +36 -2
  574. package/src/record/skybox-post-pass.ts +150 -200
  575. package/src/record/typed-frame-graph.ts +824 -44
  576. package/src/record/view-ubo.ts +26 -3
  577. package/src/record/volume-params.ts +225 -0
  578. package/src/recovery/render-system-candidate.ts +257 -52
  579. package/src/reflection/record-owner.ts +7 -3
  580. package/src/render-contract.ts +71 -8
  581. package/src/render-graph-primitives.ts +7 -1
  582. package/src/render-pipeline.ts +136 -1
  583. package/src/render-system-extract-tail.ts +72 -60
  584. package/src/render-system-extract.ts +117 -87
  585. package/src/render-system-presentation.ts +56 -0
  586. package/src/render-system-producer-roots.ts +96 -0
  587. package/src/render-system-residency.ts +159 -0
  588. package/src/render-system.ts +601 -482
  589. package/src/scene/probe-blend.ts +96 -4
  590. package/src/scene/render-scene.ts +431 -281
  591. package/src/ssao-buffers.ts +95 -2
  592. package/src/surface/__tests__/dynamic-input.unit.test.ts +374 -0
  593. package/src/surface/__tests__/gpu-driven.unit.test.ts +87 -0
  594. package/src/surface/dynamic-input.ts +658 -0
  595. package/src/surface/gpu-driven.ts +147 -0
  596. package/src/surface/submission-observation.ts +517 -0
  597. package/src/systems/skin-palette-allocator.ts +4 -1
  598. package/src/temporal/__tests__/temporal-frame-transaction.unit.test.ts +36 -0
  599. package/src/temporal/frame-coordinator.ts +8 -1
  600. package/src/temporal/frame.ts +20 -0
  601. package/src/temporal/gpu.ts +101 -17
  602. package/src/temporal/inspection.ts +7 -1
  603. package/src/temporal/standard-scene-data.ts +5 -0
  604. package/src/temporal/target-coverage-attachment.ts +4 -0
  605. package/src/temporal/view.ts +13 -2
  606. package/src/transmission/__tests__/single-layer-medium.unit.test.ts +149 -0
  607. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +12 -11
  608. package/src/transmission/single-layer-medium.ts +322 -0
  609. package/src/typed-render-graph-primitives.ts +191 -50
  610. package/src/volume/component.ts +10 -10
  611. package/src/volume/extract.ts +11 -5
  612. package/src/volume/inspection.ts +3 -0
  613. package/src/volume/passes.ts +95 -8
  614. package/src/volume/resources.ts +2 -1
  615. package/src/volume/temporal.ts +11 -0
  616. package/dist/chunk-4NNZQCL3.mjs.map +0 -1
  617. package/dist/chunk-6TFBXLRJ.mjs.map +0 -1
  618. package/dist/chunk-DRR7D3KX.mjs.map +0 -1
  619. package/dist/chunk-GWCEMDVR.mjs.map +0 -1
  620. package/dist/chunk-JCIRYQ4V.mjs.map +0 -1
  621. package/dist/chunk-JRIPCMYC.mjs.map +0 -1
  622. package/dist/chunk-LULCA3OF.mjs +0 -1713
  623. package/dist/chunk-LULCA3OF.mjs.map +0 -1
  624. package/dist/chunk-PA5DRJNI.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,
@@ -84,7 +98,12 @@ import {
84
98
  type ShadowViewUpdateInput,
85
99
  shadowViewIdentityKey,
86
100
  } from './shadow-views';
87
- import { type GpuDrivenLodSelectionInspection, GpuDrivenView, selectGpuLodLane } from './view-gpu';
101
+ import {
102
+ type GpuDrivenLodSelectionInspection,
103
+ type GpuDrivenLodSubmitIdentity,
104
+ GpuDrivenView,
105
+ selectGpuLodLane,
106
+ } from './view-gpu';
88
107
 
89
108
  export interface StandardPbrSceneCandidate {
90
109
  readonly primitiveIndex: number;
@@ -95,6 +114,39 @@ export interface StandardPbrSceneCandidate {
95
114
  readonly materialIndex?: number;
96
115
  }
97
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
+
98
150
  export interface StandardPbrSceneProjection {
99
151
  readonly entryPoint: string;
100
152
  readonly layoutIdentity: string;
@@ -253,21 +305,34 @@ interface PreparedBatch {
253
305
  readonly slotGeneration: number;
254
306
  }
255
307
 
256
- function bufferBinding(
257
- buffer: import('@forgeax/engine-rhi').Buffer,
258
- offset?: number,
259
- size?: number,
260
- ) {
261
- return {
262
- kind: 'buffer' as const,
263
- value: {
264
- buffer,
265
- ...(offset === undefined ? {} : { offset }),
266
- ...(size === undefined ? {} : { size }),
267
- },
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;
268
323
  };
269
324
  }
270
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
+
271
336
  function activeFrustum(camera: CameraSnapshot): Float32Array {
272
337
  const projection = mat4.create();
273
338
  if (camera.projection === 'orthographic') {
@@ -925,6 +990,136 @@ function shadowCasterMembershipSignature(
925
990
 
926
991
  type FilteredProductionPlan = ReturnType<typeof filteredPlan>;
927
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
+
928
1123
  function sceneRequiresGpuDrivenArtifact(scene: PersistentGpuDrivenState): boolean {
929
1124
  return scene.plan.batches.some((batch) =>
930
1125
  batch.candidates.some((candidate) => {
@@ -1134,736 +1329,11 @@ function materialArtifactSignature(
1134
1329
  }
1135
1330
 
1136
1331
  /** The GPU-driven bridge to the existing four-group Standard PBR pipeline. */
1137
- class MaterialAbiRasterAdapter {
1138
- private readonly pipelines = new Map<string, RenderPipeline>();
1139
- private readonly projectionBuffers = new Set<Buffer>();
1140
- private readonly projectionBindings = new Map<
1141
- string,
1142
- {
1143
- readonly buffer: Buffer;
1144
- readonly bindGroup: BindGroup;
1145
- readonly capacityBytes: number;
1146
- rows: Float32Array;
1147
- }
1148
- >();
1149
- /**
1150
- * Projection rows are graph-generation resources. A new topology may stage
1151
- * a replacement before the graph is promoted; keeping staged bindings out
1152
- * of the active map lets an aborted graph leave the last submitted rows
1153
- * intact. Superseded buffers are fence-retired on promotion.
1154
- */
1155
- private readonly stagedProjectionBindings = new Map<
1156
- string,
1157
- {
1158
- readonly buffer: Buffer;
1159
- readonly bindGroup: BindGroup;
1160
- readonly capacityBytes: number;
1161
- rows: Float32Array;
1162
- }
1163
- >();
1164
- private readonly pendingProjectionRetirements = new Set<Buffer>();
1165
- private readonly destroyedProjectionBuffers = new Set<Buffer>();
1166
- private projectionDisposed = false;
1167
- private stagedProjectionKeys = new Set<string>();
1168
- private readonly activeProjectionRowsAtStage = new Map<string, Float32Array>();
1169
- private readonly visibleWindowBindGroups = new Map<Buffer, Map<string, BindGroup>>();
1170
-
1171
- private constructor(
1172
- private readonly device: RhiDevice,
1173
- private readonly pipelineLayout: import('@forgeax/engine-rhi').PipelineLayout,
1174
- private readonly meshLayout: BindGroupLayout,
1175
- private readonly instancesBindGroupLayout: BindGroupLayout,
1176
- private readonly programs: ReadonlyMap<string, GpuDrivenPbrProgram> | undefined,
1177
- private instancesBindGroup: BindGroup,
1178
- private readonly instanceBuffer: Buffer,
1179
- private readonly instanceBufferSize: number,
1180
- private visibleBuffer: Buffer,
1181
- private readonly artifact: MaterialShaderArtifact,
1182
- private readonly deformation: 'rigid' | 'skin',
1183
- private readonly pipelineFactory: MaterialPipelineFactory | undefined,
1184
- private paletteBuffer: Buffer | undefined,
1185
- private paletteBindingWindowBytes: number | undefined,
1186
- ) {}
1187
-
1188
- static create(input: {
1189
- readonly device: RhiDevice;
1190
- readonly pipelineState: PipelineState;
1191
- readonly artifact: MaterialShaderArtifact;
1192
- readonly visibleBuffer: Buffer;
1193
- readonly deformation?: 'rigid' | 'skin';
1194
- readonly paletteBuffer?: Buffer;
1195
- readonly paletteBindingWindowBytes?: number;
1196
- /** Renderer-owned factory for the selected artifact's actual PSO. */
1197
- readonly pipelineFactory?: MaterialPipelineFactory;
1198
- }): Result<MaterialAbiRasterAdapter, RhiError | GpuDrivenPreparationError> {
1199
- const deformation = input.deformation ?? 'rigid';
1200
- const receipt = input.artifact.receipt;
1201
- if (receipt === undefined) {
1202
- return err(
1203
- new RhiError({
1204
- code: 'rhi-not-available',
1205
- expected: 'published MaterialProgramAbi receipt for the selected material artifact',
1206
- hint: 'recook and publish the material before creating a GPU-driven raster owner',
1207
- }),
1208
- );
1209
- }
1210
- const materialRowMatchesSceneTable =
1211
- receipt.materialRow.byteLength === GPU_SCENE_LAYOUTS.material.stride &&
1212
- new Set(receipt.materialRow.fields).size === receipt.materialRow.fields.length;
1213
- if (!materialRowMatchesSceneTable) {
1214
- return err(
1215
- new GpuDrivenPreparationError('material-abi-not-executable', {
1216
- reason: 'material-abi-not-executable',
1217
- owner: 'material',
1218
- expected: `scene-index material row ${GPU_SCENE_LAYOUTS.material.stride} bytes with unique producer fields`,
1219
- actual: `row ${receipt.materialRow.byteLength} bytes with fields ${receipt.materialRow.fields.join(',')}`,
1220
- }),
1221
- );
1222
- }
1223
- if (
1224
- receipt.reflection.layoutIdentity !== input.artifact.layoutIdentity ||
1225
- receipt.directEntry.length === 0 ||
1226
- receipt.sceneIndexEntry.length === 0 ||
1227
- receipt.resourceSlots.some((slot) => slot.group !== 1)
1228
- ) {
1229
- return err(
1230
- new RhiError({
1231
- code: 'rhi-descriptor-invalid',
1232
- expected: `complete selected MaterialProgramAbi reflection, scene row stride ${GPU_SCENE_LAYOUTS.material.stride}, and resource slots`,
1233
- hint: 'repair the producer publication or use the owner for its non-scene material layout',
1234
- }),
1235
- );
1236
- }
1237
- // Scene-index shaders declare the dedicated slot-3 ABI. Keep the legacy
1238
- // fields as a compatibility fallback for synthetic fixtures; the real
1239
- // factory publishes the dedicated handles whenever the capable storage
1240
- // lane is enabled, so production draws never bind the direct-draw BGL.
1241
- const pipelineLayout =
1242
- deformation === 'skin'
1243
- ? input.pipelineState.gpuDrivenPbrSkinPipelineLayout
1244
- : input.pipelineState.gpuDrivenPbrPipelineLayout;
1245
- const instancesBindGroupLayout = input.pipelineState.gpuDrivenInstancesBindGroupLayout;
1246
- if (pipelineLayout == null || instancesBindGroupLayout == null) {
1247
- return err(
1248
- new RhiError({
1249
- code: 'rhi-not-available',
1250
- expected: `${deformation === 'skin' ? 'skinned ' : ''}published material pipeline layout`,
1251
- hint: 'initialize the producer material pipeline before enabling GPU-driven raster',
1252
- }),
1253
- );
1254
- }
1255
- const programs = input.pipelineState.gpuDrivenPbrPrograms;
1256
- if (
1257
- input.pipelineFactory === undefined &&
1258
- (programs === undefined ||
1259
- resolveStandardPbrProgram(
1260
- programs,
1261
- input.artifact.material,
1262
- input.artifact.receipt?.vertexInputs.some((attribute) => attribute.semantic === 'color'),
1263
- ) === undefined)
1264
- ) {
1265
- return err(
1266
- new RhiError({
1267
- code: 'rhi-not-available',
1268
- expected: `producer-owned ${deformation === 'skin' ? 'skinned ' : ''}material shader module for the current device generation`,
1269
- hint: 'publish the selected material module from buildReadyWebGPU before enabling GPU-driven raster',
1270
- }),
1271
- );
1272
- }
1273
- const meshLayout =
1274
- deformation === 'skin'
1275
- ? input.pipelineState.pbrSkinMeshBindGroupLayout
1276
- : input.pipelineState.meshBindGroupLayout;
1277
- if (meshLayout === null) {
1278
- return err(
1279
- new RhiError({
1280
- code: 'rhi-not-available',
1281
- expected: `${deformation === 'skin' ? 'skinned ' : ''}material mesh bind-group layout`,
1282
- hint: 'publish the exact mesh-array layout selected by the material receipt',
1283
- }),
1284
- );
1285
- }
1286
- if (
1287
- deformation === 'skin' &&
1288
- (input.paletteBuffer === undefined || input.paletteBindingWindowBytes === undefined)
1289
- ) {
1290
- return err(
1291
- new RhiError({
1292
- code: 'rhi-not-available',
1293
- expected: 'persistent skin palette buffer and binding window',
1294
- hint: 'keep the palette receipt and the GPU-driven skinned projection on one frame identity',
1295
- }),
1296
- );
1297
- }
1298
- const identity = new Float32Array(16);
1299
- identity[0] = 1;
1300
- identity[5] = 1;
1301
- identity[10] = 1;
1302
- identity[15] = 1;
1303
- const instancePayload = packInstanceStorageBuffer(identity);
1304
- const instanceBuffer = input.device.createBuffer({
1305
- label: 'standard-pbr-gpu-driven-instance-data',
1306
- size: instancePayload.byteLength,
1307
- usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
1308
- });
1309
- if (!instanceBuffer.ok) return instanceBuffer;
1310
- const instanceWrite = input.device.queue.writeBuffer(instanceBuffer.value, 0, instancePayload);
1311
- if (!instanceWrite.ok) return instanceWrite;
1312
- const instancesBindGroup = input.device.createBindGroup({
1313
- label: 'standard-pbr-gpu-driven-visible-instances-bg',
1314
- layout: instancesBindGroupLayout,
1315
- entries: [
1316
- {
1317
- binding: 0,
1318
- resource: bufferBinding(instanceBuffer.value, 0, instancePayload.byteLength),
1319
- },
1320
- {
1321
- binding: 1,
1322
- resource: bufferBinding(input.visibleBuffer),
1323
- },
1324
- {
1325
- binding: 2,
1326
- resource: bufferBinding(input.visibleBuffer),
1327
- },
1328
- ],
1329
- });
1330
- if (!instancesBindGroup.ok) return instancesBindGroup;
1331
- return ok(
1332
- new MaterialAbiRasterAdapter(
1333
- input.device,
1334
- pipelineLayout,
1335
- meshLayout,
1336
- instancesBindGroupLayout,
1337
- programs,
1338
- instancesBindGroup.value,
1339
- instanceBuffer.value,
1340
- instancePayload.byteLength,
1341
- input.visibleBuffer,
1342
- input.artifact,
1343
- deformation,
1344
- input.pipelineFactory,
1345
- input.paletteBuffer,
1346
- input.paletteBindingWindowBytes,
1347
- ),
1348
- );
1349
- }
1350
-
1351
- ensurePaletteBuffer(
1352
- paletteBuffer: Buffer | undefined,
1353
- paletteBindingWindowBytes: number | undefined,
1354
- ): Result<void, RhiError> {
1355
- if (this.deformation !== 'skin') return ok(undefined);
1356
- if (paletteBuffer === undefined || paletteBindingWindowBytes === undefined) {
1357
- return err(
1358
- new RhiError({
1359
- code: 'rhi-not-available',
1360
- expected: 'persistent skin palette buffer and binding window',
1361
- hint: 'publish a storage-backed SkinPaletteReceipt before the GPU-driven skinned lane',
1362
- }),
1363
- );
1364
- }
1365
- this.paletteBuffer = paletteBuffer;
1366
- this.paletteBindingWindowBytes = paletteBindingWindowBytes;
1367
- return ok(undefined);
1368
- }
1369
-
1370
- ensureVisibleBuffer(visibleBuffer: Buffer): Result<void, RhiError> {
1371
- if (this.visibleBuffer === visibleBuffer) return ok(undefined);
1372
- const created = this.device.createBindGroup({
1373
- label: 'standard-pbr-gpu-driven-visible-instances-bg',
1374
- layout: this.instancesBindGroupLayout,
1375
- entries: [
1376
- {
1377
- binding: 0,
1378
- resource: bufferBinding(this.instanceBuffer, 0, this.instanceBufferSize),
1379
- },
1380
- { binding: 1, resource: bufferBinding(visibleBuffer) },
1381
- { binding: 2, resource: bufferBinding(visibleBuffer) },
1382
- ],
1383
- });
1384
- if (!created.ok) return created;
1385
- this.instancesBindGroup = created.value;
1386
- this.visibleBuffer = visibleBuffer;
1387
- return ok(undefined);
1388
- }
1389
-
1390
- visibleWindowBindGroup(
1391
- visibleBase: number,
1392
- visibleCapacity: number,
1393
- ): Result<BindGroup, RhiError> {
1394
- return this.visibleWindowBindGroupForBuffer(this.visibleBuffer, visibleBase, visibleCapacity);
1395
- }
1396
-
1397
- visibleWindowBindGroupForBuffer(
1398
- visibleBuffer: Buffer,
1399
- visibleBase: number,
1400
- visibleCapacity: number,
1401
- ): Result<BindGroup, RhiError> {
1402
- const offset = visibleBase * 8;
1403
- const size = visibleCapacity * 8;
1404
- if (offset % 256 !== 0 || size <= 0) {
1405
- return err(
1406
- new RhiError({
1407
- code: 'rhi-descriptor-invalid',
1408
- expected: '256-byte-aligned non-empty visible window',
1409
- hint: `allocate each GPU-driven batch visible window on the portable storage-buffer boundary (base=${visibleBase}, capacity=${visibleCapacity}, offset=${offset}, size=${size})`,
1410
- }),
1411
- );
1412
- }
1413
- let groups = this.visibleWindowBindGroups.get(visibleBuffer);
1414
- if (groups === undefined) {
1415
- groups = new Map();
1416
- this.visibleWindowBindGroups.set(visibleBuffer, groups);
1417
- }
1418
- const key = `${visibleBase}:${visibleCapacity}`;
1419
- const cached = groups.get(key);
1420
- if (cached !== undefined) return ok(cached);
1421
- const created = this.device.createBindGroup({
1422
- label: `standard-pbr-gpu-driven-visible-window-${visibleBase}`,
1423
- layout: this.instancesBindGroupLayout,
1424
- entries: [
1425
- {
1426
- binding: 0,
1427
- resource: bufferBinding(this.instanceBuffer, 0, this.instanceBufferSize),
1428
- },
1429
- { binding: 1, resource: bufferBinding(visibleBuffer, offset, size) },
1430
- { binding: 2, resource: bufferBinding(visibleBuffer, offset, size) },
1431
- ],
1432
- });
1433
- if (!created.ok) return created;
1434
- groups.set(key, created.value);
1435
- return ok(created.value);
1436
- }
1437
-
1438
- pipeline(
1439
- format: TextureFormat,
1440
- sampleCount: 1 | 4,
1441
- layoutProjection: MeshGpuHandles['layoutProjection'],
1442
- shaderUvSetCount: number,
1443
- topology: GpuDrivenBatch['key']['topology'],
1444
- stripIndexFormat: 'uint16' | 'uint32' | undefined,
1445
- renderState: MaterialRenderState | undefined,
1446
- selectedArtifact: MaterialShaderArtifact = this.artifact,
1447
- additionalColorFormats: readonly TextureFormat[] = [],
1448
- ): Result<RenderPipeline, RhiError> {
1449
- const vertexLayout = deriveVertexBufferLayoutFromProjection(layoutProjection, {
1450
- shaderUvSetCount,
1451
- })[0];
1452
- if (vertexLayout === undefined) {
1453
- return err(
1454
- new RhiError({
1455
- code: 'shader-compile-failed',
1456
- expected: 'non-empty geometry VertexLayoutProjection',
1457
- hint: 'prepare a receipt-compatible mesh projection before Standard PBR raster',
1458
- }),
1459
- );
1460
- }
1461
- const receipt = selectedArtifact.receipt;
1462
- if (receipt === undefined) {
1463
- return err(
1464
- new RhiError({
1465
- code: 'rhi-not-available',
1466
- expected: 'selected MaterialProgramAbi receipt for GPU-driven raster',
1467
- hint: 'keep the draw on the producer recovery path until its ABI is published',
1468
- }),
1469
- );
1470
- }
1471
- const program =
1472
- this.programs === undefined
1473
- ? undefined
1474
- : resolveStandardPbrProgram(
1475
- this.programs,
1476
- this.artifact.material,
1477
- layoutProjection.attributes.some((attribute) => attribute.key === 'color'),
1478
- );
1479
- if (this.pipelineFactory === undefined && program === undefined) {
1480
- return err(
1481
- new RhiError({
1482
- code: 'rhi-not-available',
1483
- expected: 'producer-owned Standard PBR program matching the geometry vertex layout',
1484
- hint: 'publish both vertex-color variants for the current device generation',
1485
- }),
1486
- );
1487
- }
1488
- const key = `${format}:${additionalColorFormats.join(',')}|${sampleCount}|${vertexLayout.arrayStride}|${topology}|${stripIndexFormat ?? ''}|${JSON.stringify(renderState ?? null)}|${selectedArtifact.material}|${selectedArtifact.layoutIdentity}|${receipt.receiptIdentity}|${receipt.generation}|${selectedArtifact.variantSet ?? ''}|${materialArtifactProgramIdentity(selectedArtifact)}|${selectedArtifact.vertexEntry ?? receipt.sceneIndexEntry}|${selectedArtifact.fragmentEntry ?? ''}|${layoutProjection.digest}|uv${shaderUvSetCount}`;
1489
- const cached = this.pipelines.get(key);
1490
- if (cached !== undefined) return ok(cached);
1491
- if (this.pipelineFactory !== undefined) {
1492
- const pipelineEntry = this.pipelineFactory(
1493
- selectedArtifact.specializationKey ?? selectedArtifact.material,
1494
- false,
1495
- renderState,
1496
- topology,
1497
- stripIndexFormat,
1498
- selectedArtifact.variantSet,
1499
- 'forward',
1500
- undefined,
1501
- sampleCount,
1502
- format,
1503
- selectedArtifact.uvSetCount ?? shaderUvSetCount,
1504
- 'depth24plus-stencil8',
1505
- undefined,
1506
- layoutProjection,
1507
- // GPU-driven receipts are already producer-validated. Use the
1508
- // synchronous shader-module adapter for this submission seam so a
1509
- // newly loaded authored material cannot lose its first draw to the
1510
- // ordinary async pre-bake warm-up window.
1511
- 'immediate',
1512
- this.deformation === 'skin' ? 'gpu-driven-skin' : 'gpu-driven-pbr',
1513
- receipt.sceneIndexEntry,
1514
- additionalColorFormats,
1515
- selectedArtifact.vertexEntry,
1516
- selectedArtifact.fragmentEntry,
1517
- );
1518
- if (pipelineEntry === null) {
1519
- return err(
1520
- new RhiError({
1521
- code: 'rhi-not-available',
1522
- expected: `producer-owned scene-index pipeline for material '${selectedArtifact.material}'`,
1523
- hint: 'publish a matching program, entry, layout and resource contract before GPU admission',
1524
- }),
1525
- );
1526
- }
1527
- const expectedGroup2Contract = resolvePipelineGroup2Contract(selectedArtifact.wgsl);
1528
- if (pipelineEntry.group2Contract !== expectedGroup2Contract) {
1529
- return err(
1530
- new RhiError({
1531
- code: 'rhi-descriptor-invalid',
1532
- expected: `GPU-driven ${this.deformation === 'skin' ? 'skinned ' : ''}material pipeline group(2) ${expectedGroup2Contract} contract`,
1533
- hint: 'custom attachment or incompatible group(2) layouts remain on their specialized lane',
1534
- }),
1535
- );
1536
- }
1537
- this.pipelines.set(key, pipelineEntry.pipeline);
1538
- return ok(pipelineEntry.pipeline);
1539
- }
1540
- if (
1541
- program === undefined ||
1542
- selectedArtifact.material !== this.artifact.material ||
1543
- selectedArtifact.layoutIdentity !== this.artifact.layoutIdentity ||
1544
- selectedArtifact.receipt?.receiptIdentity !== this.artifact.receipt?.receiptIdentity
1545
- ) {
1546
- return err(
1547
- new RhiError({
1548
- code: 'rhi-not-available',
1549
- expected: 'selected material pipeline factory for the requested ABI artifact',
1550
- hint: 'do not pair a receipt-backed custom or color variant with the legacy fixed module',
1551
- }),
1552
- );
1553
- }
1554
- const pipeline = this.device.createRenderPipeline({
1555
- label: `standard-pbr-gpu-driven.${key}`,
1556
- layout: this.pipelineLayout,
1557
- vertex: {
1558
- module: program.module,
1559
- entryPoint: selectedArtifact.vertexEntry ?? receipt.sceneIndexEntry,
1560
- buffers: [
1561
- {
1562
- arrayStride: vertexLayout.arrayStride,
1563
- stepMode: 'vertex',
1564
- attributes: vertexLayout.attributes.map(({ shaderLocation, offset, format }) => ({
1565
- shaderLocation,
1566
- offset,
1567
- format: format as GPUVertexFormat,
1568
- })),
1569
- },
1570
- ],
1571
- },
1572
- fragment: {
1573
- module: program.module,
1574
- entryPoint: 'fs_main',
1575
- targets: [format, ...additionalColorFormats].map((format) => ({ format })),
1576
- },
1577
- primitive: {
1578
- topology,
1579
- cullMode: renderState?.cullMode ?? 'back',
1580
- frontFace: renderState?.frontFace ?? 'ccw',
1581
- ...(stripIndexFormat === undefined ? {} : { stripIndexFormat }),
1582
- },
1583
- depthStencil: {
1584
- format: 'depth24plus-stencil8',
1585
- depthWriteEnabled: renderState?.depthWriteEnabled ?? true,
1586
- depthCompare: renderState?.depthCompare ?? 'less',
1587
- },
1588
- ...(sampleCount === 4 ? { multisample: { count: 4 } } : {}),
1589
- });
1590
- if (!pipeline.ok) return pipeline;
1591
- this.pipelines.set(key, pipeline.value);
1592
- return pipeline;
1593
- }
1594
-
1595
- /**
1596
- * Drop only program-derived caches when a publication switches. Resource
1597
- * buffers stay alive until their normal fence retirement, while the next
1598
- * draw must obtain a PSO for the new artifact rather than reuse the old one.
1599
- */
1600
- invalidatePipelineCache(): void {
1601
- this.pipelines.clear();
1602
- }
1603
-
1604
- projectionBindGroup(
1605
- rows: Float32Array,
1606
- key?: string,
1607
- capacityRows?: number,
1608
- ): Result<BindGroup, RhiError> {
1609
- if (key !== undefined) {
1610
- const capacityBytes = Math.max(
1611
- MESH_SSBO_BYTES,
1612
- (capacityRows ?? Math.ceil(rows.byteLength / MESH_SSBO_BYTES)) * MESH_SSBO_BYTES,
1613
- );
1614
- const retained = this.stagedProjectionBindings.get(key) ?? this.projectionBindings.get(key);
1615
- if (retained !== undefined && retained.capacityBytes >= capacityBytes) {
1616
- const write = this.writeProjectionRows(key, retained, rows);
1617
- if (!write.ok) return write;
1618
- return ok(retained.bindGroup);
1619
- }
1620
- const created = this.createProjectionBinding(rows, capacityBytes);
1621
- if (!created.ok) return created;
1622
- this.stagedProjectionBindings.set(key, created.value);
1623
- return ok(created.value.bindGroup);
1624
- }
1625
- const created = this.createProjectionBinding(rows, Math.max(MESH_SSBO_BYTES, rows.byteLength));
1626
- if (!created.ok) return created;
1627
- return ok(created.value.bindGroup);
1628
- }
1629
-
1630
- refreshProjectionRows(key: string, rows: Float32Array): Result<void, RhiError> {
1631
- const retained = this.stagedProjectionBindings.get(key) ?? this.projectionBindings.get(key);
1632
- if (retained === undefined) return ok(undefined);
1633
- if (rows.byteLength > retained.capacityBytes) {
1634
- const capacityBytes = Math.max(
1635
- MESH_SSBO_BYTES,
1636
- Math.ceil(rows.byteLength / MESH_SSBO_BYTES) * MESH_SSBO_BYTES,
1637
- );
1638
- const replacement = this.createProjectionBinding(rows, capacityBytes);
1639
- if (!replacement.ok) return replacement;
1640
- const previousStaged = this.stagedProjectionBindings.get(key);
1641
- if (previousStaged !== undefined && previousStaged.buffer !== replacement.value.buffer) {
1642
- this.retireProjectionBuffer(previousStaged.buffer);
1643
- }
1644
- this.stagedProjectionBindings.set(key, replacement.value);
1645
- return ok(undefined);
1646
- }
1647
- return this.writeProjectionRows(key, retained, rows);
1648
- }
1649
-
1650
- sceneMeshBinding(key: string): GpuDrivenSceneMeshBinding {
1651
- const binding = this.stagedProjectionBindings.get(key) ?? this.projectionBindings.get(key);
1652
- if (binding === undefined)
1653
- throw new RhiError({
1654
- code: 'rhi-not-available',
1655
- expected: 'retained scene mesh projection',
1656
- hint: 'prepare this batch projection before binding its lighting resources',
1657
- });
1658
- return {
1659
- meshBuffer: binding.buffer,
1660
- meshBytes: binding.capacityBytes,
1661
- ...(this.deformation === 'skin' && this.paletteBuffer !== undefined
1662
- ? { paletteBuffer: this.paletteBuffer }
1663
- : {}),
1664
- };
1665
- }
1666
-
1667
- /** Stage the projection keys required by the frame being prepared. */
1668
- stageProjectionKeys(keys: ReadonlySet<string>): void {
1669
- this.stagedProjectionKeys = new Set(keys);
1670
- this.activeProjectionRowsAtStage.clear();
1671
- }
1672
-
1673
- /** Add a shadow projection key discovered while updating a view plan. */
1674
- noteProjectionKey(key: string): void {
1675
- this.stagedProjectionKeys.add(key);
1676
- }
1677
-
1678
- /** Promote staged projection resources after the command buffer submits. */
1679
- commitProjectionBindings(): void {
1680
- const keep = new Set(this.stagedProjectionKeys);
1681
- for (const [key, staged] of this.stagedProjectionBindings) {
1682
- if (!keep.has(key)) {
1683
- this.retireProjectionBuffer(staged.buffer);
1684
- continue;
1685
- }
1686
- const previous = this.projectionBindings.get(key);
1687
- this.projectionBindings.set(key, staged);
1688
- if (previous !== undefined && previous.buffer !== staged.buffer) {
1689
- this.retireProjectionBuffer(previous.buffer);
1690
- }
1691
- }
1692
- this.stagedProjectionBindings.clear();
1693
- for (const [key, active] of this.projectionBindings) {
1694
- if (!keep.has(key)) {
1695
- this.projectionBindings.delete(key);
1696
- this.retireProjectionBuffer(active.buffer);
1697
- }
1698
- }
1699
- this.stagedProjectionKeys.clear();
1700
- this.activeProjectionRowsAtStage.clear();
1701
- }
1702
-
1703
- /** Discard resources created for an aborted graph while retaining LKG rows. */
1704
- abortProjectionBindings(): void {
1705
- for (const [key, rows] of this.activeProjectionRowsAtStage) {
1706
- const active = this.projectionBindings.get(key);
1707
- if (active === undefined) continue;
1708
- const restored = this.device.queue.writeBuffer(active.buffer, 0, rows);
1709
- if (restored.ok) active.rows = new Float32Array(rows);
1710
- }
1711
- for (const staged of this.stagedProjectionBindings.values()) {
1712
- this.retireProjectionBuffer(staged.buffer);
1713
- }
1714
- this.stagedProjectionBindings.clear();
1715
- this.stagedProjectionKeys.clear();
1716
- this.activeProjectionRowsAtStage.clear();
1717
- }
1718
-
1719
- private createProjectionBinding(
1720
- rows: Float32Array,
1721
- capacityBytes: number,
1722
- ): Result<
1723
- {
1724
- readonly buffer: Buffer;
1725
- readonly bindGroup: BindGroup;
1726
- readonly capacityBytes: number;
1727
- rows: Float32Array;
1728
- },
1729
- RhiError
1730
- > {
1731
- const buffer = this.device.createBuffer({
1732
- label: 'standard-pbr-gpu-driven-scene-meshes',
1733
- size: capacityBytes,
1734
- usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
1735
- });
1736
- if (!buffer.ok) return buffer;
1737
- const write = this.device.queue.writeBuffer(buffer.value, 0, rows);
1738
- if (!write.ok) {
1739
- this.destroyProjectionBuffer(buffer.value);
1740
- return write;
1741
- }
1742
- this.projectionBuffers.add(buffer.value);
1743
- const entries = [
1744
- { binding: 0, resource: bufferBinding(buffer.value, 0, capacityBytes) },
1745
- ...(this.deformation === 'skin' &&
1746
- this.paletteBuffer !== undefined &&
1747
- this.paletteBindingWindowBytes !== undefined
1748
- ? [
1749
- {
1750
- binding: 1,
1751
- // The scene-index second lane is a global palette row. Bind the
1752
- // complete arena so rows beyond the legacy 255-joint window
1753
- // remain addressable; the shader performs the row selection.
1754
- resource: bufferBinding(this.paletteBuffer),
1755
- },
1756
- {
1757
- binding: 2,
1758
- // The dedicated GPU-driven skin layout keeps the current /
1759
- // previous palette pair used by the direct skin layout. The
1760
- // scene-index shader currently reads the current arena, but
1761
- // the shared layout still requires a complete binding shape.
1762
- resource: bufferBinding(this.paletteBuffer),
1763
- },
1764
- ]
1765
- : []),
1766
- ];
1767
- const bindGroup = this.device.createBindGroup({
1768
- label: 'standard-pbr-gpu-driven-scene-mesh-bg',
1769
- layout: this.meshLayout,
1770
- entries,
1771
- });
1772
- if (!bindGroup.ok) {
1773
- this.destroyProjectionBuffer(buffer.value);
1774
- return bindGroup;
1775
- }
1776
- return ok({
1777
- buffer: buffer.value,
1778
- bindGroup: bindGroup.value,
1779
- capacityBytes,
1780
- rows: new Float32Array(rows),
1781
- });
1782
- }
1783
-
1784
- private writeProjectionRows(
1785
- key: string,
1786
- binding: {
1787
- readonly buffer: Buffer;
1788
- readonly bindGroup: BindGroup;
1789
- readonly capacityBytes: number;
1790
- rows: Float32Array;
1791
- },
1792
- rows: Float32Array,
1793
- ): Result<void, RhiError> {
1794
- // Active bindings are the last-known-good graph resources. Keep one CPU
1795
- // snapshot so an aborted candidate can restore their exact contents
1796
- // without allocating a second GPU buffer on every stable frame.
1797
- if (
1798
- this.stagedProjectionBindings.get(key) === undefined &&
1799
- !this.activeProjectionRowsAtStage.has(key)
1800
- ) {
1801
- this.activeProjectionRowsAtStage.set(key, new Float32Array(binding.rows));
1802
- }
1803
- const write = this.device.queue.writeBuffer(binding.buffer, 0, rows);
1804
- if (!write.ok) return write;
1805
- binding.rows = new Float32Array(rows);
1806
- return ok(undefined);
1807
- }
1808
-
1809
- dispose(): void {
1810
- this.projectionDisposed = true;
1811
- for (const binding of this.projectionBindings.values()) {
1812
- this.destroyProjectionBuffer(binding.buffer);
1813
- }
1814
- for (const binding of this.stagedProjectionBindings.values()) {
1815
- this.destroyProjectionBuffer(binding.buffer);
1816
- }
1817
- for (const buffer of this.projectionBuffers) this.destroyProjectionBuffer(buffer);
1818
- for (const buffer of this.pendingProjectionRetirements) {
1819
- this.destroyProjectionBuffer(buffer);
1820
- }
1821
- this.projectionBindings.clear();
1822
- this.stagedProjectionBindings.clear();
1823
- this.activeProjectionRowsAtStage.clear();
1824
- this.projectionBuffers.clear();
1825
- this.pendingProjectionRetirements.clear();
1826
- this.device.destroyBuffer(this.instanceBuffer);
1827
- }
1828
-
1829
- private retireProjectionBuffer(buffer: Buffer): void {
1830
- if (this.destroyedProjectionBuffers.has(buffer)) return;
1831
- this.projectionBuffers.delete(buffer);
1832
- if (this.projectionDisposed) {
1833
- this.destroyProjectionBuffer(buffer);
1834
- return;
1835
- }
1836
- if (!this.pendingProjectionRetirements.add(buffer)) return;
1837
- void this.device.queue.onSubmittedWorkDone().then(
1838
- () => {
1839
- this.pendingProjectionRetirements.delete(buffer);
1840
- this.destroyProjectionBuffer(buffer);
1841
- },
1842
- () => {
1843
- this.pendingProjectionRetirements.delete(buffer);
1844
- this.destroyProjectionBuffer(buffer);
1845
- },
1846
- );
1847
- }
1848
-
1849
- private destroyProjectionBuffer(buffer: Buffer): void {
1850
- if (this.destroyedProjectionBuffers.has(buffer)) return;
1851
- this.destroyedProjectionBuffers.add(buffer);
1852
- this.projectionBuffers.delete(buffer);
1853
- this.device.destroyBuffer(buffer);
1854
- }
1855
-
1856
- get instancesFrameGroup(): BindGroup {
1857
- return this.instancesBindGroup;
1858
- }
1859
-
1860
- get kind(): 'rigid' | 'skin' {
1861
- return this.deformation;
1862
- }
1863
- }
1864
1332
 
1865
1333
  export interface PreparedGpuDrivenFrame {
1866
1334
  readonly topologySignature: string;
1335
+ /** Composition-local world indices mapped to the stable identities used by draw keys. */
1336
+ readonly worldKeys: readonly number[];
1867
1337
  /** True when every source draw item/instance is claimed by the GPU lane. */
1868
1338
  readonly ownsAllDrawItems: boolean;
1869
1339
  /** True only when the extracted ShadowCaster channel is fully represented. */
@@ -1871,6 +1341,8 @@ export interface PreparedGpuDrivenFrame {
1871
1341
  /** Concrete main-pass draw identities claimed by the GPU projection. */
1872
1342
  readonly drawKeys?: ReadonlySet<string>;
1873
1343
  readonly standardPbrFrameResources: GpuDrivenStandardPbrFrameResources;
1344
+ /** Published Surface admission and its pass lane decision. */
1345
+ readonly surfaceSubmission?: SurfaceGpuSubmissionAdmission;
1874
1346
  readonly occlusion?: OcclusionFrameProjection;
1875
1347
  /** @internal Commits a replacement generation only after graph promotion. */
1876
1348
  readonly _commitResourceReplacement: () => void;
@@ -1878,6 +1350,8 @@ export interface PreparedGpuDrivenFrame {
1878
1350
  readonly _commitGpuResourceReplacement?: () => void;
1879
1351
  /** @internal Discards staged shadow cache publication after a failed frame. */
1880
1352
  readonly _abortResourceReplacement?: () => void;
1353
+ /** @internal Marks validated dynamic ranges consumed only after queue submit. */
1354
+ readonly _consumeSurfaceDynamicInput?: (frameNumber: number) => Result<void, RhiError>;
1881
1355
  /** Concrete ShadowCaster keys claimed by the GPU subset for this frame. */
1882
1356
  readonly shadowDrawKeys?: ReadonlySet<string>;
1883
1357
  /** View-scoped ShadowCaster claims consumed by each shadow record pass. */
@@ -1898,6 +1372,7 @@ export interface PreparedGpuDrivenFrame {
1898
1372
  graph: RenderGraphBuilder<RenderPipelineFrame>,
1899
1373
  format: TextureFormat,
1900
1374
  sampleCount: 1 | 4,
1375
+ surfaceSubmissionObservation?: () => SurfaceSubmissionCandidate | undefined,
1901
1376
  additionalColorFormats?: readonly TextureFormat[],
1902
1377
  ): Result<RenderPipelineGpuDrivenProjection, RenderGraphError | RhiError>;
1903
1378
  }
@@ -1931,6 +1406,7 @@ interface GpuDrivenCpuValidationTelemetryCache {
1931
1406
  readonly cacheKey: string;
1932
1407
  readonly validated: readonly { readonly source: RenderableSnapshot }[];
1933
1408
  readonly gpuOwnedDrawKeys: ReadonlySet<string>;
1409
+ readonly worldKeys: readonly number[] | undefined;
1934
1410
  readonly gpuOwnedDrawCount: number;
1935
1411
  readonly totalDrawItems: number;
1936
1412
  readonly gpuOwnedRows: number;
@@ -2042,6 +1518,26 @@ export class GpuDrivenProduction {
2042
1518
  readonly value: FilteredProductionPlan;
2043
1519
  }
2044
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;
2045
1541
  private shadowFilteredCache:
2046
1542
  | {
2047
1543
  readonly source: SubmissionPlan;
@@ -2094,6 +1590,8 @@ export class GpuDrivenProduction {
2094
1590
  >();
2095
1591
  private shadowDrawKeysByView = new Map<string, ReadonlySet<string>>();
2096
1592
  private shadowExpectedDrawKeys = new Set<string>();
1593
+ private surfaceSubmission: SurfaceGpuSubmissionAdmission | undefined;
1594
+ private surfaceArtifactInspection: GpuDrivenProductionInspection['surfaceArtifact'];
2097
1595
  private shadowBatchProjectionSource: SubmissionPlan | undefined;
2098
1596
  private skinPaletteBuffer: Buffer | undefined;
2099
1597
  private committedPaletteContentRevision: number | undefined;
@@ -2133,6 +1631,110 @@ export class GpuDrivenProduction {
2133
1631
  private readonly recoveryCount = 0,
2134
1632
  ) {}
2135
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
+
2136
1738
  /**
2137
1739
  * Reuse the last producer-resolved residency rows when all source identity
2138
1740
  * and generation fences still match. Renderables and dispatch entries are
@@ -2219,6 +1821,10 @@ export class GpuDrivenProduction {
2219
1821
  readonly meshBySlot: ReadonlyMap<number, MeshGpuHandles>;
2220
1822
  readonly viewBindGroupLayout: BindGroupLayout;
2221
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;
2222
1828
  readonly materialBindingClasses?: ReadonlyMap<string, string>;
2223
1829
  /** Cooked producer artifact selected from the frame material catalog. */
2224
1830
  readonly materialArtifact?: MaterialShaderArtifact;
@@ -2232,6 +1838,8 @@ export class GpuDrivenProduction {
2232
1838
  readonly materialArtifacts?: ReadonlyMap<string, MaterialShaderArtifact>;
2233
1839
  /** Per-draw shadow-pass artifacts keyed by the same prepared identity. */
2234
1840
  readonly shadowMaterialArtifacts?: ReadonlyMap<string, MaterialShaderArtifact>;
1841
+ /** Retained ProbeBlend record page; slot N is addressed at aligned lane N+1. */
1842
+ readonly probeBlendRecordBuffer?: Buffer;
2235
1843
  readonly materialPipelineState?: PipelineState;
2236
1844
  readonly standardPbrPipelineState?: PipelineState;
2237
1845
  /** Existing renderer owner that builds the selected artifact PSO/BGL. */
@@ -2246,10 +1854,19 @@ export class GpuDrivenProduction {
2246
1854
  readonly activeEntityRevision?: number;
2247
1855
  /** Total extracted candidates, including facet-suppressed rows. */
2248
1856
  readonly telemetryCandidateCount?: number;
1857
+ /** Public receipt identity for the GPU telemetry copy encoded this frame. */
1858
+ readonly telemetrySubmit?: GpuDrivenLodSubmitIdentity;
2249
1859
  readonly occlusion?: OcclusionFrameProjection;
2250
1860
  /** Per-frame deltas from the existing RenderScene/GPU Scene/palette owners. */
2251
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;
2252
1868
  }): Result<PreparedGpuDrivenFrame | undefined, RhiError | GpuDrivenPreparationError> {
1869
+ this.surfaceArtifactInspection = undefined;
2253
1870
  this.gpuOwnedSnapshotsMaterialized = 0;
2254
1871
  this.filteredPlanBuilds = 0;
2255
1872
  this.batchBindGroupCreates = 0;
@@ -2278,6 +1895,7 @@ export class GpuDrivenProduction {
2278
1895
  this.laneReason = 'capability';
2279
1896
  this.submissionState = 'idle';
2280
1897
  this.shadowExpectedDrawKeys = new Set();
1898
+ this.surfaceSubmission = undefined;
2281
1899
  const capability = classifyRecoveryCapability({
2282
1900
  generation: 0,
2283
1901
  shaderCompilation: true,
@@ -2288,6 +1906,7 @@ export class GpuDrivenProduction {
2288
1906
  });
2289
1907
  if (
2290
1908
  input.scene === undefined ||
1909
+ (input.hdrp === true && input.clustered !== true) ||
2291
1910
  selectGpuLodLane(this.device.caps) === 'cpu' ||
2292
1911
  capability.status === 'disabled'
2293
1912
  ) {
@@ -2325,6 +1944,57 @@ export class GpuDrivenProduction {
2325
1944
  const materialArtifact = input.materialArtifact ?? input.standardPbrArtifact;
2326
1945
  const skinArtifact = input.materialSkinArtifact ?? input.standardPbrSkinArtifact;
2327
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
+ }
2328
1998
  const artifactSignature = `${materialArtifactSignature(
2329
1999
  input.materialArtifacts,
2330
2000
  materialArtifact,
@@ -2560,6 +2230,7 @@ export class GpuDrivenProduction {
2560
2230
  }
2561
2231
  const updated = this.view.update(filtered.plan, scene.scene, activeFrustum(input.camera));
2562
2232
  if (!updated.ok) return updated;
2233
+ this.view.setTelemetrySubmit(input.telemetrySubmit);
2563
2234
  this.telemetryPrepared = true;
2564
2235
  const view = this.view;
2565
2236
  const visibleBuffer = view.visibleBuffer;
@@ -2582,6 +2253,20 @@ export class GpuDrivenProduction {
2582
2253
  }),
2583
2254
  );
2584
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
+ }
2585
2270
  const committedArtifactChanged =
2586
2271
  this.committedProjectionSource !== undefined &&
2587
2272
  this.committedProjectionSource.materialArtifactSignature !== artifactSignature;
@@ -2607,10 +2292,15 @@ export class GpuDrivenProduction {
2607
2292
  ...(input.materialPipelineFactory === undefined
2608
2293
  ? {}
2609
2294
  : { pipelineFactory: input.materialPipelineFactory }),
2295
+ ...(input.probeBlendRecordBuffer === undefined
2296
+ ? {}
2297
+ : { probeBlendRecordBuffer: input.probeBlendRecordBuffer }),
2610
2298
  });
2611
2299
  if (!created.ok) return created;
2612
2300
  this.materialRaster = created.value;
2613
2301
  } else {
2302
+ const probe = this.materialRaster.ensureProbeBlendRecordBuffer(input.probeBlendRecordBuffer);
2303
+ if (!probe.ok) return probe;
2614
2304
  const rebound = this.materialRaster.ensureVisibleBuffer(visibleBuffer);
2615
2305
  if (!rebound.ok) return rebound;
2616
2306
  }
@@ -2656,6 +2346,10 @@ export class GpuDrivenProduction {
2656
2346
  deformation: 'skin',
2657
2347
  paletteBuffer: skinSnapshot.buffer,
2658
2348
  paletteBindingWindowBytes: skinAllocator.bindingWindowBytes,
2349
+ sharedSurfaceFrame: materialRaster.surfaceDynamicSharedFrameState,
2350
+ ...(input.probeBlendRecordBuffer === undefined
2351
+ ? {}
2352
+ : { probeBlendRecordBuffer: input.probeBlendRecordBuffer }),
2659
2353
  ...(input.materialPipelineFactory === undefined
2660
2354
  ? {}
2661
2355
  : { pipelineFactory: input.materialPipelineFactory }),
@@ -2663,6 +2357,8 @@ export class GpuDrivenProduction {
2663
2357
  if (!created.ok) return created;
2664
2358
  this.skinRaster = created.value;
2665
2359
  } else {
2360
+ const probe = this.skinRaster.ensureProbeBlendRecordBuffer(input.probeBlendRecordBuffer);
2361
+ if (!probe.ok) return probe;
2666
2362
  const rebound = this.skinRaster.ensureVisibleBuffer(visibleBuffer);
2667
2363
  if (!rebound.ok) return rebound;
2668
2364
  const palette = this.skinRaster.ensurePaletteBuffer(
@@ -2679,6 +2375,52 @@ export class GpuDrivenProduction {
2679
2375
  }
2680
2376
  }
2681
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
+ }
2682
2424
  const shadowViewPool = this.shadowViews;
2683
2425
  if (shadowViewPool === undefined) {
2684
2426
  return err(
@@ -2834,25 +2576,83 @@ export class GpuDrivenProduction {
2834
2576
  )
2835
2577
  .join('|') ?? '';
2836
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;
2837
2587
  return ok({
2838
2588
  get topologySignature() {
2839
2589
  const shadow = shadowTopologySignature();
2840
2590
  return shadow.length === 0 ? signature : `${signature}|shadow=${shadow}`;
2841
2591
  },
2842
2592
  ownsAllDrawItems: filtered.ownsAllDrawItems,
2593
+ worldKeys: scene.worldKeys ?? [],
2843
2594
  ...(input.occlusion === undefined ? {} : { occlusion: input.occlusion }),
2844
2595
  ownsAllShadowCasters,
2845
2596
  drawKeys: filtered.drawKeys,
2846
2597
  shadowDrawKeys: shadowFiltered.shadowDrawKeys,
2598
+ ...(this.surfaceSubmission === undefined
2599
+ ? {}
2600
+ : { surfaceSubmission: this.surfaceSubmission }),
2847
2601
  get shadowDrawKeysByView() {
2848
2602
  return new Map(production.shadowDrawKeysByView);
2849
2603
  },
2850
2604
  standardPbrFrameResources: {
2605
+ resourceGeneration: viewInspection.resourceGeneration,
2851
2606
  materialBindGroups: [],
2852
2607
  materialSlotIndicesByEntity: new Map(),
2853
2608
  instancesBindGroup: materialRaster.instancesFrameGroup,
2854
2609
  materialStride: MATERIAL_PER_ENTITY_STRIDE,
2855
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
+ }),
2856
2656
  },
2857
2657
  _commitGpuResourceReplacement: () => {
2858
2658
  view._commitResourceReplacement();
@@ -2868,6 +2668,8 @@ export class GpuDrivenProduction {
2868
2668
  this.lastKnownGoodGeneration = view.inspect().resourceGeneration;
2869
2669
  },
2870
2670
  _abortResourceReplacement: () => {
2671
+ materialRaster.abortSurfaceDynamicInput();
2672
+ skinRaster?.abortSurfaceDynamicInput();
2871
2673
  shadowViewPool._abortResourceReplacement();
2872
2674
  materialRaster.abortProjectionBindings();
2873
2675
  skinRaster?.abortProjectionBindings();
@@ -2875,6 +2677,18 @@ export class GpuDrivenProduction {
2875
2677
  this.submissionState = 'aborted';
2876
2678
  this.retryCount += 1;
2877
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
+ }),
2878
2692
  projectShadow: (graph, identity) => {
2879
2693
  if (!shadowViewPool.isActive(identity) || shadowViewPool.submission(identity) === undefined)
2880
2694
  return ok(undefined);
@@ -2902,25 +2716,75 @@ export class GpuDrivenProduction {
2902
2716
  ),
2903
2717
  );
2904
2718
  },
2905
- project: (graph, format, sampleCount, additionalColorFormats) => {
2719
+ project: (
2720
+ graph,
2721
+ format,
2722
+ sampleCount,
2723
+ surfaceSubmissionObservation,
2724
+ additionalColorFormats,
2725
+ ) => {
2906
2726
  this.indirectDrawCount = admittedRasterBatchCount(filtered.plan);
2907
- const outputs = view.addPasses(graph);
2727
+ const outputs = view.addPasses(graph, 'gpu-driven', true, surfaceSubmissionObservation);
2908
2728
  if (!outputs.ok) return outputs;
2909
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
+ }
2910
2765
  const standardProjected: Array<{
2911
2766
  readonly batch: GpuDrivenBatch;
2767
+ readonly artifact: MaterialShaderArtifact;
2912
2768
  readonly mesh: MeshGpuHandles;
2913
2769
  readonly materialEntityKey: number;
2914
2770
  readonly vertex: GraphBuffer;
2915
2771
  readonly index?: GraphBuffer;
2916
- readonly pipelineFor: (
2917
- formats: readonly TextureFormat[],
2918
- ) => Result<RenderPipeline, RhiError>;
2919
2772
  readonly meshBindGroup: BindGroup;
2773
+ readonly meshProjectionBuffer: Buffer;
2774
+ readonly meshProjectionBytes: number;
2920
2775
  readonly raster: MaterialAbiRasterAdapter;
2921
2776
  readonly deformation: 'rigid' | 'skin';
2922
2777
  readonly clustered: boolean;
2923
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>;
2924
2788
  }> = [];
2925
2789
  const standardAccesses: GraphAccess[] = [
2926
2790
  { resource: outputs.value.primitive, usage: 'storage-read' },
@@ -2929,6 +2793,15 @@ export class GpuDrivenProduction {
2929
2793
  { resource: outputs.value.visible, usage: 'storage-read' },
2930
2794
  { resource: outputs.value.transform, usage: 'storage-read' },
2931
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 }]),
2932
2805
  ];
2933
2806
  const meshBuffers = new Map<
2934
2807
  MeshGpuHandles,
@@ -2955,10 +2828,10 @@ export class GpuDrivenProduction {
2955
2828
  );
2956
2829
  }
2957
2830
  // The visible stream is compacted after the persistent visibility
2958
- // facet applies its active-entity admission. Project the mesh rows
2959
- // from that same admitted sequence so visible.x remains the
2960
- // batch-local row identity even when a suppressed candidate sits
2961
- // between two submitted candidates.
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.
2962
2835
  const candidates = admittedCandidates.flatMap((candidate) => {
2963
2836
  const slot = projectionSlot(candidate.primitiveIndex);
2964
2837
  return slot === undefined ? [] : [sceneCandidate(slot, candidate)];
@@ -2976,12 +2849,23 @@ export class GpuDrivenProduction {
2976
2849
  }),
2977
2850
  );
2978
2851
  }
2852
+ const projectionKey = gpuDrivenProjectionKey(prepared.batch, deformation, artifact);
2979
2853
  const meshBindGroup = raster.projectionBindGroup(
2980
2854
  projection.value.rows,
2981
- gpuDrivenProjectionKey(prepared.batch, deformation, artifact),
2855
+ projectionKey,
2982
2856
  prepared.batch.candidates.length,
2983
2857
  );
2984
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
+ }
2985
2869
  const materialSlot = projectionSlot(admittedCandidates[0]?.primitiveIndex ?? -1);
2986
2870
  if (materialSlot === undefined) {
2987
2871
  return err(
@@ -3024,8 +2908,18 @@ export class GpuDrivenProduction {
3024
2908
  standardAccesses.push({ resource: buffers.index, usage: 'index-read' });
3025
2909
  }
3026
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;
3027
2920
  const pipelineFor = (
3028
2921
  formats: readonly TextureFormat[],
2922
+ selectedArtifact: MaterialShaderArtifact = artifact,
3029
2923
  ): Result<RenderPipeline, RhiError> => {
3030
2924
  return raster.pipeline(
3031
2925
  formats[0] ?? format,
@@ -3035,22 +2929,31 @@ export class GpuDrivenProduction {
3035
2929
  // Standard count is only a fallback for legacy synthetic
3036
2930
  // fixtures; using it for a custom program would add aliases
3037
2931
  // the shader never declares (or omit a declared custom set).
3038
- artifact.uvSetCount ?? materialPipelineState.standardPbrShaderUvSetCount,
2932
+ shaderUvSetCount,
3039
2933
  prepared.batch.key.topology,
3040
- prepared.batch.key.drawKind === 'indexed' &&
3041
- (prepared.batch.key.topology === 'line-strip' ||
3042
- prepared.batch.key.topology === 'triangle-strip')
3043
- ? prepared.mesh.indexFormat
3044
- : undefined,
2934
+ stripIndexFormat,
3045
2935
  prepared.renderState,
3046
- artifact,
2936
+ selectedArtifact,
3047
2937
  formats.slice(1),
3048
2938
  );
3049
2939
  };
3050
2940
  const initialPipeline = pipelineFor([format, ...(additionalColorFormats ?? [])]);
3051
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
+ }
3052
2954
  standardProjected.push({
3053
2955
  batch: prepared.batch,
2956
+ artifact,
3054
2957
  mesh: prepared.mesh,
3055
2958
  materialEntityKey: worldEntityKey(
3056
2959
  materialSlot.snapshot.worldId,
@@ -3058,7 +2961,11 @@ export class GpuDrivenProduction {
3058
2961
  ),
3059
2962
  ...buffers,
3060
2963
  pipelineFor,
2964
+ ...(nearestPipelineFor === undefined ? {} : { nearestPipelineFor }),
2965
+ ...(colorPipelineFor === undefined ? {} : { colorPipelineFor }),
3061
2966
  meshBindGroup: meshBindGroup.value,
2967
+ meshProjectionBuffer: meshProjection.buffer,
2968
+ meshProjectionBytes: meshProjection.bindingBytes,
3062
2969
  raster,
3063
2970
  deformation,
3064
2971
  clustered: ['cluster', 'skin-cluster'].includes(
@@ -3087,6 +2994,8 @@ export class GpuDrivenProduction {
3087
2994
  pass,
3088
2995
  resources: GraphResourceResolver,
3089
2996
  frameResources: GpuDrivenStandardPbrFrameResources,
2997
+ filter,
2998
+ fragmentEntryPoint,
3090
2999
  ) => {
3091
3000
  // The shared Standard view BGL carries two dynamic buffers:
3092
3001
  // the frame view UBO and the per-frame shadow/light table. GPU
@@ -3098,6 +3007,18 @@ export class GpuDrivenProduction {
3098
3007
  let currentVertex: GraphBuffer | undefined;
3099
3008
  let currentIndex: GraphBuffer | undefined;
3100
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
+ }
3101
3022
  const globalMaterialSlots = frameResources.materialSlotIndicesByEntity.get(
3102
3023
  batch.materialEntityKey,
3103
3024
  );
@@ -3118,14 +3039,19 @@ export class GpuDrivenProduction {
3118
3039
  !frameResources.selectedMaterialSlots.has(globalMaterialSlot)
3119
3040
  )
3120
3041
  continue;
3121
- 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(
3122
3049
  frameResources.colorFormats ?? [format, ...(additionalColorFormats ?? [])],
3123
3050
  );
3124
3051
  if (!selectedPipeline.ok) throw selectedPipeline.error;
3125
- const pipeline = selectedPipeline.value;
3126
- if (currentPipeline !== pipeline) {
3127
- pass.setPipeline(pipeline);
3128
- currentPipeline = pipeline;
3052
+ if (currentPipeline !== selectedPipeline.value) {
3053
+ pass.setPipeline(selectedPipeline.value);
3054
+ currentPipeline = selectedPipeline.value;
3129
3055
  }
3130
3056
  const materialBindGroup = frameResources.materialBindGroups[globalMaterialSlot];
3131
3057
  if (materialBindGroup === undefined) {
@@ -3138,21 +3064,30 @@ export class GpuDrivenProduction {
3138
3064
  pass.setBindGroup(1, materialBindGroup, [
3139
3065
  globalMaterialSlot * frameResources.materialStride,
3140
3066
  ]);
3141
- const meshBindGroup = batch.clustered
3142
- ? frameResources.resolveClusteredMeshBindGroup?.(
3143
- batch.raster.sceneMeshBinding(batch.projectionKey),
3144
- )
3145
- : batch.meshBindGroup;
3146
- 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) {
3147
3077
  throw new RhiError({
3148
3078
  code: 'rhi-not-available',
3149
- expected: 'current scene-index lighting binding',
3150
- 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',
3151
3081
  });
3152
- pass.setBindGroup(2, meshBindGroup, batch.deformation === 'skin' ? [0, 0, 0] : [0]);
3082
+ }
3083
+ pass.setBindGroup(2, group2, batch.deformation === 'skin' ? [0, 0, 0] : [0]);
3153
3084
  const visibleWindow = batch.raster.visibleWindowBindGroup(
3154
3085
  batch.batch.visibleBase,
3155
3086
  batch.batch.visibleCapacity,
3087
+ isSingleLayerMediumArtifact(batch.artifact) ? batch.batch.visibleBase : undefined,
3088
+ isSingleLayerMediumArtifact(batch.artifact)
3089
+ ? batch.batch.visibleCapacity
3090
+ : undefined,
3156
3091
  );
3157
3092
  if (!visibleWindow.ok) throw visibleWindow.error;
3158
3093
  pass.setBindGroup(3, visibleWindow.value);
@@ -3175,6 +3110,29 @@ export class GpuDrivenProduction {
3175
3110
  } else {
3176
3111
  pass.drawIndirect(indirect.value, batch.batch.indirectOffset);
3177
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
+ }
3178
3136
  this.encodedIndirectDraws += 1;
3179
3137
  }
3180
3138
  },
@@ -3363,7 +3321,7 @@ export class GpuDrivenProduction {
3363
3321
  batch.candidates.length,
3364
3322
  );
3365
3323
  if (!meshBindGroup.ok) return meshBindGroup;
3366
- const visibleBindGroup = raster.visibleWindowBindGroupForBuffer(
3324
+ const visibleBindGroup = raster.shadowVisibleWindowBindGroupForBuffer(
3367
3325
  visibleBuffer,
3368
3326
  batch.visibleBase,
3369
3327
  batch.visibleCapacity,
@@ -3443,6 +3401,8 @@ export class GpuDrivenProduction {
3443
3401
  this.skinRaster?.dispose();
3444
3402
  this.skinRaster = undefined;
3445
3403
  this.filteredCache = undefined;
3404
+ this.surfaceFrameRangeCache = undefined;
3405
+ this.consumedSurfaceInput = undefined;
3446
3406
  this.signatureCache = undefined;
3447
3407
  this.validatedResidencyCache = undefined;
3448
3408
  this.cpuValidationTelemetryCache = undefined;
@@ -3457,11 +3417,22 @@ export class GpuDrivenProduction {
3457
3417
  this.skinPaletteBuffer = undefined;
3458
3418
  this.committedPaletteContentRevision = undefined;
3459
3419
  this.preparationFailure = undefined;
3420
+ this.surfaceArtifactInspection = undefined;
3460
3421
  this.overflowRecoveryRequired = false;
3461
3422
  }
3462
3423
 
3463
3424
  inspect(): GpuDrivenProductionInspection {
3464
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) });
3465
3436
  const channels: GpuDrivenLaneSummary[] = [];
3466
3437
  const mainTotalDrawCount = this.mainTotalDrawItems;
3467
3438
  const mainClaimedDrawCount = this.telemetryOverflow
@@ -3567,11 +3538,23 @@ export class GpuDrivenProduction {
3567
3538
  }
3568
3539
  }
3569
3540
  return {
3541
+ ...(this.surfaceArtifactInspection === undefined
3542
+ ? {}
3543
+ : { surfaceArtifact: this.surfaceArtifactInspection }),
3570
3544
  residencyValidationScans: this.residencyValidationScans,
3571
3545
  residencyValidationCacheHits: this.residencyValidationCacheHits,
3572
3546
  cpuValidationScans: this.cpuValidationScans,
3573
3547
  cpuValidationCacheHits: this.cpuValidationCacheHits,
3574
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,
3575
3558
  gpuOwnedSnapshotsMaterialized: this.gpuOwnedSnapshotsMaterialized,
3576
3559
  filteredPlanBuilds: this.filteredPlanBuilds,
3577
3560
  gpuOwnedEntityCount: this.gpuOwnedEntityCount,
@@ -3602,6 +3585,23 @@ export class GpuDrivenProduction {
3602
3585
  recoveryCount: this.recoveryCount,
3603
3586
  retryCount: this.retryCount,
3604
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
+ }),
3605
3605
  };
3606
3606
  }
3607
3607
 
@@ -3638,11 +3638,53 @@ export class GpuDrivenProduction {
3638
3638
  this.laneReason = 'overflow';
3639
3639
  }
3640
3640
  }
3641
- if (selection === undefined || telemetryCandidateCount <= 0) return selection;
3641
+ if (selection === undefined) return selection;
3642
3642
  if (filtered === undefined) return selection;
3643
- const preparedByBatchId = new Map(
3644
- filtered.batches.map((prepared) => [prepared.batch.batchId, prepared] as const),
3643
+ type LodBatchAttribution = {
3644
+ readonly batch: GpuDrivenBatch;
3645
+ readonly worldKey: number;
3646
+ readonly primitiveSlot: number;
3647
+ readonly slotGeneration: number;
3648
+ readonly artifact?: MaterialShaderArtifact;
3649
+ };
3650
+ const preparedByBatchId = new Map<number, LodBatchAttribution>(
3651
+ filtered.batches.map((prepared) => [
3652
+ prepared.batch.batchId,
3653
+ {
3654
+ batch: prepared.batch,
3655
+ worldKey: prepared.worldKey,
3656
+ primitiveSlot: prepared.primitiveSlot,
3657
+ slotGeneration: prepared.slotGeneration,
3658
+ artifact: prepared.artifact,
3659
+ },
3660
+ ]),
3645
3661
  );
3662
+ const projectedCandidateCount =
3663
+ telemetryCandidateCount > 0 ? telemetryCandidateCount : selection.candidateCount;
3664
+ // Selector-only batches remain in filtered.plan so their GPU counters
3665
+ // can describe candidates suppressed from the raster admission lane.
3666
+ // They have no PreparedBatch entry because no draw is encoded, but they
3667
+ // still need the same World join for receipt attribution. Recover that
3668
+ // metadata from the stable primitive slot instead of dropping the whole
3669
+ // readback when the first such batch is encountered.
3670
+ const filteredSlots = this.filteredCache?.slots;
3671
+ const filteredWorldKeys = this.filteredCache?.worldKeys;
3672
+ if (filteredSlots !== undefined) {
3673
+ const slotByPrimitive = new Map(filteredSlots.map((slot) => [slot.slot, slot] as const));
3674
+ for (const batch of filtered.plan.batches) {
3675
+ if (preparedByBatchId.has(batch.batchId)) continue;
3676
+ const candidate = batch.candidates[0];
3677
+ const slot =
3678
+ candidate === undefined ? undefined : slotByPrimitive.get(candidate.primitiveIndex);
3679
+ if (slot === undefined) continue;
3680
+ preparedByBatchId.set(batch.batchId, {
3681
+ batch,
3682
+ worldKey: filteredWorldKeys?.[slot.snapshot.worldId] ?? slot.snapshot.worldId,
3683
+ primitiveSlot: slot.slot,
3684
+ slotGeneration: slot.generation,
3685
+ });
3686
+ }
3687
+ }
3646
3688
  const worldSelections = new Map<
3647
3689
  number,
3648
3690
  {
@@ -3655,9 +3697,36 @@ export class GpuDrivenProduction {
3655
3697
  readonly lodHistogram: Map<number, number>;
3656
3698
  }
3657
3699
  >();
3700
+ const lodHistogram = new Map<number, number>();
3701
+ // The selector visits the full captured plan, including candidates that
3702
+ // the CPU visibility projection suppressed. Use that same plan for the
3703
+ // LOD denominator so World attribution cannot fail merely because the
3704
+ // two lanes have different frustum scopes.
3705
+ let lodCandidateCount = 0;
3706
+ let lodVisible = 0;
3658
3707
  for (const batchSelection of selection.batches) {
3659
3708
  const prepared = preparedByBatchId.get(batchSelection.batchId);
3660
- if (prepared === undefined) return selection;
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;
3713
+ // The GPU selector also visits ordinary GPU-driven meshes so one
3714
+ // compact compute path can populate their indirect args. Those rows
3715
+ // are level-0 by construction, but they are not LOD candidates and
3716
+ // must not enter receipt-bound LOD attribution. Batch topology keeps
3717
+ // LOD coverage/ranges in the grouping key, so a batch is expected to
3718
+ // be homogeneous; fail closed if a producer violates that invariant
3719
+ // instead of guessing a split for aggregate counters.
3720
+ const lodCandidateRows = prepared.batch.candidates.filter(
3721
+ (candidate) => (candidate.lodCoverages?.length ?? 0) > 1,
3722
+ );
3723
+ if (lodCandidateRows.length === 0) continue;
3724
+ if (lodCandidateRows.length !== batchSelection.candidateCount) return undefined;
3725
+ lodCandidateCount += batchSelection.candidateCount;
3726
+ lodVisible += batchSelection.visible;
3727
+ for (const row of batchSelection.lodHistogram) {
3728
+ lodHistogram.set(row.level, (lodHistogram.get(row.level) ?? 0) + row.count);
3729
+ }
3661
3730
  let aggregate = worldSelections.get(prepared.worldKey);
3662
3731
  if (aggregate === undefined) {
3663
3732
  aggregate = {
@@ -3683,8 +3752,50 @@ export class GpuDrivenProduction {
3683
3752
  }
3684
3753
  return {
3685
3754
  ...selection,
3686
- candidateCount: telemetryCandidateCount,
3687
- occluded: Math.max(0, telemetryCandidateCount - selection.visible),
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
+ ),
3762
+ lodHistogram: Object.freeze(
3763
+ [...lodHistogram.entries()]
3764
+ .sort(([left], [right]) => left - right)
3765
+ .map(([level, count]) => Object.freeze({ level, count })),
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
+ ),
3688
3799
  worldSelections: Object.freeze(
3689
3800
  [...worldSelections.values()].map((aggregate) =>
3690
3801
  Object.freeze({
@@ -3710,6 +3821,7 @@ export class GpuDrivenProduction {
3710
3821
  validated: readonly { readonly source: RenderableSnapshot }[],
3711
3822
  gpuOwnedDrawKeys: ReadonlySet<string>,
3712
3823
  cacheKey?: string,
3824
+ worldKeys?: readonly number[],
3713
3825
  ): void {
3714
3826
  const cached = this.cpuValidationTelemetryCache;
3715
3827
  if (
@@ -3717,7 +3829,8 @@ export class GpuDrivenProduction {
3717
3829
  cached !== undefined &&
3718
3830
  cached.cacheKey === cacheKey &&
3719
3831
  cached.validated === validated &&
3720
- cached.gpuOwnedDrawKeys === gpuOwnedDrawKeys
3832
+ cached.gpuOwnedDrawKeys === gpuOwnedDrawKeys &&
3833
+ cached.worldKeys === worldKeys
3721
3834
  ) {
3722
3835
  this.applyCpuValidationTelemetry(cached);
3723
3836
  this.cpuValidationCacheHits += 1;
@@ -3755,7 +3868,10 @@ export class GpuDrivenProduction {
3755
3868
  for (const [compactIndex, draw] of draws.entries()) {
3756
3869
  const material = row.source.materials[draw.materialSlot] ?? row.source.material;
3757
3870
  const key = gpuDrivenDrawKey(
3758
- worldEntityKey(row.source.worldId, row.source.entityKey),
3871
+ worldEntityKey(
3872
+ worldKeys?.[row.source.worldId] ?? row.source.worldId,
3873
+ row.source.entityKey,
3874
+ ),
3759
3875
  material.materialHandle ?? -1,
3760
3876
  gpuDrivenSourceDrawItemIndex(draw, compactIndex),
3761
3877
  );
@@ -3800,6 +3916,7 @@ export class GpuDrivenProduction {
3800
3916
  cacheKey,
3801
3917
  validated,
3802
3918
  gpuOwnedDrawKeys,
3919
+ worldKeys,
3803
3920
  gpuOwnedDrawCount: this.gpuOwnedDrawItems,
3804
3921
  totalDrawItems: this.mainTotalDrawItems,
3805
3922
  gpuOwnedRows: this.validatedGpuOwnedRows,