@forgeax/engine-render 0.1.20 → 0.1.23

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 (645) hide show
  1. package/README.md +365 -29
  2. package/dist/assembly/backend-contract.d.ts +13 -0
  3. package/dist/assembly/backend-contract.d.ts.map +1 -1
  4. package/dist/assembly/device-feature-admission.d.ts +18 -0
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -0
  6. package/dist/assembly/directional-shadow-inspection.d.ts +38 -0
  7. package/dist/assembly/directional-shadow-inspection.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +30 -19
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/host-contract.d.ts +19 -0
  11. package/dist/assembly/host-contract.d.ts.map +1 -1
  12. package/dist/assembly/render-target-host.d.ts +46 -0
  13. package/dist/assembly/render-target-host.d.ts.map +1 -0
  14. package/dist/assets/render-asset-port.d.ts.map +1 -1
  15. package/dist/authoring.mjs +4 -1
  16. package/dist/authoring.mjs.map +1 -1
  17. package/dist/capture/cube-views.d.ts +21 -0
  18. package/dist/capture/cube-views.d.ts.map +1 -0
  19. package/dist/capture/scheduler.d.ts +54 -0
  20. package/dist/capture/scheduler.d.ts.map +1 -0
  21. package/dist/{chunk-PCHBPYML.mjs → chunk-4L3P6DT5.mjs} +2 -2
  22. package/dist/chunk-4L3P6DT5.mjs.map +1 -0
  23. package/dist/{chunk-VFPLTKCU.mjs → chunk-4NPUASZR.mjs} +15 -4
  24. package/dist/chunk-4NPUASZR.mjs.map +1 -0
  25. package/dist/chunk-DIYW3M5D.mjs +5085 -0
  26. package/dist/chunk-DIYW3M5D.mjs.map +1 -0
  27. package/dist/chunk-FBC4R44J.mjs +1003 -0
  28. package/dist/chunk-FBC4R44J.mjs.map +1 -0
  29. package/dist/{chunk-CDTWTHZR.mjs → chunk-FF7K3FSO.mjs} +474 -168
  30. package/dist/chunk-FF7K3FSO.mjs.map +1 -0
  31. package/dist/chunk-HZUPSYW7.mjs +1104 -0
  32. package/dist/chunk-HZUPSYW7.mjs.map +1 -0
  33. package/dist/{chunk-TBQJOK2L.mjs → chunk-QODA5UKU.mjs} +321 -269
  34. package/dist/chunk-QODA5UKU.mjs.map +1 -0
  35. package/dist/chunk-YALPS44A.mjs +296 -0
  36. package/dist/chunk-YALPS44A.mjs.map +1 -0
  37. package/dist/{chunk-4XINDLAY.mjs → chunk-ZUWKAZ2I.mjs} +1963 -2178
  38. package/dist/chunk-ZUWKAZ2I.mjs.map +1 -0
  39. package/dist/cluster-binner.d.ts +5 -0
  40. package/dist/cluster-binner.d.ts.map +1 -1
  41. package/dist/components/camera.d.ts +1 -0
  42. package/dist/components/camera.d.ts.map +1 -1
  43. package/dist/components/cube-camera.d.ts +20 -0
  44. package/dist/components/cube-camera.d.ts.map +1 -0
  45. package/dist/components/directional-light.d.ts +11 -2
  46. package/dist/components/directional-light.d.ts.map +1 -1
  47. package/dist/components/directional-shadow-filter.d.ts +25 -0
  48. package/dist/components/directional-shadow-filter.d.ts.map +1 -0
  49. package/dist/components/index.d.ts +5 -0
  50. package/dist/components/index.d.ts.map +1 -1
  51. package/dist/components/light-helpers.d.ts +4 -0
  52. package/dist/components/light-helpers.d.ts.map +1 -1
  53. package/dist/components/light-probe.d.ts +9 -0
  54. package/dist/components/light-probe.d.ts.map +1 -0
  55. package/dist/components/rect-area-light.d.ts +12 -0
  56. package/dist/components/rect-area-light.d.ts.map +1 -0
  57. package/dist/components/reflection-probe.d.ts +17 -0
  58. package/dist/components/reflection-probe.d.ts.map +1 -0
  59. package/dist/components/scene-instance.d.ts +4 -0
  60. package/dist/components/scene-instance.d.ts.map +1 -1
  61. package/dist/components/spot-light.d.ts +3 -0
  62. package/dist/components/spot-light.d.ts.map +1 -1
  63. package/dist/construct-renderer.mjs +14836 -9620
  64. package/dist/construct-renderer.mjs.map +1 -1
  65. package/dist/device/gpu-residency.d.ts +8 -1
  66. package/dist/device/gpu-residency.d.ts.map +1 -1
  67. package/dist/device/lifecycle-transaction.d.ts +2 -0
  68. package/dist/device/lifecycle-transaction.d.ts.map +1 -0
  69. package/dist/environment/frame.d.ts.map +1 -1
  70. package/dist/environment/lifecycle.d.ts +5 -0
  71. package/dist/environment/lifecycle.d.ts.map +1 -1
  72. package/dist/errors/gpu-driven.d.ts +26 -0
  73. package/dist/errors/gpu-driven.d.ts.map +1 -0
  74. package/dist/errors/index.d.ts +1 -0
  75. package/dist/errors/index.d.ts.map +1 -1
  76. package/dist/errors/recover.d.ts +9 -1
  77. package/dist/errors/recover.d.ts.map +1 -1
  78. package/dist/errors/render.d.ts +166 -93
  79. package/dist/errors/render.d.ts.map +1 -1
  80. package/dist/extract/directional-shadow-projection.d.ts +23 -0
  81. package/dist/extract/directional-shadow-projection.d.ts.map +1 -0
  82. package/dist/extract/environment.d.ts +14 -0
  83. package/dist/extract/environment.d.ts.map +1 -1
  84. package/dist/extract/gpu-driven.d.ts +50 -0
  85. package/dist/extract/gpu-driven.d.ts.map +1 -0
  86. package/dist/extract/visibility.d.ts.map +1 -1
  87. package/dist/features/fullscreen.d.ts.map +1 -1
  88. package/dist/features/targets.d.ts.map +1 -1
  89. package/dist/features/types.d.ts +10 -0
  90. package/dist/features/types.d.ts.map +1 -1
  91. package/dist/fullscreen-post-process-pass.d.ts +13 -2
  92. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  93. package/dist/gpu-driven/batch-topology.d.ts +34 -0
  94. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  95. package/dist/gpu-driven/prepared-draw.d.ts +63 -0
  96. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -0
  97. package/dist/gpu-driven/production-raster.d.ts +97 -2
  98. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  99. package/dist/gpu-driven/view-gpu.d.ts +64 -1
  100. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  101. package/dist/gpu-scene-schema.d.ts +14 -0
  102. package/dist/gpu-scene-schema.d.ts.map +1 -1
  103. package/dist/gpu-scene.d.ts +8 -1
  104. package/dist/gpu-scene.d.ts.map +1 -1
  105. package/dist/gpu-usage.d.ts +0 -1
  106. package/dist/gpu-usage.d.ts.map +1 -1
  107. package/dist/hdrp-buffers.d.ts +21 -19
  108. package/dist/hdrp-buffers.d.ts.map +1 -1
  109. package/dist/ibl/IblPipelineCache.d.ts +4 -0
  110. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  111. package/dist/ibl/kernel-cache.d.ts +36 -0
  112. package/dist/ibl/kernel-cache.d.ts.map +1 -0
  113. package/dist/index.d.ts +39 -3
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.mjs +91 -6
  116. package/dist/index.mjs.map +1 -1
  117. package/dist/inspection-types.d.ts +214 -1
  118. package/dist/inspection-types.d.ts.map +1 -1
  119. package/dist/instances-derived-bounds.d.ts +50 -0
  120. package/dist/instances-derived-bounds.d.ts.map +1 -0
  121. package/dist/internal.d.ts +9 -0
  122. package/dist/internal.d.ts.map +1 -1
  123. package/dist/internal.mjs +23 -2
  124. package/dist/internal.mjs.map +1 -1
  125. package/dist/light-buffer-layout.d.ts +58 -140
  126. package/dist/light-buffer-layout.d.ts.map +1 -1
  127. package/dist/materials.d.ts.map +1 -1
  128. package/dist/mesh-material-bindings.d.ts +54 -0
  129. package/dist/mesh-material-bindings.d.ts.map +1 -1
  130. package/dist/pbr-pipeline.d.ts +38 -14
  131. package/dist/pbr-pipeline.d.ts.map +1 -1
  132. package/dist/pipeline/standard-forward-lane.d.ts +2 -1
  133. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  134. package/dist/pipeline/standard-lighting/errors.d.ts +4 -0
  135. package/dist/pipeline/standard-lighting/errors.d.ts.map +1 -0
  136. package/dist/pipeline/standard-lighting/graph.d.ts +24 -0
  137. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -0
  138. package/dist/pipeline/standard-lighting/inspection.d.ts +38 -0
  139. package/dist/pipeline/standard-lighting/inspection.d.ts.map +1 -0
  140. package/dist/pipeline/standard-lighting/layout.d.ts +28 -0
  141. package/dist/pipeline/standard-lighting/layout.d.ts.map +1 -0
  142. package/dist/pipeline/standard-lighting/light-frame.d.ts +33 -0
  143. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -0
  144. package/dist/pipeline/standard-lighting/membership.d.ts +14 -0
  145. package/dist/pipeline/standard-lighting/membership.d.ts.map +1 -0
  146. package/dist/pipeline/standard-lighting/prepare.d.ts +16 -0
  147. package/dist/pipeline/standard-lighting/prepare.d.ts.map +1 -0
  148. package/dist/pipeline/standard-lighting/topology.d.ts +45 -0
  149. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -0
  150. package/dist/pipeline/standard-lighting/transport.d.ts +36 -0
  151. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -0
  152. package/dist/pipeline/standard-pipeline.d.ts +4 -13
  153. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  154. package/dist/pipeline/standard-post.d.ts.map +1 -1
  155. package/dist/pipeline/standard-profile.d.ts +5 -26
  156. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  157. package/dist/pipeline-builder.d.ts +2 -0
  158. package/dist/pipeline-builder.d.ts.map +1 -1
  159. package/dist/pipeline-spec.d.ts +40 -7
  160. package/dist/pipeline-spec.d.ts.map +1 -1
  161. package/dist/plugin.d.ts.map +1 -1
  162. package/dist/point-shadow-inspection.d.ts +21 -0
  163. package/dist/point-shadow-inspection.d.ts.map +1 -0
  164. package/dist/prepare/extended-lighting/ltc-resources.d.ts +31 -0
  165. package/dist/prepare/extended-lighting/ltc-resources.d.ts.map +1 -0
  166. package/dist/prepare/extended-lighting/resources.d.ts +72 -0
  167. package/dist/prepare/extended-lighting/resources.d.ts.map +1 -0
  168. package/dist/prepare/extended-lighting/spot-modifiers.d.ts +42 -0
  169. package/dist/prepare/extended-lighting/spot-modifiers.d.ts.map +1 -0
  170. package/dist/prepare/extended-lighting/state.d.ts +24 -0
  171. package/dist/prepare/extended-lighting/state.d.ts.map +1 -0
  172. package/dist/record/frame-lighting.d.ts +33 -36
  173. package/dist/record/frame-lighting.d.ts.map +1 -1
  174. package/dist/record/frame-snapshot.d.ts +78 -24
  175. package/dist/record/frame-snapshot.d.ts.map +1 -1
  176. package/dist/record/frame-targets.d.ts +5 -0
  177. package/dist/record/frame-targets.d.ts.map +1 -1
  178. package/dist/record/frame.d.ts +16 -6
  179. package/dist/record/frame.d.ts.map +1 -1
  180. package/dist/record/gpu-pass-timing/contract.d.ts +96 -0
  181. package/dist/record/gpu-pass-timing/contract.d.ts.map +1 -0
  182. package/dist/record/gpu-pass-timing/errors.d.ts +18 -0
  183. package/dist/record/gpu-pass-timing/errors.d.ts.map +1 -0
  184. package/dist/record/gpu-pass-timing/index.d.ts +8 -0
  185. package/dist/record/gpu-pass-timing/index.d.ts.map +1 -0
  186. package/dist/record/gpu-pass-timing/parser.d.ts +11 -0
  187. package/dist/record/gpu-pass-timing/parser.d.ts.map +1 -0
  188. package/dist/record/gpu-pass-timing/session.d.ts +43 -0
  189. package/dist/record/gpu-pass-timing/session.d.ts.map +1 -0
  190. package/dist/record/gpu-timing.d.ts +20 -3
  191. package/dist/record/gpu-timing.d.ts.map +1 -1
  192. package/dist/record/helpers.d.ts +0 -20
  193. package/dist/record/helpers.d.ts.map +1 -1
  194. package/dist/record/main-pass-geometry.d.ts +6 -1
  195. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  196. package/dist/record/main-pass-material.d.ts +25 -2
  197. package/dist/record/main-pass-material.d.ts.map +1 -1
  198. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  199. package/dist/record/main-pass.d.ts +2 -0
  200. package/dist/record/main-pass.d.ts.map +1 -1
  201. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  202. package/dist/record/render-context.d.ts +98 -6
  203. package/dist/record/render-context.d.ts.map +1 -1
  204. package/dist/record/shadow-pass.d.ts.map +1 -1
  205. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  206. package/dist/record/typed-frame-graph.d.ts +119 -4
  207. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  208. package/dist/record/view-ubo.d.ts +7 -5
  209. package/dist/record/view-ubo.d.ts.map +1 -1
  210. package/dist/reflection/filter.d.ts +22 -0
  211. package/dist/reflection/filter.d.ts.map +1 -0
  212. package/dist/reflection/gpu-table.d.ts +25 -0
  213. package/dist/reflection/gpu-table.d.ts.map +1 -0
  214. package/dist/reflection/inspection.d.ts +10 -0
  215. package/dist/reflection/inspection.d.ts.map +1 -0
  216. package/dist/reflection/projection.d.ts +140 -0
  217. package/dist/reflection/projection.d.ts.map +1 -0
  218. package/dist/reflection/record-owner.d.ts +60 -0
  219. package/dist/reflection/record-owner.d.ts.map +1 -0
  220. package/dist/render-contract.d.ts +62 -14
  221. package/dist/render-contract.d.ts.map +1 -1
  222. package/dist/render-graph-primitives.d.ts +1 -0
  223. package/dist/render-graph-primitives.d.ts.map +1 -1
  224. package/dist/render-pipeline.d.ts +49 -2
  225. package/dist/render-pipeline.d.ts.map +1 -1
  226. package/dist/render-system-extract.d.ts +160 -31
  227. package/dist/render-system-extract.d.ts.map +1 -1
  228. package/dist/render-system.d.ts +28 -2
  229. package/dist/render-system.d.ts.map +1 -1
  230. package/dist/renderer.d.ts +43 -0
  231. package/dist/renderer.d.ts.map +1 -0
  232. package/dist/scene/probe-blend-record.d.ts +22 -0
  233. package/dist/scene/probe-blend-record.d.ts.map +1 -0
  234. package/dist/scene/probe-blend.d.ts +142 -0
  235. package/dist/scene/probe-blend.d.ts.map +1 -0
  236. package/dist/scene/render-scene-types.d.ts +0 -6
  237. package/dist/scene/render-scene-types.d.ts.map +1 -1
  238. package/dist/scene/render-scene.d.ts +117 -9
  239. package/dist/scene/render-scene.d.ts.map +1 -1
  240. package/dist/scene/visibility/bounds.d.ts +20 -0
  241. package/dist/scene/visibility/bounds.d.ts.map +1 -0
  242. package/dist/scene/visibility/budget.d.ts +16 -0
  243. package/dist/scene/visibility/budget.d.ts.map +1 -0
  244. package/dist/scene/visibility/facet.d.ts +73 -0
  245. package/dist/scene/visibility/facet.d.ts.map +1 -0
  246. package/dist/scene/visibility/gpu-frame-timing.d.ts +41 -0
  247. package/dist/scene/visibility/gpu-frame-timing.d.ts.map +1 -0
  248. package/dist/scene/visibility/gpu-lod.d.ts +46 -0
  249. package/dist/scene/visibility/gpu-lod.d.ts.map +1 -0
  250. package/dist/scene/visibility/inspection.d.ts +132 -0
  251. package/dist/scene/visibility/inspection.d.ts.map +1 -0
  252. package/dist/scene/visibility/lod-selector.d.ts +25 -0
  253. package/dist/scene/visibility/lod-selector.d.ts.map +1 -0
  254. package/dist/scene/visibility/occlusion-confidence.d.ts +81 -0
  255. package/dist/scene/visibility/occlusion-confidence.d.ts.map +1 -0
  256. package/dist/scene/visibility/occlusion-pass.d.ts +57 -0
  257. package/dist/scene/visibility/occlusion-pass.d.ts.map +1 -0
  258. package/dist/scene/visibility/occlusion-query-pool.d.ts +57 -0
  259. package/dist/scene/visibility/occlusion-query-pool.d.ts.map +1 -0
  260. package/dist/scene/visibility/occlusion-runtime.d.ts +96 -0
  261. package/dist/scene/visibility/occlusion-runtime.d.ts.map +1 -0
  262. package/dist/scene/visibility/types.d.ts +34 -0
  263. package/dist/scene/visibility/types.d.ts.map +1 -0
  264. package/dist/shadow-atlas.d.ts +3 -3
  265. package/dist/shadow-atlas.d.ts.map +1 -1
  266. package/dist/ssao-buffers.d.ts +3 -0
  267. package/dist/ssao-buffers.d.ts.map +1 -1
  268. package/dist/ssr/admission.d.ts +122 -0
  269. package/dist/ssr/admission.d.ts.map +1 -0
  270. package/dist/ssr/composition.d.ts +32 -0
  271. package/dist/ssr/composition.d.ts.map +1 -0
  272. package/dist/ssr/errors.d.ts +39 -0
  273. package/dist/ssr/errors.d.ts.map +1 -0
  274. package/dist/ssr/identity.d.ts +13 -0
  275. package/dist/ssr/identity.d.ts.map +1 -0
  276. package/dist/systems/skin-palette-allocator.d.ts +1 -1
  277. package/dist/targets/contracts.d.ts +66 -0
  278. package/dist/targets/contracts.d.ts.map +1 -0
  279. package/dist/targets/graph-projection.d.ts +29 -0
  280. package/dist/targets/graph-projection.d.ts.map +1 -0
  281. package/dist/targets/material-source.d.ts +34 -0
  282. package/dist/targets/material-source.d.ts.map +1 -0
  283. package/dist/targets/owner.d.ts +32 -0
  284. package/dist/targets/owner.d.ts.map +1 -0
  285. package/dist/targets/physical.d.ts +18 -0
  286. package/dist/targets/physical.d.ts.map +1 -0
  287. package/dist/targets/readback.d.ts +39 -0
  288. package/dist/targets/readback.d.ts.map +1 -0
  289. package/dist/temporal/frame-coordinator.d.ts +13 -0
  290. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  291. package/dist/temporal/frame.d.ts +2 -0
  292. package/dist/temporal/frame.d.ts.map +1 -1
  293. package/dist/temporal/index.mjs +6 -5
  294. package/dist/temporal/view.d.ts +7 -0
  295. package/dist/temporal/view.d.ts.map +1 -1
  296. package/dist/tilemap-chunk-extract-system.d.ts +1 -1
  297. package/dist/typed-render-graph-primitives.d.ts +5 -1
  298. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  299. package/dist/typed-shadow-passes.d.ts +1 -1
  300. package/dist/typed-shadow-passes.d.ts.map +1 -1
  301. package/dist/volume/passes.d.ts +9 -5
  302. package/dist/volume/passes.d.ts.map +1 -1
  303. package/package.json +20 -20
  304. package/src/__tests__/bloom-exact-zero.unit.test.ts +48 -19
  305. package/src/__tests__/bloom-lifecycle.integration.test.ts +41 -13
  306. package/src/__tests__/camera-target-consumption.unit.test.ts +53 -0
  307. package/src/__tests__/cluster-binner.unit.test.ts +35 -0
  308. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +12 -2
  309. package/src/__tests__/cube-camera-scheduler.unit.test.ts +70 -0
  310. package/src/__tests__/cube-camera-single-submit.perf.test.ts +75 -0
  311. package/src/__tests__/cube-camera-view-matrices.unit.test.ts +41 -0
  312. package/src/__tests__/cube-camera-vocabulary.unit.test.ts +47 -0
  313. package/src/__tests__/cube-camera.integration.test.ts +42 -0
  314. package/src/__tests__/detached-inspection.integration.test.ts +6 -0
  315. package/src/__tests__/device-feature-admission.unit.test.ts +85 -0
  316. package/src/__tests__/direct-light-snapshot.unit.test.ts +62 -6
  317. package/src/__tests__/docs-gate-green.unit.test.ts +38 -0
  318. package/src/__tests__/environment-generation.integration.test.ts +16 -0
  319. package/src/__tests__/evidence-identity.ts +13 -0
  320. package/src/__tests__/evidence-identity.unit.test.ts +24 -0
  321. package/src/__tests__/extended-lighting-recovery.browser.test.ts +14 -0
  322. package/src/__tests__/extended-lighting-recovery.dawn.test.ts +14 -0
  323. package/src/__tests__/extended-lighting-recovery.integration.test.ts +81 -0
  324. package/src/__tests__/extended-lighting-resources.unit.test.ts +51 -0
  325. package/src/__tests__/extended-lighting-zero-feature.integration.test.ts +52 -0
  326. package/src/__tests__/factory-contract.integration.test.ts +288 -29
  327. package/src/__tests__/fog-component.unit.test.ts +67 -0
  328. package/src/__tests__/fog.integration.test.ts +86 -0
  329. package/src/__tests__/frame-observation.contract.test.ts +15 -0
  330. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +37 -0
  331. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +146 -0
  332. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +10 -8
  333. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +18 -1
  334. package/src/__tests__/gpu-driven-production.integration.test.ts +579 -17
  335. package/src/__tests__/gpu-driven-scaling.unit.test.ts +66 -2
  336. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +26 -1
  337. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +101 -6
  338. package/src/__tests__/gpu-driven-view.browser.test.ts +3 -3
  339. package/src/__tests__/gpu-driven-view.dawn.test.ts +3 -3
  340. package/src/__tests__/gpu-driven-view.unit.test.ts +50 -0
  341. package/src/__tests__/gpu-lod-evidence.ts +188 -0
  342. package/src/__tests__/gpu-lod-fallback.integration.test.ts +10 -0
  343. package/src/__tests__/gpu-lod-indirect.integration.test.ts +40 -0
  344. package/src/__tests__/gpu-lod-parity.dawn.test.ts +29 -0
  345. package/src/__tests__/gpu-lod-readiness.integration.test.ts +52 -0
  346. package/src/__tests__/gpu-lod-schema.unit.test.ts +66 -0
  347. package/src/__tests__/gpu-lod-selector.unit.test.ts +28 -0
  348. package/src/__tests__/gpu-lod-silhouette.browser.test.ts +13 -0
  349. package/src/__tests__/gpu-pass-timing-browser-runner.ts +281 -0
  350. package/src/__tests__/gpu-pass-timing-coexistence.integration.test.ts +80 -0
  351. package/src/__tests__/gpu-pass-timing-contract.unit.test.ts +243 -0
  352. package/src/__tests__/gpu-pass-timing-exact-zero.unit.test.ts +131 -0
  353. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +230 -0
  354. package/src/__tests__/gpu-pass-timing-observation.unit.test.ts +233 -0
  355. package/src/__tests__/gpu-pass-timing-session.unit.test.ts +438 -0
  356. package/src/__tests__/gpu-pass-timing.browser.test.ts +26 -0
  357. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +125 -0
  358. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +66 -1
  359. package/src/__tests__/gpu-scene.dawn.test.ts +8 -1
  360. package/src/__tests__/gpu-scene.unit.test.ts +49 -3
  361. package/src/__tests__/hdrp-group2-visibility.unit.test.ts +24 -0
  362. package/src/__tests__/instances-culling.integration.test.ts +94 -0
  363. package/src/__tests__/instances-derived-bounds.unit.test.ts +118 -0
  364. package/src/__tests__/light-buffer-layout.unit.test.ts +113 -3
  365. package/src/__tests__/light-inspection.unit.test.ts +55 -0
  366. package/src/__tests__/light-resource-error.unit.test.ts +43 -0
  367. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +109 -0
  368. package/src/__tests__/lod-bounds.unit.test.ts +42 -0
  369. package/src/__tests__/lod-cpu-lane.integration.test.ts +55 -0
  370. package/src/__tests__/lod-cpu-no-change.perf.test.ts +19 -0
  371. package/src/__tests__/lod-gpu-frame-timing.unit.test.ts +49 -0
  372. package/src/__tests__/lod-multi-view.integration.test.ts +63 -0
  373. package/src/__tests__/lod-occlusion-device-loss.integration.test.ts +27 -0
  374. package/src/__tests__/lod-occlusion-final-decision.integration.test.ts +47 -0
  375. package/src/__tests__/lod-occlusion-inspection.integration.test.ts +217 -0
  376. package/src/__tests__/lod-occlusion-inspection.schema.test-d.ts +14 -0
  377. package/src/__tests__/lod-occlusion-inspection.unit.test.ts +72 -0
  378. package/src/__tests__/lod-occlusion-visual-evidence.ts +91 -0
  379. package/src/__tests__/lod-occlusion.browser.test.ts +15 -0
  380. package/src/__tests__/lod-occlusion.dawn.test.ts +20 -0
  381. package/src/__tests__/lod-selector.profile.test.ts +35 -0
  382. package/src/__tests__/lod-selector.unit.test.ts +68 -0
  383. package/src/__tests__/lod-visibility-identity.unit.test.ts +198 -0
  384. package/src/__tests__/lod-visibility-lifecycle.integration.test.ts +132 -0
  385. package/src/__tests__/ltc-resources.unit.test.ts +32 -0
  386. package/src/__tests__/material-contract-inventory.unit.test.ts +17 -0
  387. package/src/__tests__/material-observation.integration.test.ts +101 -0
  388. package/src/__tests__/occlusion-confidence.unit.test.ts +168 -0
  389. package/src/__tests__/occlusion-invalidation.integration.test.ts +37 -0
  390. package/src/__tests__/occlusion-pass-routing.unit.test.ts +16 -0
  391. package/src/__tests__/occlusion-pass.dawn.test.ts +20 -0
  392. package/src/__tests__/occlusion-pass.integration.test.ts +43 -0
  393. package/src/__tests__/occlusion-pass.rhi-null.test.ts +22 -0
  394. package/src/__tests__/occlusion-query-pool.rhi-null.test.ts +21 -0
  395. package/src/__tests__/occlusion-query-pool.unit.test.ts +95 -0
  396. package/src/__tests__/occlusion-query-ticket.integration.test.ts +47 -0
  397. package/src/__tests__/occlusion-query.dawn.test.ts +16 -0
  398. package/src/__tests__/occlusion-query.evidence.ts +178 -0
  399. package/src/__tests__/occlusion-query.rhi-null.test.ts +31 -0
  400. package/src/__tests__/occlusion-runtime.unit.test.ts +120 -0
  401. package/src/__tests__/pbr-view-layout.unit.test.ts +16 -0
  402. package/src/__tests__/point-shadow-inspection.unit.test.ts +60 -0
  403. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +196 -0
  404. package/src/__tests__/probe-bindings.unit.test.ts +200 -0
  405. package/src/__tests__/probe-blend-numeric.unit.test.ts +67 -0
  406. package/src/__tests__/probe-blend-oracle.unit.test.ts +136 -0
  407. package/src/__tests__/probe-capacity.integration.test.ts +97 -0
  408. package/src/__tests__/probe-recovery.integration.test.ts +104 -0
  409. package/src/__tests__/probe-scene-dirty.integration.test.ts +211 -0
  410. package/src/__tests__/profiler-phase-catalog.test.ts +4 -1
  411. package/src/__tests__/profiler-phase-token.test.ts +1 -0
  412. package/src/__tests__/public-surface.test-d.ts +10 -0
  413. package/src/__tests__/rect-area-boundary.unit.test.ts +24 -0
  414. package/src/__tests__/reflection-fallback-graph.unit.test.ts +67 -0
  415. package/src/__tests__/reflection-fallback-inspection.unit.test.ts +77 -0
  416. package/src/__tests__/reflection-fallback-lifecycle.integration.test.ts +21 -0
  417. package/src/__tests__/reflection-fallback-projection.unit.test.ts +90 -0
  418. package/src/__tests__/reflection-fallback-source.unit.test.ts +32 -0
  419. package/src/__tests__/reflection-probe-authoring.unit.test.ts +71 -0
  420. package/src/__tests__/reflection-probe-filter.unit.test.ts +54 -0
  421. package/src/__tests__/reflection-probe-ibl-topology.unit.test.ts +45 -0
  422. package/src/__tests__/reflection-probe-owner-placement.unit.test.ts +39 -0
  423. package/src/__tests__/reflection-probe-pass-encode.integration.test.ts +101 -0
  424. package/src/__tests__/reflection-probe-performance.perf.test.ts +60 -0
  425. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +120 -0
  426. package/src/__tests__/reflection-probe-recovery.integration.test.ts +55 -0
  427. package/src/__tests__/reflection-probe-selection.unit.test.ts +46 -0
  428. package/src/__tests__/render-error-code-owner.test-d.ts +19 -3
  429. package/src/__tests__/render-feature-docs.unit.test.ts +12 -7
  430. package/src/__tests__/render-graph-fallback.integration.test.ts +24 -0
  431. package/src/__tests__/render-perf/extended-lighting.bench.test.ts +66 -0
  432. package/src/__tests__/render-scene-projection.unit.test.ts +447 -18
  433. package/src/__tests__/render-target-admission.unit.test.ts +80 -0
  434. package/src/__tests__/render-target-graph-projection.unit.test.ts +102 -0
  435. package/src/__tests__/render-target-inspection.unit.test.ts +52 -0
  436. package/src/__tests__/render-target-material-source.test-d.ts +30 -0
  437. package/src/__tests__/render-target-material-source.unit.test.ts +97 -0
  438. package/src/__tests__/render-target-owner-placement.unit.test.ts +203 -0
  439. package/src/__tests__/render-target-owner.unit.test.ts +122 -0
  440. package/src/__tests__/render-target-probe-soak.perf.test.ts +94 -0
  441. package/src/__tests__/render-target-public-schema.json +107 -0
  442. package/src/__tests__/render-target-public-schema.unit.test.ts +91 -0
  443. package/src/__tests__/render-target-public-surface.test-d.ts +83 -0
  444. package/src/__tests__/render-target-readback.integration.test.ts +90 -0
  445. package/src/__tests__/render-target-recovery.integration.test.ts +85 -0
  446. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +124 -0
  447. package/src/__tests__/renderer-lifecycle.integration.test.ts +14 -0
  448. package/src/__tests__/shader-stage-surface.unit.test.ts +19 -4
  449. package/src/__tests__/skin-motion-regression.unit.test.ts +154 -0
  450. package/src/__tests__/skinned-shadow-caster.test.ts +13 -4
  451. package/src/__tests__/spot-modifier-projection.unit.test.ts +70 -0
  452. package/src/__tests__/ssr-admission-matrix.integration.test.ts +206 -0
  453. package/src/__tests__/ssr-composition.unit.test.ts +46 -0
  454. package/src/__tests__/ssr-fallback-zero-work.bench.test.ts +94 -0
  455. package/src/__tests__/ssr-recovery-errors.unit.test.ts +58 -0
  456. package/src/__tests__/standard-cluster-capability.integration.test.ts +63 -0
  457. package/src/__tests__/standard-cluster-transport.unit.test.ts +121 -0
  458. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +299 -0
  459. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +15 -5
  460. package/src/__tests__/standard-lighting-inspection.integration.test.ts +138 -0
  461. package/src/__tests__/standard-lighting-ssot.unit.test.ts +18 -0
  462. package/src/__tests__/standard-lighting-write.unit.test.ts +441 -0
  463. package/src/__tests__/standard-lighting.integration.test.ts +67 -0
  464. package/src/__tests__/standard-pipeline.dawn.test.ts +206 -5
  465. package/src/__tests__/standard-pipeline.integration.test.ts +181 -150
  466. package/src/__tests__/standard-probe-binding.unit.test.ts +25 -0
  467. package/src/__tests__/standard-profile-oracle.unit.test.ts +23 -38
  468. package/src/__tests__/temporal-fallback-generation.integration.test.ts +16 -0
  469. package/src/__tests__/temporal-view.unit.test.ts +3 -3
  470. package/src/__tests__/transmission-prewarm.unit.test.ts +69 -0
  471. package/src/__tests__/typed-pipeline-topology.unit.test.ts +395 -14
  472. package/src/__tests__/urp-pipeline-targets.unit.test.ts +2 -2
  473. package/src/__tests__/visibility-budget.integration.test.ts +140 -0
  474. package/src/__tests__/visibility-budget.unit.test.ts +70 -0
  475. package/src/__tests__/visibility-extract.integration.test.ts +11 -6
  476. package/src/__tests__/visibility-resolver.unit.test.ts +15 -0
  477. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +6 -3
  478. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +31 -28
  479. package/src/assembly/backend-contract.ts +13 -0
  480. package/src/assembly/device-feature-admission.ts +80 -0
  481. package/src/assembly/directional-shadow-inspection.ts +122 -0
  482. package/src/assembly/factory.ts +1411 -366
  483. package/src/assembly/host-contract.ts +35 -0
  484. package/src/assembly/render-target-host.ts +847 -0
  485. package/src/assets/asset-decoders.ts +1 -1
  486. package/src/assets/render-asset-port.ts +7 -1
  487. package/src/capture/cube-views.ts +69 -0
  488. package/src/capture/scheduler.ts +265 -0
  489. package/src/cluster-binner.ts +31 -0
  490. package/src/components/__tests__/lighting-authoring.unit.test.ts +39 -0
  491. package/src/components/__tests__/rect-area-light.unit.test.ts +33 -0
  492. package/src/components/__tests__/schema-contract.unit.test.ts +12 -0
  493. package/src/components/__tests__/schema-parity.unit.test.ts +9 -0
  494. package/src/components/camera.ts +4 -0
  495. package/src/components/cube-camera.ts +52 -0
  496. package/src/components/directional-light.ts +22 -2
  497. package/src/components/directional-shadow-filter.ts +57 -0
  498. package/src/components/index.ts +5 -0
  499. package/src/components/light-helpers.ts +133 -8
  500. package/src/components/light-probe.ts +13 -0
  501. package/src/components/rect-area-light.ts +15 -0
  502. package/src/components/reflection-probe.ts +49 -0
  503. package/src/components/scene-instance.ts +6 -0
  504. package/src/components/spot-light.ts +3 -0
  505. package/src/device/__tests__/extended-lighting-recovery.unit.test.ts +24 -0
  506. package/src/device/gpu-residency.ts +140 -9
  507. package/src/device/lifecycle-transaction.ts +12 -0
  508. package/src/environment/frame.ts +6 -24
  509. package/src/environment/lifecycle.ts +20 -1
  510. package/src/errors/gpu-driven.ts +92 -0
  511. package/src/errors/index.ts +1 -0
  512. package/src/errors/recover.ts +32 -0
  513. package/src/errors/render.ts +341 -143
  514. package/src/extract/camera.ts +3 -3
  515. package/src/extract/directional-shadow-projection.ts +61 -0
  516. package/src/extract/environment.ts +57 -0
  517. package/src/extract/gpu-driven.ts +108 -0
  518. package/src/extract/visibility.ts +2 -1
  519. package/src/features/fullscreen.ts +3 -0
  520. package/src/features/targets.ts +4 -1
  521. package/src/features/types.ts +10 -0
  522. package/src/fullscreen-post-process-pass.ts +43 -2
  523. package/src/gpu-driven/batch-topology.ts +207 -28
  524. package/src/gpu-driven/prepared-draw.ts +208 -0
  525. package/src/gpu-driven/production-raster.ts +953 -43
  526. package/src/gpu-driven/view-gpu.ts +411 -29
  527. package/src/gpu-scene-schema.ts +13 -0
  528. package/src/gpu-scene.ts +140 -64
  529. package/src/gpu-usage.ts +0 -1
  530. package/src/hdrp-buffers.ts +169 -63
  531. package/src/ibl/IblPipelineCache.ts +11 -0
  532. package/src/ibl/kernel-cache.ts +88 -0
  533. package/src/index.ts +222 -0
  534. package/src/inspection-types.ts +276 -1
  535. package/src/instances-derived-bounds.ts +171 -0
  536. package/src/internal.ts +38 -0
  537. package/src/light-buffer-layout.ts +155 -310
  538. package/src/materials.ts +16 -2
  539. package/src/mesh-material-bindings.ts +132 -0
  540. package/src/pbr-pipeline.ts +255 -60
  541. package/src/pipeline/standard-forward-lane.ts +93 -16
  542. package/src/pipeline/standard-lighting/errors.ts +6 -0
  543. package/src/pipeline/standard-lighting/graph.ts +158 -0
  544. package/src/pipeline/standard-lighting/inspection.ts +88 -0
  545. package/src/pipeline/standard-lighting/layout.ts +31 -0
  546. package/src/pipeline/standard-lighting/light-frame.ts +35 -0
  547. package/src/pipeline/standard-lighting/membership.ts +43 -0
  548. package/src/pipeline/standard-lighting/prepare.ts +37 -0
  549. package/src/pipeline/standard-lighting/topology.ts +120 -0
  550. package/src/pipeline/standard-lighting/transport.ts +78 -0
  551. package/src/pipeline/standard-pipeline.ts +124 -193
  552. package/src/pipeline/standard-post.ts +26 -7
  553. package/src/pipeline/standard-profile.ts +17 -47
  554. package/src/pipeline-builder.ts +5 -2
  555. package/src/pipeline-spec.ts +95 -21
  556. package/src/plugin.ts +4 -0
  557. package/src/point-shadow-inspection.ts +55 -0
  558. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +56 -0
  559. package/src/prepare/extended-lighting/ltc-resources.ts +136 -0
  560. package/src/prepare/extended-lighting/resources.ts +389 -0
  561. package/src/prepare/extended-lighting/spot-modifiers.ts +194 -0
  562. package/src/prepare/extended-lighting/state.ts +92 -0
  563. package/src/record/__tests__/gpu-timing.unit.test.ts +177 -0
  564. package/src/record/__tests__/temporal-payload.unit.test.ts +64 -1
  565. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +73 -0
  566. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +20 -1
  567. package/src/record/__tests__/view-ubo-layout.unit.test.ts +40 -0
  568. package/src/record/frame-lighting.ts +591 -452
  569. package/src/record/frame-snapshot.ts +92 -39
  570. package/src/record/frame-targets.ts +17 -0
  571. package/src/record/frame.ts +662 -158
  572. package/src/record/gpu-pass-timing/contract.ts +180 -0
  573. package/src/record/gpu-pass-timing/errors.ts +34 -0
  574. package/src/record/gpu-pass-timing/index.ts +37 -0
  575. package/src/record/gpu-pass-timing/parser.ts +97 -0
  576. package/src/record/gpu-pass-timing/session.ts +938 -0
  577. package/src/record/gpu-timing.ts +112 -39
  578. package/src/record/helpers.ts +0 -58
  579. package/src/record/main-pass-geometry.ts +324 -119
  580. package/src/record/main-pass-material.ts +103 -10
  581. package/src/record/main-pass-sprite-draws.ts +99 -103
  582. package/src/record/main-pass.ts +71 -15
  583. package/src/record/mesh-ssbo.ts +47 -36
  584. package/src/record/render-context.ts +136 -3
  585. package/src/record/shadow-pass.ts +83 -68
  586. package/src/record/skybox-post-pass.ts +20 -4
  587. package/src/record/typed-frame-graph.ts +1034 -43
  588. package/src/record/view-ubo.ts +72 -47
  589. package/src/reflection/filter.ts +81 -0
  590. package/src/reflection/gpu-table.ts +44 -0
  591. package/src/reflection/inspection.ts +67 -0
  592. package/src/reflection/projection.ts +476 -0
  593. package/src/reflection/record-owner.ts +1283 -0
  594. package/src/render-contract.ts +93 -10
  595. package/src/render-graph-primitives.ts +9 -4
  596. package/src/render-pipeline.ts +122 -2
  597. package/src/render-system-extract.ts +1041 -225
  598. package/src/render-system.ts +1310 -85
  599. package/src/renderer.ts +222 -0
  600. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +36 -0
  601. package/src/scene/probe-blend-record.ts +54 -0
  602. package/src/scene/probe-blend.ts +813 -0
  603. package/src/scene/render-scene-types.ts +0 -7
  604. package/src/scene/render-scene.ts +942 -171
  605. package/src/scene/visibility/bounds.ts +57 -0
  606. package/src/scene/visibility/budget.ts +40 -0
  607. package/src/scene/visibility/facet.ts +400 -0
  608. package/src/scene/visibility/gpu-frame-timing.ts +101 -0
  609. package/src/scene/visibility/gpu-lod.ts +119 -0
  610. package/src/scene/visibility/inspection.ts +313 -0
  611. package/src/scene/visibility/lod-selector.ts +84 -0
  612. package/src/scene/visibility/occlusion-confidence.ts +478 -0
  613. package/src/scene/visibility/occlusion-pass.ts +165 -0
  614. package/src/scene/visibility/occlusion-query-pool.ts +178 -0
  615. package/src/scene/visibility/occlusion-runtime.ts +672 -0
  616. package/src/scene/visibility/types.ts +50 -0
  617. package/src/shadow-atlas.ts +5 -5
  618. package/src/ssao-buffers.ts +9 -2
  619. package/src/ssr/admission.ts +451 -0
  620. package/src/ssr/composition.ts +73 -0
  621. package/src/ssr/errors.ts +76 -0
  622. package/src/ssr/identity.ts +12 -0
  623. package/src/systems/skin-palette-allocator.ts +1 -1
  624. package/src/targets/contracts.ts +182 -0
  625. package/src/targets/graph-projection.ts +169 -0
  626. package/src/targets/material-source.ts +143 -0
  627. package/src/targets/owner.ts +214 -0
  628. package/src/targets/physical.ts +18 -0
  629. package/src/targets/readback.ts +153 -0
  630. package/src/temporal/frame-coordinator.ts +26 -0
  631. package/src/temporal/frame.ts +14 -1
  632. package/src/temporal/view.ts +11 -0
  633. package/src/tilemap-chunk-extract-system.ts +4 -4
  634. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +59 -20
  635. package/src/typed-render-graph-primitives.ts +53 -3
  636. package/src/typed-shadow-passes.ts +46 -40
  637. package/src/volume/passes.ts +61 -58
  638. package/dist/.tsbuildinfo +0 -1
  639. package/dist/chunk-4XINDLAY.mjs.map +0 -1
  640. package/dist/chunk-CDTWTHZR.mjs.map +0 -1
  641. package/dist/chunk-M2MP4ZOU.mjs +0 -73
  642. package/dist/chunk-M2MP4ZOU.mjs.map +0 -1
  643. package/dist/chunk-PCHBPYML.mjs.map +0 -1
  644. package/dist/chunk-TBQJOK2L.mjs.map +0 -1
  645. package/dist/chunk-VFPLTKCU.mjs.map +0 -1
@@ -1,4 +1,5 @@
1
- import { frustum, mat4 } from '@forgeax/engine-math';
1
+ import { deriveVertexBufferLayoutFromProjection } from '@forgeax/engine-geometry';
2
+ import { frustum, mat3, mat4 } from '@forgeax/engine-math';
2
3
  import type {
3
4
  GraphAccess,
4
5
  GraphBuffer,
@@ -9,6 +10,7 @@ import type {
9
10
  import type {
10
11
  BindGroup,
11
12
  BindGroupLayout,
13
+ Buffer,
12
14
  RenderPipeline,
13
15
  Result,
14
16
  RhiDevice,
@@ -16,29 +18,165 @@ import type {
16
18
  TextureFormat,
17
19
  } from '@forgeax/engine-rhi';
18
20
  import { err, ok, RhiError } from '@forgeax/engine-rhi';
21
+ import type { MaterialShaderArtifact } from '@forgeax/engine-shader';
19
22
  import type { MaterialRenderState } from '@forgeax/engine-types';
20
23
  import type { MeshGpuHandles } from '../device/gpu-residency';
21
24
  import { GPU_SCENE_WGSL } from '../gpu-scene-schema';
22
25
  import {
23
26
  GPU_BUFFER_USAGE_COPY_DST,
24
27
  GPU_BUFFER_USAGE_INDEX,
28
+ GPU_BUFFER_USAGE_STORAGE,
29
+ GPU_BUFFER_USAGE_UNIFORM,
25
30
  GPU_BUFFER_USAGE_VERTEX,
26
31
  } from '../gpu-usage';
32
+ import { assembleMaterialWithSkylightEntries } from '../ibl/skylight-bind-group';
27
33
  import type { GpuDrivenProductionInspection } from '../inspection-types';
28
34
  import type { PipelineBuilderShaderModuleFactory } from '../pipeline-builder';
29
35
  import { worldEntityKey } from '../record/frame-snapshot';
30
36
  import { geometryRenderStateForTopology } from '../record/main-pass-material';
37
+ import { MESH_SSBO_BYTES } from '../record/mesh-ssbo';
38
+ import type { PipelineState } from '../record/render-context';
31
39
  import type { CameraSnapshot } from '../render-contract';
32
40
  import type { RenderPipelineFrame, RenderPipelineGpuDrivenProjection } from '../render-pipeline';
33
41
  import type { RenderableSnapshot } from '../render-system-extract';
34
42
  import type { PersistentGpuDrivenState } from '../scene/render-scene';
35
- import type { GpuDrivenBatch, SubmissionPlan } from './batch-topology';
36
- import { GpuDrivenView } from './view-gpu';
43
+ import { selectLod } from '../scene/visibility/lod-selector';
44
+ import type { OcclusionFrameProjection } from '../scene/visibility/occlusion-runtime';
45
+ import {
46
+ type GpuDrivenBatch,
47
+ type GpuDrivenCandidate,
48
+ projectedHeightForCandidate,
49
+ type SubmissionPlan,
50
+ } from './batch-topology';
51
+ import { type GpuDrivenLodSelectionInspection, GpuDrivenView, selectGpuLodLane } from './view-gpu';
37
52
 
38
53
  const VERTEX_STAGE = 0x1;
39
54
  const FRAGMENT_STAGE = 0x2;
40
55
  const EMPTY_RESOURCE_CLASS = JSON.stringify({ textures: [], samplers: [], video: [] });
41
56
 
57
+ export interface StandardPbrSceneCandidate {
58
+ readonly primitiveIndex: number;
59
+ readonly world: Float32Array;
60
+ }
61
+
62
+ export interface StandardPbrSceneProjection {
63
+ readonly entryPoint: string;
64
+ readonly layoutIdentity: string;
65
+ readonly receiptGeneration: number;
66
+ /** Packed Mesh rows: mat4 + padded mat3 + previous mat4 + temporal vec4. */
67
+ readonly rows: Float32Array;
68
+ }
69
+
70
+ /**
71
+ * Build the producer-owned Standard PBR scene-index projection. The receipt is
72
+ * the only source of the vertex entry and layout identity; callers cannot
73
+ * silently fall back to the legacy GPU-driven shader ABI.
74
+ */
75
+ export function projectStandardPbrScene(input: {
76
+ readonly artifact: MaterialShaderArtifact;
77
+ readonly candidates: readonly StandardPbrSceneCandidate[];
78
+ }):
79
+ | { readonly ok: true; readonly value: StandardPbrSceneProjection }
80
+ | {
81
+ readonly ok: false;
82
+ readonly error: { readonly code: 'missing-material-receipt' | 'invalid-material-receipt' };
83
+ } {
84
+ const receipt = input.artifact.receipt;
85
+ if (receipt === undefined) return { ok: false, error: { code: 'missing-material-receipt' } };
86
+ if (receipt.reflection.layoutIdentity !== input.artifact.layoutIdentity) {
87
+ return { ok: false, error: { code: 'invalid-material-receipt' } };
88
+ }
89
+ const rows = new Float32Array(input.candidates.length * 48);
90
+ const normal = mat3.create();
91
+ for (let index = 0; index < input.candidates.length; index += 1) {
92
+ const candidate = input.candidates[index];
93
+ if (candidate === undefined) continue;
94
+ const base = index * 48;
95
+ for (let value = 0; value < 16; value += 1) {
96
+ rows[base + value] = candidate.world[value] ?? 0;
97
+ rows[base + 28 + value] = candidate.world[value] ?? 0;
98
+ }
99
+ const normalMatrix = mat3.normalMatrix(normal, candidate.world);
100
+ for (let column = 0; column < 3; column += 1) {
101
+ const source = column * 3;
102
+ const target = base + 16 + column * 4;
103
+ rows[target] = normalMatrix[source] ?? 0;
104
+ rows[target + 1] = normalMatrix[source + 1] ?? 0;
105
+ rows[target + 2] = normalMatrix[source + 2] ?? 0;
106
+ }
107
+ }
108
+ return {
109
+ ok: true,
110
+ value: {
111
+ entryPoint: receipt.sceneIndexEntry,
112
+ layoutIdentity: input.artifact.layoutIdentity,
113
+ receiptGeneration: receipt.generation,
114
+ rows,
115
+ },
116
+ };
117
+ }
118
+
119
+ export interface StandardPbrFrameResourceBinding {
120
+ readonly entryPoint: string;
121
+ readonly layoutIdentity: string;
122
+ readonly receiptGeneration: number;
123
+ readonly bindGroups: readonly [BindGroup, BindGroup, BindGroup, BindGroup];
124
+ readonly resourceSlots: NonNullable<MaterialShaderArtifact['receipt']>['resourceSlots'];
125
+ }
126
+
127
+ /**
128
+ * Adapt the frame's already-created Standard PBR groups to the GPU-driven
129
+ * scene-index draw. This deliberately accepts group handles rather than
130
+ * rebuilding layouts or shader artifacts; the receipt remains the binding
131
+ * contract and the regular frame producers remain the resource owners.
132
+ */
133
+ export function adaptStandardPbrFrameResources(input: {
134
+ readonly artifact: MaterialShaderArtifact;
135
+ readonly view: BindGroup;
136
+ readonly material: BindGroup;
137
+ readonly mesh: BindGroup;
138
+ readonly instances: BindGroup;
139
+ }):
140
+ | { readonly ok: true; readonly value: StandardPbrFrameResourceBinding }
141
+ | {
142
+ readonly ok: false;
143
+ readonly error: { readonly code: 'missing-material-receipt' | 'invalid-material-receipt' };
144
+ } {
145
+ const receipt = input.artifact.receipt;
146
+ if (receipt === undefined) return { ok: false, error: { code: 'missing-material-receipt' } };
147
+ const reflected = receipt.reflection;
148
+ const sameSlots =
149
+ reflected.resourceSlots.length === receipt.resourceSlots.length &&
150
+ reflected.resourceSlots.every((slot, index) => {
151
+ const expected = receipt.resourceSlots[index];
152
+ return (
153
+ expected !== undefined &&
154
+ slot.name === expected.name &&
155
+ slot.parameter === expected.parameter &&
156
+ slot.kind === expected.kind &&
157
+ slot.group === expected.group &&
158
+ slot.binding === expected.binding
159
+ );
160
+ });
161
+ if (
162
+ reflected.layoutIdentity !== input.artifact.layoutIdentity ||
163
+ !sameSlots ||
164
+ receipt.resourceSlots.some((slot) => slot.group !== 1)
165
+ ) {
166
+ return { ok: false, error: { code: 'invalid-material-receipt' } };
167
+ }
168
+ return {
169
+ ok: true,
170
+ value: {
171
+ entryPoint: receipt.sceneIndexEntry,
172
+ layoutIdentity: input.artifact.layoutIdentity,
173
+ receiptGeneration: receipt.generation,
174
+ bindGroups: [input.view, input.material, input.mesh, input.instances],
175
+ resourceSlots: receipt.resourceSlots,
176
+ },
177
+ };
178
+ }
179
+
42
180
  export const GPU_DRIVEN_RIGID_UNLIT_WGSL = /* wgsl */ `
43
181
  ${GPU_SCENE_WGSL}
44
182
 
@@ -87,6 +225,11 @@ interface PreparedBatch {
87
225
  readonly batch: GpuDrivenBatch;
88
226
  readonly mesh: MeshGpuHandles;
89
227
  readonly renderState: MaterialRenderState | undefined;
228
+ /** Stable World attachment key used to join same-submit GPU counters. */
229
+ readonly worldKey: number;
230
+ /** Stable representative slot identity for this homogeneous batch. */
231
+ readonly primitiveSlot: number;
232
+ readonly slotGeneration: number;
90
233
  }
91
234
 
92
235
  interface ProjectedBatch extends PreparedBatch {
@@ -133,6 +276,22 @@ function activeFrustum(camera: CameraSnapshot): Float32Array {
133
276
  );
134
277
  }
135
278
 
279
+ /**
280
+ * LOD projected height is a view fact, not a scene-topology fact. Keep the
281
+ * filtered plan cache scene-stable while still rebuilding its candidate
282
+ * heights whenever a camera input used by the selector changes.
283
+ */
284
+ function lodProjectionFingerprint(camera: CameraSnapshot): string {
285
+ return JSON.stringify({
286
+ position: Array.from(camera.position),
287
+ world: Array.from(camera.world),
288
+ projection: camera.projection,
289
+ fov: camera.fov,
290
+ aspect: camera.aspect,
291
+ orthoHeight: camera.orthoTop - camera.orthoBottom,
292
+ });
293
+ }
294
+
136
295
  function productionEligible(snapshot: RenderableSnapshot, batch: GpuDrivenBatch): boolean {
137
296
  const material = snapshot.materials[batch.key.materialSlot] ?? snapshot.material;
138
297
  const shader = material.materialShaderId ?? 'forgeax::default-unlit';
@@ -144,44 +303,142 @@ function productionEligible(snapshot: RenderableSnapshot, batch: GpuDrivenBatch)
144
303
  );
145
304
  }
146
305
 
306
+ /**
307
+ * Prepared Standard PBR draws have an explicit receipt identity on their
308
+ * batch key. Unprepared batches stay on the rigid production shader; the
309
+ * presence of a frame-wide PBR artifact is not permission to reinterpret an
310
+ * unlit batch with the PBR bind-group layout.
311
+ */
312
+ function isStandardPbrBatch(batch: GpuDrivenBatch): boolean {
313
+ return batch.key.preparedIdentity !== undefined;
314
+ }
315
+
147
316
  function meshSupportsBatch(mesh: MeshGpuHandles, batch: GpuDrivenBatch): boolean {
148
- if (mesh.layoutProjection.arrayStride !== 48) return false;
317
+ if (
318
+ batch.candidates.some((candidate) => {
319
+ const lodCount = candidate.lodCoverages?.length ?? 0;
320
+ return (
321
+ lodCount > 1 &&
322
+ (mesh.lodRanges === undefined ||
323
+ mesh.lodRanges.length < lodCount ||
324
+ mesh.lodRanges.some((ranges) => ranges[candidate.drawItemIndex] === undefined))
325
+ );
326
+ })
327
+ ) {
328
+ return false;
329
+ }
149
330
  return batch.key.drawKind === 'indexed'
150
331
  ? mesh.indexed && mesh.indexBuffer !== null
151
332
  : !mesh.indexed;
152
333
  }
153
334
 
335
+ function selectedLodLevel(candidate: GpuDrivenCandidate): number {
336
+ const coverages = candidate.lodCoverages;
337
+ if (coverages === undefined || coverages.length <= 1) return 0;
338
+ return selectLod({
339
+ levels: coverages.slice(1).map((screenCoverage) => ({ screenCoverage })),
340
+ projectedHeight: candidate.projectedHeight ?? Number.NaN,
341
+ previousLevel: 0,
342
+ hysteresis: candidate.lodHysteresis ?? 0.08,
343
+ ready: coverages.map(() => true),
344
+ historyValid: false,
345
+ }).level;
346
+ }
347
+
348
+ function selectedLodRange(
349
+ batch: GpuDrivenBatch,
350
+ candidate: GpuDrivenCandidate,
351
+ ): { readonly first: number; readonly count: number; readonly baseVertex: number } {
352
+ const level = selectedLodLevel(candidate);
353
+ return (
354
+ (level === 0 ? undefined : candidate.lodRanges?.[level - 1]) ?? {
355
+ first: batch.key.first,
356
+ count: batch.key.count,
357
+ baseVertex: batch.key.baseVertex,
358
+ }
359
+ );
360
+ }
361
+
362
+ /**
363
+ * One raster indirect command is emitted for each selector batch that has at
364
+ * least one admitted candidate. Selector-only batches remain in the compute
365
+ * and readback plan, but they must not inflate the raster command count.
366
+ */
367
+ function admittedRasterBatchCount(plan: SubmissionPlan): number {
368
+ return plan.batches.reduce((count, batch) => count + (batch.visibleCapacity > 0 ? 1 : 0), 0);
369
+ }
370
+
154
371
  function filteredPlan(
155
372
  source: SubmissionPlan,
156
373
  slots: PersistentGpuDrivenState['slots'],
157
374
  meshes: ReadonlyMap<number, MeshGpuHandles>,
375
+ camera: CameraSnapshot,
376
+ activeEntityKeys?: ReadonlySet<number>,
377
+ worldKeys?: readonly number[],
158
378
  ): {
159
379
  readonly plan: SubmissionPlan;
160
380
  readonly batches: readonly PreparedBatch[];
161
381
  readonly entityKeys: ReadonlySet<number>;
382
+ /** No source primitive can enter this lane without a topology/material change. */
383
+ readonly topologyInvariantEmpty: boolean;
162
384
  } {
163
385
  const slotByPrimitive = new Map(slots.map((slot) => [slot.slot, slot] as const));
386
+ const participatingByPrimitive = new Map<number, Set<GpuDrivenBatch>>();
387
+ for (const batch of source.batches) {
388
+ for (const candidate of batch.candidates) {
389
+ const participating = participatingByPrimitive.get(candidate.primitiveIndex);
390
+ if (participating === undefined) {
391
+ participatingByPrimitive.set(candidate.primitiveIndex, new Set([batch]));
392
+ } else {
393
+ participating.add(batch);
394
+ }
395
+ }
396
+ }
164
397
  const entityKeys = new Set<number>();
398
+ const worldKeyFor = (slot: PersistentGpuDrivenState['slots'][number]): number =>
399
+ worldKeys?.[slot.snapshot.worldId] ?? slot.snapshot.worldId;
400
+ const activeKey = (slot: PersistentGpuDrivenState['slots'][number]): number =>
401
+ worldEntityKey(worldKeyFor(slot), slot.snapshot.entityKey);
165
402
  const eligiblePrimitives = new Set<number>();
403
+ let topologyEligiblePrimitiveCount = 0;
166
404
  for (const slot of slots) {
167
405
  const mesh = meshes.get(slot.snapshot.assetHandle);
168
- const participating = source.batches.filter((batch) =>
169
- batch.candidates.some((candidate) => candidate.primitiveIndex === slot.slot),
170
- );
406
+ const participating = participatingByPrimitive.get(slot.slot);
407
+ const participatingBatches = participating === undefined ? [] : [...participating];
408
+ const topologyEligible =
409
+ participatingBatches.length > 0 &&
410
+ participatingBatches.length === (slot.snapshot.gpuDrivenDraws?.length ?? 0) &&
411
+ participatingBatches.every((batch) => productionEligible(slot.snapshot, batch));
412
+ if (topologyEligible) topologyEligiblePrimitiveCount += 1;
171
413
  if (
172
414
  mesh !== undefined &&
173
- participating.length === (slot.snapshot.gpuDrivenDraws?.length ?? 0) &&
174
- participating.every(
175
- (batch) => productionEligible(slot.snapshot, batch) && meshSupportsBatch(mesh, batch),
176
- )
415
+ topologyEligible &&
416
+ participatingBatches.every((batch) => meshSupportsBatch(mesh, batch))
177
417
  ) {
178
418
  eligiblePrimitives.add(slot.slot);
179
- entityKeys.add(worldEntityKey(slot.snapshot.worldId, slot.snapshot.entityKey));
419
+ // Record validation still keys extracted renderables by their current
420
+ // stable world/entity key. Suppressed candidates remain in the GPU
421
+ // selector, but only admitted entities are owned by the compacted draw.
422
+ if (activeEntityKeys === undefined || activeEntityKeys.has(activeKey(slot))) {
423
+ entityKeys.add(activeKey(slot));
424
+ }
180
425
  }
181
426
  }
182
- const batches: PreparedBatch[] = [];
183
- let visibleBase = 0;
184
- const filteredBatches: GpuDrivenBatch[] = [];
427
+ // BatchTopology deliberately keeps LOD candidates individually addressable
428
+ // so topology updates never alias a candidate's selector state. Once the
429
+ // camera has projected the candidates, the renderer can safely coalesce
430
+ // candidates that have identical immutable geometry/LOD facts and the same
431
+ // selected range. The GPU selector uses the same no-history inputs below,
432
+ // so one indirect range remains correct for every candidate in a group.
433
+ const groups = new Map<
434
+ string,
435
+ {
436
+ readonly source: GpuDrivenBatch;
437
+ readonly mesh: MeshGpuHandles;
438
+ readonly renderState: MaterialRenderState | undefined;
439
+ readonly candidates: GpuDrivenCandidate[];
440
+ }
441
+ >();
185
442
  for (const batch of source.batches) {
186
443
  const candidates = batch.candidates.filter((candidate) => {
187
444
  const slot = slotByPrimitive.get(candidate.primitiveIndex);
@@ -190,24 +447,84 @@ function filteredPlan(
190
447
  return mesh !== undefined && meshSupportsBatch(mesh, batch);
191
448
  });
192
449
  if (candidates.length === 0) continue;
450
+ for (const candidate of candidates) {
451
+ const slot = slotByPrimitive.get(candidate.primitiveIndex);
452
+ if (slot === undefined) continue;
453
+ const mesh = meshes.get(slot.snapshot.assetHandle);
454
+ if (mesh === undefined) continue;
455
+ const physicalLodRanges = mesh.lodRanges
456
+ ?.slice(1)
457
+ .map((ranges) => ranges[candidate.drawItemIndex]);
458
+ const projected = Object.freeze({
459
+ ...candidate,
460
+ submitAdmission: activeEntityKeys === undefined || activeEntityKeys.has(activeKey(slot)),
461
+ projectedHeight: projectedHeightForCandidate(slot, camera),
462
+ ...(physicalLodRanges?.every((range) => range !== undefined)
463
+ ? { lodRanges: physicalLodRanges as NonNullable<typeof candidate.lodRanges> }
464
+ : {}),
465
+ });
466
+ const selectedRange = selectedLodRange(batch, projected);
467
+ const groupKey = JSON.stringify({
468
+ key: batch.key,
469
+ worldKey: worldKeyFor(slot),
470
+ lodCoverages: projected.lodCoverages ?? null,
471
+ lodRanges: projected.lodRanges ?? null,
472
+ selectedRange,
473
+ selectedLevel: selectedLodLevel(projected),
474
+ });
475
+ const existing = groups.get(groupKey);
476
+ if (existing === undefined) {
477
+ const material = slot.snapshot.materials[batch.key.materialSlot] ?? slot.snapshot.material;
478
+ groups.set(groupKey, {
479
+ source: batch,
480
+ mesh,
481
+ renderState: geometryRenderStateForTopology(batch.key.topology, material.renderState),
482
+ candidates: [projected],
483
+ });
484
+ } else {
485
+ existing.candidates.push(projected);
486
+ }
487
+ }
488
+ }
489
+
490
+ const batches: PreparedBatch[] = [];
491
+ let visibleBase = 0;
492
+ const filteredBatches: GpuDrivenBatch[] = [];
493
+ let batchId = 0;
494
+ for (const group of groups.values()) {
193
495
  visibleBase = Math.ceil(visibleBase / 64) * 64;
496
+ const admittedCount = group.candidates.reduce(
497
+ (count, candidate) =>
498
+ count +
499
+ ((candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
500
+ .submitAdmission === false
501
+ ? 0
502
+ : 1),
503
+ 0,
504
+ );
194
505
  const filtered: GpuDrivenBatch = Object.freeze({
195
- ...batch,
196
- candidates: Object.freeze(candidates),
506
+ ...group.source,
507
+ batchId,
508
+ candidates: Object.freeze(group.candidates),
197
509
  visibleBase,
198
- visibleCapacity: candidates.length,
510
+ visibleCapacity: admittedCount,
511
+ indirectOffset: batchId * 20,
199
512
  });
200
- visibleBase += candidates.length;
201
- const first = slotByPrimitive.get(candidates[0]?.primitiveIndex ?? -1);
202
- const mesh = first === undefined ? undefined : meshes.get(first.snapshot.assetHandle);
203
- if (mesh === undefined || first === undefined) continue;
513
+ visibleBase += admittedCount;
204
514
  filteredBatches.push(filtered);
205
- const material = first.snapshot.materials[batch.key.materialSlot] ?? first.snapshot.material;
515
+ const firstCandidate = group.candidates[0];
516
+ const firstSlot =
517
+ firstCandidate === undefined ? undefined : slotByPrimitive.get(firstCandidate.primitiveIndex);
518
+ if (firstCandidate === undefined || firstSlot === undefined) continue;
206
519
  batches.push({
207
520
  batch: filtered,
208
- mesh,
209
- renderState: geometryRenderStateForTopology(batch.key.topology, material.renderState),
521
+ mesh: group.mesh,
522
+ renderState: group.renderState,
523
+ worldKey: worldKeyFor(firstSlot),
524
+ primitiveSlot: firstSlot.slot,
525
+ slotGeneration: firstSlot.generation,
210
526
  });
527
+ batchId += 1;
211
528
  }
212
529
  return {
213
530
  plan: Object.freeze({
@@ -218,11 +535,32 @@ function filteredPlan(
218
535
  }),
219
536
  batches: Object.freeze(batches),
220
537
  entityKeys,
538
+ topologyInvariantEmpty: topologyEligiblePrimitiveCount === 0,
221
539
  };
222
540
  }
223
541
 
224
542
  type FilteredProductionPlan = ReturnType<typeof filteredPlan>;
225
543
 
544
+ export interface GpuDrivenWorldLodSelectionInspection {
545
+ readonly worldKey: number;
546
+ readonly primitiveSlot: number;
547
+ readonly slotGeneration: number;
548
+ readonly candidateCount: number;
549
+ readonly visible: number;
550
+ readonly occluded: number;
551
+ readonly lodHistogram: readonly { readonly level: number; readonly count: number }[];
552
+ }
553
+
554
+ function sameEntityKeySet(
555
+ left: ReadonlySet<number> | undefined,
556
+ right: ReadonlySet<number> | undefined,
557
+ ): boolean {
558
+ if (left === right) return true;
559
+ if (left === undefined || right === undefined || left.size !== right.size) return false;
560
+ for (const key of left) if (!right.has(key)) return false;
561
+ return true;
562
+ }
563
+
226
564
  class GpuDrivenRigidRaster {
227
565
  private readonly pipelines = new Map<string, RenderPipeline>();
228
566
 
@@ -375,10 +713,212 @@ class GpuDrivenRigidRaster {
375
713
  }
376
714
  }
377
715
 
716
+ /** The GPU-driven bridge to the existing four-group Standard PBR pipeline. */
717
+ class StandardPbrRasterAdapter {
718
+ private readonly pipelines = new Map<string, RenderPipeline>();
719
+ private readonly projectionBuffers: Buffer[] = [];
720
+
721
+ private constructor(
722
+ private readonly device: RhiDevice,
723
+ private readonly pipelineLayout: import('@forgeax/engine-rhi').PipelineLayout,
724
+ private readonly meshLayout: BindGroupLayout,
725
+ private readonly module: ShaderModule,
726
+ private readonly materialBindGroup: BindGroup,
727
+ private readonly instancesBindGroup: BindGroup,
728
+ private readonly artifact: MaterialShaderArtifact,
729
+ ) {}
730
+
731
+ static create(input: {
732
+ readonly device: RhiDevice;
733
+ readonly shaderModuleFactory: PipelineBuilderShaderModuleFactory;
734
+ readonly pipelineState: PipelineState;
735
+ readonly artifact: MaterialShaderArtifact;
736
+ }): Result<StandardPbrRasterAdapter, RhiError> {
737
+ const pipelineLayout = input.pipelineState.pbrPipelineLayout;
738
+ const fallback = input.pipelineState.skylightFallback;
739
+ if (pipelineLayout === null || fallback === null) {
740
+ return err(
741
+ new RhiError({
742
+ code: 'rhi-not-available',
743
+ expected: 'Standard PBR pipeline layout and Skylight fallback',
744
+ hint: 'initialize the Standard PBR pipeline before enabling GPU-driven raster',
745
+ }),
746
+ );
747
+ }
748
+ const module = input.shaderModuleFactory.createShaderModule({
749
+ label: 'standard-pbr-gpu-driven-scene-index',
750
+ code: input.artifact.wgsl,
751
+ });
752
+ if (!module.ok) return module;
753
+ const materialBuffer = input.device.createBuffer({
754
+ label: 'standard-pbr-gpu-driven-material-row',
755
+ size: 384,
756
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
757
+ });
758
+ if (!materialBuffer.ok) return materialBuffer;
759
+ const materialPayload = new Float32Array(96);
760
+ materialPayload[0] = 1;
761
+ materialPayload[1] = 1;
762
+ materialPayload[2] = 1;
763
+ materialPayload[3] = 1;
764
+ materialPayload[5] = 1;
765
+ materialPayload[6] = 1;
766
+ const materialWrite = input.device.queue.writeBuffer(materialBuffer.value, 0, materialPayload);
767
+ if (!materialWrite.ok) return materialWrite;
768
+ const baseEntries = [
769
+ { binding: 0, resource: bufferBinding(materialBuffer.value, 0, 384) },
770
+ ...Array.from({ length: 8 }, (_, index) => [
771
+ {
772
+ binding: index * 2 + 1,
773
+ resource: { kind: 'sampler' as const, value: fallback.sampler },
774
+ },
775
+ {
776
+ binding: index * 2 + 2,
777
+ resource: {
778
+ kind: 'textureView' as const,
779
+ value:
780
+ index === 2
781
+ ? input.pipelineState.defaultNormalTextureView
782
+ : input.pipelineState.defaultWhiteTextureView,
783
+ },
784
+ },
785
+ ]).flat(),
786
+ ];
787
+ const materialEntries = assembleMaterialWithSkylightEntries(
788
+ baseEntries,
789
+ {
790
+ irradianceView: fallback.irradianceView,
791
+ irradianceSampler: fallback.sampler,
792
+ prefilterView: fallback.prefilterView,
793
+ prefilterSampler: fallback.sampler,
794
+ brdfLutView: fallback.brdfLutView,
795
+ brdfLutSampler: fallback.sampler,
796
+ intensityBuffer: fallback.intensityBuffer,
797
+ },
798
+ { sampler: fallback.sampler, backdropView: input.pipelineState.defaultWhiteTextureView },
799
+ );
800
+ const materialBindGroup = input.device.createBindGroup({
801
+ label: 'standard-pbr-gpu-driven-material-bg',
802
+ layout: input.pipelineState.materialBindGroupLayout,
803
+ entries: materialEntries,
804
+ });
805
+ if (!materialBindGroup.ok) return materialBindGroup;
806
+ const instancesBindGroup = input.device.createBindGroup({
807
+ label: 'standard-pbr-gpu-driven-identity-instances-bg',
808
+ layout: input.pipelineState.instancesBindGroupLayout,
809
+ entries: [
810
+ {
811
+ binding: 0,
812
+ resource: bufferBinding(input.pipelineState.identityInstanceBuffer),
813
+ },
814
+ ],
815
+ });
816
+ if (!instancesBindGroup.ok) return instancesBindGroup;
817
+ return ok(
818
+ new StandardPbrRasterAdapter(
819
+ input.device,
820
+ pipelineLayout,
821
+ input.pipelineState.meshBindGroupLayout,
822
+ module.value,
823
+ materialBindGroup.value,
824
+ instancesBindGroup.value,
825
+ input.artifact,
826
+ ),
827
+ );
828
+ }
829
+
830
+ pipeline(
831
+ format: TextureFormat,
832
+ sampleCount: 1 | 4,
833
+ layoutProjection: MeshGpuHandles['layoutProjection'],
834
+ topology: GpuDrivenBatch['key']['topology'],
835
+ stripIndexFormat: 'uint16' | 'uint32' | undefined,
836
+ renderState: MaterialRenderState | undefined,
837
+ ): Result<RenderPipeline, RhiError> {
838
+ const projectedLayout = deriveVertexBufferLayoutFromProjection(
839
+ layoutProjection,
840
+ this.artifact.uvSetCount === undefined
841
+ ? undefined
842
+ : { shaderUvSetCount: this.artifact.uvSetCount },
843
+ )[0];
844
+ const vertexStride = projectedLayout?.arrayStride ?? layoutProjection.arrayStride;
845
+ const attributes = projectedLayout?.attributes ?? layoutProjection.attributes;
846
+ const key = `${format}|${sampleCount}|${vertexStride}|${topology}|${stripIndexFormat ?? ''}|${JSON.stringify(renderState ?? null)}|${this.artifact.layoutIdentity}`;
847
+ const cached = this.pipelines.get(key);
848
+ if (cached !== undefined) return ok(cached);
849
+ const pipeline = this.device.createRenderPipeline({
850
+ label: `standard-pbr-gpu-driven.${key}`,
851
+ layout: this.pipelineLayout,
852
+ vertex: {
853
+ module: this.module,
854
+ entryPoint: this.artifact.receipt?.sceneIndexEntry ?? 'vs_scene_index',
855
+ buffers: [
856
+ {
857
+ arrayStride: vertexStride,
858
+ stepMode: 'vertex',
859
+ attributes: attributes.map(({ shaderLocation, offset, format }) => ({
860
+ shaderLocation,
861
+ offset,
862
+ format: format as GPUVertexFormat,
863
+ })),
864
+ },
865
+ ],
866
+ },
867
+ fragment: {
868
+ module: this.module,
869
+ entryPoint: 'fs_main',
870
+ targets: [{ format }],
871
+ },
872
+ primitive: {
873
+ topology,
874
+ cullMode: renderState?.cullMode ?? 'back',
875
+ frontFace: renderState?.frontFace ?? 'ccw',
876
+ ...(stripIndexFormat === undefined ? {} : { stripIndexFormat }),
877
+ },
878
+ depthStencil: {
879
+ format: 'depth24plus-stencil8',
880
+ depthWriteEnabled: renderState?.depthWriteEnabled ?? true,
881
+ depthCompare: renderState?.depthCompare ?? 'less',
882
+ },
883
+ ...(sampleCount === 4 ? { multisample: { count: 4 } } : {}),
884
+ });
885
+ if (!pipeline.ok) return pipeline;
886
+ this.pipelines.set(key, pipeline.value);
887
+ return pipeline;
888
+ }
889
+
890
+ projectionBindGroup(rows: Float32Array): Result<BindGroup, RhiError> {
891
+ const buffer = this.device.createBuffer({
892
+ label: 'standard-pbr-gpu-driven-scene-meshes',
893
+ size: Math.max(MESH_SSBO_BYTES, rows.byteLength),
894
+ usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
895
+ });
896
+ if (!buffer.ok) return buffer;
897
+ const write = this.device.queue.writeBuffer(buffer.value, 0, rows);
898
+ if (!write.ok) return write;
899
+ this.projectionBuffers.push(buffer.value);
900
+ return this.device.createBindGroup({
901
+ label: 'standard-pbr-gpu-driven-scene-mesh-bg',
902
+ layout: this.meshLayout,
903
+ entries: [{ binding: 0, resource: bufferBinding(buffer.value, 0, rows.byteLength) }],
904
+ });
905
+ }
906
+
907
+ dispose(): void {
908
+ for (const buffer of this.projectionBuffers) this.device.destroyBuffer(buffer);
909
+ this.projectionBuffers.length = 0;
910
+ }
911
+
912
+ get frameGroups(): readonly [BindGroup, BindGroup] {
913
+ return [this.materialBindGroup, this.instancesBindGroup];
914
+ }
915
+ }
916
+
378
917
  export interface PreparedGpuDrivenFrame {
379
918
  readonly topologySignature: string;
380
919
  readonly entityKeys: ReadonlySet<number>;
381
920
  readonly ownsAllRenderables: boolean;
921
+ readonly occlusion?: OcclusionFrameProjection;
382
922
  /** @internal Commits a replacement generation only after graph promotion. */
383
923
  readonly _commitResourceReplacement: () => void;
384
924
  project(
@@ -391,12 +931,16 @@ export interface PreparedGpuDrivenFrame {
391
931
  export class GpuDrivenProduction {
392
932
  private view: GpuDrivenView | undefined;
393
933
  private raster: GpuDrivenRigidRaster | undefined;
934
+ private standardRaster: StandardPbrRasterAdapter | undefined;
394
935
  private readonly sceneIdentities = new WeakMap<object, number>();
395
936
  private nextSceneIdentity = 1;
396
937
  private filteredCache:
397
938
  | {
398
939
  readonly source: SubmissionPlan;
399
940
  readonly meshResidencyEpoch: number;
941
+ readonly lodProjectionFingerprint: string;
942
+ readonly activeEntityRevision?: number;
943
+ readonly activeEntityKeys?: ReadonlySet<number>;
400
944
  readonly value: FilteredProductionPlan;
401
945
  }
402
946
  | undefined;
@@ -406,6 +950,13 @@ export class GpuDrivenProduction {
406
950
  private batchBindGroupCreates = 0;
407
951
  private validatedGpuOwnedRows = 0;
408
952
  private cpuFallbackDrawItems = 0;
953
+ private telemetryPrepared = false;
954
+ private telemetryCandidateCount = 0;
955
+ private telemetryGeometryWork = 0;
956
+ private telemetryRootGeometryWork = 0;
957
+ private indirectDrawCount = 0;
958
+ private sceneRowsRequired = true;
959
+ private standardPbrProjection: StandardPbrSceneProjection | undefined;
409
960
  private signatureCache:
410
961
  | {
411
962
  readonly filtered: FilteredProductionPlan;
@@ -428,40 +979,130 @@ export class GpuDrivenProduction {
428
979
  readonly viewBindGroupLayout: BindGroupLayout;
429
980
  readonly meshResidencyEpoch: number;
430
981
  readonly hdrp: boolean;
982
+ /** Entity keys admitted by the persistent visibility facet for this view. */
983
+ readonly activeEntityKeys?: ReadonlySet<number>;
984
+ /** Collision-free renderer-owned revision for the active visibility set. */
985
+ readonly activeEntityRevision?: number;
986
+ /** Total extracted candidates, including facet-suppressed rows. */
987
+ readonly telemetryCandidateCount?: number;
988
+ readonly occlusion?: OcclusionFrameProjection;
989
+ /** Cooked producer artifact selected from the frame material catalog. */
990
+ readonly standardPbrArtifact?: MaterialShaderArtifact;
991
+ readonly standardPbrPipelineState?: PipelineState;
431
992
  }): Result<PreparedGpuDrivenFrame | undefined, RhiError> {
432
993
  this.gpuOwnedSnapshotsMaterialized = 0;
433
994
  this.filteredPlanBuilds = 0;
434
995
  this.batchBindGroupCreates = 0;
435
996
  this.validatedGpuOwnedRows = 0;
436
997
  this.cpuFallbackDrawItems = 0;
437
- if (
438
- input.hdrp ||
439
- input.scene === undefined ||
440
- !this.device.caps.compute ||
441
- !this.device.caps.storageBuffer ||
442
- !this.device.caps.indirectDrawing
443
- ) {
998
+ this.telemetryPrepared = false;
999
+ this.telemetryCandidateCount = input.telemetryCandidateCount ?? 0;
1000
+ this.telemetryGeometryWork = 0;
1001
+ this.telemetryRootGeometryWork = 0;
1002
+ this.indirectDrawCount = 0;
1003
+ if (input.hdrp || input.scene === undefined || selectGpuLodLane(this.device.caps) === 'cpu') {
444
1004
  this.gpuOwnedEntityCount = 0;
1005
+ this.sceneRowsRequired = false;
445
1006
  return ok(undefined);
446
1007
  }
447
1008
  const scene = input.scene;
1009
+ const projectionFingerprint = lodProjectionFingerprint(input.camera);
1010
+ // An empty filtered plan means this topology/mesh-residency pair has no
1011
+ // renderables that this production lane can own. Camera and visibility
1012
+ // inputs only affect admitted candidates and LOD selection, so they cannot
1013
+ // turn that empty topology into an eligible one. Keep the negative result
1014
+ // resident instead of rescanning every scene slot as the camera moves.
1015
+ const cachedPlanIsTopologyInvariant = this.filteredCache?.value.topologyInvariantEmpty === true;
1016
+ const cachedPlanIsCameraInvariant = cachedPlanIsTopologyInvariant;
1017
+ const cachedVisibilityMatches =
1018
+ cachedPlanIsCameraInvariant || this.filteredCache === undefined
1019
+ ? true
1020
+ : input.activeEntityRevision !== undefined &&
1021
+ this.filteredCache.activeEntityRevision !== undefined
1022
+ ? input.activeEntityRevision === this.filteredCache.activeEntityRevision
1023
+ : sameEntityKeySet(this.filteredCache.activeEntityKeys, input.activeEntityKeys);
448
1024
  let filtered = this.filteredCache?.value;
449
1025
  if (
450
1026
  filtered === undefined ||
451
1027
  this.filteredCache?.source !== scene.plan ||
452
- this.filteredCache.meshResidencyEpoch !== input.meshResidencyEpoch
1028
+ (!cachedPlanIsTopologyInvariant &&
1029
+ this.filteredCache.meshResidencyEpoch !== input.meshResidencyEpoch) ||
1030
+ (!cachedPlanIsCameraInvariant &&
1031
+ this.filteredCache.lodProjectionFingerprint !== projectionFingerprint) ||
1032
+ !cachedVisibilityMatches
453
1033
  ) {
454
- filtered = filteredPlan(scene.plan, scene.slots, input.meshes);
1034
+ filtered = filteredPlan(
1035
+ scene.plan,
1036
+ scene.slots,
1037
+ input.meshes,
1038
+ input.camera,
1039
+ input.activeEntityKeys,
1040
+ scene.worldKeys,
1041
+ );
455
1042
  this.filteredCache = {
456
1043
  source: scene.plan,
457
1044
  meshResidencyEpoch: input.meshResidencyEpoch,
1045
+ lodProjectionFingerprint: projectionFingerprint,
1046
+ ...(input.activeEntityRevision === undefined
1047
+ ? {}
1048
+ : { activeEntityRevision: input.activeEntityRevision }),
1049
+ ...(input.activeEntityKeys === undefined
1050
+ ? {}
1051
+ : { activeEntityKeys: input.activeEntityKeys }),
458
1052
  value: filtered,
459
1053
  };
460
1054
  this.gpuOwnedSnapshotsMaterialized = scene.slots.length;
461
1055
  this.filteredPlanBuilds = 1;
462
1056
  }
463
1057
  this.gpuOwnedEntityCount = filtered.entityKeys.size;
1058
+ this.sceneRowsRequired = !filtered.topologyInvariantEmpty;
1059
+ // Publish the command count from the current filtered plan on every
1060
+ // prepare call. A cached graph does not call project() again on stable
1061
+ // frames, so deriving this only while compiling the raster pass would
1062
+ // transiently report zero after prepare() reset the per-frame counters.
1063
+ this.indirectDrawCount = admittedRasterBatchCount(filtered.plan);
464
1064
  if (filtered.batches.length === 0) return ok(undefined);
1065
+ this.standardPbrProjection = undefined;
1066
+ const usesStandardPbrRaster = filtered.batches.every(({ batch }) => isStandardPbrBatch(batch));
1067
+ if (usesStandardPbrRaster && input.standardPbrArtifact !== undefined) {
1068
+ const slotsByPrimitive = new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
1069
+ const projection = projectStandardPbrScene({
1070
+ artifact: input.standardPbrArtifact,
1071
+ candidates: filtered.batches.flatMap(({ batch }) =>
1072
+ batch.candidates.flatMap((candidate) => {
1073
+ const slot = slotsByPrimitive.get(candidate.primitiveIndex);
1074
+ return slot === undefined
1075
+ ? []
1076
+ : [
1077
+ {
1078
+ primitiveIndex: candidate.primitiveIndex,
1079
+ world: slot.snapshot.transform.world,
1080
+ },
1081
+ ];
1082
+ }),
1083
+ ),
1084
+ });
1085
+ if (!projection.ok) {
1086
+ return err(
1087
+ new RhiError({
1088
+ code: 'shader-compile-failed',
1089
+ expected: 'receipt-derived Standard PBR scene projection',
1090
+ hint: 'publish one validated MaterialShaderArtifact receipt before entering the GPU lane',
1091
+ }),
1092
+ );
1093
+ }
1094
+ this.standardPbrProjection = projection.value;
1095
+ if (input.standardPbrPipelineState !== undefined && this.standardRaster === undefined) {
1096
+ const created = StandardPbrRasterAdapter.create({
1097
+ device: this.device,
1098
+ shaderModuleFactory: this.shaderModuleFactory,
1099
+ pipelineState: input.standardPbrPipelineState,
1100
+ artifact: input.standardPbrArtifact,
1101
+ });
1102
+ if (!created.ok) return created;
1103
+ this.standardRaster = created.value;
1104
+ }
1105
+ }
465
1106
  if (this.view === undefined) {
466
1107
  const created = GpuDrivenView.create({
467
1108
  device: this.device,
@@ -481,8 +1122,11 @@ export class GpuDrivenProduction {
481
1122
  }
482
1123
  const updated = this.view.update(filtered.plan, scene.scene, activeFrustum(input.camera));
483
1124
  if (!updated.ok) return updated;
1125
+ this.telemetryPrepared = true;
484
1126
  const view = this.view;
485
1127
  const raster = this.raster;
1128
+ const standardRaster = this.standardRaster;
1129
+ const standardArtifact = input.standardPbrArtifact;
486
1130
  const sceneIdentity = this.sceneIdentity(scene.scene);
487
1131
  const viewInspection = view.inspect();
488
1132
  let signature = this.signatureCache?.value;
@@ -497,10 +1141,12 @@ export class GpuDrivenProduction {
497
1141
  sceneIdentity,
498
1142
  revision: filtered.plan.revision,
499
1143
  meshResidencyEpoch: input.meshResidencyEpoch,
1144
+ standardPbrLayoutIdentity: this.standardPbrProjection?.layoutIdentity,
500
1145
  viewResourceGeneration: viewInspection.resourceGeneration,
501
1146
  batches: filtered.batches.map(({ batch, mesh }) => [
502
1147
  batch.batchId,
503
1148
  batch.generation,
1149
+ batch.visibleCapacity,
504
1150
  batch.visibleBase,
505
1151
  batch.candidates.map((candidate) => [
506
1152
  candidate.primitiveIndex,
@@ -514,6 +1160,7 @@ export class GpuDrivenProduction {
514
1160
  ]),
515
1161
  capacities: [
516
1162
  viewInspection.candidateCapacity,
1163
+ viewInspection.visibleBufferCapacity,
517
1164
  viewInspection.batchCapacity,
518
1165
  viewInspection.indirectCapacity,
519
1166
  ],
@@ -529,11 +1176,165 @@ export class GpuDrivenProduction {
529
1176
  return ok({
530
1177
  topologySignature: signature,
531
1178
  entityKeys: filtered.entityKeys,
532
- ownsAllRenderables: filtered.entityKeys.size === scene.slots.length,
1179
+ ownsAllRenderables:
1180
+ input.activeEntityKeys === undefined
1181
+ ? filtered.entityKeys.size === scene.slots.length
1182
+ : filtered.entityKeys.size === input.activeEntityKeys.size,
1183
+ ...(input.occlusion === undefined ? {} : { occlusion: input.occlusion }),
533
1184
  _commitResourceReplacement: () => view._commitResourceReplacement(),
534
1185
  project: (graph, format, sampleCount) => {
1186
+ this.indirectDrawCount = admittedRasterBatchCount(filtered.plan);
535
1187
  const outputs = view.addPasses(graph);
536
1188
  if (!outputs.ok) return outputs;
1189
+ if (
1190
+ usesStandardPbrRaster &&
1191
+ standardRaster !== undefined &&
1192
+ standardArtifact !== undefined
1193
+ ) {
1194
+ const standardProjected: Array<{
1195
+ readonly batch: GpuDrivenBatch;
1196
+ readonly mesh: MeshGpuHandles;
1197
+ readonly vertex: GraphBuffer;
1198
+ readonly index?: GraphBuffer;
1199
+ readonly pipeline: RenderPipeline;
1200
+ readonly meshBindGroup: BindGroup;
1201
+ }> = [];
1202
+ const standardAccesses: GraphAccess[] = [
1203
+ { resource: outputs.value.primitive, usage: 'storage-read' },
1204
+ { resource: outputs.value.instance, usage: 'storage-read' },
1205
+ { resource: outputs.value.material, usage: 'storage-read' },
1206
+ { resource: outputs.value.visible, usage: 'storage-read' },
1207
+ { resource: outputs.value.transform, usage: 'storage-read' },
1208
+ { resource: outputs.value.indirect, usage: 'indirect-read' },
1209
+ ];
1210
+ const slotsByPrimitive = new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
1211
+ const meshBuffers = new Map<
1212
+ number,
1213
+ { readonly vertex: GraphBuffer; readonly index?: GraphBuffer }
1214
+ >();
1215
+ for (const prepared of filtered.batches) {
1216
+ const candidates = prepared.batch.candidates.flatMap((candidate) => {
1217
+ const slot = slotsByPrimitive.get(candidate.primitiveIndex);
1218
+ return slot === undefined
1219
+ ? []
1220
+ : [
1221
+ {
1222
+ primitiveIndex: candidate.primitiveIndex,
1223
+ world: slot.snapshot.transform.world,
1224
+ },
1225
+ ];
1226
+ });
1227
+ const projection = projectStandardPbrScene({
1228
+ artifact: standardArtifact,
1229
+ candidates,
1230
+ });
1231
+ if (!projection.ok) {
1232
+ return err(
1233
+ new RhiError({
1234
+ code: 'shader-compile-failed',
1235
+ expected: 'receipt-derived Standard PBR scene projection',
1236
+ hint: 'keep the frame artifact receipt and projected rows on one identity',
1237
+ }),
1238
+ );
1239
+ }
1240
+ const meshBindGroup = standardRaster.projectionBindGroup(projection.value.rows);
1241
+ if (!meshBindGroup.ok) return meshBindGroup;
1242
+ let buffers = meshBuffers.get(prepared.batch.key.assetHandle);
1243
+ if (buffers === undefined) {
1244
+ const vertex = graph.importBuffer(
1245
+ `gpu-driven.standard.${prepared.batch.key.assetHandle}.vertex`,
1246
+ {
1247
+ size: prepared.mesh.vboBytes,
1248
+ usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
1249
+ },
1250
+ () => prepared.mesh.vertexBuffer.handle,
1251
+ );
1252
+ if (!vertex.ok) return vertex;
1253
+ let index: GraphBuffer | undefined;
1254
+ if (prepared.mesh.indexBuffer !== null) {
1255
+ const indexHandle = prepared.mesh.indexBuffer;
1256
+ const imported = graph.importBuffer(
1257
+ `gpu-driven.standard.${prepared.batch.key.assetHandle}.index`,
1258
+ {
1259
+ size: prepared.mesh.iboBytes,
1260
+ usage: GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST,
1261
+ },
1262
+ () => indexHandle.handle,
1263
+ );
1264
+ if (!imported.ok) return imported;
1265
+ index = imported.value;
1266
+ }
1267
+ buffers = { vertex: vertex.value, ...(index === undefined ? {} : { index }) };
1268
+ meshBuffers.set(prepared.batch.key.assetHandle, buffers);
1269
+ standardAccesses.push({ resource: buffers.vertex, usage: 'vertex-read' });
1270
+ if (buffers.index !== undefined) {
1271
+ standardAccesses.push({ resource: buffers.index, usage: 'index-read' });
1272
+ }
1273
+ }
1274
+ const pipeline = standardRaster.pipeline(
1275
+ format,
1276
+ sampleCount,
1277
+ prepared.mesh.layoutProjection,
1278
+ prepared.batch.key.topology,
1279
+ prepared.batch.key.drawKind === 'indexed' &&
1280
+ (prepared.batch.key.topology === 'line-strip' ||
1281
+ prepared.batch.key.topology === 'triangle-strip')
1282
+ ? prepared.mesh.indexFormat
1283
+ : undefined,
1284
+ prepared.renderState,
1285
+ );
1286
+ if (!pipeline.ok) return pipeline;
1287
+ standardProjected.push({
1288
+ batch: prepared.batch,
1289
+ mesh: prepared.mesh,
1290
+ ...buffers,
1291
+ pipeline: pipeline.value,
1292
+ meshBindGroup: meshBindGroup.value,
1293
+ });
1294
+ }
1295
+ const [materialBindGroup, instancesBindGroup] = standardRaster.frameGroups;
1296
+ return ok({
1297
+ accesses: standardAccesses,
1298
+ encode: (viewBindGroup, pass, resources: GraphResourceResolver) => {
1299
+ // pbr-view-bgl has two dynamic uniform bindings: the frame view
1300
+ // slot and the optional Points/Lines style slot. Standard
1301
+ // draws do not use the latter, but WebGPU still requires one
1302
+ // offset per dynamic binding.
1303
+ pass.setBindGroup(0, viewBindGroup, [0, 0]);
1304
+ pass.setBindGroup(1, materialBindGroup, [0]);
1305
+ pass.setBindGroup(3, instancesBindGroup);
1306
+ let currentPipeline: RenderPipeline | undefined;
1307
+ let currentVertex: GraphBuffer | undefined;
1308
+ let currentIndex: GraphBuffer | undefined;
1309
+ for (const batch of standardProjected) {
1310
+ if (currentPipeline !== batch.pipeline) {
1311
+ pass.setPipeline(batch.pipeline);
1312
+ currentPipeline = batch.pipeline;
1313
+ }
1314
+ pass.setBindGroup(2, batch.meshBindGroup, [0]);
1315
+ if (currentVertex !== batch.vertex) {
1316
+ const vertex = resources.buffer(batch.vertex);
1317
+ if (!vertex.ok) throw vertex.error;
1318
+ pass.setVertexBuffer(0, vertex.value);
1319
+ currentVertex = batch.vertex;
1320
+ }
1321
+ if (batch.index !== undefined && currentIndex !== batch.index) {
1322
+ const index = resources.buffer(batch.index);
1323
+ if (!index.ok) throw index.error;
1324
+ pass.setIndexBuffer(index.value, batch.mesh.indexFormat);
1325
+ currentIndex = batch.index;
1326
+ }
1327
+ const indirect = resources.buffer(outputs.value.indirect);
1328
+ if (!indirect.ok) throw indirect.error;
1329
+ if (batch.batch.key.drawKind === 'indexed') {
1330
+ pass.drawIndexedIndirect(indirect.value, batch.batch.indirectOffset);
1331
+ } else {
1332
+ pass.drawIndirect(indirect.value, batch.batch.indirectOffset);
1333
+ }
1334
+ }
1335
+ },
1336
+ });
1337
+ }
537
1338
  const projected: ProjectedBatch[] = [];
538
1339
  const accesses: GraphAccess[] = [
539
1340
  { resource: outputs.value.primitive, usage: 'storage-read' },
@@ -548,6 +1349,10 @@ export class GpuDrivenProduction {
548
1349
  { readonly vertex: GraphBuffer; readonly index?: GraphBuffer }
549
1350
  >();
550
1351
  for (const prepared of filtered.batches) {
1352
+ // A selector-only batch can have zero admitted candidates. Keep it
1353
+ // in the GPU selector and readback counters, but do not create a
1354
+ // raster bind group or encode a zero-instance indirect draw.
1355
+ if (prepared.batch.visibleCapacity === 0) continue;
551
1356
  let buffers = meshBuffers.get(prepared.batch.key.assetHandle);
552
1357
  if (buffers === undefined) {
553
1358
  const vertex = graph.importBuffer(
@@ -583,7 +1388,7 @@ export class GpuDrivenProduction {
583
1388
  const pipeline = raster.pipeline(
584
1389
  format,
585
1390
  sampleCount,
586
- 48 + (prepared.mesh.uvSetCount - 1) * 8,
1391
+ prepared.mesh.layoutProjection.arrayStride,
587
1392
  prepared.batch.key.topology,
588
1393
  prepared.batch.key.drawKind === 'indexed' &&
589
1394
  (prepared.batch.key.topology === 'line-strip' ||
@@ -603,6 +1408,7 @@ export class GpuDrivenProduction {
603
1408
  bindings: bindings.value,
604
1409
  });
605
1410
  }
1411
+ this.indirectDrawCount = projected.length;
606
1412
  return ok({
607
1413
  accesses,
608
1414
  encode: (viewBindGroup, pass, resources: GraphResourceResolver) => {
@@ -611,7 +1417,7 @@ export class GpuDrivenProduction {
611
1417
  // shared by the GPU-driven raster pipeline, so even rigid draws
612
1418
  // must provide the binding-10 offset (zero selects the frame
613
1419
  // default slot).
614
- pass.setBindGroup(0, viewBindGroup, [0]);
1420
+ pass.setBindGroup(0, viewBindGroup, [0, 0]);
615
1421
  let currentPipeline: RenderPipeline | undefined;
616
1422
  let currentVertex: GraphBuffer | undefined;
617
1423
  let currentIndex: GraphBuffer | undefined;
@@ -635,11 +1441,14 @@ export class GpuDrivenProduction {
635
1441
  }
636
1442
  const indirect = resources.buffer(outputs.value.indirect);
637
1443
  if (!indirect.ok) throw indirect.error;
638
- if (batch.batch.key.drawKind === 'indexed') {
639
- pass.drawIndexedIndirect(indirect.value, batch.batch.indirectOffset);
640
- } else {
641
- pass.drawIndirect(indirect.value, batch.batch.indirectOffset);
642
- }
1444
+ const draw = (): void => {
1445
+ if (batch.batch.key.drawKind === 'indexed') {
1446
+ pass.drawIndexedIndirect(indirect.value, batch.batch.indirectOffset);
1447
+ } else {
1448
+ pass.drawIndirect(indirect.value, batch.batch.indirectOffset);
1449
+ }
1450
+ };
1451
+ draw();
643
1452
  }
644
1453
  },
645
1454
  });
@@ -653,6 +1462,12 @@ export class GpuDrivenProduction {
653
1462
  this.raster = undefined;
654
1463
  this.filteredCache = undefined;
655
1464
  this.signatureCache = undefined;
1465
+ this.sceneRowsRequired = true;
1466
+ }
1467
+
1468
+ /** Whether the current production topology can consume GPU Scene row updates. */
1469
+ requiresSceneRows(): boolean {
1470
+ return this.sceneRowsRequired;
656
1471
  }
657
1472
 
658
1473
  inspect(): GpuDrivenProductionInspection {
@@ -669,17 +1484,112 @@ export class GpuDrivenProduction {
669
1484
  topologyRevision: view?.topologyRevision,
670
1485
  validatedGpuOwnedRows: this.validatedGpuOwnedRows,
671
1486
  cpuFallbackDrawItems: this.cpuFallbackDrawItems,
1487
+ geometryWork: this.telemetryGeometryWork,
1488
+ rootGeometryWork: this.telemetryRootGeometryWork,
1489
+ geometryWorkReduction:
1490
+ this.telemetryRootGeometryWork > 0
1491
+ ? 1 - this.telemetryGeometryWork / this.telemetryRootGeometryWork
1492
+ : 0,
1493
+ batchCount: view?.batchCount ?? 0,
1494
+ indirectDrawCount: this.indirectDrawCount,
672
1495
  };
673
1496
  }
674
1497
 
1498
+ /** Read renderer-owned LOD counters copied by the last GPU submission. */
1499
+ readLodSelection(): Promise<GpuDrivenLodSelectionInspection | undefined> {
1500
+ if (!this.telemetryPrepared) return Promise.resolve(undefined);
1501
+ return (this.view?.readLodSelection() ?? Promise.resolve(undefined)).then((selection) => {
1502
+ if (selection !== undefined) {
1503
+ this.telemetryGeometryWork = selection.geometryWork;
1504
+ this.telemetryRootGeometryWork = selection.rootGeometryWork;
1505
+ }
1506
+ if (selection === undefined || this.telemetryCandidateCount <= 0) return selection;
1507
+ const filtered = this.filteredCache?.value;
1508
+ if (filtered === undefined) return selection;
1509
+ const preparedByBatchId = new Map(
1510
+ filtered.batches.map((prepared) => [prepared.batch.batchId, prepared] as const),
1511
+ );
1512
+ const worldSelections = new Map<
1513
+ number,
1514
+ {
1515
+ readonly worldKey: number;
1516
+ readonly primitiveSlot: number;
1517
+ readonly slotGeneration: number;
1518
+ candidateCount: number;
1519
+ visible: number;
1520
+ occluded: number;
1521
+ readonly lodHistogram: Map<number, number>;
1522
+ }
1523
+ >();
1524
+ for (const batchSelection of selection.batches) {
1525
+ const prepared = preparedByBatchId.get(batchSelection.batchId);
1526
+ if (prepared === undefined) return selection;
1527
+ let aggregate = worldSelections.get(prepared.worldKey);
1528
+ if (aggregate === undefined) {
1529
+ aggregate = {
1530
+ worldKey: prepared.worldKey,
1531
+ primitiveSlot: prepared.primitiveSlot,
1532
+ slotGeneration: prepared.slotGeneration,
1533
+ candidateCount: 0,
1534
+ visible: 0,
1535
+ occluded: 0,
1536
+ lodHistogram: new Map(),
1537
+ };
1538
+ worldSelections.set(prepared.worldKey, aggregate);
1539
+ }
1540
+ aggregate.candidateCount += batchSelection.candidateCount;
1541
+ aggregate.visible += batchSelection.visible;
1542
+ aggregate.occluded += batchSelection.occluded;
1543
+ for (const row of batchSelection.lodHistogram) {
1544
+ aggregate.lodHistogram.set(
1545
+ row.level,
1546
+ (aggregate.lodHistogram.get(row.level) ?? 0) + row.count,
1547
+ );
1548
+ }
1549
+ }
1550
+ return {
1551
+ ...selection,
1552
+ candidateCount: this.telemetryCandidateCount,
1553
+ occluded: Math.max(0, this.telemetryCandidateCount - selection.visible),
1554
+ worldSelections: Object.freeze(
1555
+ [...worldSelections.values()].map((aggregate) =>
1556
+ Object.freeze({
1557
+ worldKey: aggregate.worldKey,
1558
+ primitiveSlot: aggregate.primitiveSlot,
1559
+ slotGeneration: aggregate.slotGeneration,
1560
+ candidateCount: aggregate.candidateCount,
1561
+ visible: aggregate.visible,
1562
+ occluded: aggregate.occluded,
1563
+ lodHistogram: Object.freeze(
1564
+ [...aggregate.lodHistogram.entries()]
1565
+ .sort(([left], [right]) => left - right)
1566
+ .map(([level, count]) => Object.freeze({ level, count })),
1567
+ ),
1568
+ } satisfies GpuDrivenWorldLodSelectionInspection),
1569
+ ),
1570
+ ),
1571
+ };
1572
+ });
1573
+ }
1574
+
675
1575
  recordCpuValidation(
676
1576
  validated: readonly { readonly source: RenderableSnapshot }[],
677
1577
  gpuOwned: ReadonlySet<number>,
1578
+ worldKeys?: readonly number[],
678
1579
  ): void {
679
1580
  this.cpuFallbackDrawItems = validated.length;
680
1581
  let gpuOwnedRows = 0;
681
1582
  for (const row of validated) {
682
- if (gpuOwned.has(worldEntityKey(row.source.worldId, row.source.entityKey))) gpuOwnedRows += 1;
1583
+ if (
1584
+ gpuOwned.has(
1585
+ worldEntityKey(
1586
+ worldKeys?.[row.source.worldId] ?? row.source.worldId,
1587
+ row.source.entityKey,
1588
+ ),
1589
+ )
1590
+ ) {
1591
+ gpuOwnedRows += 1;
1592
+ }
683
1593
  }
684
1594
  this.validatedGpuOwnedRows = gpuOwnedRows;
685
1595
  }