@forgeax/engine-render 0.1.21 → 0.1.24

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 (584) hide show
  1. package/README.md +281 -38
  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/bundler-contract.d.ts +9 -0
  5. package/dist/assembly/bundler-contract.d.ts.map +1 -0
  6. package/dist/assembly/device-feature-admission.d.ts +8 -0
  7. package/dist/assembly/device-feature-admission.d.ts.map +1 -1
  8. package/dist/assembly/extended-lighting-fallback.d.ts +13 -0
  9. package/dist/assembly/extended-lighting-fallback.d.ts.map +1 -0
  10. package/dist/assembly/factory.d.ts +7 -235
  11. package/dist/assembly/factory.d.ts.map +1 -1
  12. package/dist/assembly/host-contract.d.ts +10 -0
  13. package/dist/assembly/host-contract.d.ts.map +1 -1
  14. package/dist/assembly/material/assembly.d.ts +3 -2
  15. package/dist/assembly/material/assembly.d.ts.map +1 -1
  16. package/dist/assembly/material/standard-layer-projection.d.ts +10 -0
  17. package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -0
  18. package/dist/assembly/material-shader-policy.d.ts +168 -0
  19. package/dist/assembly/material-shader-policy.d.ts.map +1 -0
  20. package/dist/assembly/material-shader-prewarm.d.ts +21 -0
  21. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
  22. package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
  23. package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
  24. package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
  25. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
  26. package/dist/assembly/recovery/generation.d.ts +46 -0
  27. package/dist/assembly/recovery/generation.d.ts.map +1 -0
  28. package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
  29. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
  30. package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
  31. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
  32. package/dist/assembly/renderer-facade.d.ts +10 -0
  33. package/dist/assembly/renderer-facade.d.ts.map +1 -0
  34. package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
  35. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  36. package/dist/assembly/renderer-helpers.d.ts +19 -0
  37. package/dist/assembly/renderer-helpers.d.ts.map +1 -0
  38. package/dist/assembly/renderer-lifecycle.d.ts +34 -0
  39. package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
  40. package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
  41. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
  42. package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
  43. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
  44. package/dist/assembly/webgpu-ready.d.ts +55 -0
  45. package/dist/assembly/webgpu-ready.d.ts.map +1 -0
  46. package/dist/assembly/webgpu-renderer.d.ts +152 -0
  47. package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
  48. package/dist/authoring.mjs +1 -1
  49. package/dist/{chunk-PCHBPYML.mjs → chunk-GE3SDD72.mjs} +37 -5
  50. package/dist/chunk-GE3SDD72.mjs.map +1 -0
  51. package/dist/chunk-KZFTK6DO.mjs +1191 -0
  52. package/dist/chunk-KZFTK6DO.mjs.map +1 -0
  53. package/dist/{chunk-FS2KNUQH.mjs → chunk-Q752SHI5.mjs} +332 -568
  54. package/dist/chunk-Q752SHI5.mjs.map +1 -0
  55. package/dist/{chunk-6OPXDQ2E.mjs → chunk-RR5L4SRS.mjs} +33 -294
  56. package/dist/chunk-RR5L4SRS.mjs.map +1 -0
  57. package/dist/chunk-TFH5I7JJ.mjs +296 -0
  58. package/dist/chunk-TFH5I7JJ.mjs.map +1 -0
  59. package/dist/chunk-UQYS7FRM.mjs +1003 -0
  60. package/dist/chunk-UQYS7FRM.mjs.map +1 -0
  61. package/dist/chunk-YKB5DQV7.mjs +345 -0
  62. package/dist/chunk-YKB5DQV7.mjs.map +1 -0
  63. package/dist/{chunk-5YADUCA7.mjs → chunk-YRUYGA4A.mjs} +1189 -1217
  64. package/dist/chunk-YRUYGA4A.mjs.map +1 -0
  65. package/dist/chunk-Z3YYH2MB.mjs +5082 -0
  66. package/dist/chunk-Z3YYH2MB.mjs.map +1 -0
  67. package/dist/cluster-binner.d.ts +5 -0
  68. package/dist/cluster-binner.d.ts.map +1 -1
  69. package/dist/components/index.d.ts +3 -0
  70. package/dist/components/index.d.ts.map +1 -1
  71. package/dist/components/light-helpers.d.ts +4 -0
  72. package/dist/components/light-helpers.d.ts.map +1 -1
  73. package/dist/components/light-probe.d.ts +9 -0
  74. package/dist/components/light-probe.d.ts.map +1 -0
  75. package/dist/components/rect-area-light.d.ts +12 -0
  76. package/dist/components/rect-area-light.d.ts.map +1 -0
  77. package/dist/components/scene-instance.d.ts +4 -0
  78. package/dist/components/scene-instance.d.ts.map +1 -1
  79. package/dist/components/spot-light.d.ts +3 -0
  80. package/dist/components/spot-light.d.ts.map +1 -1
  81. package/dist/construct-renderer.mjs +27276 -23232
  82. package/dist/construct-renderer.mjs.map +1 -1
  83. package/dist/device/device-scope.d.ts +2 -0
  84. package/dist/device/device-scope.d.ts.map +1 -1
  85. package/dist/device/gpu-residency.d.ts +32 -5
  86. package/dist/device/gpu-residency.d.ts.map +1 -1
  87. package/dist/device/lifecycle-transaction.d.ts +2 -0
  88. package/dist/device/lifecycle-transaction.d.ts.map +1 -0
  89. package/dist/environment/frame.d.ts +5 -0
  90. package/dist/environment/frame.d.ts.map +1 -1
  91. package/dist/environment/inspection.d.ts +7 -0
  92. package/dist/environment/inspection.d.ts.map +1 -1
  93. package/dist/environment/lifecycle.d.ts +20 -2
  94. package/dist/environment/lifecycle.d.ts.map +1 -1
  95. package/dist/errors/gpu-driven.d.ts +26 -0
  96. package/dist/errors/gpu-driven.d.ts.map +1 -0
  97. package/dist/errors/index.d.ts +1 -0
  98. package/dist/errors/index.d.ts.map +1 -1
  99. package/dist/errors/recover.d.ts +67 -3
  100. package/dist/errors/recover.d.ts.map +1 -1
  101. package/dist/errors/render.d.ts +39 -1
  102. package/dist/errors/render.d.ts.map +1 -1
  103. package/dist/extract/contracts.d.ts +899 -0
  104. package/dist/extract/contracts.d.ts.map +1 -0
  105. package/dist/extract/gpu-driven.d.ts +50 -0
  106. package/dist/extract/gpu-driven.d.ts.map +1 -0
  107. package/dist/extract/visibility.d.ts.map +1 -1
  108. package/dist/features/host.d.ts +2 -0
  109. package/dist/features/host.d.ts.map +1 -1
  110. package/dist/features/prepared-gpu-work.d.ts +3 -0
  111. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  112. package/dist/features/prepared-graphics-store.d.ts +2 -0
  113. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  114. package/dist/gpu-driven/batch-topology.d.ts +34 -0
  115. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  116. package/dist/gpu-driven/prepared-draw.d.ts +63 -0
  117. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -0
  118. package/dist/gpu-driven/production-raster.d.ts +126 -2
  119. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  120. package/dist/gpu-driven/view-gpu.d.ts +64 -1
  121. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  122. package/dist/gpu-scene-schema.d.ts +14 -0
  123. package/dist/gpu-scene-schema.d.ts.map +1 -1
  124. package/dist/gpu-scene.d.ts +9 -1
  125. package/dist/gpu-scene.d.ts.map +1 -1
  126. package/dist/gpu-usage.d.ts +0 -1
  127. package/dist/gpu-usage.d.ts.map +1 -1
  128. package/dist/hdrp-buffers.d.ts +3 -3
  129. package/dist/hdrp-buffers.d.ts.map +1 -1
  130. package/dist/ibl/IblPipelineCache.d.ts +7 -6
  131. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  132. package/dist/ibl/skylight-bind-group.d.ts +7 -1
  133. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  134. package/dist/index.d.ts +13 -2
  135. package/dist/index.d.ts.map +1 -1
  136. package/dist/index.mjs +90 -5
  137. package/dist/index.mjs.map +1 -1
  138. package/dist/inspection-types.d.ts +163 -1
  139. package/dist/inspection-types.d.ts.map +1 -1
  140. package/dist/internal.d.ts +9 -0
  141. package/dist/internal.d.ts.map +1 -1
  142. package/dist/internal.mjs +23 -2
  143. package/dist/internal.mjs.map +1 -1
  144. package/dist/light-buffer-layout.d.ts +60 -148
  145. package/dist/light-buffer-layout.d.ts.map +1 -1
  146. package/dist/materials.d.ts +34 -10
  147. package/dist/materials.d.ts.map +1 -1
  148. package/dist/mesh-material-bindings.d.ts +56 -0
  149. package/dist/mesh-material-bindings.d.ts.map +1 -1
  150. package/dist/occlusion-proxy.d.ts +4 -0
  151. package/dist/occlusion-proxy.d.ts.map +1 -0
  152. package/dist/pbr-pipeline.d.ts +58 -21
  153. package/dist/pbr-pipeline.d.ts.map +1 -1
  154. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  155. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -1
  156. package/dist/pipeline/standard-lighting/light-frame.d.ts +3 -3
  157. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -1
  158. package/dist/pipeline/standard-lighting/topology.d.ts +1 -1
  159. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -1
  160. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -1
  161. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  162. package/dist/pipeline-builder.d.ts +2 -0
  163. package/dist/pipeline-builder.d.ts.map +1 -1
  164. package/dist/pipeline-spec.d.ts +1 -1
  165. package/dist/pipeline-spec.d.ts.map +1 -1
  166. package/dist/plugin.d.ts.map +1 -1
  167. package/dist/points-lines/prepare.d.ts +3 -0
  168. package/dist/points-lines/prepare.d.ts.map +1 -1
  169. package/dist/points-lines/standard-owner.d.ts +33 -0
  170. package/dist/points-lines/standard-owner.d.ts.map +1 -0
  171. package/dist/prepare/extended-lighting/ltc-resources.d.ts +31 -0
  172. package/dist/prepare/extended-lighting/ltc-resources.d.ts.map +1 -0
  173. package/dist/prepare/extended-lighting/resources.d.ts +72 -0
  174. package/dist/prepare/extended-lighting/resources.d.ts.map +1 -0
  175. package/dist/prepare/extended-lighting/spot-modifiers.d.ts +42 -0
  176. package/dist/prepare/extended-lighting/spot-modifiers.d.ts.map +1 -0
  177. package/dist/prepare/extended-lighting/state.d.ts +24 -0
  178. package/dist/prepare/extended-lighting/state.d.ts.map +1 -0
  179. package/dist/record/frame-lighting.d.ts +8 -6
  180. package/dist/record/frame-lighting.d.ts.map +1 -1
  181. package/dist/record/frame-snapshot.d.ts +55 -10
  182. package/dist/record/frame-snapshot.d.ts.map +1 -1
  183. package/dist/record/frame-targets.d.ts.map +1 -1
  184. package/dist/record/frame.d.ts +21 -5
  185. package/dist/record/frame.d.ts.map +1 -1
  186. package/dist/record/gpu-timing.d.ts +19 -7
  187. package/dist/record/gpu-timing.d.ts.map +1 -1
  188. package/dist/record/helpers.d.ts +0 -20
  189. package/dist/record/helpers.d.ts.map +1 -1
  190. package/dist/record/main-pass-geometry.d.ts +20 -2
  191. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  192. package/dist/record/main-pass-material.d.ts +10 -18
  193. package/dist/record/main-pass-material.d.ts.map +1 -1
  194. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  195. package/dist/record/main-pass.d.ts.map +1 -1
  196. package/dist/record/mesh-ssbo.d.ts +11 -3
  197. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  198. package/dist/record/recovery-pipeline.d.ts +30 -0
  199. package/dist/record/recovery-pipeline.d.ts.map +1 -0
  200. package/dist/record/render-context.d.ts +82 -9
  201. package/dist/record/render-context.d.ts.map +1 -1
  202. package/dist/record/shadow-pass.d.ts.map +1 -1
  203. package/dist/record/typed-frame-graph.d.ts +49 -2
  204. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  205. package/dist/record/view-ubo.d.ts +1 -2
  206. package/dist/record/view-ubo.d.ts.map +1 -1
  207. package/dist/recovery/render-system-candidate.d.ts +65 -0
  208. package/dist/recovery/render-system-candidate.d.ts.map +1 -0
  209. package/dist/reflection/inspection.d.ts +10 -0
  210. package/dist/reflection/inspection.d.ts.map +1 -0
  211. package/dist/reflection/projection.d.ts +49 -0
  212. package/dist/reflection/projection.d.ts.map +1 -1
  213. package/dist/reflection/record-owner.d.ts +60 -0
  214. package/dist/reflection/record-owner.d.ts.map +1 -0
  215. package/dist/render-contract.d.ts +78 -15
  216. package/dist/render-contract.d.ts.map +1 -1
  217. package/dist/render-pipeline.d.ts +7 -0
  218. package/dist/render-pipeline.d.ts.map +1 -1
  219. package/dist/render-system-extract-tail.d.ts +12 -0
  220. package/dist/render-system-extract-tail.d.ts.map +1 -0
  221. package/dist/render-system-extract.d.ts +252 -30
  222. package/dist/render-system-extract.d.ts.map +1 -1
  223. package/dist/render-system.d.ts +140 -5
  224. package/dist/render-system.d.ts.map +1 -1
  225. package/dist/scene/probe-blend-record.d.ts +22 -0
  226. package/dist/scene/probe-blend-record.d.ts.map +1 -0
  227. package/dist/scene/probe-blend.d.ts +142 -0
  228. package/dist/scene/probe-blend.d.ts.map +1 -0
  229. package/dist/scene/render-scene-types.d.ts +0 -6
  230. package/dist/scene/render-scene-types.d.ts.map +1 -1
  231. package/dist/scene/render-scene.d.ts +122 -9
  232. package/dist/scene/render-scene.d.ts.map +1 -1
  233. package/dist/scene/visibility/bounds.d.ts +20 -0
  234. package/dist/scene/visibility/bounds.d.ts.map +1 -0
  235. package/dist/scene/visibility/budget.d.ts +16 -0
  236. package/dist/scene/visibility/budget.d.ts.map +1 -0
  237. package/dist/scene/visibility/facet.d.ts +73 -0
  238. package/dist/scene/visibility/facet.d.ts.map +1 -0
  239. package/dist/scene/visibility/gpu-frame-timing.d.ts +41 -0
  240. package/dist/scene/visibility/gpu-frame-timing.d.ts.map +1 -0
  241. package/dist/scene/visibility/gpu-lod.d.ts +46 -0
  242. package/dist/scene/visibility/gpu-lod.d.ts.map +1 -0
  243. package/dist/scene/visibility/inspection.d.ts +132 -0
  244. package/dist/scene/visibility/inspection.d.ts.map +1 -0
  245. package/dist/scene/visibility/lod-selector.d.ts +25 -0
  246. package/dist/scene/visibility/lod-selector.d.ts.map +1 -0
  247. package/dist/scene/visibility/occlusion-confidence.d.ts +81 -0
  248. package/dist/scene/visibility/occlusion-confidence.d.ts.map +1 -0
  249. package/dist/scene/visibility/occlusion-pass.d.ts +57 -0
  250. package/dist/scene/visibility/occlusion-pass.d.ts.map +1 -0
  251. package/dist/scene/visibility/occlusion-query-pool.d.ts +57 -0
  252. package/dist/scene/visibility/occlusion-query-pool.d.ts.map +1 -0
  253. package/dist/scene/visibility/occlusion-runtime.d.ts +96 -0
  254. package/dist/scene/visibility/occlusion-runtime.d.ts.map +1 -0
  255. package/dist/scene/visibility/types.d.ts +34 -0
  256. package/dist/scene/visibility/types.d.ts.map +1 -0
  257. package/dist/ssr/admission.d.ts +122 -0
  258. package/dist/ssr/admission.d.ts.map +1 -0
  259. package/dist/ssr/composition.d.ts +32 -0
  260. package/dist/ssr/composition.d.ts.map +1 -0
  261. package/dist/ssr/errors.d.ts +39 -0
  262. package/dist/ssr/errors.d.ts.map +1 -0
  263. package/dist/ssr/identity.d.ts +13 -0
  264. package/dist/ssr/identity.d.ts.map +1 -0
  265. package/dist/systems/skin-palette-allocator.d.ts +1 -1
  266. package/dist/temporal/frame-coordinator.d.ts +13 -0
  267. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  268. package/dist/temporal/frame.d.ts +2 -0
  269. package/dist/temporal/frame.d.ts.map +1 -1
  270. package/dist/temporal/index.mjs +6 -5
  271. package/dist/temporal/taa-history-store.d.ts +15 -1
  272. package/dist/temporal/taa-history-store.d.ts.map +1 -1
  273. package/dist/temporal/transaction.d.ts +5 -0
  274. package/dist/temporal/transaction.d.ts.map +1 -1
  275. package/dist/temporal/view.d.ts +7 -0
  276. package/dist/temporal/view.d.ts.map +1 -1
  277. package/dist/tilemap-chunk-extract-system.d.ts +1 -1
  278. package/dist/typed-render-graph-primitives.d.ts +4 -0
  279. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  280. package/dist/volume/passes.d.ts +8 -4
  281. package/dist/volume/passes.d.ts.map +1 -1
  282. package/package.json +20 -20
  283. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +5 -0
  284. package/src/__tests__/detached-inspection.integration.test.ts +6 -0
  285. package/src/__tests__/device-feature-admission.unit.test.ts +19 -1
  286. package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
  287. package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
  288. package/src/__tests__/direct-light-snapshot.unit.test.ts +59 -3
  289. package/src/__tests__/environment-generation.integration.test.ts +39 -0
  290. package/src/__tests__/evidence-identity.ts +13 -0
  291. package/src/__tests__/evidence-identity.unit.test.ts +24 -0
  292. package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
  293. package/src/__tests__/extended-lighting-recovery.browser.test.ts +14 -0
  294. package/src/__tests__/extended-lighting-recovery.dawn.test.ts +14 -0
  295. package/src/__tests__/extended-lighting-recovery.integration.test.ts +81 -0
  296. package/src/__tests__/extended-lighting-resources.unit.test.ts +51 -0
  297. package/src/__tests__/extended-lighting-zero-feature.integration.test.ts +52 -0
  298. package/src/__tests__/factory-contract.integration.test.ts +154 -6
  299. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
  300. package/src/__tests__/frame-observation.contract.test.ts +15 -0
  301. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +146 -0
  302. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +1 -0
  303. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +1 -0
  304. package/src/__tests__/gpu-driven-production.integration.test.ts +572 -9
  305. package/src/__tests__/gpu-driven-scaling.unit.test.ts +66 -2
  306. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +26 -1
  307. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +89 -2
  308. package/src/__tests__/gpu-driven-view.browser.test.ts +3 -3
  309. package/src/__tests__/gpu-driven-view.dawn.test.ts +3 -3
  310. package/src/__tests__/gpu-driven-view.unit.test.ts +50 -0
  311. package/src/__tests__/gpu-lod-evidence.ts +188 -0
  312. package/src/__tests__/gpu-lod-fallback.integration.test.ts +10 -0
  313. package/src/__tests__/gpu-lod-indirect.integration.test.ts +40 -0
  314. package/src/__tests__/gpu-lod-parity.dawn.test.ts +29 -0
  315. package/src/__tests__/gpu-lod-readiness.integration.test.ts +52 -0
  316. package/src/__tests__/gpu-lod-schema.unit.test.ts +66 -0
  317. package/src/__tests__/gpu-lod-selector.unit.test.ts +28 -0
  318. package/src/__tests__/gpu-lod-silhouette.browser.test.ts +13 -0
  319. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +66 -1
  320. package/src/__tests__/gpu-scene.dawn.test.ts +8 -1
  321. package/src/__tests__/gpu-scene.unit.test.ts +49 -3
  322. package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
  323. package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
  324. package/src/__tests__/light-buffer-layout.unit.test.ts +113 -3
  325. package/src/__tests__/light-inspection.unit.test.ts +55 -0
  326. package/src/__tests__/light-resource-error.unit.test.ts +43 -0
  327. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +109 -0
  328. package/src/__tests__/lod-bounds.unit.test.ts +42 -0
  329. package/src/__tests__/lod-cpu-lane.integration.test.ts +55 -0
  330. package/src/__tests__/lod-cpu-no-change.perf.test.ts +19 -0
  331. package/src/__tests__/lod-gpu-frame-timing.unit.test.ts +49 -0
  332. package/src/__tests__/lod-multi-view.integration.test.ts +63 -0
  333. package/src/__tests__/lod-occlusion-device-loss.integration.test.ts +27 -0
  334. package/src/__tests__/lod-occlusion-final-decision.integration.test.ts +47 -0
  335. package/src/__tests__/lod-occlusion-inspection.integration.test.ts +217 -0
  336. package/src/__tests__/lod-occlusion-inspection.schema.test-d.ts +14 -0
  337. package/src/__tests__/lod-occlusion-inspection.unit.test.ts +72 -0
  338. package/src/__tests__/lod-occlusion-visual-evidence.ts +91 -0
  339. package/src/__tests__/lod-occlusion.browser.test.ts +15 -0
  340. package/src/__tests__/lod-occlusion.dawn.test.ts +20 -0
  341. package/src/__tests__/lod-selector.profile.test.ts +35 -0
  342. package/src/__tests__/lod-selector.unit.test.ts +68 -0
  343. package/src/__tests__/lod-visibility-identity.unit.test.ts +198 -0
  344. package/src/__tests__/lod-visibility-lifecycle.integration.test.ts +132 -0
  345. package/src/__tests__/ltc-resources.unit.test.ts +32 -0
  346. package/src/__tests__/material-contract-inventory.unit.test.ts +41 -9
  347. package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
  348. package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
  349. package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
  350. package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
  351. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
  352. package/src/__tests__/occlusion-confidence.unit.test.ts +168 -0
  353. package/src/__tests__/occlusion-invalidation.integration.test.ts +37 -0
  354. package/src/__tests__/occlusion-pass-routing.unit.test.ts +16 -0
  355. package/src/__tests__/occlusion-pass.dawn.test.ts +20 -0
  356. package/src/__tests__/occlusion-pass.integration.test.ts +43 -0
  357. package/src/__tests__/occlusion-pass.rhi-null.test.ts +22 -0
  358. package/src/__tests__/occlusion-query-pool.rhi-null.test.ts +21 -0
  359. package/src/__tests__/occlusion-query-pool.unit.test.ts +95 -0
  360. package/src/__tests__/occlusion-query-ticket.integration.test.ts +47 -0
  361. package/src/__tests__/occlusion-query.dawn.test.ts +16 -0
  362. package/src/__tests__/occlusion-query.evidence.ts +178 -0
  363. package/src/__tests__/occlusion-query.rhi-null.test.ts +31 -0
  364. package/src/__tests__/occlusion-runtime.unit.test.ts +120 -0
  365. package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
  366. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +196 -0
  367. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
  368. package/src/__tests__/probe-bindings.unit.test.ts +200 -0
  369. package/src/__tests__/probe-blend-numeric.unit.test.ts +67 -0
  370. package/src/__tests__/probe-blend-oracle.unit.test.ts +136 -0
  371. package/src/__tests__/probe-capacity.integration.test.ts +97 -0
  372. package/src/__tests__/probe-recovery.integration.test.ts +104 -0
  373. package/src/__tests__/probe-scene-dirty.integration.test.ts +211 -0
  374. package/src/__tests__/profiler-phase-catalog.test.ts +4 -1
  375. package/src/__tests__/profiler-phase-token.test.ts +1 -0
  376. package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
  377. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
  378. package/src/__tests__/rect-area-boundary.unit.test.ts +24 -0
  379. package/src/__tests__/reflection-fallback-graph.unit.test.ts +67 -0
  380. package/src/__tests__/reflection-fallback-inspection.unit.test.ts +77 -0
  381. package/src/__tests__/reflection-fallback-lifecycle.integration.test.ts +21 -0
  382. package/src/__tests__/reflection-fallback-projection.unit.test.ts +90 -0
  383. package/src/__tests__/reflection-fallback-source.unit.test.ts +32 -0
  384. package/src/__tests__/reflection-probe-owner-placement.unit.test.ts +39 -0
  385. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +120 -0
  386. package/src/__tests__/render-error-code-owner.test-d.ts +1 -0
  387. package/src/__tests__/render-feature-docs.unit.test.ts +12 -7
  388. package/src/__tests__/render-graph-fallback.integration.test.ts +24 -0
  389. package/src/__tests__/render-perf/extended-lighting.bench.test.ts +66 -0
  390. package/src/__tests__/render-scene-projection.unit.test.ts +447 -18
  391. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +160 -17
  392. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
  393. package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
  394. package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
  395. package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
  396. package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
  397. package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
  398. package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
  399. package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
  400. package/src/__tests__/shader-manifest-fixture.ts +13 -16
  401. package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
  402. package/src/__tests__/skin-motion-regression.unit.test.ts +22 -0
  403. package/src/__tests__/skinned-shadow-caster.test.ts +2 -2
  404. package/src/__tests__/spot-modifier-projection.unit.test.ts +70 -0
  405. package/src/__tests__/ssr-admission-matrix.integration.test.ts +206 -0
  406. package/src/__tests__/ssr-composition.unit.test.ts +46 -0
  407. package/src/__tests__/ssr-fallback-zero-work.bench.test.ts +94 -0
  408. package/src/__tests__/ssr-recovery-errors.unit.test.ts +58 -0
  409. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +67 -0
  410. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +7 -0
  411. package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
  412. package/src/__tests__/standard-lighting-inspection.integration.test.ts +1 -1
  413. package/src/__tests__/standard-lighting-write.unit.test.ts +66 -2
  414. package/src/__tests__/temporal-fallback-generation.integration.test.ts +16 -0
  415. package/src/__tests__/temporal-view.unit.test.ts +3 -3
  416. package/src/__tests__/transmission-prewarm.unit.test.ts +111 -32
  417. package/src/__tests__/visibility-budget.integration.test.ts +140 -0
  418. package/src/__tests__/visibility-budget.unit.test.ts +70 -0
  419. package/src/__tests__/visibility-extract.integration.test.ts +11 -6
  420. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +6 -3
  421. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +31 -28
  422. package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
  423. package/src/assembly/backend-contract.ts +13 -0
  424. package/src/assembly/bundler-contract.ts +9 -0
  425. package/src/assembly/device-feature-admission.ts +26 -9
  426. package/src/assembly/extended-lighting-fallback.ts +216 -0
  427. package/src/assembly/factory.ts +81 -9634
  428. package/src/assembly/host-contract.ts +10 -0
  429. package/src/assembly/material/assembly.ts +18 -2
  430. package/src/assembly/material/standard-layer-projection.ts +16 -0
  431. package/src/assembly/material-shader-policy.ts +854 -0
  432. package/src/assembly/material-shader-prewarm.ts +88 -0
  433. package/src/assembly/mesh-ssbo-grow.ts +270 -0
  434. package/src/assembly/recovery/device-loss-fanout.ts +89 -0
  435. package/src/assembly/recovery/generation.ts +141 -0
  436. package/src/assembly/recovery/recovery-attempt.ts +800 -0
  437. package/src/assembly/recovery/renderer-recover.ts +906 -0
  438. package/src/assembly/renderer-facade.ts +342 -0
  439. package/src/assembly/renderer-frame-transaction.ts +45 -0
  440. package/src/assembly/renderer-helpers.ts +99 -0
  441. package/src/assembly/renderer-lifecycle.ts +159 -0
  442. package/src/assembly/shader-prewarm-policy.ts +157 -0
  443. package/src/assembly/webgpu-ready-contract.ts +14 -0
  444. package/src/assembly/webgpu-ready.ts +4079 -0
  445. package/src/assembly/webgpu-renderer.ts +4095 -0
  446. package/src/cluster-binner.ts +31 -0
  447. package/src/components/__tests__/lighting-authoring.unit.test.ts +39 -0
  448. package/src/components/__tests__/rect-area-light.unit.test.ts +33 -0
  449. package/src/components/camera.ts +1 -1
  450. package/src/components/index.ts +3 -0
  451. package/src/components/light-helpers.ts +52 -1
  452. package/src/components/light-probe.ts +13 -0
  453. package/src/components/rect-area-light.ts +15 -0
  454. package/src/components/scene-instance.ts +6 -0
  455. package/src/components/spot-light.ts +3 -0
  456. package/src/debug-draw-glue.ts +1 -1
  457. package/src/device/__tests__/extended-lighting-recovery.unit.test.ts +24 -0
  458. package/src/device/device-scope.ts +35 -6
  459. package/src/device/gpu-residency.ts +334 -35
  460. package/src/device/lifecycle-transaction.ts +12 -0
  461. package/src/environment/frame.ts +6 -0
  462. package/src/environment/inspection.ts +10 -0
  463. package/src/environment/lifecycle.ts +122 -9
  464. package/src/errors/gpu-driven.ts +92 -0
  465. package/src/errors/index.ts +1 -0
  466. package/src/errors/recover.ts +108 -5
  467. package/src/errors/render.ts +72 -1
  468. package/src/extract/camera.ts +3 -3
  469. package/src/extract/contracts.ts +974 -0
  470. package/src/extract/gpu-driven.ts +108 -0
  471. package/src/extract/visibility.ts +0 -23
  472. package/src/features/host.ts +16 -0
  473. package/src/features/prepared-gpu-work.ts +20 -2
  474. package/src/features/prepared-graphics-store.ts +17 -0
  475. package/src/gpu-driven/batch-topology.ts +206 -28
  476. package/src/gpu-driven/prepared-draw.ts +208 -0
  477. package/src/gpu-driven/production-raster.ts +1046 -39
  478. package/src/gpu-driven/view-gpu.ts +411 -29
  479. package/src/gpu-scene-schema.ts +13 -0
  480. package/src/gpu-scene.ts +137 -67
  481. package/src/gpu-usage.ts +0 -1
  482. package/src/hdrp-buffers.ts +6 -6
  483. package/src/ibl/IblPipelineCache.ts +134 -50
  484. package/src/ibl/skylight-bind-group.ts +26 -0
  485. package/src/index.ts +83 -1
  486. package/src/inspection-types.ts +220 -1
  487. package/src/internal.ts +38 -0
  488. package/src/light-buffer-layout.ts +157 -324
  489. package/src/materials.ts +278 -47
  490. package/src/mesh-material-bindings.ts +59 -0
  491. package/src/occlusion-proxy.ts +64 -0
  492. package/src/pbr-pipeline.ts +290 -60
  493. package/src/pipeline/standard-forward-lane.ts +47 -0
  494. package/src/pipeline/standard-lighting/graph.ts +2 -1
  495. package/src/pipeline/standard-lighting/light-frame.ts +3 -2
  496. package/src/pipeline/standard-lighting/topology.ts +1 -1
  497. package/src/pipeline/standard-lighting/transport.ts +2 -1
  498. package/src/pipeline/standard-pipeline.ts +2 -0
  499. package/src/pipeline-builder.ts +5 -2
  500. package/src/pipeline-spec.ts +12 -1
  501. package/src/plugin.ts +4 -0
  502. package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
  503. package/src/points-lines/prepare.ts +9 -0
  504. package/src/points-lines/standard-owner.ts +403 -0
  505. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +56 -0
  506. package/src/prepare/extended-lighting/ltc-resources.ts +136 -0
  507. package/src/prepare/extended-lighting/resources.ts +389 -0
  508. package/src/prepare/extended-lighting/spot-modifiers.ts +194 -0
  509. package/src/prepare/extended-lighting/state.ts +92 -0
  510. package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
  511. package/src/record/__tests__/gpu-timing.unit.test.ts +177 -0
  512. package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
  513. package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
  514. package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
  515. package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
  516. package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
  517. package/src/record/__tests__/temporal-payload.unit.test.ts +29 -28
  518. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +21 -2
  519. package/src/record/frame-lighting.ts +252 -53
  520. package/src/record/frame-snapshot.ts +72 -12
  521. package/src/record/frame-targets.ts +11 -1
  522. package/src/record/frame.ts +416 -95
  523. package/src/record/gpu-timing.ts +108 -44
  524. package/src/record/helpers.ts +1 -59
  525. package/src/record/main-pass-geometry.ts +434 -44
  526. package/src/record/main-pass-material.ts +324 -210
  527. package/src/record/main-pass-sprite-draws.ts +58 -92
  528. package/src/record/main-pass.ts +94 -6
  529. package/src/record/mesh-ssbo.ts +79 -46
  530. package/src/record/recovery-pipeline.ts +695 -0
  531. package/src/record/render-context.ts +99 -7
  532. package/src/record/shadow-pass.ts +66 -40
  533. package/src/record/typed-frame-graph.ts +270 -84
  534. package/src/record/view-ubo.ts +17 -27
  535. package/src/recovery/render-system-candidate.ts +839 -0
  536. package/src/reflection/inspection.ts +67 -0
  537. package/src/reflection/projection.ts +196 -1
  538. package/src/reflection/record-owner.ts +1283 -0
  539. package/src/render-contract.ts +82 -12
  540. package/src/render-pipeline.ts +5 -0
  541. package/src/render-system-extract-tail.ts +2983 -0
  542. package/src/render-system-extract.ts +290 -2784
  543. package/src/render-system.ts +1597 -1192
  544. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +36 -0
  545. package/src/scene/probe-blend-record.ts +54 -0
  546. package/src/scene/probe-blend.ts +813 -0
  547. package/src/scene/render-scene-types.ts +0 -7
  548. package/src/scene/render-scene.ts +972 -184
  549. package/src/scene/visibility/bounds.ts +57 -0
  550. package/src/scene/visibility/budget.ts +40 -0
  551. package/src/scene/visibility/facet.ts +400 -0
  552. package/src/scene/visibility/gpu-frame-timing.ts +101 -0
  553. package/src/scene/visibility/gpu-lod.ts +119 -0
  554. package/src/scene/visibility/inspection.ts +313 -0
  555. package/src/scene/visibility/lod-selector.ts +84 -0
  556. package/src/scene/visibility/occlusion-confidence.ts +478 -0
  557. package/src/scene/visibility/occlusion-pass.ts +165 -0
  558. package/src/scene/visibility/occlusion-query-pool.ts +178 -0
  559. package/src/scene/visibility/occlusion-runtime.ts +672 -0
  560. package/src/scene/visibility/types.ts +50 -0
  561. package/src/ssr/admission.ts +451 -0
  562. package/src/ssr/composition.ts +73 -0
  563. package/src/ssr/errors.ts +76 -0
  564. package/src/ssr/identity.ts +12 -0
  565. package/src/systems/skin-palette-allocator.ts +1 -1
  566. package/src/temporal/frame-coordinator.ts +30 -1
  567. package/src/temporal/frame.ts +14 -1
  568. package/src/temporal/taa-history-store.ts +46 -2
  569. package/src/temporal/temporal-view.ts +1 -1
  570. package/src/temporal/transaction.ts +9 -0
  571. package/src/temporal/view.ts +11 -0
  572. package/src/tilemap-chunk-extract-system.ts +5 -5
  573. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
  574. package/src/typed-render-graph-primitives.ts +48 -0
  575. package/src/volume/passes.ts +46 -49
  576. package/dist/.tsbuildinfo +0 -1
  577. package/dist/chunk-5YADUCA7.mjs.map +0 -1
  578. package/dist/chunk-6OPXDQ2E.mjs.map +0 -1
  579. package/dist/chunk-BXBQQKFQ.mjs +0 -67
  580. package/dist/chunk-BXBQQKFQ.mjs.map +0 -1
  581. package/dist/chunk-FS2KNUQH.mjs.map +0 -1
  582. package/dist/chunk-PCHBPYML.mjs.map +0 -1
  583. package/dist/chunk-VFPLTKCU.mjs +0 -192
  584. 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,166 @@ 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';
23
+ import type { DeviceScope, LifecycleResourceSpec } from '../device/device-scope';
20
24
  import type { MeshGpuHandles } from '../device/gpu-residency';
21
25
  import { GPU_SCENE_WGSL } from '../gpu-scene-schema';
22
26
  import {
23
27
  GPU_BUFFER_USAGE_COPY_DST,
24
28
  GPU_BUFFER_USAGE_INDEX,
29
+ GPU_BUFFER_USAGE_STORAGE,
30
+ GPU_BUFFER_USAGE_UNIFORM,
25
31
  GPU_BUFFER_USAGE_VERTEX,
26
32
  } from '../gpu-usage';
33
+ import { assembleMaterialWithSkylightEntries } from '../ibl/skylight-bind-group';
27
34
  import type { GpuDrivenProductionInspection } from '../inspection-types';
28
35
  import type { PipelineBuilderShaderModuleFactory } from '../pipeline-builder';
29
36
  import { worldEntityKey } from '../record/frame-snapshot';
30
37
  import { geometryRenderStateForTopology } from '../record/main-pass-material';
38
+ import { MESH_SSBO_BYTES } from '../record/mesh-ssbo';
39
+ import type { PipelineState } from '../record/render-context';
31
40
  import type { CameraSnapshot } from '../render-contract';
32
41
  import type { RenderPipelineFrame, RenderPipelineGpuDrivenProjection } from '../render-pipeline';
33
42
  import type { RenderableSnapshot } from '../render-system-extract';
34
43
  import type { PersistentGpuDrivenState } from '../scene/render-scene';
35
- import type { GpuDrivenBatch, SubmissionPlan } from './batch-topology';
36
- import { GpuDrivenView } from './view-gpu';
44
+ import { selectLod } from '../scene/visibility/lod-selector';
45
+ import type { OcclusionFrameProjection } from '../scene/visibility/occlusion-runtime';
46
+ import {
47
+ type GpuDrivenBatch,
48
+ type GpuDrivenCandidate,
49
+ projectedHeightForCandidate,
50
+ type SubmissionPlan,
51
+ } from './batch-topology';
52
+ import { type GpuDrivenLodSelectionInspection, GpuDrivenView, selectGpuLodLane } from './view-gpu';
37
53
 
38
54
  const VERTEX_STAGE = 0x1;
39
55
  const FRAGMENT_STAGE = 0x2;
40
56
  const EMPTY_RESOURCE_CLASS = JSON.stringify({ textures: [], samplers: [], video: [] });
41
57
 
58
+ export interface StandardPbrSceneCandidate {
59
+ readonly primitiveIndex: number;
60
+ readonly world: Float32Array;
61
+ }
62
+
63
+ export interface StandardPbrSceneProjection {
64
+ readonly entryPoint: string;
65
+ readonly layoutIdentity: string;
66
+ readonly receiptGeneration: number;
67
+ /** Packed Mesh rows: mat4 + padded mat3 + previous mat4 + temporal vec4. */
68
+ readonly rows: Float32Array;
69
+ }
70
+
71
+ /**
72
+ * Build the producer-owned Standard PBR scene-index projection. The receipt is
73
+ * the only source of the vertex entry and layout identity; callers cannot
74
+ * silently fall back to the legacy GPU-driven shader ABI.
75
+ */
76
+ export function projectStandardPbrScene(input: {
77
+ readonly artifact: MaterialShaderArtifact;
78
+ readonly candidates: readonly StandardPbrSceneCandidate[];
79
+ }):
80
+ | { readonly ok: true; readonly value: StandardPbrSceneProjection }
81
+ | {
82
+ readonly ok: false;
83
+ readonly error: { readonly code: 'missing-material-receipt' | 'invalid-material-receipt' };
84
+ } {
85
+ const receipt = input.artifact.receipt;
86
+ if (receipt === undefined) return { ok: false, error: { code: 'missing-material-receipt' } };
87
+ if (receipt.reflection.layoutIdentity !== input.artifact.layoutIdentity) {
88
+ return { ok: false, error: { code: 'invalid-material-receipt' } };
89
+ }
90
+ const rows = new Float32Array(input.candidates.length * 48);
91
+ const normal = mat3.create();
92
+ for (let index = 0; index < input.candidates.length; index += 1) {
93
+ const candidate = input.candidates[index];
94
+ if (candidate === undefined) continue;
95
+ const base = index * 48;
96
+ for (let value = 0; value < 16; value += 1) {
97
+ rows[base + value] = candidate.world[value] ?? 0;
98
+ rows[base + 28 + value] = candidate.world[value] ?? 0;
99
+ }
100
+ const normalMatrix = mat3.normalMatrix(normal, candidate.world);
101
+ for (let column = 0; column < 3; column += 1) {
102
+ const source = column * 3;
103
+ const target = base + 16 + column * 4;
104
+ rows[target] = normalMatrix[source] ?? 0;
105
+ rows[target + 1] = normalMatrix[source + 1] ?? 0;
106
+ rows[target + 2] = normalMatrix[source + 2] ?? 0;
107
+ }
108
+ }
109
+ return {
110
+ ok: true,
111
+ value: {
112
+ entryPoint: receipt.sceneIndexEntry,
113
+ layoutIdentity: input.artifact.layoutIdentity,
114
+ receiptGeneration: receipt.generation,
115
+ rows,
116
+ },
117
+ };
118
+ }
119
+
120
+ export interface StandardPbrFrameResourceBinding {
121
+ readonly entryPoint: string;
122
+ readonly layoutIdentity: string;
123
+ readonly receiptGeneration: number;
124
+ readonly bindGroups: readonly [BindGroup, BindGroup, BindGroup, BindGroup];
125
+ readonly resourceSlots: NonNullable<MaterialShaderArtifact['receipt']>['resourceSlots'];
126
+ }
127
+
128
+ /**
129
+ * Adapt the frame's already-created Standard PBR groups to the GPU-driven
130
+ * scene-index draw. This deliberately accepts group handles rather than
131
+ * rebuilding layouts or shader artifacts; the receipt remains the binding
132
+ * contract and the regular frame producers remain the resource owners.
133
+ */
134
+ export function adaptStandardPbrFrameResources(input: {
135
+ readonly artifact: MaterialShaderArtifact;
136
+ readonly view: BindGroup;
137
+ readonly material: BindGroup;
138
+ readonly mesh: BindGroup;
139
+ readonly instances: BindGroup;
140
+ }):
141
+ | { readonly ok: true; readonly value: StandardPbrFrameResourceBinding }
142
+ | {
143
+ readonly ok: false;
144
+ readonly error: { readonly code: 'missing-material-receipt' | 'invalid-material-receipt' };
145
+ } {
146
+ const receipt = input.artifact.receipt;
147
+ if (receipt === undefined) return { ok: false, error: { code: 'missing-material-receipt' } };
148
+ const reflected = receipt.reflection;
149
+ const sameSlots =
150
+ reflected.resourceSlots.length === receipt.resourceSlots.length &&
151
+ reflected.resourceSlots.every((slot, index) => {
152
+ const expected = receipt.resourceSlots[index];
153
+ return (
154
+ expected !== undefined &&
155
+ slot.name === expected.name &&
156
+ slot.parameter === expected.parameter &&
157
+ slot.kind === expected.kind &&
158
+ slot.group === expected.group &&
159
+ slot.binding === expected.binding
160
+ );
161
+ });
162
+ if (
163
+ reflected.layoutIdentity !== input.artifact.layoutIdentity ||
164
+ !sameSlots ||
165
+ receipt.resourceSlots.some((slot) => slot.group !== 1)
166
+ ) {
167
+ return { ok: false, error: { code: 'invalid-material-receipt' } };
168
+ }
169
+ return {
170
+ ok: true,
171
+ value: {
172
+ entryPoint: receipt.sceneIndexEntry,
173
+ layoutIdentity: input.artifact.layoutIdentity,
174
+ receiptGeneration: receipt.generation,
175
+ bindGroups: [input.view, input.material, input.mesh, input.instances],
176
+ resourceSlots: receipt.resourceSlots,
177
+ },
178
+ };
179
+ }
180
+
42
181
  export const GPU_DRIVEN_RIGID_UNLIT_WGSL = /* wgsl */ `
43
182
  ${GPU_SCENE_WGSL}
44
183
 
@@ -87,6 +226,11 @@ interface PreparedBatch {
87
226
  readonly batch: GpuDrivenBatch;
88
227
  readonly mesh: MeshGpuHandles;
89
228
  readonly renderState: MaterialRenderState | undefined;
229
+ /** Stable World attachment key used to join same-submit GPU counters. */
230
+ readonly worldKey: number;
231
+ /** Stable representative slot identity for this homogeneous batch. */
232
+ readonly primitiveSlot: number;
233
+ readonly slotGeneration: number;
90
234
  }
91
235
 
92
236
  interface ProjectedBatch extends PreparedBatch {
@@ -118,8 +262,8 @@ function activeFrustum(camera: CameraSnapshot): Float32Array {
118
262
  projection,
119
263
  camera.orthoLeft,
120
264
  camera.orthoRight,
121
- camera.orthoBottom,
122
265
  camera.orthoTop,
266
+ camera.orthoBottom,
123
267
  camera.near,
124
268
  camera.far,
125
269
  );
@@ -133,55 +277,172 @@ function activeFrustum(camera: CameraSnapshot): Float32Array {
133
277
  );
134
278
  }
135
279
 
280
+ /**
281
+ * LOD projected height is a view fact, not a scene-topology fact. Keep the
282
+ * filtered plan cache scene-stable while still rebuilding its candidate
283
+ * heights whenever a camera input used by the selector changes.
284
+ */
285
+ function lodProjectionFingerprint(camera: CameraSnapshot): string {
286
+ return JSON.stringify({
287
+ position: Array.from(camera.position),
288
+ world: Array.from(camera.world),
289
+ projection: camera.projection,
290
+ fov: camera.fov,
291
+ aspect: camera.aspect,
292
+ orthoHeight: camera.orthoTop - camera.orthoBottom,
293
+ });
294
+ }
295
+
136
296
  function productionEligible(snapshot: RenderableSnapshot, batch: GpuDrivenBatch): boolean {
137
297
  const material = snapshot.materials[batch.key.materialSlot] ?? snapshot.material;
138
298
  const shader = material.materialShaderId ?? 'forgeax::default-unlit';
139
299
  return (
140
300
  shader === 'forgeax::default-unlit' &&
301
+ // The production rigid shader owns triangle-list raster only. Lines and
302
+ // points stay with their CPU/specialized topology owners.
303
+ batch.key.topology === 'triangle-list' &&
141
304
  batch.key.materialResourceClass === EMPTY_RESOURCE_CLASS &&
142
305
  material.baseColorTexture === undefined &&
143
306
  material.transparent !== true
144
307
  );
145
308
  }
146
309
 
310
+ /**
311
+ * Prepared Standard PBR draws have an explicit receipt identity on their
312
+ * batch key. Unprepared batches stay on the rigid production shader; the
313
+ * presence of a frame-wide PBR artifact is not permission to reinterpret an
314
+ * unlit batch with the PBR bind-group layout.
315
+ */
316
+ function isStandardPbrBatch(batch: GpuDrivenBatch): boolean {
317
+ return batch.key.preparedIdentity !== undefined;
318
+ }
319
+
147
320
  function meshSupportsBatch(mesh: MeshGpuHandles, batch: GpuDrivenBatch): boolean {
148
- if (mesh.layoutProjection.arrayStride !== 48) return false;
321
+ if (
322
+ batch.candidates.some((candidate) => {
323
+ const lodCount = candidate.lodCoverages?.length ?? 0;
324
+ return (
325
+ lodCount > 1 &&
326
+ (mesh.lodRanges === undefined ||
327
+ mesh.lodRanges.length < lodCount ||
328
+ mesh.lodRanges.some((ranges) => ranges[candidate.drawItemIndex] === undefined))
329
+ );
330
+ })
331
+ ) {
332
+ return false;
333
+ }
149
334
  return batch.key.drawKind === 'indexed'
150
335
  ? mesh.indexed && mesh.indexBuffer !== null
151
336
  : !mesh.indexed;
152
337
  }
153
338
 
339
+ function selectedLodLevel(candidate: GpuDrivenCandidate): number {
340
+ const coverages = candidate.lodCoverages;
341
+ if (coverages === undefined || coverages.length <= 1) return 0;
342
+ return selectLod({
343
+ levels: coverages.slice(1).map((screenCoverage) => ({ screenCoverage })),
344
+ projectedHeight: candidate.projectedHeight ?? Number.NaN,
345
+ previousLevel: 0,
346
+ hysteresis: candidate.lodHysteresis ?? 0.08,
347
+ ready: coverages.map(() => true),
348
+ historyValid: false,
349
+ }).level;
350
+ }
351
+
352
+ function selectedLodRange(
353
+ batch: GpuDrivenBatch,
354
+ candidate: GpuDrivenCandidate,
355
+ ): { readonly first: number; readonly count: number; readonly baseVertex: number } {
356
+ const level = selectedLodLevel(candidate);
357
+ return (
358
+ (level === 0 ? undefined : candidate.lodRanges?.[level - 1]) ?? {
359
+ first: batch.key.first,
360
+ count: batch.key.count,
361
+ baseVertex: batch.key.baseVertex,
362
+ }
363
+ );
364
+ }
365
+
366
+ /**
367
+ * One raster indirect command is emitted for each selector batch that has at
368
+ * least one admitted candidate. Selector-only batches remain in the compute
369
+ * and readback plan, but they must not inflate the raster command count.
370
+ */
371
+ function admittedRasterBatchCount(plan: SubmissionPlan): number {
372
+ return plan.batches.reduce((count, batch) => count + (batch.visibleCapacity > 0 ? 1 : 0), 0);
373
+ }
374
+
154
375
  function filteredPlan(
155
376
  source: SubmissionPlan,
156
377
  slots: PersistentGpuDrivenState['slots'],
157
378
  meshes: ReadonlyMap<number, MeshGpuHandles>,
379
+ camera: CameraSnapshot,
380
+ activeEntityKeys?: ReadonlySet<number>,
381
+ worldKeys?: readonly number[],
158
382
  ): {
159
383
  readonly plan: SubmissionPlan;
160
384
  readonly batches: readonly PreparedBatch[];
161
385
  readonly entityKeys: ReadonlySet<number>;
386
+ /** No source primitive can enter this lane without a topology/material change. */
387
+ readonly topologyInvariantEmpty: boolean;
162
388
  } {
163
389
  const slotByPrimitive = new Map(slots.map((slot) => [slot.slot, slot] as const));
390
+ const participatingByPrimitive = new Map<number, Set<GpuDrivenBatch>>();
391
+ for (const batch of source.batches) {
392
+ for (const candidate of batch.candidates) {
393
+ const participating = participatingByPrimitive.get(candidate.primitiveIndex);
394
+ if (participating === undefined) {
395
+ participatingByPrimitive.set(candidate.primitiveIndex, new Set([batch]));
396
+ } else {
397
+ participating.add(batch);
398
+ }
399
+ }
400
+ }
164
401
  const entityKeys = new Set<number>();
402
+ const worldKeyFor = (slot: PersistentGpuDrivenState['slots'][number]): number =>
403
+ worldKeys?.[slot.snapshot.worldId] ?? slot.snapshot.worldId;
404
+ const activeKey = (slot: PersistentGpuDrivenState['slots'][number]): number =>
405
+ worldEntityKey(worldKeyFor(slot), slot.snapshot.entityKey);
165
406
  const eligiblePrimitives = new Set<number>();
407
+ let topologyEligiblePrimitiveCount = 0;
166
408
  for (const slot of slots) {
167
409
  const mesh = meshes.get(slot.snapshot.assetHandle);
168
- const participating = source.batches.filter((batch) =>
169
- batch.candidates.some((candidate) => candidate.primitiveIndex === slot.slot),
170
- );
410
+ const participating = participatingByPrimitive.get(slot.slot);
411
+ const participatingBatches = participating === undefined ? [] : [...participating];
412
+ const topologyEligible =
413
+ participatingBatches.length > 0 &&
414
+ participatingBatches.length === (slot.snapshot.gpuDrivenDraws?.length ?? 0) &&
415
+ participatingBatches.every((batch) => productionEligible(slot.snapshot, batch));
416
+ if (topologyEligible) topologyEligiblePrimitiveCount += 1;
171
417
  if (
172
418
  mesh !== undefined &&
173
- participating.length === (slot.snapshot.gpuDrivenDraws?.length ?? 0) &&
174
- participating.every(
175
- (batch) => productionEligible(slot.snapshot, batch) && meshSupportsBatch(mesh, batch),
176
- )
419
+ topologyEligible &&
420
+ participatingBatches.every((batch) => meshSupportsBatch(mesh, batch))
177
421
  ) {
178
422
  eligiblePrimitives.add(slot.slot);
179
- entityKeys.add(worldEntityKey(slot.snapshot.worldId, slot.snapshot.entityKey));
423
+ // Record validation still keys extracted renderables by their current
424
+ // stable world/entity key. Suppressed candidates remain in the GPU
425
+ // selector, but only admitted entities are owned by the compacted draw.
426
+ if (activeEntityKeys === undefined || activeEntityKeys.has(activeKey(slot))) {
427
+ entityKeys.add(activeKey(slot));
428
+ }
180
429
  }
181
430
  }
182
- const batches: PreparedBatch[] = [];
183
- let visibleBase = 0;
184
- const filteredBatches: GpuDrivenBatch[] = [];
431
+ // BatchTopology deliberately keeps LOD candidates individually addressable
432
+ // so topology updates never alias a candidate's selector state. Once the
433
+ // camera has projected the candidates, the renderer can safely coalesce
434
+ // candidates that have identical immutable geometry/LOD facts and the same
435
+ // selected range. The GPU selector uses the same no-history inputs below,
436
+ // so one indirect range remains correct for every candidate in a group.
437
+ const groups = new Map<
438
+ string,
439
+ {
440
+ readonly source: GpuDrivenBatch;
441
+ readonly mesh: MeshGpuHandles;
442
+ readonly renderState: MaterialRenderState | undefined;
443
+ readonly candidates: GpuDrivenCandidate[];
444
+ }
445
+ >();
185
446
  for (const batch of source.batches) {
186
447
  const candidates = batch.candidates.filter((candidate) => {
187
448
  const slot = slotByPrimitive.get(candidate.primitiveIndex);
@@ -190,24 +451,84 @@ function filteredPlan(
190
451
  return mesh !== undefined && meshSupportsBatch(mesh, batch);
191
452
  });
192
453
  if (candidates.length === 0) continue;
454
+ for (const candidate of candidates) {
455
+ const slot = slotByPrimitive.get(candidate.primitiveIndex);
456
+ if (slot === undefined) continue;
457
+ const mesh = meshes.get(slot.snapshot.assetHandle);
458
+ if (mesh === undefined) continue;
459
+ const physicalLodRanges = mesh.lodRanges
460
+ ?.slice(1)
461
+ .map((ranges) => ranges[candidate.drawItemIndex]);
462
+ const projected = Object.freeze({
463
+ ...candidate,
464
+ submitAdmission: activeEntityKeys === undefined || activeEntityKeys.has(activeKey(slot)),
465
+ projectedHeight: projectedHeightForCandidate(slot, camera),
466
+ ...(physicalLodRanges?.every((range) => range !== undefined)
467
+ ? { lodRanges: physicalLodRanges as NonNullable<typeof candidate.lodRanges> }
468
+ : {}),
469
+ });
470
+ const selectedRange = selectedLodRange(batch, projected);
471
+ const groupKey = JSON.stringify({
472
+ key: batch.key,
473
+ worldKey: worldKeyFor(slot),
474
+ lodCoverages: projected.lodCoverages ?? null,
475
+ lodRanges: projected.lodRanges ?? null,
476
+ selectedRange,
477
+ selectedLevel: selectedLodLevel(projected),
478
+ });
479
+ const existing = groups.get(groupKey);
480
+ if (existing === undefined) {
481
+ const material = slot.snapshot.materials[batch.key.materialSlot] ?? slot.snapshot.material;
482
+ groups.set(groupKey, {
483
+ source: batch,
484
+ mesh,
485
+ renderState: geometryRenderStateForTopology(batch.key.topology, material.renderState),
486
+ candidates: [projected],
487
+ });
488
+ } else {
489
+ existing.candidates.push(projected);
490
+ }
491
+ }
492
+ }
493
+
494
+ const batches: PreparedBatch[] = [];
495
+ let visibleBase = 0;
496
+ const filteredBatches: GpuDrivenBatch[] = [];
497
+ let batchId = 0;
498
+ for (const group of groups.values()) {
193
499
  visibleBase = Math.ceil(visibleBase / 64) * 64;
500
+ const admittedCount = group.candidates.reduce(
501
+ (count, candidate) =>
502
+ count +
503
+ ((candidate as GpuDrivenCandidate & { readonly submitAdmission?: boolean })
504
+ .submitAdmission === false
505
+ ? 0
506
+ : 1),
507
+ 0,
508
+ );
194
509
  const filtered: GpuDrivenBatch = Object.freeze({
195
- ...batch,
196
- candidates: Object.freeze(candidates),
510
+ ...group.source,
511
+ batchId,
512
+ candidates: Object.freeze(group.candidates),
197
513
  visibleBase,
198
- visibleCapacity: candidates.length,
514
+ visibleCapacity: admittedCount,
515
+ indirectOffset: batchId * 20,
199
516
  });
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;
517
+ visibleBase += admittedCount;
204
518
  filteredBatches.push(filtered);
205
- const material = first.snapshot.materials[batch.key.materialSlot] ?? first.snapshot.material;
519
+ const firstCandidate = group.candidates[0];
520
+ const firstSlot =
521
+ firstCandidate === undefined ? undefined : slotByPrimitive.get(firstCandidate.primitiveIndex);
522
+ if (firstCandidate === undefined || firstSlot === undefined) continue;
206
523
  batches.push({
207
524
  batch: filtered,
208
- mesh,
209
- renderState: geometryRenderStateForTopology(batch.key.topology, material.renderState),
525
+ mesh: group.mesh,
526
+ renderState: group.renderState,
527
+ worldKey: worldKeyFor(firstSlot),
528
+ primitiveSlot: firstSlot.slot,
529
+ slotGeneration: firstSlot.generation,
210
530
  });
531
+ batchId += 1;
211
532
  }
212
533
  return {
213
534
  plan: Object.freeze({
@@ -218,11 +539,32 @@ function filteredPlan(
218
539
  }),
219
540
  batches: Object.freeze(batches),
220
541
  entityKeys,
542
+ topologyInvariantEmpty: topologyEligiblePrimitiveCount === 0,
221
543
  };
222
544
  }
223
545
 
224
546
  type FilteredProductionPlan = ReturnType<typeof filteredPlan>;
225
547
 
548
+ export interface GpuDrivenWorldLodSelectionInspection {
549
+ readonly worldKey: number;
550
+ readonly primitiveSlot: number;
551
+ readonly slotGeneration: number;
552
+ readonly candidateCount: number;
553
+ readonly visible: number;
554
+ readonly occluded: number;
555
+ readonly lodHistogram: readonly { readonly level: number; readonly count: number }[];
556
+ }
557
+
558
+ function sameEntityKeySet(
559
+ left: ReadonlySet<number> | undefined,
560
+ right: ReadonlySet<number> | undefined,
561
+ ): boolean {
562
+ if (left === right) return true;
563
+ if (left === undefined || right === undefined || left.size !== right.size) return false;
564
+ for (const key of left) if (!right.has(key)) return false;
565
+ return true;
566
+ }
567
+
226
568
  class GpuDrivenRigidRaster {
227
569
  private readonly pipelines = new Map<string, RenderPipeline>();
228
570
 
@@ -375,10 +717,212 @@ class GpuDrivenRigidRaster {
375
717
  }
376
718
  }
377
719
 
720
+ /** The GPU-driven bridge to the existing four-group Standard PBR pipeline. */
721
+ class StandardPbrRasterAdapter {
722
+ private readonly pipelines = new Map<string, RenderPipeline>();
723
+ private readonly projectionBuffers: Buffer[] = [];
724
+
725
+ private constructor(
726
+ private readonly device: RhiDevice,
727
+ private readonly pipelineLayout: import('@forgeax/engine-rhi').PipelineLayout,
728
+ private readonly meshLayout: BindGroupLayout,
729
+ private readonly module: ShaderModule,
730
+ private readonly materialBindGroup: BindGroup,
731
+ private readonly instancesBindGroup: BindGroup,
732
+ private readonly artifact: MaterialShaderArtifact,
733
+ ) {}
734
+
735
+ static create(input: {
736
+ readonly device: RhiDevice;
737
+ readonly shaderModuleFactory: PipelineBuilderShaderModuleFactory;
738
+ readonly pipelineState: PipelineState;
739
+ readonly artifact: MaterialShaderArtifact;
740
+ }): Result<StandardPbrRasterAdapter, RhiError> {
741
+ const pipelineLayout = input.pipelineState.pbrPipelineLayout;
742
+ const fallback = input.pipelineState.skylightFallback;
743
+ if (pipelineLayout === null || fallback === null) {
744
+ return err(
745
+ new RhiError({
746
+ code: 'rhi-not-available',
747
+ expected: 'Standard PBR pipeline layout and Skylight fallback',
748
+ hint: 'initialize the Standard PBR pipeline before enabling GPU-driven raster',
749
+ }),
750
+ );
751
+ }
752
+ const module = input.shaderModuleFactory.createShaderModule({
753
+ label: 'standard-pbr-gpu-driven-scene-index',
754
+ code: input.artifact.wgsl,
755
+ });
756
+ if (!module.ok) return module;
757
+ const materialBuffer = input.device.createBuffer({
758
+ label: 'standard-pbr-gpu-driven-material-row',
759
+ size: 384,
760
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
761
+ });
762
+ if (!materialBuffer.ok) return materialBuffer;
763
+ const materialPayload = new Float32Array(96);
764
+ materialPayload[0] = 1;
765
+ materialPayload[1] = 1;
766
+ materialPayload[2] = 1;
767
+ materialPayload[3] = 1;
768
+ materialPayload[5] = 1;
769
+ materialPayload[6] = 1;
770
+ const materialWrite = input.device.queue.writeBuffer(materialBuffer.value, 0, materialPayload);
771
+ if (!materialWrite.ok) return materialWrite;
772
+ const baseEntries = [
773
+ { binding: 0, resource: bufferBinding(materialBuffer.value, 0, 384) },
774
+ ...Array.from({ length: 8 }, (_, index) => [
775
+ {
776
+ binding: index * 2 + 1,
777
+ resource: { kind: 'sampler' as const, value: fallback.sampler },
778
+ },
779
+ {
780
+ binding: index * 2 + 2,
781
+ resource: {
782
+ kind: 'textureView' as const,
783
+ value:
784
+ index === 2
785
+ ? input.pipelineState.defaultNormalTextureView
786
+ : input.pipelineState.defaultWhiteTextureView,
787
+ },
788
+ },
789
+ ]).flat(),
790
+ ];
791
+ const materialEntries = assembleMaterialWithSkylightEntries(
792
+ baseEntries,
793
+ {
794
+ irradianceView: fallback.irradianceView,
795
+ irradianceSampler: fallback.sampler,
796
+ prefilterView: fallback.prefilterView,
797
+ prefilterSampler: fallback.sampler,
798
+ brdfLutView: fallback.brdfLutView,
799
+ brdfLutSampler: fallback.sampler,
800
+ intensityBuffer: fallback.intensityBuffer,
801
+ },
802
+ { sampler: fallback.sampler, backdropView: input.pipelineState.defaultWhiteTextureView },
803
+ );
804
+ const materialBindGroup = input.device.createBindGroup({
805
+ label: 'standard-pbr-gpu-driven-material-bg',
806
+ layout: input.pipelineState.materialBindGroupLayout,
807
+ entries: materialEntries,
808
+ });
809
+ if (!materialBindGroup.ok) return materialBindGroup;
810
+ const instancesBindGroup = input.device.createBindGroup({
811
+ label: 'standard-pbr-gpu-driven-identity-instances-bg',
812
+ layout: input.pipelineState.instancesBindGroupLayout,
813
+ entries: [
814
+ {
815
+ binding: 0,
816
+ resource: bufferBinding(input.pipelineState.identityInstanceBuffer),
817
+ },
818
+ ],
819
+ });
820
+ if (!instancesBindGroup.ok) return instancesBindGroup;
821
+ return ok(
822
+ new StandardPbrRasterAdapter(
823
+ input.device,
824
+ pipelineLayout,
825
+ input.pipelineState.meshBindGroupLayout,
826
+ module.value,
827
+ materialBindGroup.value,
828
+ instancesBindGroup.value,
829
+ input.artifact,
830
+ ),
831
+ );
832
+ }
833
+
834
+ pipeline(
835
+ format: TextureFormat,
836
+ sampleCount: 1 | 4,
837
+ layoutProjection: MeshGpuHandles['layoutProjection'],
838
+ topology: GpuDrivenBatch['key']['topology'],
839
+ stripIndexFormat: 'uint16' | 'uint32' | undefined,
840
+ renderState: MaterialRenderState | undefined,
841
+ ): Result<RenderPipeline, RhiError> {
842
+ const projectedLayout = deriveVertexBufferLayoutFromProjection(
843
+ layoutProjection,
844
+ this.artifact.uvSetCount === undefined
845
+ ? undefined
846
+ : { shaderUvSetCount: this.artifact.uvSetCount },
847
+ )[0];
848
+ const vertexStride = projectedLayout?.arrayStride ?? layoutProjection.arrayStride;
849
+ const attributes = projectedLayout?.attributes ?? layoutProjection.attributes;
850
+ const key = `${format}|${sampleCount}|${vertexStride}|${topology}|${stripIndexFormat ?? ''}|${JSON.stringify(renderState ?? null)}|${this.artifact.layoutIdentity}`;
851
+ const cached = this.pipelines.get(key);
852
+ if (cached !== undefined) return ok(cached);
853
+ const pipeline = this.device.createRenderPipeline({
854
+ label: `standard-pbr-gpu-driven.${key}`,
855
+ layout: this.pipelineLayout,
856
+ vertex: {
857
+ module: this.module,
858
+ entryPoint: this.artifact.receipt?.sceneIndexEntry ?? 'vs_scene_index',
859
+ buffers: [
860
+ {
861
+ arrayStride: vertexStride,
862
+ stepMode: 'vertex',
863
+ attributes: attributes.map(({ shaderLocation, offset, format }) => ({
864
+ shaderLocation,
865
+ offset,
866
+ format: format as GPUVertexFormat,
867
+ })),
868
+ },
869
+ ],
870
+ },
871
+ fragment: {
872
+ module: this.module,
873
+ entryPoint: 'fs_main',
874
+ targets: [{ format }],
875
+ },
876
+ primitive: {
877
+ topology,
878
+ cullMode: renderState?.cullMode ?? 'back',
879
+ frontFace: renderState?.frontFace ?? 'ccw',
880
+ ...(stripIndexFormat === undefined ? {} : { stripIndexFormat }),
881
+ },
882
+ depthStencil: {
883
+ format: 'depth24plus-stencil8',
884
+ depthWriteEnabled: renderState?.depthWriteEnabled ?? true,
885
+ depthCompare: renderState?.depthCompare ?? 'less',
886
+ },
887
+ ...(sampleCount === 4 ? { multisample: { count: 4 } } : {}),
888
+ });
889
+ if (!pipeline.ok) return pipeline;
890
+ this.pipelines.set(key, pipeline.value);
891
+ return pipeline;
892
+ }
893
+
894
+ projectionBindGroup(rows: Float32Array): Result<BindGroup, RhiError> {
895
+ const buffer = this.device.createBuffer({
896
+ label: 'standard-pbr-gpu-driven-scene-meshes',
897
+ size: Math.max(MESH_SSBO_BYTES, rows.byteLength),
898
+ usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
899
+ });
900
+ if (!buffer.ok) return buffer;
901
+ const write = this.device.queue.writeBuffer(buffer.value, 0, rows);
902
+ if (!write.ok) return write;
903
+ this.projectionBuffers.push(buffer.value);
904
+ return this.device.createBindGroup({
905
+ label: 'standard-pbr-gpu-driven-scene-mesh-bg',
906
+ layout: this.meshLayout,
907
+ entries: [{ binding: 0, resource: bufferBinding(buffer.value, 0, rows.byteLength) }],
908
+ });
909
+ }
910
+
911
+ dispose(): void {
912
+ for (const buffer of this.projectionBuffers) this.device.destroyBuffer(buffer);
913
+ this.projectionBuffers.length = 0;
914
+ }
915
+
916
+ get frameGroups(): readonly [BindGroup, BindGroup] {
917
+ return [this.materialBindGroup, this.instancesBindGroup];
918
+ }
919
+ }
920
+
378
921
  export interface PreparedGpuDrivenFrame {
379
922
  readonly topologySignature: string;
380
923
  readonly entityKeys: ReadonlySet<number>;
381
924
  readonly ownsAllRenderables: boolean;
925
+ readonly occlusion?: OcclusionFrameProjection;
382
926
  /** @internal Commits a replacement generation only after graph promotion. */
383
927
  readonly _commitResourceReplacement: () => void;
384
928
  project(
@@ -388,15 +932,74 @@ export interface PreparedGpuDrivenFrame {
388
932
  ): Result<RenderPipelineGpuDrivenProjection, RenderGraphError | RhiError>;
389
933
  }
390
934
 
935
+ export interface RecoveryCapabilityFacts {
936
+ readonly generation: number;
937
+ readonly shaderCompilation: boolean;
938
+ readonly compute: boolean;
939
+ readonly storageBuffer: boolean;
940
+ readonly indirectDrawing: boolean;
941
+ readonly multisample: boolean;
942
+ }
943
+
944
+ export type RecoveryCapabilityOutcome =
945
+ | {
946
+ readonly status: 'ready' | 'fallback';
947
+ readonly generation: number;
948
+ readonly disabled: readonly string[];
949
+ }
950
+ | {
951
+ readonly status: 'disabled';
952
+ readonly generation: number;
953
+ readonly disabled: readonly string[];
954
+ readonly allocations: 0;
955
+ }
956
+ | {
957
+ readonly status: 'refused';
958
+ readonly generation: number;
959
+ readonly failedOwner: 'shader-material-pipeline';
960
+ readonly resourceKind: 'pipeline';
961
+ };
962
+
963
+ export function classifyRecoveryCapability(
964
+ facts: RecoveryCapabilityFacts,
965
+ ): RecoveryCapabilityOutcome {
966
+ if (!facts.shaderCompilation) {
967
+ return {
968
+ status: 'refused',
969
+ generation: facts.generation,
970
+ failedOwner: 'shader-material-pipeline',
971
+ resourceKind: 'pipeline',
972
+ };
973
+ }
974
+ if (!facts.compute || !facts.storageBuffer || !facts.indirectDrawing) {
975
+ return {
976
+ status: 'disabled',
977
+ generation: facts.generation,
978
+ disabled: ['gpu-driven'],
979
+ allocations: 0,
980
+ };
981
+ }
982
+ return {
983
+ status: facts.multisample ? 'ready' : 'fallback',
984
+ generation: facts.generation,
985
+ disabled: [],
986
+ };
987
+ }
988
+
391
989
  export class GpuDrivenProduction {
990
+ private disposed = false;
392
991
  private view: GpuDrivenView | undefined;
393
992
  private raster: GpuDrivenRigidRaster | undefined;
993
+ private standardRaster: StandardPbrRasterAdapter | undefined;
394
994
  private readonly sceneIdentities = new WeakMap<object, number>();
395
995
  private nextSceneIdentity = 1;
396
996
  private filteredCache:
397
997
  | {
398
998
  readonly source: SubmissionPlan;
399
999
  readonly meshResidencyEpoch: number;
1000
+ readonly lodProjectionFingerprint: string;
1001
+ readonly activeEntityRevision?: number;
1002
+ readonly activeEntityKeys?: ReadonlySet<number>;
400
1003
  readonly value: FilteredProductionPlan;
401
1004
  }
402
1005
  | undefined;
@@ -406,6 +1009,13 @@ export class GpuDrivenProduction {
406
1009
  private batchBindGroupCreates = 0;
407
1010
  private validatedGpuOwnedRows = 0;
408
1011
  private cpuFallbackDrawItems = 0;
1012
+ private telemetryPrepared = false;
1013
+ private telemetryCandidateCount = 0;
1014
+ private telemetryGeometryWork = 0;
1015
+ private telemetryRootGeometryWork = 0;
1016
+ private indirectDrawCount = 0;
1017
+ private sceneRowsRequired = true;
1018
+ private standardPbrProjection: StandardPbrSceneProjection | undefined;
409
1019
  private signatureCache:
410
1020
  | {
411
1021
  readonly filtered: FilteredProductionPlan;
@@ -421,45 +1031,171 @@ export class GpuDrivenProduction {
421
1031
  private readonly shaderModuleFactory: PipelineBuilderShaderModuleFactory,
422
1032
  ) {}
423
1033
 
1034
+ static forDevice(
1035
+ device: RhiDevice,
1036
+ shaderModuleFactory: PipelineBuilderShaderModuleFactory,
1037
+ ): GpuDrivenProduction {
1038
+ return new GpuDrivenProduction(device, shaderModuleFactory);
1039
+ }
1040
+
1041
+ createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown> {
1042
+ return {
1043
+ kind: 'scene-table',
1044
+ create: () => {
1045
+ if (!scope.isAlive()) throw new Error('GPU-driven candidate scope is not active.');
1046
+ // Keep the actual detached production owner in the generation
1047
+ // aggregate. The recovery transaction, not a marker object, is the
1048
+ // owner that is published or discarded exactly once.
1049
+ return this;
1050
+ },
1051
+ cleanup: () => undefined,
1052
+ };
1053
+ }
1054
+
424
1055
  prepare(input: {
425
1056
  readonly scene: PersistentGpuDrivenState | undefined;
426
1057
  readonly camera: CameraSnapshot;
427
1058
  readonly meshes: ReadonlyMap<number, MeshGpuHandles>;
428
1059
  readonly viewBindGroupLayout: BindGroupLayout;
429
1060
  readonly meshResidencyEpoch: number;
1061
+ readonly hdrp: boolean;
1062
+ /** Entity keys admitted by the persistent visibility facet for this view. */
1063
+ readonly activeEntityKeys?: ReadonlySet<number>;
1064
+ /** Collision-free renderer-owned revision for the active visibility set. */
1065
+ readonly activeEntityRevision?: number;
1066
+ /** Total extracted candidates, including facet-suppressed rows. */
1067
+ readonly telemetryCandidateCount?: number;
1068
+ readonly occlusion?: OcclusionFrameProjection;
1069
+ /** Cooked producer artifact selected from the frame material catalog. */
1070
+ readonly standardPbrArtifact?: MaterialShaderArtifact;
1071
+ readonly standardPbrPipelineState?: PipelineState;
430
1072
  }): Result<PreparedGpuDrivenFrame | undefined, RhiError> {
431
1073
  this.gpuOwnedSnapshotsMaterialized = 0;
432
1074
  this.filteredPlanBuilds = 0;
433
1075
  this.batchBindGroupCreates = 0;
434
1076
  this.validatedGpuOwnedRows = 0;
435
1077
  this.cpuFallbackDrawItems = 0;
1078
+ this.telemetryPrepared = false;
1079
+ this.telemetryCandidateCount = input.telemetryCandidateCount ?? 0;
1080
+ this.telemetryGeometryWork = 0;
1081
+ this.telemetryRootGeometryWork = 0;
1082
+ this.indirectDrawCount = 0;
1083
+ const capability = classifyRecoveryCapability({
1084
+ generation: 0,
1085
+ shaderCompilation: true,
1086
+ compute: this.device.caps.compute,
1087
+ storageBuffer: this.device.caps.storageBuffer,
1088
+ indirectDrawing: this.device.caps.indirectDrawing,
1089
+ multisample: true,
1090
+ });
436
1091
  if (
1092
+ input.hdrp ||
437
1093
  input.scene === undefined ||
438
- !this.device.caps.compute ||
439
- !this.device.caps.storageBuffer ||
440
- !this.device.caps.indirectDrawing
1094
+ capability.status === 'disabled' ||
1095
+ selectGpuLodLane(this.device.caps) === 'cpu'
441
1096
  ) {
442
1097
  this.gpuOwnedEntityCount = 0;
1098
+ this.sceneRowsRequired = false;
443
1099
  return ok(undefined);
444
1100
  }
445
1101
  const scene = input.scene;
1102
+ const projectionFingerprint = lodProjectionFingerprint(input.camera);
1103
+ // An empty filtered plan means this topology/mesh-residency pair has no
1104
+ // renderables that this production lane can own. Camera and visibility
1105
+ // inputs only affect admitted candidates and LOD selection, so they cannot
1106
+ // turn that empty topology into an eligible one. Keep the negative result
1107
+ // resident instead of rescanning every scene slot as the camera moves.
1108
+ const cachedPlanIsTopologyInvariant = this.filteredCache?.value.topologyInvariantEmpty === true;
1109
+ const cachedPlanIsCameraInvariant = cachedPlanIsTopologyInvariant;
1110
+ const cachedVisibilityMatches =
1111
+ cachedPlanIsCameraInvariant || this.filteredCache === undefined
1112
+ ? true
1113
+ : input.activeEntityRevision !== undefined &&
1114
+ this.filteredCache.activeEntityRevision !== undefined
1115
+ ? input.activeEntityRevision === this.filteredCache.activeEntityRevision
1116
+ : sameEntityKeySet(this.filteredCache.activeEntityKeys, input.activeEntityKeys);
446
1117
  let filtered = this.filteredCache?.value;
447
1118
  if (
448
1119
  filtered === undefined ||
449
1120
  this.filteredCache?.source !== scene.plan ||
450
- this.filteredCache.meshResidencyEpoch !== input.meshResidencyEpoch
1121
+ (!cachedPlanIsTopologyInvariant &&
1122
+ this.filteredCache.meshResidencyEpoch !== input.meshResidencyEpoch) ||
1123
+ (!cachedPlanIsCameraInvariant &&
1124
+ this.filteredCache.lodProjectionFingerprint !== projectionFingerprint) ||
1125
+ !cachedVisibilityMatches
451
1126
  ) {
452
- filtered = filteredPlan(scene.plan, scene.slots, input.meshes);
1127
+ filtered = filteredPlan(
1128
+ scene.plan,
1129
+ scene.slots,
1130
+ input.meshes,
1131
+ input.camera,
1132
+ input.activeEntityKeys,
1133
+ scene.worldKeys,
1134
+ );
453
1135
  this.filteredCache = {
454
1136
  source: scene.plan,
455
1137
  meshResidencyEpoch: input.meshResidencyEpoch,
1138
+ lodProjectionFingerprint: projectionFingerprint,
1139
+ ...(input.activeEntityRevision === undefined
1140
+ ? {}
1141
+ : { activeEntityRevision: input.activeEntityRevision }),
1142
+ ...(input.activeEntityKeys === undefined
1143
+ ? {}
1144
+ : { activeEntityKeys: input.activeEntityKeys }),
456
1145
  value: filtered,
457
1146
  };
458
1147
  this.gpuOwnedSnapshotsMaterialized = scene.slots.length;
459
1148
  this.filteredPlanBuilds = 1;
460
1149
  }
461
1150
  this.gpuOwnedEntityCount = filtered.entityKeys.size;
1151
+ this.sceneRowsRequired = !filtered.topologyInvariantEmpty;
1152
+ // Publish the command count from the current filtered plan on every
1153
+ // prepare call. A cached graph does not call project() again on stable
1154
+ // frames, so deriving this only while compiling the raster pass would
1155
+ // transiently report zero after prepare() reset the per-frame counters.
1156
+ this.indirectDrawCount = admittedRasterBatchCount(filtered.plan);
462
1157
  if (filtered.batches.length === 0) return ok(undefined);
1158
+ this.standardPbrProjection = undefined;
1159
+ const usesStandardPbrRaster = filtered.batches.every(({ batch }) => isStandardPbrBatch(batch));
1160
+ if (usesStandardPbrRaster && input.standardPbrArtifact !== undefined) {
1161
+ const slotsByPrimitive = new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
1162
+ const projection = projectStandardPbrScene({
1163
+ artifact: input.standardPbrArtifact,
1164
+ candidates: filtered.batches.flatMap(({ batch }) =>
1165
+ batch.candidates.flatMap((candidate) => {
1166
+ const slot = slotsByPrimitive.get(candidate.primitiveIndex);
1167
+ return slot === undefined
1168
+ ? []
1169
+ : [
1170
+ {
1171
+ primitiveIndex: candidate.primitiveIndex,
1172
+ world: slot.snapshot.transform.world,
1173
+ },
1174
+ ];
1175
+ }),
1176
+ ),
1177
+ });
1178
+ if (!projection.ok) {
1179
+ return err(
1180
+ new RhiError({
1181
+ code: 'shader-compile-failed',
1182
+ expected: 'receipt-derived Standard PBR scene projection',
1183
+ hint: 'publish one validated MaterialShaderArtifact receipt before entering the GPU lane',
1184
+ }),
1185
+ );
1186
+ }
1187
+ this.standardPbrProjection = projection.value;
1188
+ if (input.standardPbrPipelineState !== undefined && this.standardRaster === undefined) {
1189
+ const created = StandardPbrRasterAdapter.create({
1190
+ device: this.device,
1191
+ shaderModuleFactory: this.shaderModuleFactory,
1192
+ pipelineState: input.standardPbrPipelineState,
1193
+ artifact: input.standardPbrArtifact,
1194
+ });
1195
+ if (!created.ok) return created;
1196
+ this.standardRaster = created.value;
1197
+ }
1198
+ }
463
1199
  if (this.view === undefined) {
464
1200
  const created = GpuDrivenView.create({
465
1201
  device: this.device,
@@ -479,8 +1215,11 @@ export class GpuDrivenProduction {
479
1215
  }
480
1216
  const updated = this.view.update(filtered.plan, scene.scene, activeFrustum(input.camera));
481
1217
  if (!updated.ok) return updated;
1218
+ this.telemetryPrepared = true;
482
1219
  const view = this.view;
483
1220
  const raster = this.raster;
1221
+ const standardRaster = this.standardRaster;
1222
+ const standardArtifact = input.standardPbrArtifact;
484
1223
  const sceneIdentity = this.sceneIdentity(scene.scene);
485
1224
  const viewInspection = view.inspect();
486
1225
  let signature = this.signatureCache?.value;
@@ -495,10 +1234,12 @@ export class GpuDrivenProduction {
495
1234
  sceneIdentity,
496
1235
  revision: filtered.plan.revision,
497
1236
  meshResidencyEpoch: input.meshResidencyEpoch,
1237
+ standardPbrLayoutIdentity: this.standardPbrProjection?.layoutIdentity,
498
1238
  viewResourceGeneration: viewInspection.resourceGeneration,
499
1239
  batches: filtered.batches.map(({ batch, mesh }) => [
500
1240
  batch.batchId,
501
1241
  batch.generation,
1242
+ batch.visibleCapacity,
502
1243
  batch.visibleBase,
503
1244
  batch.candidates.map((candidate) => [
504
1245
  candidate.primitiveIndex,
@@ -512,6 +1253,7 @@ export class GpuDrivenProduction {
512
1253
  ]),
513
1254
  capacities: [
514
1255
  viewInspection.candidateCapacity,
1256
+ viewInspection.visibleBufferCapacity,
515
1257
  viewInspection.batchCapacity,
516
1258
  viewInspection.indirectCapacity,
517
1259
  ],
@@ -527,11 +1269,165 @@ export class GpuDrivenProduction {
527
1269
  return ok({
528
1270
  topologySignature: signature,
529
1271
  entityKeys: filtered.entityKeys,
530
- ownsAllRenderables: filtered.entityKeys.size === scene.slots.length,
1272
+ ownsAllRenderables:
1273
+ input.activeEntityKeys === undefined
1274
+ ? filtered.entityKeys.size === scene.slots.length
1275
+ : filtered.entityKeys.size === input.activeEntityKeys.size,
1276
+ ...(input.occlusion === undefined ? {} : { occlusion: input.occlusion }),
531
1277
  _commitResourceReplacement: () => view._commitResourceReplacement(),
532
1278
  project: (graph, format, sampleCount) => {
1279
+ this.indirectDrawCount = admittedRasterBatchCount(filtered.plan);
533
1280
  const outputs = view.addPasses(graph);
534
1281
  if (!outputs.ok) return outputs;
1282
+ if (
1283
+ usesStandardPbrRaster &&
1284
+ standardRaster !== undefined &&
1285
+ standardArtifact !== undefined
1286
+ ) {
1287
+ const standardProjected: Array<{
1288
+ readonly batch: GpuDrivenBatch;
1289
+ readonly mesh: MeshGpuHandles;
1290
+ readonly vertex: GraphBuffer;
1291
+ readonly index?: GraphBuffer;
1292
+ readonly pipeline: RenderPipeline;
1293
+ readonly meshBindGroup: BindGroup;
1294
+ }> = [];
1295
+ const standardAccesses: GraphAccess[] = [
1296
+ { resource: outputs.value.primitive, usage: 'storage-read' },
1297
+ { resource: outputs.value.instance, usage: 'storage-read' },
1298
+ { resource: outputs.value.material, usage: 'storage-read' },
1299
+ { resource: outputs.value.visible, usage: 'storage-read' },
1300
+ { resource: outputs.value.transform, usage: 'storage-read' },
1301
+ { resource: outputs.value.indirect, usage: 'indirect-read' },
1302
+ ];
1303
+ const slotsByPrimitive = new Map(scene.slots.map((slot) => [slot.slot, slot] as const));
1304
+ const meshBuffers = new Map<
1305
+ number,
1306
+ { readonly vertex: GraphBuffer; readonly index?: GraphBuffer }
1307
+ >();
1308
+ for (const prepared of filtered.batches) {
1309
+ const candidates = prepared.batch.candidates.flatMap((candidate) => {
1310
+ const slot = slotsByPrimitive.get(candidate.primitiveIndex);
1311
+ return slot === undefined
1312
+ ? []
1313
+ : [
1314
+ {
1315
+ primitiveIndex: candidate.primitiveIndex,
1316
+ world: slot.snapshot.transform.world,
1317
+ },
1318
+ ];
1319
+ });
1320
+ const projection = projectStandardPbrScene({
1321
+ artifact: standardArtifact,
1322
+ candidates,
1323
+ });
1324
+ if (!projection.ok) {
1325
+ return err(
1326
+ new RhiError({
1327
+ code: 'shader-compile-failed',
1328
+ expected: 'receipt-derived Standard PBR scene projection',
1329
+ hint: 'keep the frame artifact receipt and projected rows on one identity',
1330
+ }),
1331
+ );
1332
+ }
1333
+ const meshBindGroup = standardRaster.projectionBindGroup(projection.value.rows);
1334
+ if (!meshBindGroup.ok) return meshBindGroup;
1335
+ let buffers = meshBuffers.get(prepared.batch.key.assetHandle);
1336
+ if (buffers === undefined) {
1337
+ const vertex = graph.importBuffer(
1338
+ `gpu-driven.standard.${prepared.batch.key.assetHandle}.vertex`,
1339
+ {
1340
+ size: prepared.mesh.vboBytes,
1341
+ usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
1342
+ },
1343
+ () => prepared.mesh.vertexBuffer.handle,
1344
+ );
1345
+ if (!vertex.ok) return vertex;
1346
+ let index: GraphBuffer | undefined;
1347
+ if (prepared.mesh.indexBuffer !== null) {
1348
+ const indexHandle = prepared.mesh.indexBuffer;
1349
+ const imported = graph.importBuffer(
1350
+ `gpu-driven.standard.${prepared.batch.key.assetHandle}.index`,
1351
+ {
1352
+ size: prepared.mesh.iboBytes,
1353
+ usage: GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST,
1354
+ },
1355
+ () => indexHandle.handle,
1356
+ );
1357
+ if (!imported.ok) return imported;
1358
+ index = imported.value;
1359
+ }
1360
+ buffers = { vertex: vertex.value, ...(index === undefined ? {} : { index }) };
1361
+ meshBuffers.set(prepared.batch.key.assetHandle, buffers);
1362
+ standardAccesses.push({ resource: buffers.vertex, usage: 'vertex-read' });
1363
+ if (buffers.index !== undefined) {
1364
+ standardAccesses.push({ resource: buffers.index, usage: 'index-read' });
1365
+ }
1366
+ }
1367
+ const pipeline = standardRaster.pipeline(
1368
+ format,
1369
+ sampleCount,
1370
+ prepared.mesh.layoutProjection,
1371
+ prepared.batch.key.topology,
1372
+ prepared.batch.key.drawKind === 'indexed' &&
1373
+ (prepared.batch.key.topology === 'line-strip' ||
1374
+ prepared.batch.key.topology === 'triangle-strip')
1375
+ ? prepared.mesh.indexFormat
1376
+ : undefined,
1377
+ prepared.renderState,
1378
+ );
1379
+ if (!pipeline.ok) return pipeline;
1380
+ standardProjected.push({
1381
+ batch: prepared.batch,
1382
+ mesh: prepared.mesh,
1383
+ ...buffers,
1384
+ pipeline: pipeline.value,
1385
+ meshBindGroup: meshBindGroup.value,
1386
+ });
1387
+ }
1388
+ const [materialBindGroup, instancesBindGroup] = standardRaster.frameGroups;
1389
+ return ok({
1390
+ accesses: standardAccesses,
1391
+ encode: (viewBindGroup, pass, resources: GraphResourceResolver) => {
1392
+ // pbr-view-bgl has two dynamic uniform bindings: the frame view
1393
+ // slot and the optional Points/Lines style slot. Standard
1394
+ // draws do not use the latter, but WebGPU still requires one
1395
+ // offset per dynamic binding.
1396
+ pass.setBindGroup(0, viewBindGroup, [0, 0]);
1397
+ pass.setBindGroup(1, materialBindGroup, [0]);
1398
+ pass.setBindGroup(3, instancesBindGroup);
1399
+ let currentPipeline: RenderPipeline | undefined;
1400
+ let currentVertex: GraphBuffer | undefined;
1401
+ let currentIndex: GraphBuffer | undefined;
1402
+ for (const batch of standardProjected) {
1403
+ if (currentPipeline !== batch.pipeline) {
1404
+ pass.setPipeline(batch.pipeline);
1405
+ currentPipeline = batch.pipeline;
1406
+ }
1407
+ pass.setBindGroup(2, batch.meshBindGroup, [0]);
1408
+ if (currentVertex !== batch.vertex) {
1409
+ const vertex = resources.buffer(batch.vertex);
1410
+ if (!vertex.ok) throw vertex.error;
1411
+ pass.setVertexBuffer(0, vertex.value);
1412
+ currentVertex = batch.vertex;
1413
+ }
1414
+ if (batch.index !== undefined && currentIndex !== batch.index) {
1415
+ const index = resources.buffer(batch.index);
1416
+ if (!index.ok) throw index.error;
1417
+ pass.setIndexBuffer(index.value, batch.mesh.indexFormat);
1418
+ currentIndex = batch.index;
1419
+ }
1420
+ const indirect = resources.buffer(outputs.value.indirect);
1421
+ if (!indirect.ok) throw indirect.error;
1422
+ if (batch.batch.key.drawKind === 'indexed') {
1423
+ pass.drawIndexedIndirect(indirect.value, batch.batch.indirectOffset);
1424
+ } else {
1425
+ pass.drawIndirect(indirect.value, batch.batch.indirectOffset);
1426
+ }
1427
+ }
1428
+ },
1429
+ });
1430
+ }
535
1431
  const projected: ProjectedBatch[] = [];
536
1432
  const accesses: GraphAccess[] = [
537
1433
  { resource: outputs.value.primitive, usage: 'storage-read' },
@@ -546,6 +1442,10 @@ export class GpuDrivenProduction {
546
1442
  { readonly vertex: GraphBuffer; readonly index?: GraphBuffer }
547
1443
  >();
548
1444
  for (const prepared of filtered.batches) {
1445
+ // A selector-only batch can have zero admitted candidates. Keep it
1446
+ // in the GPU selector and readback counters, but do not create a
1447
+ // raster bind group or encode a zero-instance indirect draw.
1448
+ if (prepared.batch.visibleCapacity === 0) continue;
549
1449
  let buffers = meshBuffers.get(prepared.batch.key.assetHandle);
550
1450
  if (buffers === undefined) {
551
1451
  const vertex = graph.importBuffer(
@@ -581,7 +1481,7 @@ export class GpuDrivenProduction {
581
1481
  const pipeline = raster.pipeline(
582
1482
  format,
583
1483
  sampleCount,
584
- 48 + (prepared.mesh.uvSetCount - 1) * 8,
1484
+ prepared.mesh.layoutProjection.arrayStride,
585
1485
  prepared.batch.key.topology,
586
1486
  prepared.batch.key.drawKind === 'indexed' &&
587
1487
  (prepared.batch.key.topology === 'line-strip' ||
@@ -601,6 +1501,7 @@ export class GpuDrivenProduction {
601
1501
  bindings: bindings.value,
602
1502
  });
603
1503
  }
1504
+ this.indirectDrawCount = projected.length;
604
1505
  return ok({
605
1506
  accesses,
606
1507
  encode: (viewBindGroup, pass, resources: GraphResourceResolver) => {
@@ -633,11 +1534,14 @@ export class GpuDrivenProduction {
633
1534
  }
634
1535
  const indirect = resources.buffer(outputs.value.indirect);
635
1536
  if (!indirect.ok) throw indirect.error;
636
- if (batch.batch.key.drawKind === 'indexed') {
637
- pass.drawIndexedIndirect(indirect.value, batch.batch.indirectOffset);
638
- } else {
639
- pass.drawIndirect(indirect.value, batch.batch.indirectOffset);
640
- }
1537
+ const draw = (): void => {
1538
+ if (batch.batch.key.drawKind === 'indexed') {
1539
+ pass.drawIndexedIndirect(indirect.value, batch.batch.indirectOffset);
1540
+ } else {
1541
+ pass.drawIndirect(indirect.value, batch.batch.indirectOffset);
1542
+ }
1543
+ };
1544
+ draw();
641
1545
  }
642
1546
  },
643
1547
  });
@@ -646,11 +1550,19 @@ export class GpuDrivenProduction {
646
1550
  }
647
1551
 
648
1552
  dispose(): void {
1553
+ if (this.disposed) return;
1554
+ this.disposed = true;
649
1555
  this.view?.dispose();
650
1556
  this.view = undefined;
651
1557
  this.raster = undefined;
652
1558
  this.filteredCache = undefined;
653
1559
  this.signatureCache = undefined;
1560
+ this.sceneRowsRequired = true;
1561
+ }
1562
+
1563
+ /** Whether the current production topology can consume GPU Scene row updates. */
1564
+ requiresSceneRows(): boolean {
1565
+ return this.sceneRowsRequired;
654
1566
  }
655
1567
 
656
1568
  inspect(): GpuDrivenProductionInspection {
@@ -667,17 +1579,112 @@ export class GpuDrivenProduction {
667
1579
  topologyRevision: view?.topologyRevision,
668
1580
  validatedGpuOwnedRows: this.validatedGpuOwnedRows,
669
1581
  cpuFallbackDrawItems: this.cpuFallbackDrawItems,
1582
+ geometryWork: this.telemetryGeometryWork,
1583
+ rootGeometryWork: this.telemetryRootGeometryWork,
1584
+ geometryWorkReduction:
1585
+ this.telemetryRootGeometryWork > 0
1586
+ ? 1 - this.telemetryGeometryWork / this.telemetryRootGeometryWork
1587
+ : 0,
1588
+ batchCount: view?.batchCount ?? 0,
1589
+ indirectDrawCount: this.indirectDrawCount,
670
1590
  };
671
1591
  }
672
1592
 
1593
+ /** Read renderer-owned LOD counters copied by the last GPU submission. */
1594
+ readLodSelection(): Promise<GpuDrivenLodSelectionInspection | undefined> {
1595
+ if (!this.telemetryPrepared) return Promise.resolve(undefined);
1596
+ return (this.view?.readLodSelection() ?? Promise.resolve(undefined)).then((selection) => {
1597
+ if (selection !== undefined) {
1598
+ this.telemetryGeometryWork = selection.geometryWork;
1599
+ this.telemetryRootGeometryWork = selection.rootGeometryWork;
1600
+ }
1601
+ if (selection === undefined || this.telemetryCandidateCount <= 0) return selection;
1602
+ const filtered = this.filteredCache?.value;
1603
+ if (filtered === undefined) return selection;
1604
+ const preparedByBatchId = new Map(
1605
+ filtered.batches.map((prepared) => [prepared.batch.batchId, prepared] as const),
1606
+ );
1607
+ const worldSelections = new Map<
1608
+ number,
1609
+ {
1610
+ readonly worldKey: number;
1611
+ readonly primitiveSlot: number;
1612
+ readonly slotGeneration: number;
1613
+ candidateCount: number;
1614
+ visible: number;
1615
+ occluded: number;
1616
+ readonly lodHistogram: Map<number, number>;
1617
+ }
1618
+ >();
1619
+ for (const batchSelection of selection.batches) {
1620
+ const prepared = preparedByBatchId.get(batchSelection.batchId);
1621
+ if (prepared === undefined) return selection;
1622
+ let aggregate = worldSelections.get(prepared.worldKey);
1623
+ if (aggregate === undefined) {
1624
+ aggregate = {
1625
+ worldKey: prepared.worldKey,
1626
+ primitiveSlot: prepared.primitiveSlot,
1627
+ slotGeneration: prepared.slotGeneration,
1628
+ candidateCount: 0,
1629
+ visible: 0,
1630
+ occluded: 0,
1631
+ lodHistogram: new Map(),
1632
+ };
1633
+ worldSelections.set(prepared.worldKey, aggregate);
1634
+ }
1635
+ aggregate.candidateCount += batchSelection.candidateCount;
1636
+ aggregate.visible += batchSelection.visible;
1637
+ aggregate.occluded += batchSelection.occluded;
1638
+ for (const row of batchSelection.lodHistogram) {
1639
+ aggregate.lodHistogram.set(
1640
+ row.level,
1641
+ (aggregate.lodHistogram.get(row.level) ?? 0) + row.count,
1642
+ );
1643
+ }
1644
+ }
1645
+ return {
1646
+ ...selection,
1647
+ candidateCount: this.telemetryCandidateCount,
1648
+ occluded: Math.max(0, this.telemetryCandidateCount - selection.visible),
1649
+ worldSelections: Object.freeze(
1650
+ [...worldSelections.values()].map((aggregate) =>
1651
+ Object.freeze({
1652
+ worldKey: aggregate.worldKey,
1653
+ primitiveSlot: aggregate.primitiveSlot,
1654
+ slotGeneration: aggregate.slotGeneration,
1655
+ candidateCount: aggregate.candidateCount,
1656
+ visible: aggregate.visible,
1657
+ occluded: aggregate.occluded,
1658
+ lodHistogram: Object.freeze(
1659
+ [...aggregate.lodHistogram.entries()]
1660
+ .sort(([left], [right]) => left - right)
1661
+ .map(([level, count]) => Object.freeze({ level, count })),
1662
+ ),
1663
+ } satisfies GpuDrivenWorldLodSelectionInspection),
1664
+ ),
1665
+ ),
1666
+ };
1667
+ });
1668
+ }
1669
+
673
1670
  recordCpuValidation(
674
1671
  validated: readonly { readonly source: RenderableSnapshot }[],
675
1672
  gpuOwned: ReadonlySet<number>,
1673
+ worldKeys?: readonly number[],
676
1674
  ): void {
677
1675
  this.cpuFallbackDrawItems = validated.length;
678
1676
  let gpuOwnedRows = 0;
679
1677
  for (const row of validated) {
680
- if (gpuOwned.has(worldEntityKey(row.source.worldId, row.source.entityKey))) gpuOwnedRows += 1;
1678
+ if (
1679
+ gpuOwned.has(
1680
+ worldEntityKey(
1681
+ worldKeys?.[row.source.worldId] ?? row.source.worldId,
1682
+ row.source.entityKey,
1683
+ ),
1684
+ )
1685
+ ) {
1686
+ gpuOwnedRows += 1;
1687
+ }
681
1688
  }
682
1689
  this.validatedGpuOwnedRows = gpuOwnedRows;
683
1690
  }