@forgeax/engine-render 0.1.21 → 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 (426) hide show
  1. package/README.md +150 -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.map +1 -1
  5. package/dist/assembly/factory.d.ts +14 -6
  6. package/dist/assembly/factory.d.ts.map +1 -1
  7. package/dist/assembly/host-contract.d.ts +10 -0
  8. package/dist/assembly/host-contract.d.ts.map +1 -1
  9. package/dist/authoring.mjs +1 -1
  10. package/dist/{chunk-PCHBPYML.mjs → chunk-4L3P6DT5.mjs} +2 -2
  11. package/dist/chunk-4L3P6DT5.mjs.map +1 -0
  12. package/dist/{chunk-VFPLTKCU.mjs → chunk-4NPUASZR.mjs} +15 -4
  13. package/dist/chunk-4NPUASZR.mjs.map +1 -0
  14. package/dist/chunk-DIYW3M5D.mjs +5085 -0
  15. package/dist/chunk-DIYW3M5D.mjs.map +1 -0
  16. package/dist/chunk-FBC4R44J.mjs +1003 -0
  17. package/dist/chunk-FBC4R44J.mjs.map +1 -0
  18. package/dist/{chunk-FS2KNUQH.mjs → chunk-FF7K3FSO.mjs} +253 -489
  19. package/dist/chunk-FF7K3FSO.mjs.map +1 -0
  20. package/dist/chunk-HZUPSYW7.mjs +1104 -0
  21. package/dist/chunk-HZUPSYW7.mjs.map +1 -0
  22. package/dist/{chunk-6OPXDQ2E.mjs → chunk-QODA5UKU.mjs} +33 -294
  23. package/dist/chunk-QODA5UKU.mjs.map +1 -0
  24. package/dist/chunk-YALPS44A.mjs +296 -0
  25. package/dist/chunk-YALPS44A.mjs.map +1 -0
  26. package/dist/{chunk-5YADUCA7.mjs → chunk-ZUWKAZ2I.mjs} +585 -991
  27. package/dist/chunk-ZUWKAZ2I.mjs.map +1 -0
  28. package/dist/cluster-binner.d.ts +5 -0
  29. package/dist/cluster-binner.d.ts.map +1 -1
  30. package/dist/components/index.d.ts +3 -0
  31. package/dist/components/index.d.ts.map +1 -1
  32. package/dist/components/light-helpers.d.ts +4 -0
  33. package/dist/components/light-helpers.d.ts.map +1 -1
  34. package/dist/components/light-probe.d.ts +9 -0
  35. package/dist/components/light-probe.d.ts.map +1 -0
  36. package/dist/components/rect-area-light.d.ts +12 -0
  37. package/dist/components/rect-area-light.d.ts.map +1 -0
  38. package/dist/components/scene-instance.d.ts +4 -0
  39. package/dist/components/scene-instance.d.ts.map +1 -1
  40. package/dist/components/spot-light.d.ts +3 -0
  41. package/dist/components/spot-light.d.ts.map +1 -1
  42. package/dist/construct-renderer.mjs +10437 -10044
  43. package/dist/construct-renderer.mjs.map +1 -1
  44. package/dist/device/gpu-residency.d.ts +8 -1
  45. package/dist/device/gpu-residency.d.ts.map +1 -1
  46. package/dist/device/lifecycle-transaction.d.ts +2 -0
  47. package/dist/device/lifecycle-transaction.d.ts.map +1 -0
  48. package/dist/environment/lifecycle.d.ts.map +1 -1
  49. package/dist/errors/gpu-driven.d.ts +26 -0
  50. package/dist/errors/gpu-driven.d.ts.map +1 -0
  51. package/dist/errors/index.d.ts +1 -0
  52. package/dist/errors/index.d.ts.map +1 -1
  53. package/dist/errors/recover.d.ts +9 -1
  54. package/dist/errors/recover.d.ts.map +1 -1
  55. package/dist/errors/render.d.ts +26 -1
  56. package/dist/errors/render.d.ts.map +1 -1
  57. package/dist/extract/gpu-driven.d.ts +50 -0
  58. package/dist/extract/gpu-driven.d.ts.map +1 -0
  59. package/dist/extract/visibility.d.ts.map +1 -1
  60. package/dist/gpu-driven/batch-topology.d.ts +34 -0
  61. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  62. package/dist/gpu-driven/prepared-draw.d.ts +63 -0
  63. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -0
  64. package/dist/gpu-driven/production-raster.d.ts +98 -2
  65. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  66. package/dist/gpu-driven/view-gpu.d.ts +64 -1
  67. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  68. package/dist/gpu-scene-schema.d.ts +14 -0
  69. package/dist/gpu-scene-schema.d.ts.map +1 -1
  70. package/dist/gpu-scene.d.ts +8 -1
  71. package/dist/gpu-scene.d.ts.map +1 -1
  72. package/dist/gpu-usage.d.ts +0 -1
  73. package/dist/gpu-usage.d.ts.map +1 -1
  74. package/dist/hdrp-buffers.d.ts +3 -3
  75. package/dist/hdrp-buffers.d.ts.map +1 -1
  76. package/dist/index.d.ts +12 -1
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.mjs +90 -5
  79. package/dist/index.mjs.map +1 -1
  80. package/dist/inspection-types.d.ts +163 -1
  81. package/dist/inspection-types.d.ts.map +1 -1
  82. package/dist/internal.d.ts +9 -0
  83. package/dist/internal.d.ts.map +1 -1
  84. package/dist/internal.mjs +23 -2
  85. package/dist/internal.mjs.map +1 -1
  86. package/dist/light-buffer-layout.d.ts +58 -149
  87. package/dist/light-buffer-layout.d.ts.map +1 -1
  88. package/dist/materials.d.ts.map +1 -1
  89. package/dist/pbr-pipeline.d.ts +26 -12
  90. package/dist/pbr-pipeline.d.ts.map +1 -1
  91. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  92. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -1
  93. package/dist/pipeline/standard-lighting/light-frame.d.ts +3 -3
  94. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -1
  95. package/dist/pipeline/standard-lighting/topology.d.ts +1 -1
  96. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -1
  97. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -1
  98. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  99. package/dist/pipeline-builder.d.ts +2 -0
  100. package/dist/pipeline-builder.d.ts.map +1 -1
  101. package/dist/pipeline-spec.d.ts +1 -1
  102. package/dist/pipeline-spec.d.ts.map +1 -1
  103. package/dist/plugin.d.ts.map +1 -1
  104. package/dist/prepare/extended-lighting/ltc-resources.d.ts +31 -0
  105. package/dist/prepare/extended-lighting/ltc-resources.d.ts.map +1 -0
  106. package/dist/prepare/extended-lighting/resources.d.ts +72 -0
  107. package/dist/prepare/extended-lighting/resources.d.ts.map +1 -0
  108. package/dist/prepare/extended-lighting/spot-modifiers.d.ts +42 -0
  109. package/dist/prepare/extended-lighting/spot-modifiers.d.ts.map +1 -0
  110. package/dist/prepare/extended-lighting/state.d.ts +24 -0
  111. package/dist/prepare/extended-lighting/state.d.ts.map +1 -0
  112. package/dist/record/frame-lighting.d.ts +8 -6
  113. package/dist/record/frame-lighting.d.ts.map +1 -1
  114. package/dist/record/frame-snapshot.d.ts +50 -10
  115. package/dist/record/frame-snapshot.d.ts.map +1 -1
  116. package/dist/record/frame.d.ts +7 -3
  117. package/dist/record/frame.d.ts.map +1 -1
  118. package/dist/record/gpu-timing.d.ts +19 -7
  119. package/dist/record/gpu-timing.d.ts.map +1 -1
  120. package/dist/record/helpers.d.ts +0 -20
  121. package/dist/record/helpers.d.ts.map +1 -1
  122. package/dist/record/main-pass-geometry.d.ts +6 -1
  123. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  124. package/dist/record/main-pass-material.d.ts +3 -1
  125. package/dist/record/main-pass-material.d.ts.map +1 -1
  126. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  127. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  128. package/dist/record/render-context.d.ts +57 -6
  129. package/dist/record/render-context.d.ts.map +1 -1
  130. package/dist/record/shadow-pass.d.ts.map +1 -1
  131. package/dist/record/typed-frame-graph.d.ts +49 -2
  132. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  133. package/dist/record/view-ubo.d.ts +1 -2
  134. package/dist/record/view-ubo.d.ts.map +1 -1
  135. package/dist/reflection/inspection.d.ts +10 -0
  136. package/dist/reflection/inspection.d.ts.map +1 -0
  137. package/dist/reflection/projection.d.ts +49 -0
  138. package/dist/reflection/projection.d.ts.map +1 -1
  139. package/dist/reflection/record-owner.d.ts +60 -0
  140. package/dist/reflection/record-owner.d.ts.map +1 -0
  141. package/dist/render-contract.d.ts +14 -8
  142. package/dist/render-contract.d.ts.map +1 -1
  143. package/dist/render-pipeline.d.ts +7 -0
  144. package/dist/render-pipeline.d.ts.map +1 -1
  145. package/dist/render-system-extract.d.ts +104 -16
  146. package/dist/render-system-extract.d.ts.map +1 -1
  147. package/dist/render-system.d.ts +12 -1
  148. package/dist/render-system.d.ts.map +1 -1
  149. package/dist/scene/probe-blend-record.d.ts +22 -0
  150. package/dist/scene/probe-blend-record.d.ts.map +1 -0
  151. package/dist/scene/probe-blend.d.ts +142 -0
  152. package/dist/scene/probe-blend.d.ts.map +1 -0
  153. package/dist/scene/render-scene-types.d.ts +0 -6
  154. package/dist/scene/render-scene-types.d.ts.map +1 -1
  155. package/dist/scene/render-scene.d.ts +103 -9
  156. package/dist/scene/render-scene.d.ts.map +1 -1
  157. package/dist/scene/visibility/bounds.d.ts +20 -0
  158. package/dist/scene/visibility/bounds.d.ts.map +1 -0
  159. package/dist/scene/visibility/budget.d.ts +16 -0
  160. package/dist/scene/visibility/budget.d.ts.map +1 -0
  161. package/dist/scene/visibility/facet.d.ts +73 -0
  162. package/dist/scene/visibility/facet.d.ts.map +1 -0
  163. package/dist/scene/visibility/gpu-frame-timing.d.ts +41 -0
  164. package/dist/scene/visibility/gpu-frame-timing.d.ts.map +1 -0
  165. package/dist/scene/visibility/gpu-lod.d.ts +46 -0
  166. package/dist/scene/visibility/gpu-lod.d.ts.map +1 -0
  167. package/dist/scene/visibility/inspection.d.ts +132 -0
  168. package/dist/scene/visibility/inspection.d.ts.map +1 -0
  169. package/dist/scene/visibility/lod-selector.d.ts +25 -0
  170. package/dist/scene/visibility/lod-selector.d.ts.map +1 -0
  171. package/dist/scene/visibility/occlusion-confidence.d.ts +81 -0
  172. package/dist/scene/visibility/occlusion-confidence.d.ts.map +1 -0
  173. package/dist/scene/visibility/occlusion-pass.d.ts +57 -0
  174. package/dist/scene/visibility/occlusion-pass.d.ts.map +1 -0
  175. package/dist/scene/visibility/occlusion-query-pool.d.ts +57 -0
  176. package/dist/scene/visibility/occlusion-query-pool.d.ts.map +1 -0
  177. package/dist/scene/visibility/occlusion-runtime.d.ts +96 -0
  178. package/dist/scene/visibility/occlusion-runtime.d.ts.map +1 -0
  179. package/dist/scene/visibility/types.d.ts +34 -0
  180. package/dist/scene/visibility/types.d.ts.map +1 -0
  181. package/dist/ssr/admission.d.ts +122 -0
  182. package/dist/ssr/admission.d.ts.map +1 -0
  183. package/dist/ssr/composition.d.ts +32 -0
  184. package/dist/ssr/composition.d.ts.map +1 -0
  185. package/dist/ssr/errors.d.ts +39 -0
  186. package/dist/ssr/errors.d.ts.map +1 -0
  187. package/dist/ssr/identity.d.ts +13 -0
  188. package/dist/ssr/identity.d.ts.map +1 -0
  189. package/dist/systems/skin-palette-allocator.d.ts +1 -1
  190. package/dist/temporal/frame-coordinator.d.ts +13 -0
  191. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  192. package/dist/temporal/frame.d.ts +2 -0
  193. package/dist/temporal/frame.d.ts.map +1 -1
  194. package/dist/temporal/index.mjs +6 -5
  195. package/dist/temporal/view.d.ts +7 -0
  196. package/dist/temporal/view.d.ts.map +1 -1
  197. package/dist/tilemap-chunk-extract-system.d.ts +1 -1
  198. package/dist/typed-render-graph-primitives.d.ts +4 -0
  199. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  200. package/dist/volume/passes.d.ts +8 -4
  201. package/dist/volume/passes.d.ts.map +1 -1
  202. package/package.json +20 -20
  203. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +5 -0
  204. package/src/__tests__/detached-inspection.integration.test.ts +6 -0
  205. package/src/__tests__/device-feature-admission.unit.test.ts +13 -1
  206. package/src/__tests__/direct-light-snapshot.unit.test.ts +59 -3
  207. package/src/__tests__/environment-generation.integration.test.ts +16 -0
  208. package/src/__tests__/evidence-identity.ts +13 -0
  209. package/src/__tests__/evidence-identity.unit.test.ts +24 -0
  210. package/src/__tests__/extended-lighting-recovery.browser.test.ts +14 -0
  211. package/src/__tests__/extended-lighting-recovery.dawn.test.ts +14 -0
  212. package/src/__tests__/extended-lighting-recovery.integration.test.ts +81 -0
  213. package/src/__tests__/extended-lighting-resources.unit.test.ts +51 -0
  214. package/src/__tests__/extended-lighting-zero-feature.integration.test.ts +52 -0
  215. package/src/__tests__/factory-contract.integration.test.ts +33 -0
  216. package/src/__tests__/frame-observation.contract.test.ts +15 -0
  217. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +146 -0
  218. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +1 -0
  219. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +1 -0
  220. package/src/__tests__/gpu-driven-production.integration.test.ts +572 -9
  221. package/src/__tests__/gpu-driven-scaling.unit.test.ts +66 -2
  222. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +26 -1
  223. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +89 -2
  224. package/src/__tests__/gpu-driven-view.browser.test.ts +3 -3
  225. package/src/__tests__/gpu-driven-view.dawn.test.ts +3 -3
  226. package/src/__tests__/gpu-driven-view.unit.test.ts +50 -0
  227. package/src/__tests__/gpu-lod-evidence.ts +188 -0
  228. package/src/__tests__/gpu-lod-fallback.integration.test.ts +10 -0
  229. package/src/__tests__/gpu-lod-indirect.integration.test.ts +40 -0
  230. package/src/__tests__/gpu-lod-parity.dawn.test.ts +29 -0
  231. package/src/__tests__/gpu-lod-readiness.integration.test.ts +52 -0
  232. package/src/__tests__/gpu-lod-schema.unit.test.ts +66 -0
  233. package/src/__tests__/gpu-lod-selector.unit.test.ts +28 -0
  234. package/src/__tests__/gpu-lod-silhouette.browser.test.ts +13 -0
  235. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +66 -1
  236. package/src/__tests__/gpu-scene.dawn.test.ts +8 -1
  237. package/src/__tests__/gpu-scene.unit.test.ts +49 -3
  238. package/src/__tests__/light-buffer-layout.unit.test.ts +113 -3
  239. package/src/__tests__/light-inspection.unit.test.ts +55 -0
  240. package/src/__tests__/light-resource-error.unit.test.ts +43 -0
  241. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +109 -0
  242. package/src/__tests__/lod-bounds.unit.test.ts +42 -0
  243. package/src/__tests__/lod-cpu-lane.integration.test.ts +55 -0
  244. package/src/__tests__/lod-cpu-no-change.perf.test.ts +19 -0
  245. package/src/__tests__/lod-gpu-frame-timing.unit.test.ts +49 -0
  246. package/src/__tests__/lod-multi-view.integration.test.ts +63 -0
  247. package/src/__tests__/lod-occlusion-device-loss.integration.test.ts +27 -0
  248. package/src/__tests__/lod-occlusion-final-decision.integration.test.ts +47 -0
  249. package/src/__tests__/lod-occlusion-inspection.integration.test.ts +217 -0
  250. package/src/__tests__/lod-occlusion-inspection.schema.test-d.ts +14 -0
  251. package/src/__tests__/lod-occlusion-inspection.unit.test.ts +72 -0
  252. package/src/__tests__/lod-occlusion-visual-evidence.ts +91 -0
  253. package/src/__tests__/lod-occlusion.browser.test.ts +15 -0
  254. package/src/__tests__/lod-occlusion.dawn.test.ts +20 -0
  255. package/src/__tests__/lod-selector.profile.test.ts +35 -0
  256. package/src/__tests__/lod-selector.unit.test.ts +68 -0
  257. package/src/__tests__/lod-visibility-identity.unit.test.ts +198 -0
  258. package/src/__tests__/lod-visibility-lifecycle.integration.test.ts +132 -0
  259. package/src/__tests__/ltc-resources.unit.test.ts +32 -0
  260. package/src/__tests__/material-contract-inventory.unit.test.ts +17 -0
  261. package/src/__tests__/occlusion-confidence.unit.test.ts +168 -0
  262. package/src/__tests__/occlusion-invalidation.integration.test.ts +37 -0
  263. package/src/__tests__/occlusion-pass-routing.unit.test.ts +16 -0
  264. package/src/__tests__/occlusion-pass.dawn.test.ts +20 -0
  265. package/src/__tests__/occlusion-pass.integration.test.ts +43 -0
  266. package/src/__tests__/occlusion-pass.rhi-null.test.ts +22 -0
  267. package/src/__tests__/occlusion-query-pool.rhi-null.test.ts +21 -0
  268. package/src/__tests__/occlusion-query-pool.unit.test.ts +95 -0
  269. package/src/__tests__/occlusion-query-ticket.integration.test.ts +47 -0
  270. package/src/__tests__/occlusion-query.dawn.test.ts +16 -0
  271. package/src/__tests__/occlusion-query.evidence.ts +178 -0
  272. package/src/__tests__/occlusion-query.rhi-null.test.ts +31 -0
  273. package/src/__tests__/occlusion-runtime.unit.test.ts +120 -0
  274. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +196 -0
  275. package/src/__tests__/probe-bindings.unit.test.ts +200 -0
  276. package/src/__tests__/probe-blend-numeric.unit.test.ts +67 -0
  277. package/src/__tests__/probe-blend-oracle.unit.test.ts +136 -0
  278. package/src/__tests__/probe-capacity.integration.test.ts +97 -0
  279. package/src/__tests__/probe-recovery.integration.test.ts +104 -0
  280. package/src/__tests__/probe-scene-dirty.integration.test.ts +211 -0
  281. package/src/__tests__/profiler-phase-catalog.test.ts +4 -1
  282. package/src/__tests__/profiler-phase-token.test.ts +1 -0
  283. package/src/__tests__/rect-area-boundary.unit.test.ts +24 -0
  284. package/src/__tests__/reflection-fallback-graph.unit.test.ts +67 -0
  285. package/src/__tests__/reflection-fallback-inspection.unit.test.ts +77 -0
  286. package/src/__tests__/reflection-fallback-lifecycle.integration.test.ts +21 -0
  287. package/src/__tests__/reflection-fallback-projection.unit.test.ts +90 -0
  288. package/src/__tests__/reflection-fallback-source.unit.test.ts +32 -0
  289. package/src/__tests__/reflection-probe-owner-placement.unit.test.ts +39 -0
  290. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +120 -0
  291. package/src/__tests__/render-error-code-owner.test-d.ts +1 -0
  292. package/src/__tests__/render-feature-docs.unit.test.ts +12 -7
  293. package/src/__tests__/render-graph-fallback.integration.test.ts +24 -0
  294. package/src/__tests__/render-perf/extended-lighting.bench.test.ts +66 -0
  295. package/src/__tests__/render-scene-projection.unit.test.ts +447 -18
  296. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +76 -0
  297. package/src/__tests__/skin-motion-regression.unit.test.ts +22 -0
  298. package/src/__tests__/skinned-shadow-caster.test.ts +2 -2
  299. package/src/__tests__/spot-modifier-projection.unit.test.ts +70 -0
  300. package/src/__tests__/ssr-admission-matrix.integration.test.ts +206 -0
  301. package/src/__tests__/ssr-composition.unit.test.ts +46 -0
  302. package/src/__tests__/ssr-fallback-zero-work.bench.test.ts +94 -0
  303. package/src/__tests__/ssr-recovery-errors.unit.test.ts +58 -0
  304. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +67 -0
  305. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +7 -0
  306. package/src/__tests__/standard-lighting-inspection.integration.test.ts +1 -1
  307. package/src/__tests__/standard-lighting-write.unit.test.ts +66 -2
  308. package/src/__tests__/temporal-fallback-generation.integration.test.ts +16 -0
  309. package/src/__tests__/temporal-view.unit.test.ts +3 -3
  310. package/src/__tests__/transmission-prewarm.unit.test.ts +69 -0
  311. package/src/__tests__/visibility-budget.integration.test.ts +140 -0
  312. package/src/__tests__/visibility-budget.unit.test.ts +70 -0
  313. package/src/__tests__/visibility-extract.integration.test.ts +11 -6
  314. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +6 -3
  315. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +31 -28
  316. package/src/assembly/backend-contract.ts +13 -0
  317. package/src/assembly/device-feature-admission.ts +16 -9
  318. package/src/assembly/factory.ts +547 -93
  319. package/src/assembly/host-contract.ts +10 -0
  320. package/src/cluster-binner.ts +31 -0
  321. package/src/components/__tests__/lighting-authoring.unit.test.ts +39 -0
  322. package/src/components/__tests__/rect-area-light.unit.test.ts +33 -0
  323. package/src/components/index.ts +3 -0
  324. package/src/components/light-helpers.ts +52 -1
  325. package/src/components/light-probe.ts +13 -0
  326. package/src/components/rect-area-light.ts +15 -0
  327. package/src/components/scene-instance.ts +6 -0
  328. package/src/components/spot-light.ts +3 -0
  329. package/src/device/__tests__/extended-lighting-recovery.unit.test.ts +24 -0
  330. package/src/device/gpu-residency.ts +140 -9
  331. package/src/device/lifecycle-transaction.ts +12 -0
  332. package/src/environment/lifecycle.ts +8 -1
  333. package/src/errors/gpu-driven.ts +92 -0
  334. package/src/errors/index.ts +1 -0
  335. package/src/errors/recover.ts +32 -0
  336. package/src/errors/render.ts +55 -1
  337. package/src/extract/camera.ts +3 -3
  338. package/src/extract/gpu-driven.ts +108 -0
  339. package/src/extract/visibility.ts +0 -23
  340. package/src/gpu-driven/batch-topology.ts +206 -28
  341. package/src/gpu-driven/prepared-draw.ts +208 -0
  342. package/src/gpu-driven/production-raster.ts +953 -41
  343. package/src/gpu-driven/view-gpu.ts +411 -29
  344. package/src/gpu-scene-schema.ts +13 -0
  345. package/src/gpu-scene.ts +134 -65
  346. package/src/gpu-usage.ts +0 -1
  347. package/src/hdrp-buffers.ts +6 -6
  348. package/src/index.ts +74 -0
  349. package/src/inspection-types.ts +220 -1
  350. package/src/internal.ts +38 -0
  351. package/src/light-buffer-layout.ts +154 -324
  352. package/src/materials.ts +16 -2
  353. package/src/pbr-pipeline.ts +151 -45
  354. package/src/pipeline/standard-forward-lane.ts +47 -0
  355. package/src/pipeline/standard-lighting/graph.ts +2 -1
  356. package/src/pipeline/standard-lighting/light-frame.ts +3 -2
  357. package/src/pipeline/standard-lighting/topology.ts +1 -1
  358. package/src/pipeline/standard-lighting/transport.ts +2 -1
  359. package/src/pipeline/standard-pipeline.ts +2 -0
  360. package/src/pipeline-builder.ts +5 -2
  361. package/src/pipeline-spec.ts +12 -1
  362. package/src/plugin.ts +4 -0
  363. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +56 -0
  364. package/src/prepare/extended-lighting/ltc-resources.ts +136 -0
  365. package/src/prepare/extended-lighting/resources.ts +389 -0
  366. package/src/prepare/extended-lighting/spot-modifiers.ts +194 -0
  367. package/src/prepare/extended-lighting/state.ts +92 -0
  368. package/src/record/__tests__/gpu-timing.unit.test.ts +177 -0
  369. package/src/record/__tests__/temporal-payload.unit.test.ts +23 -1
  370. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +20 -1
  371. package/src/record/frame-lighting.ts +252 -53
  372. package/src/record/frame-snapshot.ts +57 -12
  373. package/src/record/frame.ts +410 -93
  374. package/src/record/gpu-timing.ts +108 -44
  375. package/src/record/helpers.ts +0 -58
  376. package/src/record/main-pass-geometry.ts +204 -39
  377. package/src/record/main-pass-material.ts +8 -1
  378. package/src/record/main-pass-sprite-draws.ts +52 -85
  379. package/src/record/mesh-ssbo.ts +47 -36
  380. package/src/record/render-context.ts +65 -4
  381. package/src/record/shadow-pass.ts +66 -40
  382. package/src/record/typed-frame-graph.ts +225 -83
  383. package/src/record/view-ubo.ts +14 -24
  384. package/src/reflection/inspection.ts +67 -0
  385. package/src/reflection/projection.ts +196 -1
  386. package/src/reflection/record-owner.ts +1283 -0
  387. package/src/render-contract.ts +17 -5
  388. package/src/render-pipeline.ts +5 -0
  389. package/src/render-system-extract.ts +493 -134
  390. package/src/render-system.ts +811 -615
  391. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +36 -0
  392. package/src/scene/probe-blend-record.ts +54 -0
  393. package/src/scene/probe-blend.ts +813 -0
  394. package/src/scene/render-scene-types.ts +0 -7
  395. package/src/scene/render-scene.ts +848 -183
  396. package/src/scene/visibility/bounds.ts +57 -0
  397. package/src/scene/visibility/budget.ts +40 -0
  398. package/src/scene/visibility/facet.ts +400 -0
  399. package/src/scene/visibility/gpu-frame-timing.ts +101 -0
  400. package/src/scene/visibility/gpu-lod.ts +119 -0
  401. package/src/scene/visibility/inspection.ts +313 -0
  402. package/src/scene/visibility/lod-selector.ts +84 -0
  403. package/src/scene/visibility/occlusion-confidence.ts +478 -0
  404. package/src/scene/visibility/occlusion-pass.ts +165 -0
  405. package/src/scene/visibility/occlusion-query-pool.ts +178 -0
  406. package/src/scene/visibility/occlusion-runtime.ts +672 -0
  407. package/src/scene/visibility/types.ts +50 -0
  408. package/src/ssr/admission.ts +451 -0
  409. package/src/ssr/composition.ts +73 -0
  410. package/src/ssr/errors.ts +76 -0
  411. package/src/ssr/identity.ts +12 -0
  412. package/src/systems/skin-palette-allocator.ts +1 -1
  413. package/src/temporal/frame-coordinator.ts +26 -0
  414. package/src/temporal/frame.ts +14 -1
  415. package/src/temporal/view.ts +11 -0
  416. package/src/tilemap-chunk-extract-system.ts +4 -4
  417. package/src/typed-render-graph-primitives.ts +48 -0
  418. package/src/volume/passes.ts +46 -49
  419. package/dist/.tsbuildinfo +0 -1
  420. package/dist/chunk-5YADUCA7.mjs.map +0 -1
  421. package/dist/chunk-6OPXDQ2E.mjs.map +0 -1
  422. package/dist/chunk-BXBQQKFQ.mjs +0 -67
  423. package/dist/chunk-BXBQQKFQ.mjs.map +0 -1
  424. package/dist/chunk-FS2KNUQH.mjs.map +0 -1
  425. package/dist/chunk-PCHBPYML.mjs.map +0 -1
  426. package/dist/chunk-VFPLTKCU.mjs.map +0 -1
@@ -19,19 +19,10 @@
19
19
  import { numMipLevels, resolveAssetHandle } from '@forgeax/engine-assets-runtime';
20
20
  import type { World } from '@forgeax/engine-ecs';
21
21
  import type { RenderReadLease } from '@forgeax/engine-ecs/projection';
22
+ import { mat4 } from '@forgeax/engine-math';
22
23
  import type { RecorderSession } from '@forgeax/engine-profiler';
23
24
  import type { CompiledRenderGraphInfo } from '@forgeax/engine-render-graph';
24
- import type {
25
- BindGroup,
26
- BindGroupEntry,
27
- BindGroupLayout,
28
- Buffer,
29
- RenderPipeline,
30
- Sampler,
31
- Texture,
32
- TextureFormat,
33
- TextureView,
34
- } from '@forgeax/engine-rhi';
25
+ import type { BindGroupEntry, BindGroupLayout, Buffer, RenderPipeline } from '@forgeax/engine-rhi';
35
26
  import { err, ok, type Result, RhiError } from '@forgeax/engine-rhi';
36
27
  import {
37
28
  derive,
@@ -82,10 +73,14 @@ import { GpuDrivenProduction } from './gpu-driven/production-raster';
82
73
  import type {
83
74
  BloomInspection,
84
75
  DirectionalShadowInspection,
76
+ GpuDrivenProductionInspection,
77
+ LodOcclusionInspection,
78
+ LodOcclusionInspectionRow,
79
+ LodOcclusionWorldInspection,
85
80
  MotionBlurInspection,
86
81
  ReflectionProbeInspection,
87
- ReflectionProbeSelectionInspection,
88
82
  RenderSceneInspection,
83
+ SsrDependenciesInspection,
89
84
  TemporalTargetInspection,
90
85
  TransmissionInspection,
91
86
  } from './inspection-types';
@@ -103,25 +98,19 @@ import {
103
98
  import { createPointsLinesLaneAdapter, type PointsLinesBackend } from './points-lines/record';
104
99
  import type { PointsLinesRetainedSnapshot } from './points-lines/snapshot';
105
100
  import type { PointsLinesRecordOwner, PointsLinesRecordSubmission } from './record/render-context';
101
+ import { ReflectionProbeRecordOwner } from './reflection/record-owner';
102
+ import {
103
+ OcclusionRenderRuntime,
104
+ type OcclusionRuntimeCandidate,
105
+ } from './scene/visibility/occlusion-runtime';
106
+ import { primitiveKey, viewKey } from './scene/visibility/types';
106
107
  import { inspectVolumetricFog } from './volume/inspection';
107
108
 
108
109
  export type { RenderSceneInspection } from './inspection-types';
109
110
 
110
111
  import { deriveVertexLayoutProjection } from '@forgeax/engine-geometry';
111
- import { mat4, vec3 } from '@forgeax/engine-math';
112
- import { buildCubeCameraFaceViews } from './capture/cube-views';
113
112
  import { createCubeCaptureScheduler } from './capture/scheduler';
114
- import {
115
- createFaceUniformsBuffer,
116
- createPrefilterUniformsBuffer,
117
- writeAllFaceUniforms,
118
- writeAllPrefilterUniforms,
119
- } from './device/gpu-residency';
120
113
  import type { EnvironmentInspection } from './environment/inspection';
121
- import {
122
- GPU_TEXTURE_USAGE_COPY_SRC,
123
- GPU_TEXTURE_USAGE_RENDER_ATTACHMENT_AND_TEXTURE_BINDING,
124
- } from './gpu-texture-usage';
125
114
  import {
126
115
  GPU_BUFFER_USAGE_COPY_DST,
127
116
  GPU_BUFFER_USAGE_INDEX,
@@ -129,13 +118,6 @@ import {
129
118
  GPU_BUFFER_USAGE_VERTEX,
130
119
  } from './gpu-usage';
131
120
  import { resetHdrpBuffers } from './hdrp-buffers';
132
- import {
133
- CUBEMAP_FACE_VERTICES,
134
- createIblPipelines,
135
- getOrCreateIblCache,
136
- PREFILTER_MIP_LEVELS,
137
- PREFILTER_SIZE,
138
- } from './ibl/IblPipelineCache';
139
121
  import { assembleMaterialWithSkylightEntries } from './ibl/skylight-bind-group';
140
122
  import { disposeInstanceBuffers, disposeTransientInstanceBuffers } from './instance-buffer-cache';
141
123
  import {
@@ -168,7 +150,7 @@ import type {
168
150
  GraphTargetCaptureRequest,
169
151
  RenderFrameState,
170
152
  } from './record/frame-snapshot';
171
- import { getTextureIdentity } from './record/frame-snapshot';
153
+ import { getTextureIdentity, worldEntityKey } from './record/frame-snapshot';
172
154
  import type { GpuPassTimingReason } from './record/gpu-pass-timing/errors';
173
155
  import type {
174
156
  GpuPassTimingCapture,
@@ -176,28 +158,15 @@ import type {
176
158
  GpuPassTimingSession,
177
159
  } from './record/gpu-pass-timing/session';
178
160
  import type { GpuTimingCapture } from './record/gpu-timing';
161
+ import { computeProjectionMatrix, computeViewMatrix } from './record/helpers';
179
162
  import { applyParamSnapshotToUbo, residentTextureView } from './record/main-pass-material';
180
- import type { ReflectionProbeRecordState, RenderSystemInternals } from './record/render-context';
181
- import type { CubeCaptureGraphState, ReflectionProbeGraphWork } from './record/typed-frame-graph';
163
+ import type { RenderSystemInternals } from './record/render-context';
164
+ import type { CubeCaptureGraphState } from './record/typed-frame-graph';
182
165
  import {
183
166
  type RenderFeatureGraphCandidate,
184
167
  resetRenderFeatureGraphState,
185
168
  settleVolumetricFogGraphCandidate,
186
169
  } from './record/typed-frame-graph';
187
- import { REFLECTION_PROBE_VIEW_SLOT_BASE, VIEW_UNIFORM_SLOT_STRIDE } from './record/view-ubo';
188
- import {
189
- advanceProbeFilter,
190
- commitProbeFilterStep,
191
- createProbeFilterState,
192
- type ProbeFilterState,
193
- } from './reflection/filter';
194
- import { buildReflectionProbeTable } from './reflection/gpu-table';
195
- import {
196
- admitReflectionProbe,
197
- DEFAULT_REFLECTION_PROBE_LIMITS,
198
- type ReflectionProbeFact,
199
- type ReflectionProbeSelectionResult,
200
- } from './reflection/projection';
201
170
  import {
202
171
  type CameraSnapshot,
203
172
  type DrawOwnerOptions,
@@ -215,7 +184,9 @@ import type {
215
184
  } from './render-system-extract';
216
185
  import { extractFrames } from './render-system-extract';
217
186
  import { PersistentRenderScene } from './scene/render-scene';
187
+ import { inspectLodOcclusion } from './scene/visibility/inspection';
218
188
  import { resolveSsaoParameters } from './ssao-config';
189
+ import { projectSsrDependencies, resolveSsrAdmissionGeneration } from './ssr/admission';
219
190
  import {
220
191
  getTransparentSortConfig,
221
192
  TRANSPARENT_SORT_MODE_DISTANCE,
@@ -239,6 +210,121 @@ import {
239
210
  inspectTransmissionFromAdmission,
240
211
  } from './transmission/inspection';
241
212
 
213
+ interface LodInspectionQuerySummary {
214
+ readonly used: number;
215
+ readonly capacity: number;
216
+ }
217
+
218
+ function createLodWorldInspections(
219
+ worlds: readonly World[],
220
+ renderables: readonly RenderableSnapshot[],
221
+ submittedRenderables: readonly RenderableSnapshot[],
222
+ camera: CameraSnapshot,
223
+ frameId: number,
224
+ worldKeys: readonly number[],
225
+ slots: ReadonlyMap<number, { readonly slot: number; readonly generation: number }>,
226
+ query: LodInspectionQuerySummary,
227
+ fallback: LodOcclusionInspection['fallback'],
228
+ degradation: LodOcclusionInspection['degradation'],
229
+ ): readonly LodOcclusionWorldInspection[] {
230
+ // The CPU projection knows which renderables belong to each World, but it
231
+ // does not receive the GPU selector/query counters split by World from the
232
+ // same submit. Keep these rows available for diagnostics while explicitly
233
+ // marking their attribution unavailable; producers must not promote them
234
+ // to World-reorder evidence by inferring facts from array order.
235
+ const attribution = Object.freeze({
236
+ status: 'unavailable' as const,
237
+ reason: 'projection-only' as const,
238
+ });
239
+ const submitted = new Set(submittedRenderables);
240
+ const sampleLimit = 64;
241
+ const accumulators = worlds.map(() => ({
242
+ allCount: 0,
243
+ allVisible: 0,
244
+ allSamples: [] as RenderableSnapshot[],
245
+ lodCount: 0,
246
+ lodVisible: 0,
247
+ lodSamples: [] as RenderableSnapshot[],
248
+ }));
249
+ // Keep this projection bounded: the old implementation filtered the full
250
+ // 100k renderable list twice per World on every frame and retained another
251
+ // Set-backed traversal just to derive the six counters below. One pass is
252
+ // enough because the inspection only needs counts, the first row identity,
253
+ // and at most 64 detached samples.
254
+ for (const renderable of renderables) {
255
+ const accumulator = accumulators[renderable.worldId];
256
+ if (accumulator === undefined) continue;
257
+ const visible = submitted.has(renderable);
258
+ accumulator.allCount += 1;
259
+ if (visible) accumulator.allVisible += 1;
260
+ if (accumulator.allSamples.length < sampleLimit) accumulator.allSamples.push(renderable);
261
+ if ((renderable.lods?.length ?? 0) > 0) {
262
+ accumulator.lodCount += 1;
263
+ if (visible) accumulator.lodVisible += 1;
264
+ if (accumulator.lodSamples.length < sampleLimit) accumulator.lodSamples.push(renderable);
265
+ }
266
+ }
267
+ const result: LodOcclusionWorldInspection[] = [];
268
+ for (let worldId = 0; worldId < worlds.length; worldId += 1) {
269
+ const world = worlds[worldId];
270
+ if (world === undefined) continue;
271
+ const accumulator = accumulators[worldId];
272
+ if (accumulator === undefined || accumulator.allCount === 0) {
273
+ result.push(
274
+ Object.freeze({ attachmentId: world.identity, rows: Object.freeze([]), attribution }),
275
+ );
276
+ continue;
277
+ }
278
+ const usesLod = accumulator.lodCount > 0;
279
+ const candidateCount = usesLod ? accumulator.lodCount : accumulator.allCount;
280
+ const visible = usesLod ? accumulator.lodVisible : accumulator.allVisible;
281
+ const samples = usesLod ? accumulator.lodSamples : accumulator.allSamples;
282
+ const first = samples[0];
283
+ const slot =
284
+ first === undefined
285
+ ? undefined
286
+ : slots.get(worldEntityKey(worldKeys[first.worldId] ?? first.worldId, first.entityKey));
287
+ const row: LodOcclusionInspectionRow = {
288
+ root: {
289
+ guid: first === undefined ? 'none' : `asset-handle:${first.assetHandle}`,
290
+ sourceKey: `render-world:${world.identity}`,
291
+ },
292
+ view: {
293
+ attachmentId: world.identity,
294
+ cameraEntity: world === worlds[camera.worldId ?? 0] ? (camera.entityKey ?? 0) : 0,
295
+ viewRole: 'main',
296
+ viewGeneration: camera.historyVersion ?? 0,
297
+ },
298
+ slot: { primitiveSlot: slot?.slot ?? 0, slotGeneration: slot?.generation ?? 0 },
299
+ generation: frameId,
300
+ count: { candidates: candidateCount, visible, occluded: candidateCount - visible },
301
+ lodHistogram: [{ level: 0, count: candidateCount }],
302
+ queryLatencyUs: { median: 0, p95: 0, last: 0 },
303
+ pagePressure: query,
304
+ fallback,
305
+ degradation,
306
+ samples: samples.map((candidate, index) => ({
307
+ primitiveSlot:
308
+ slots.get(
309
+ worldEntityKey(worldKeys[candidate.worldId] ?? candidate.worldId, candidate.entityKey),
310
+ )?.slot ?? index,
311
+ level: 0,
312
+ visible: submitted.has(candidate),
313
+ })),
314
+ };
315
+ result.push(
316
+ Object.freeze({
317
+ attachmentId: world.identity,
318
+ rows: Object.freeze([row]),
319
+ attribution,
320
+ }),
321
+ );
322
+ }
323
+ return Object.freeze(
324
+ result.sort((left, right) => left.attachmentId.localeCompare(right.attachmentId)),
325
+ );
326
+ }
327
+
242
328
  type TimingSessionHost = RenderSystemInternals & {
243
329
  gpuPassTimingSession?: GpuPassTimingSession | undefined;
244
330
  gpuPassTimingCapture?: GpuPassTimingCapture | undefined;
@@ -323,7 +409,7 @@ function sortTransparentDispatch(
323
409
  const camPos = camera.position;
324
410
 
325
411
  // Squared camera distance per entry (negated → ascending = back-to-front).
326
- // D-3: world position = translation column of Transform.world (m[12,13,14]).
412
+ // D-3: world position = translation column of GlobalTransform.world (m[12,13,14]).
327
413
  const negDistSq = (entry: DispatchEntry): number => {
328
414
  const tx = renderables[entry.renderableIndex]?.transform;
329
415
  if (tx === undefined) return 0;
@@ -429,6 +515,8 @@ export interface RenderSystem {
429
515
  observeCurrentFrame(
430
516
  options: FrameObservationOptions,
431
517
  ): Promise<Result<FrameObservation, ObservationUnavailableError>>;
518
+ /** Resolve the renderer-owned GPU LOD counters for the last accepted submit. */
519
+ observeLodOcclusion(): Promise<void>;
432
520
  /** Internal same-frame graph target access for backend diagnostics. */
433
521
  getCurrentGraphTarget(name: string): CurrentGraphTarget | undefined;
434
522
  /** Internal test capture copied into the draw's command submission. */
@@ -446,6 +534,10 @@ export interface RenderSystem {
446
534
  /** Persistent scene maintenance evidence from the ordinary single-World path. */
447
535
  readonly renderScene: RenderSceneInspection;
448
536
  readonly reflectionProbes: ReflectionProbeInspection;
537
+ /** Live producer/RHI/temporal receipts projected through SSR admission. */
538
+ readonly ssrDependencies: SsrDependenciesInspection;
539
+ /** Producer completion fence included in the public FrameReceipt. */
540
+ readonly reflectionFallbackCompletion: Promise<void> | undefined;
449
541
  /** True while a progressive CubeCamera candidate still owns unfinished faces. */
450
542
  isCubeCapturePending(target: RenderTarget): boolean;
451
543
  readonly environment: EnvironmentInspection;
@@ -453,6 +545,9 @@ export interface RenderSystem {
453
545
  readonly bloom: BloomInspection;
454
546
  readonly motionBlurInspection: MotionBlurInspection | undefined;
455
547
  readonly temporalTargetInspection: TemporalTargetInspection | undefined;
548
+ readonly lodOcclusionInspection: LodOcclusionInspection | undefined;
549
+ /** Bounded GPU-driven counters without materializing the persistent scene table. */
550
+ readonly gpuDrivenInspection: GpuDrivenProductionInspection;
456
551
  /** Last completed transmission candidate facts, detached from GPU handles. */
457
552
  readonly transmission: TransmissionInspection | undefined;
458
553
  /** Single production projection of Directional author/extract/record facts. */
@@ -1022,551 +1117,6 @@ class StandardPointsLinesOwner implements PointsLinesRecordOwner {
1022
1117
  }
1023
1118
  }
1024
1119
 
1025
- type ProbeCubeSet = {
1026
- readonly texture: Texture;
1027
- readonly cubeView: TextureView;
1028
- readonly faceViews: readonly TextureView[];
1029
- readonly mipViews: readonly (readonly TextureView[])[];
1030
- readonly depthTexture: Texture;
1031
- readonly depthView: TextureView;
1032
- readonly size: number;
1033
- };
1034
-
1035
- type ProbeGpuResource = {
1036
- fact: ReflectionProbeFact;
1037
- index: number;
1038
- filter: ProbeFilterState;
1039
- rawFaceCursor: number;
1040
- readonly raw: ProbeCubeSet;
1041
- active: ProbeCubeSet | undefined;
1042
- candidate: ProbeCubeSet;
1043
- spare: ProbeCubeSet | undefined;
1044
- readonly sampler: Sampler;
1045
- readonly uniformBuffer: Buffer;
1046
- readonly filterGroup1: BindGroup;
1047
- pending?: {
1048
- readonly rawCaptureFace: number | undefined;
1049
- readonly step: ReturnType<typeof advanceProbeFilter>;
1050
- };
1051
- };
1052
-
1053
- type ProbeSharedResources = {
1054
- readonly faceUniforms: Buffer;
1055
- readonly prefilterUniforms: Buffer;
1056
- readonly vertexBuffer: Buffer;
1057
- readonly sampler: Sampler;
1058
- readonly filterPipeline: RenderPipeline;
1059
- readonly cubeGroup1Bgl: BindGroupLayout;
1060
- readonly prefilterGroup0Bgl: BindGroupLayout;
1061
- };
1062
-
1063
- /** Renderer-owned raw capture and incremental PMREM/LKG promotion owner. */
1064
- class ReflectionProbeRecordOwner {
1065
- private readonly resources = new Map<string, ProbeGpuResource>();
1066
- private shared: ProbeSharedResources | undefined;
1067
- private pipelineWarmup: Promise<void> | undefined;
1068
- private pipelineWarmupFailure: string | undefined;
1069
- private pipelineWarmupAttempts = 0;
1070
- private lastFactCount = 0;
1071
- private lastAcceptedCount = 0;
1072
- private lastScheduledRawFaces = 0;
1073
- private lastScheduledFilteredSteps = 0;
1074
- private lastSelection: ReflectionProbeSelectionInspection = { kind: 'skylight' };
1075
- private readonly outputFormat: TextureFormat;
1076
-
1077
- constructor(private readonly internals: RenderSystemInternals) {
1078
- this.outputFormat = internals.device.caps.rgba16floatRenderable ? 'rgba16float' : 'rgba8unorm';
1079
- }
1080
-
1081
- inspect(): ReflectionProbeInspection {
1082
- let rawFacesCaptured = 0;
1083
- let filteredStepsCompleted = 0;
1084
- const filteredMipLevels = new Set<number>();
1085
- let activeCount = 0;
1086
- for (const resource of this.resources.values()) {
1087
- rawFacesCaptured += resource.rawFaceCursor;
1088
- filteredStepsCompleted += resource.filter.cursor;
1089
- const completedMipCount = Math.min(
1090
- PREFILTER_MIP_LEVELS,
1091
- Math.floor(resource.filter.cursor / resource.filter.faceCount),
1092
- );
1093
- for (let mip = 0; mip < completedMipCount; mip += 1) filteredMipLevels.add(mip);
1094
- if (resource.active !== undefined) activeCount += 1;
1095
- }
1096
- return {
1097
- selection: this.lastSelection,
1098
- factCount: this.lastFactCount,
1099
- acceptedCount: this.lastAcceptedCount,
1100
- activeCount,
1101
- rawFacesCaptured,
1102
- filteredStepsCompleted,
1103
- filteredMipLevels: [...filteredMipLevels].sort((a, b) => a - b),
1104
- scheduledRawFaces: this.lastScheduledRawFaces,
1105
- scheduledFilteredSteps: this.lastScheduledFilteredSteps,
1106
- pipelineReady: this.shared !== undefined,
1107
- pipelineWarmupAttempts: this.pipelineWarmupAttempts,
1108
- ...(this.pipelineWarmupFailure === undefined
1109
- ? {}
1110
- : { pipelineWarmupFailure: this.pipelineWarmupFailure }),
1111
- };
1112
- }
1113
-
1114
- private ensurePipelineWarmup(): void {
1115
- if (this.shared !== undefined || this.pipelineWarmup !== undefined) return;
1116
- const factory = this.internals.shaderModuleFactory;
1117
- if (factory === undefined) return;
1118
- const attempt = this.pipelineWarmupAttempts + 1;
1119
- this.pipelineWarmupAttempts = attempt;
1120
- this.pipelineWarmup = createIblPipelines(
1121
- this.internals.deviceScope,
1122
- this.internals.device,
1123
- (device, descriptor) => {
1124
- const labeled = {
1125
- ...descriptor,
1126
- label: `${descriptor.label ?? 'ibl'}-reflection-probe-${attempt}`,
1127
- };
1128
- return this.internals.createShaderModule === undefined
1129
- ? Promise.resolve(factory.createShaderModule(labeled))
1130
- : this.internals.createShaderModule(device, labeled);
1131
- },
1132
- this.outputFormat,
1133
- ).then((result) => {
1134
- if (!result.ok) {
1135
- this.pipelineWarmupFailure = `${result.error.code}: ${result.error.hint}`;
1136
- console.warn('[forgeax] ReflectionProbe IBL warmup retry', this.pipelineWarmupFailure);
1137
- this.pipelineWarmup = undefined;
1138
- return;
1139
- }
1140
- this.shared = this.createSharedResources(result.value.prefilterPipeline);
1141
- if (this.shared === undefined) {
1142
- this.pipelineWarmupFailure = 'renderer-owned IBL resources could not be allocated';
1143
- console.warn('[forgeax] ReflectionProbe IBL resource allocation retry');
1144
- this.pipelineWarmup = undefined;
1145
- }
1146
- });
1147
- }
1148
-
1149
- prepare(
1150
- facts: readonly ReflectionProbeFact[],
1151
- selections: ReadonlyMap<string, ReflectionProbeSelectionResult>,
1152
- displayCamera: CameraSnapshot | undefined,
1153
- ): ReflectionProbeRecordState {
1154
- this.lastFactCount = facts.length;
1155
- this.lastScheduledRawFaces = 0;
1156
- this.lastScheduledFilteredSteps = 0;
1157
- const selectedProbe = [...selections.values()]
1158
- .filter(
1159
- (selection): selection is Extract<ReflectionProbeSelectionResult, { kind: 'probe' }> =>
1160
- selection.kind === 'probe',
1161
- )
1162
- .sort((a, b) => a.worldId - b.worldId || a.entityKey - b.entityKey)[0];
1163
- this.lastSelection =
1164
- selectedProbe === undefined
1165
- ? { kind: 'skylight' }
1166
- : { kind: 'probe', worldId: selectedProbe.worldId, entityKey: selectedProbe.entityKey };
1167
- if (facts.length > 0) this.ensurePipelineWarmup();
1168
- void this.pipelineWarmup;
1169
- const ordered = facts
1170
- .slice()
1171
- .sort((a, b) => a.worldId - b.worldId || a.entityKey - b.entityKey);
1172
- let acceptedCount = 0;
1173
- let acceptedBytes = 0;
1174
- const accepted = new Map<string, ProbeGpuResource>();
1175
- for (const fact of ordered) {
1176
- const admission = admitReflectionProbe(
1177
- fact,
1178
- { acceptedCount, acceptedBytes },
1179
- DEFAULT_REFLECTION_PROBE_LIMITS,
1180
- );
1181
- if (!admission.ok) continue;
1182
- const key = `${fact.worldId}:${fact.entityKey}`;
1183
- const resource = this.resourceFor(key, fact, acceptedCount);
1184
- if (resource === undefined) continue;
1185
- acceptedCount += 1;
1186
- acceptedBytes = admission.acceptedBytes;
1187
- accepted.set(key, resource);
1188
- }
1189
- for (const [key, resource] of this.resources) {
1190
- if (!accepted.has(key)) {
1191
- this.destroyProbeResource(resource);
1192
- this.resources.delete(key);
1193
- } else resource.index = [...accepted.keys()].indexOf(key);
1194
- }
1195
- const rows = [...accepted.values()]
1196
- .map((resource) => {
1197
- const active = resource.active;
1198
- if (active === undefined) return undefined;
1199
- return {
1200
- primitiveKey: `${resource.fact.worldId}:${resource.fact.entityKey}`,
1201
- index: resource.index,
1202
- worldId: resource.fact.worldId,
1203
- entityKey: resource.fact.entityKey,
1204
- center: resource.fact.center,
1205
- halfExtents: resource.fact.halfExtents,
1206
- intensity: resource.fact.intensity,
1207
- generation: resource.filter.activeGeneration,
1208
- filteredView: active.cubeView,
1209
- sampler: resource.sampler,
1210
- uniformBuffer: resource.uniformBuffer,
1211
- };
1212
- })
1213
- .filter((row): row is NonNullable<typeof row> => row !== undefined);
1214
- const table = buildReflectionProbeTable(rows);
1215
- const work: ReflectionProbeGraphWork[] = [];
1216
- for (const resource of accepted.values()) {
1217
- const rawCaptureFace =
1218
- resource.rawFaceCursor < resource.raw.faceViews.length ? resource.rawFaceCursor : undefined;
1219
- const step =
1220
- resource.rawFaceCursor >= resource.raw.faceViews.length
1221
- ? advanceProbeFilter(resource.filter)
1222
- : undefined;
1223
- if (this.shared === undefined) continue;
1224
- if (rawCaptureFace === undefined && step === undefined) continue;
1225
- const filterGroup0 =
1226
- step === undefined
1227
- ? undefined
1228
- : this.internals.device.createBindGroup({
1229
- label: `reflection-probe-${resource.index}-pmrem-${step.mipLevel}-${step.faceIndex}`,
1230
- layout: this.shared.prefilterGroup0Bgl,
1231
- entries: [
1232
- {
1233
- binding: 0,
1234
- resource: {
1235
- kind: 'buffer',
1236
- value: {
1237
- buffer: this.shared.faceUniforms,
1238
- offset: step.faceIndex * 256,
1239
- size: 64,
1240
- },
1241
- },
1242
- },
1243
- {
1244
- binding: 1,
1245
- resource: {
1246
- kind: 'buffer',
1247
- value: {
1248
- buffer: this.shared.prefilterUniforms,
1249
- offset: (step.mipLevel * 6 + step.faceIndex) * 256,
1250
- size: 16,
1251
- },
1252
- },
1253
- },
1254
- ],
1255
- });
1256
- if (filterGroup0 !== undefined && !filterGroup0.ok) continue;
1257
- if (resource.pending !== undefined) continue;
1258
- resource.pending = { rawCaptureFace, step };
1259
- if (rawCaptureFace !== undefined) this.lastScheduledRawFaces += 1;
1260
- if (step !== undefined) this.lastScheduledFilteredSteps += 1;
1261
- const face =
1262
- rawCaptureFace === undefined
1263
- ? undefined
1264
- : buildCubeCameraFaceViews({
1265
- position: resource.fact.center,
1266
- near: 0.1,
1267
- far: 100,
1268
- })[rawCaptureFace];
1269
- const faceCamera =
1270
- face === undefined || displayCamera === undefined
1271
- ? undefined
1272
- : {
1273
- ...displayCamera,
1274
- position: vec3.create(
1275
- resource.fact.center[0] ?? 0,
1276
- resource.fact.center[1] ?? 0,
1277
- resource.fact.center[2] ?? 0,
1278
- ),
1279
- world: mat4.invert(mat4.create(), face.view),
1280
- fov: Math.PI / 2,
1281
- aspect: 1,
1282
- near: face.near,
1283
- far: face.far,
1284
- projection: 'perspective' as const,
1285
- };
1286
- work.push({
1287
- probeIndex: resource.index,
1288
- rawTexture: resource.raw.texture,
1289
- rawCubeView: resource.raw.cubeView,
1290
- rawFaceViews: resource.raw.faceViews,
1291
- rawDepthTexture: resource.raw.depthTexture,
1292
- filteredTexture: resource.candidate.texture,
1293
- filteredCubeView: resource.candidate.cubeView,
1294
- filteredFaceViewsByMip: resource.candidate.mipViews,
1295
- outputFormat: this.outputFormat,
1296
- sampler: resource.sampler,
1297
- filterPipeline: this.shared.filterPipeline,
1298
- filterGroup0: filterGroup0?.value,
1299
- filterGroup1: resource.filterGroup1,
1300
- cubeVertexBuffer: this.shared.vertexBuffer,
1301
- rawCaptureFace,
1302
- rawDepthView: resource.raw.depthView,
1303
- rawSize: resource.raw.size,
1304
- ...(faceCamera === undefined ? {} : { faceCamera }),
1305
- ...(rawCaptureFace === undefined
1306
- ? {}
1307
- : {
1308
- viewBindGroupDynamicOffset:
1309
- (REFLECTION_PROBE_VIEW_SLOT_BASE + resource.index) * VIEW_UNIFORM_SLOT_STRIDE,
1310
- }),
1311
- filteredSize: resource.candidate.size,
1312
- step,
1313
- });
1314
- }
1315
- return {
1316
- table,
1317
- selections,
1318
- graph: { work },
1319
- completeSubmission: (submitted, completed) => this.completeSubmission(submitted, completed),
1320
- };
1321
- }
1322
-
1323
- private completeSubmission(submitted: boolean, completed?: Promise<unknown>): void {
1324
- if (completed !== undefined) {
1325
- void completed.then(
1326
- () => this.completeSubmission(submitted),
1327
- () => this.completeSubmission(false),
1328
- );
1329
- return;
1330
- }
1331
- for (const resource of this.resources.values()) {
1332
- const pending = resource.pending;
1333
- delete resource.pending;
1334
- if (!submitted || pending === undefined) continue;
1335
- if (pending.rawCaptureFace !== undefined) {
1336
- resource.rawFaceCursor = Math.min(
1337
- resource.rawFaceCursor + 1,
1338
- resource.raw.faceViews.length,
1339
- );
1340
- continue;
1341
- }
1342
- if (pending.step === undefined) continue;
1343
- resource.filter = commitProbeFilterStep(resource.filter, true);
1344
- if (resource.filter.cursor >= resource.filter.faceCount * resource.filter.mipCount) {
1345
- const previous = resource.active;
1346
- resource.active = resource.candidate;
1347
- resource.candidate = resource.spare ?? previous ?? resource.candidate;
1348
- resource.spare = undefined;
1349
- }
1350
- }
1351
- }
1352
-
1353
- dispose(): void {
1354
- for (const resource of this.resources.values()) {
1355
- this.destroyProbeResource(resource);
1356
- }
1357
- this.resources.clear();
1358
- if (this.shared !== undefined) {
1359
- this.internals.device.destroyBuffer(this.shared.faceUniforms);
1360
- this.internals.device.destroyBuffer(this.shared.prefilterUniforms);
1361
- this.internals.device.destroyBuffer(this.shared.vertexBuffer);
1362
- }
1363
- this.shared = undefined;
1364
- this.pipelineWarmup = undefined;
1365
- }
1366
-
1367
- private destroyProbeResource(resource: ProbeGpuResource): void {
1368
- const sets = new Set<ProbeCubeSet>([resource.raw, resource.candidate]);
1369
- if (resource.active !== undefined) sets.add(resource.active);
1370
- if (resource.spare !== undefined) sets.add(resource.spare);
1371
- for (const set of sets) this.destroyCubeSet(set);
1372
- this.internals.device.destroyBuffer(resource.uniformBuffer);
1373
- }
1374
-
1375
- private destroyCubeSet(set: ProbeCubeSet): void {
1376
- this.internals.device.destroyTexture(set.texture);
1377
- this.internals.device.destroyTexture(set.depthTexture);
1378
- }
1379
-
1380
- private resourceFor(
1381
- key: string,
1382
- fact: ReflectionProbeFact,
1383
- index: number,
1384
- ): ProbeGpuResource | undefined {
1385
- const existing = this.resources.get(key);
1386
- if (existing !== undefined) {
1387
- if (existing.fact.revision !== fact.revision) {
1388
- existing.fact = fact;
1389
- existing.filter = createProbeFilterState({
1390
- probeIndex: index,
1391
- mipCount: PREFILTER_MIP_LEVELS,
1392
- activeGeneration: existing.filter.activeGeneration,
1393
- });
1394
- existing.rawFaceCursor = 0;
1395
- } else {
1396
- existing.fact = fact;
1397
- existing.index = index;
1398
- }
1399
- return existing;
1400
- }
1401
- if (this.shared === undefined) return undefined;
1402
- const raw = this.createCubeSet(`reflection-probe-${index}-raw`, fact.resolution);
1403
- const candidate = this.createCubeSet(`reflection-probe-${index}-candidate`, fact.resolution);
1404
- const spare = this.createCubeSet(`reflection-probe-${index}-spare`, fact.resolution);
1405
- if (raw === undefined || candidate === undefined || spare === undefined) return undefined;
1406
- const uniformBuffer = this.internals.device.createBuffer({
1407
- label: `reflection-probe-${index}-uniforms`,
1408
- size: 32,
1409
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
1410
- mappedAtCreation: false,
1411
- });
1412
- if (!uniformBuffer.ok) return undefined;
1413
- const payload = new Float32Array([
1414
- -Math.max(0, fact.intensity),
1415
- fact.center[0],
1416
- fact.center[1],
1417
- fact.center[2],
1418
- fact.halfExtents[0],
1419
- fact.halfExtents[1],
1420
- fact.halfExtents[2],
1421
- 0,
1422
- ]);
1423
- const written = this.internals.device.queue.writeBuffer(uniformBuffer.value, 0, payload);
1424
- if (!written.ok) return undefined;
1425
- const filterGroup1 = this.internals.device.createBindGroup({
1426
- label: `reflection-probe-${index}-filter-input`,
1427
- layout: this.shared.cubeGroup1Bgl,
1428
- entries: [
1429
- { binding: 0, resource: { kind: 'textureView', value: raw.cubeView } },
1430
- { binding: 1, resource: { kind: 'sampler', value: this.shared.sampler } },
1431
- ],
1432
- });
1433
- if (!filterGroup1.ok) return undefined;
1434
- const resource: ProbeGpuResource = {
1435
- fact,
1436
- index,
1437
- filter: createProbeFilterState({ probeIndex: index, mipCount: PREFILTER_MIP_LEVELS }),
1438
- rawFaceCursor: 0,
1439
- raw,
1440
- active: undefined,
1441
- candidate,
1442
- spare,
1443
- sampler: this.shared.sampler,
1444
- uniformBuffer: uniformBuffer.value,
1445
- filterGroup1: filterGroup1.value,
1446
- };
1447
- this.resources.set(key, resource);
1448
- return resource;
1449
- }
1450
-
1451
- private createSharedResources(filterPipeline: RenderPipeline): ProbeSharedResources | undefined {
1452
- const faceUniforms = createFaceUniformsBuffer(this.internals.device);
1453
- const prefilterUniforms = createPrefilterUniformsBuffer(this.internals.device);
1454
- if (!faceUniforms.ok || !prefilterUniforms.ok) return undefined;
1455
- if (!writeAllFaceUniforms(this.internals.device, faceUniforms.value).ok) return undefined;
1456
- if (!writeAllPrefilterUniforms(this.internals.device, prefilterUniforms.value).ok)
1457
- return undefined;
1458
- const vertexBuffer = this.internals.device.createBuffer({
1459
- label: 'reflection-probe-capture-cube-vertices',
1460
- size: CUBEMAP_FACE_VERTICES.byteLength,
1461
- usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
1462
- mappedAtCreation: false,
1463
- });
1464
- if (!vertexBuffer.ok) return undefined;
1465
- const vertexWrite = this.internals.device.queue.writeBuffer(
1466
- vertexBuffer.value,
1467
- 0,
1468
- CUBEMAP_FACE_VERTICES,
1469
- );
1470
- if (!vertexWrite.ok) return undefined;
1471
- const sampler = this.internals.device.createSampler({
1472
- label: 'reflection-probe-pmrem-sampler',
1473
- magFilter: 'linear',
1474
- minFilter: 'linear',
1475
- mipmapFilter: 'linear',
1476
- addressModeU: 'clamp-to-edge',
1477
- addressModeV: 'clamp-to-edge',
1478
- addressModeW: 'clamp-to-edge',
1479
- });
1480
- const cache = getOrCreateIblCache(this.internals.deviceScope);
1481
- if (
1482
- !sampler.ok ||
1483
- cache.cubeGroup1Bgl === undefined ||
1484
- cache.prefilterGroup0Bgl === undefined
1485
- ) {
1486
- return undefined;
1487
- }
1488
- return {
1489
- faceUniforms: faceUniforms.value,
1490
- prefilterUniforms: prefilterUniforms.value,
1491
- vertexBuffer: vertexBuffer.value,
1492
- sampler: sampler.value,
1493
- filterPipeline,
1494
- cubeGroup1Bgl: cache.cubeGroup1Bgl,
1495
- prefilterGroup0Bgl: cache.prefilterGroup0Bgl,
1496
- };
1497
- }
1498
-
1499
- private createCubeSet(label: string, requestedSize: number): ProbeCubeSet | undefined {
1500
- const size = Math.min(Math.max(1, Math.floor(requestedSize)), PREFILTER_SIZE);
1501
- const mipCount = Math.min(PREFILTER_MIP_LEVELS, Math.floor(Math.log2(size)) + 1);
1502
- const texture = this.internals.device.createTexture({
1503
- label,
1504
- size: { width: size, height: size, depthOrArrayLayers: 6 },
1505
- mipLevelCount: mipCount,
1506
- sampleCount: 1,
1507
- dimension: '2d',
1508
- format: this.outputFormat,
1509
- usage: GPU_TEXTURE_USAGE_RENDER_ATTACHMENT_AND_TEXTURE_BINDING | GPU_TEXTURE_USAGE_COPY_SRC,
1510
- viewFormats: [],
1511
- textureBindingViewDimension: undefined,
1512
- });
1513
- if (!texture.ok) return undefined;
1514
- const cubeView = this.internals.device.createTextureView(texture.value, {
1515
- label: `${label}-cube`,
1516
- dimension: 'cube',
1517
- baseMipLevel: 0,
1518
- mipLevelCount: mipCount,
1519
- arrayLayerCount: 6,
1520
- });
1521
- if (!cubeView.ok) return undefined;
1522
- const faceViews: TextureView[] = [];
1523
- const mipViews: TextureView[][] = [];
1524
- for (let mip = 0; mip < mipCount; mip += 1) {
1525
- const faces: TextureView[] = [];
1526
- for (let face = 0; face < 6; face += 1) {
1527
- const view = this.internals.device.createTextureView(texture.value, {
1528
- label: `${label}-mip${mip}-face${face}`,
1529
- dimension: '2d',
1530
- baseMipLevel: mip,
1531
- mipLevelCount: 1,
1532
- baseArrayLayer: face,
1533
- arrayLayerCount: 1,
1534
- });
1535
- if (!view.ok) return undefined;
1536
- if (mip === 0) faceViews.push(view.value);
1537
- faces.push(view.value);
1538
- }
1539
- mipViews.push(faces);
1540
- }
1541
- const depthTexture = this.internals.device.createTexture({
1542
- label: `${label}-depth`,
1543
- size: { width: size, height: size, depthOrArrayLayers: 1 },
1544
- mipLevelCount: 1,
1545
- sampleCount: 1,
1546
- dimension: '2d',
1547
- format: 'depth24plus-stencil8',
1548
- usage: 0x10,
1549
- viewFormats: [],
1550
- textureBindingViewDimension: undefined,
1551
- });
1552
- if (!depthTexture.ok) return undefined;
1553
- const depthView = this.internals.device.createTextureView(depthTexture.value, {
1554
- label: `${label}-depth-view`,
1555
- dimension: '2d',
1556
- });
1557
- if (!depthView.ok) return undefined;
1558
- return {
1559
- texture: texture.value,
1560
- cubeView: cubeView.value,
1561
- faceViews,
1562
- mipViews,
1563
- depthTexture: depthTexture.value,
1564
- depthView: depthView.value,
1565
- size,
1566
- };
1567
- }
1568
- }
1569
-
1570
1120
  export function createRenderSystem(internals: RenderSystemInternals): RenderSystem {
1571
1121
  const phaseCatalogRegistration = internals.profiler?.registerPhaseCatalog(
1572
1122
  'render',
@@ -1578,6 +1128,38 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1578
1128
  let latestCamera: CameraSnapshot | undefined;
1579
1129
  const pointsLinesOwner = new StandardPointsLinesOwner(internals);
1580
1130
  const reflectionProbeOwner = new ReflectionProbeRecordOwner(internals);
1131
+ let ssrFormatReceipt: import('@forgeax/engine-rhi').RhiTextureFormatCapabilityReceipt | undefined;
1132
+ let ssrFormatProbeGeneration = -1;
1133
+ let ssrFormatProbe: Promise<void> | undefined;
1134
+ let ssrFallbackGeneration: number | undefined;
1135
+ const ensureSsrFormatProbe = (): void => {
1136
+ if (internals.ssrIdentity === undefined) return;
1137
+ const generation = internals.deviceScope.generation;
1138
+ if (
1139
+ ssrFormatReceipt?.deviceGeneration === generation ||
1140
+ ssrFormatProbeGeneration === generation ||
1141
+ ssrFormatProbe !== undefined ||
1142
+ typeof internals.device.probeTextureFormatCapability !== 'function'
1143
+ ) {
1144
+ return;
1145
+ }
1146
+ ssrFormatProbeGeneration = generation;
1147
+ const device = internals.device;
1148
+ ssrFormatProbe = device
1149
+ .probeTextureFormatCapability()
1150
+ .then((result) => {
1151
+ if (internals.device !== device || internals.deviceScope.generation !== generation) return;
1152
+ ssrFormatReceipt = result.ok ? result.value : undefined;
1153
+ })
1154
+ .catch(() => {
1155
+ if (internals.device === device && internals.deviceScope.generation === generation) {
1156
+ ssrFormatReceipt = undefined;
1157
+ }
1158
+ })
1159
+ .finally(() => {
1160
+ ssrFormatProbe = undefined;
1161
+ });
1162
+ };
1581
1163
  const cubeCaptureScheduler = createCubeCaptureScheduler({ maxFacesPerFrame: 1 });
1582
1164
  const cubeCaptureState: CubeCaptureGraphState = { work: [] };
1583
1165
  const persistentRenderScene = new PersistentRenderScene({
@@ -1725,6 +1307,23 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1725
1307
  ),
1726
1308
  } satisfies import('./pipeline-builder').PipelineBuilderShaderModuleFactory);
1727
1309
  let gpuDrivenProduction = new GpuDrivenProduction(internals.device, gpuDrivenShaderFactory);
1310
+ let occlusionRuntime: OcclusionRenderRuntime | undefined = new OcclusionRenderRuntime(
1311
+ internals.device,
1312
+ persistentRenderScene.visibilityFacetStore(),
1313
+ gpuDrivenShaderFactory,
1314
+ persistentRenderScene.visibilityBudgetValue(),
1315
+ );
1316
+ const ensureOcclusionRuntime = (): OcclusionRenderRuntime => {
1317
+ if (occlusionRuntime === undefined) {
1318
+ occlusionRuntime = new OcclusionRenderRuntime(
1319
+ internals.device,
1320
+ persistentRenderScene.visibilityFacetStore(),
1321
+ gpuDrivenShaderFactory,
1322
+ persistentRenderScene.visibilityBudgetValue(),
1323
+ );
1324
+ }
1325
+ return occlusionRuntime;
1326
+ };
1728
1327
  const featureGpuWork: RenderFeatureGpuWorkOwner = createRenderFeatureGpuWorkOwner({
1729
1328
  getDevice: () => internals.device,
1730
1329
  getShaderModuleFactory: () =>
@@ -1772,11 +1371,13 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1772
1371
  // raw (material-handle-based, cross-world collision semantically correct).
1773
1372
  const frameState: RenderFrameState = {
1774
1373
  frameNumber: 0,
1374
+ reflectionFallbackDemand: false,
1775
1375
  graphGeneration: 0,
1776
1376
  successfulTemporalFrameIndex: 0,
1777
1377
  lastSuccessfulBloom: 'off',
1778
1378
  temporalFrameTransaction: createTemporalFrameTransaction({ deviceEpoch: 0 }),
1779
1379
  temporalFrameInput: undefined,
1380
+ reflectionFallbackReadback: undefined,
1780
1381
  directionalShadowCache: null,
1781
1382
  directionalShadowCacheRecorded: false,
1782
1383
  compiledFrameGraph: null,
@@ -1798,6 +1399,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1798
1399
  volumetricFogHistorySignature: null,
1799
1400
  retiredCompiledFrameGraphs: new Set(),
1800
1401
  currentFrameObservationSource: undefined,
1402
+ reflectionFallbackObservationSource: undefined,
1801
1403
  lastSuccessfulCameraAntialias: undefined,
1802
1404
  temporalGpuState: undefined,
1803
1405
  activeTemporalGpuState: undefined,
@@ -1809,6 +1411,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1809
1411
  currentDirectionalShadowView: null,
1810
1412
  currentSpotShadowView: null,
1811
1413
  instanceBuffers: new Map(),
1414
+ probeBlendBuffers: new Map(),
1415
+ probeBlendRecordBufferCapacity: 0,
1812
1416
  morphBuffers: new Map(),
1813
1417
  hdrpClusterBinScratch: createClusterBinScratch(),
1814
1418
  hdrpClusterGridScratch: null,
@@ -1820,8 +1424,6 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1820
1424
  transientInstanceBuffers: [],
1821
1425
  warnedZeroLightStandard: false,
1822
1426
  warnedMultiLightDirectional: false,
1823
- warnedMultiLightPoint: false,
1824
- warnedMultiLightSpot: false,
1825
1427
  // feat-20260630-equirect-kind-internalized-ibl-declarative-skyligh M3 / w19:
1826
1428
  // once-warn latches for >1 Skylight / >1 SkyboxBackground (names winner).
1827
1429
  warnedMultiSkylight: false,
@@ -2104,6 +1706,134 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2104
1706
  const dispatchCounts: { unlit: number } = {
2105
1707
  unlit: 0,
2106
1708
  };
1709
+ let lastLodOcclusionInspection: LodOcclusionInspection | undefined;
1710
+ // The GPU selector returns stable numeric World keys with its same-submit
1711
+ // counters. Keep the attachment join from that submit, rather than using
1712
+ // the current worlds[] position when the asynchronous readback completes.
1713
+ let lastLodWorldAttachments: ReadonlyMap<number, string> = new Map();
1714
+ let occlusionCandidateCache:
1715
+ | {
1716
+ readonly renderables: readonly RenderableSnapshot[];
1717
+ readonly camera: CameraSnapshot;
1718
+ readonly cameraWorldIdentity: string;
1719
+ readonly candidates: readonly OcclusionRuntimeCandidate[];
1720
+ }
1721
+ | undefined;
1722
+ let lodCandidateCountCache:
1723
+ | { readonly renderables: readonly RenderableSnapshot[]; readonly count: number }
1724
+ | undefined;
1725
+ let lodInspectionRenderablesCache:
1726
+ | {
1727
+ readonly submissionRenderables: readonly RenderableSnapshot[];
1728
+ readonly lodCandidateCount: number;
1729
+ readonly values: readonly RenderableSnapshot[];
1730
+ }
1731
+ | undefined;
1732
+ let orderedDispatchCache:
1733
+ | {
1734
+ readonly source: readonly DispatchEntry[];
1735
+ readonly world: World;
1736
+ readonly camera: readonly CameraSnapshot[];
1737
+ readonly renderables: readonly RenderableSnapshot[];
1738
+ readonly value: readonly DispatchEntry[];
1739
+ }
1740
+ | undefined;
1741
+ const applyLodSelectionTelemetry = (
1742
+ selection:
1743
+ | {
1744
+ readonly candidateCount: number;
1745
+ readonly visible: number;
1746
+ readonly occluded: number;
1747
+ readonly lodHistogram: readonly { readonly level: number; readonly count: number }[];
1748
+ readonly worldSelections?: readonly {
1749
+ readonly worldKey: number;
1750
+ readonly primitiveSlot: number;
1751
+ readonly slotGeneration: number;
1752
+ readonly candidateCount: number;
1753
+ readonly visible: number;
1754
+ readonly occluded: number;
1755
+ readonly lodHistogram: readonly { readonly level: number; readonly count: number }[];
1756
+ }[];
1757
+ }
1758
+ | undefined,
1759
+ ): void => {
1760
+ if (selection === undefined || lastLodOcclusionInspection === undefined) return;
1761
+ const inspection = lastLodOcclusionInspection;
1762
+ // The GPU readback histogram is the selector fact, while final submitted
1763
+ // counts are owned by the persistent visibility projection. Keeping the
1764
+ // projection count here excludes ordinary non-LOD geometry (for example a
1765
+ // benchmark occluder) from the LOD workload without inventing a second
1766
+ // visibility owner or mixing CPU/GPU admission decisions.
1767
+ let worlds = inspection.worlds;
1768
+ const worldSelections = selection.worldSelections;
1769
+ if (worldSelections !== undefined && worldSelections.length > 0) {
1770
+ const selectionsByAttachment = new Map<string, (typeof worldSelections)[number][]>();
1771
+ let attributionValid = true;
1772
+ for (const selected of worldSelections) {
1773
+ const attachmentId = lastLodWorldAttachments.get(selected.worldKey);
1774
+ if (attachmentId === undefined) {
1775
+ attributionValid = false;
1776
+ break;
1777
+ }
1778
+ const rows = selectionsByAttachment.get(attachmentId);
1779
+ if (rows === undefined) selectionsByAttachment.set(attachmentId, [selected]);
1780
+ else rows.push(selected);
1781
+ }
1782
+ if (attributionValid) {
1783
+ const nextWorlds = worlds.map((world) => {
1784
+ const selected = selectionsByAttachment.get(world.attachmentId);
1785
+ const baseRow = world.rows[0];
1786
+ if (selected === undefined || baseRow === undefined) {
1787
+ if (world.rows.length > 0) attributionValid = false;
1788
+ return world;
1789
+ }
1790
+ const candidateCount = selected.reduce((sum, row) => sum + row.candidateCount, 0);
1791
+ if (candidateCount !== baseRow.count.candidates) {
1792
+ attributionValid = false;
1793
+ return world;
1794
+ }
1795
+ return Object.freeze({
1796
+ ...world,
1797
+ attribution: Object.freeze({
1798
+ status: 'same-submit' as const,
1799
+ submit: inspection.submit,
1800
+ }),
1801
+ rows: Object.freeze(
1802
+ selected.map((row) => ({
1803
+ ...baseRow,
1804
+ slot: {
1805
+ primitiveSlot: row.primitiveSlot,
1806
+ slotGeneration: row.slotGeneration,
1807
+ },
1808
+ count: {
1809
+ candidates: row.candidateCount,
1810
+ visible: row.visible,
1811
+ occluded: row.occluded,
1812
+ },
1813
+ lodHistogram: row.lodHistogram,
1814
+ })),
1815
+ ),
1816
+ });
1817
+ });
1818
+ if (attributionValid) worlds = Object.freeze(nextWorlds);
1819
+ }
1820
+ }
1821
+ lastLodOcclusionInspection = inspectLodOcclusion({
1822
+ ...inspection,
1823
+ count: {
1824
+ ...lastLodOcclusionInspection.count,
1825
+ candidates: selection.candidateCount,
1826
+ visible: selection.visible,
1827
+ occluded: selection.occluded,
1828
+ },
1829
+ lodHistogram: selection.lodHistogram,
1830
+ // Per-primitive samples are intentionally omitted until the GPU readback
1831
+ // carries stable primitive identities; the histogram is the authoritative
1832
+ // producer fact and is copied from the same GPU cull counters.
1833
+ samples: [],
1834
+ worlds,
1835
+ });
1836
+ };
2107
1837
  // feat-20260531-per-frame-bind-group-cache M1 / w4: per-frame
2108
1838
  // createBindGroup counter scaffolding. Reset on every draw([world], { cameraOwner: 0, resourceOwner: 0 }) entry,
2109
1839
  // bumped on cache-miss in render-system-record.ts (M2-M4 bump points).
@@ -2466,9 +2196,44 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2466
2196
  gpuDriven: gpuDrivenProduction.inspect(),
2467
2197
  };
2468
2198
  },
2199
+ get gpuDrivenInspection(): GpuDrivenProductionInspection {
2200
+ return gpuDrivenProduction.inspect();
2201
+ },
2469
2202
  get reflectionProbes(): ReflectionProbeInspection {
2470
2203
  return reflectionProbeOwner.inspect();
2471
2204
  },
2205
+ get ssrDependencies(): SsrDependenciesInspection {
2206
+ ensureSsrFormatProbe();
2207
+ const owner = reflectionProbeOwner.inspect();
2208
+ const nextGeneration = owner.reflectionFallback.projectionGeneration;
2209
+ const reset = resolveSsrAdmissionGeneration(ssrFallbackGeneration, nextGeneration);
2210
+ if (reset.changed) {
2211
+ frameState.temporalFrameTransaction.reset('signature-change');
2212
+ frameState.temporalFrame = undefined;
2213
+ frameState.temporalFrameInput = undefined;
2214
+ frameState.lastSuccessfulTemporalView = undefined;
2215
+ frameState.successfulTemporalFrameIndex = 0;
2216
+ frameState.pendingTemporalCommit = { kind: 'none' };
2217
+ }
2218
+ ssrFallbackGeneration = reset.generation;
2219
+ const temporal =
2220
+ frameState.lastSuccessfulTemporalView === undefined
2221
+ ? undefined
2222
+ : ({
2223
+ successfulSubmit: true,
2224
+ generation: frameState.lastSuccessfulTemporalView.input.deviceGeneration,
2225
+ } as const);
2226
+ return projectSsrDependencies({
2227
+ requested: frameState.reflectionFallbackDemand === true,
2228
+ identity: internals.ssrIdentity,
2229
+ reflectionFallback: owner.reflectionFallback,
2230
+ format: ssrFormatReceipt,
2231
+ temporal,
2232
+ });
2233
+ },
2234
+ get reflectionFallbackCompletion(): Promise<void> | undefined {
2235
+ return frameState.reflectionFallbackCompletion;
2236
+ },
2472
2237
  isCubeCapturePending(target: RenderTarget): boolean {
2473
2238
  return cubeCaptureScheduler.inspect(target).candidateGeneration !== undefined;
2474
2239
  },
@@ -2538,6 +2303,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2538
2303
  }),
2539
2304
  });
2540
2305
  },
2306
+ get lodOcclusionInspection(): LodOcclusionInspection | undefined {
2307
+ return lastLodOcclusionInspection;
2308
+ },
2541
2309
  get bloom(): BloomInspection {
2542
2310
  const pipelineState = internals.getPipelineState();
2543
2311
  const resources = pipelineState?.perPassResources;
@@ -2676,14 +2444,20 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2676
2444
  worlds,
2677
2445
  { cameraOwner, resourceOwner },
2678
2446
  internals.assets.catalogEpoch,
2679
- () =>
2447
+ (renderables) =>
2680
2448
  extractFrames(
2681
2449
  worlds,
2682
2450
  { cameraOwner, resourceOwner },
2683
2451
  internals.assets,
2684
2452
  internals.getPipelineState(),
2685
2453
  persistentRenderScene.materialSnapshotCacheStore(),
2686
- { cull: 'none' },
2454
+ {
2455
+ cull: 'none',
2456
+ renderables,
2457
+ ...(internals.getMaterialShaderArtifact === undefined
2458
+ ? {}
2459
+ : { getMaterialShaderArtifact: internals.getMaterialShaderArtifact }),
2460
+ },
2687
2461
  ),
2688
2462
  renderReadLeases,
2689
2463
  );
@@ -2701,7 +2475,6 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2701
2475
  skylightCount,
2702
2476
  skybox,
2703
2477
  skyboxCount,
2704
- fog,
2705
2478
  fogFailure,
2706
2479
  frustumStats,
2707
2480
  visibilityStats,
@@ -2714,12 +2487,279 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2714
2487
  frame.reflectionProbes ?? [],
2715
2488
  persistentRenderScene.reflectionProbeProjection().selected,
2716
2489
  cameras[0],
2490
+ frameState.frameNumber,
2491
+ skylight,
2717
2492
  );
2718
2493
  latestCamera = cameras[0];
2494
+ persistentRenderScene.updateVisibilityFacet(worlds, latestCamera, renderables);
2495
+ let occlusionProjection:
2496
+ | import('./scene/visibility/occlusion-runtime').OcclusionFrameProjection
2497
+ | undefined;
2498
+ let occlusionFallback: LodOcclusionInspection['fallback'] = { active: false };
2499
+ const worldKeys = persistentRenderScene.visibilityWorldKeysFor(worlds);
2500
+ const slotByEntity = persistentRenderScene.compositionSlotByStableEntity();
2501
+ if (latestCamera !== undefined) {
2502
+ const camera = latestCamera;
2503
+ const cameraWorld = worlds[camera.worldId ?? cameraOwner];
2504
+ const view = viewKey({
2505
+ attachmentId: cameraWorld?.identity ?? 'missing-camera-world',
2506
+ cameraEntity: camera.entityKey ?? 0,
2507
+ viewRole: 'main',
2508
+ viewGeneration: camera.historyVersion ?? 0,
2509
+ });
2510
+ const cachedCandidates = occlusionCandidateCache;
2511
+ const candidates =
2512
+ cachedCandidates?.renderables === renderables &&
2513
+ cachedCandidates.camera === camera &&
2514
+ cachedCandidates.cameraWorldIdentity === (cameraWorld?.identity ?? '')
2515
+ ? cachedCandidates.candidates
2516
+ : renderables.flatMap((renderable) => {
2517
+ const world = worlds[renderable.worldId];
2518
+ const aabb = renderable.localAabb;
2519
+ // Occlusion is a LOD transport concern. A plain mesh has no
2520
+ // lower-detail range to select, so keep it on the ordinary CPU
2521
+ // visibility path and do not allocate query resources for it.
2522
+ if (
2523
+ world === undefined ||
2524
+ cameraWorld === undefined ||
2525
+ aabb === undefined ||
2526
+ aabb.length < 6 ||
2527
+ renderable.lods === undefined ||
2528
+ renderable.lods.length === 0
2529
+ )
2530
+ return [];
2531
+ const slot = slotByEntity.get(
2532
+ worldEntityKey(
2533
+ worldKeys[renderable.worldId] ?? renderable.worldId,
2534
+ renderable.entityKey,
2535
+ ),
2536
+ );
2537
+ if (slot === undefined) return [];
2538
+ const primitive = primitiveKey({
2539
+ attachmentId: cameraWorld.identity,
2540
+ worldGeneration: worldKeys[renderable.worldId] ?? renderable.worldId,
2541
+ primitiveSlot: slot.slot,
2542
+ slotGeneration: slot.generation,
2543
+ });
2544
+ return [
2545
+ {
2546
+ view,
2547
+ primitive,
2548
+ epoch: 0,
2549
+ bounds: {
2550
+ min: [aabb[0] ?? 0, aabb[1] ?? 0, aabb[2] ?? 0] as [number, number, number],
2551
+ max: [aabb[3] ?? 0, aabb[4] ?? 0, aabb[5] ?? 0] as [number, number, number],
2552
+ },
2553
+ candidate: { level: 0, confidence: 1 },
2554
+ deviceGeneration: internals.deviceScope.generation,
2555
+ proxyVertices: buildOcclusionProxyVertices(
2556
+ aabb,
2557
+ renderable.transform.world,
2558
+ camera,
2559
+ ),
2560
+ },
2561
+ ];
2562
+ });
2563
+ occlusionCandidateCache = {
2564
+ renderables,
2565
+ camera,
2566
+ cameraWorldIdentity: cameraWorld?.identity ?? '',
2567
+ candidates,
2568
+ };
2569
+ occlusionProjection = runProfiledRenderPhase(profileSession, 'occlusion-prepare', () =>
2570
+ ensureOcclusionRuntime().prepareBatch(candidates, camera.antialias === 'msaa' ? 4 : 1),
2571
+ );
2572
+ const queryPool = ensureOcclusionRuntime().inspect();
2573
+ const queryUnavailable = ensureOcclusionRuntime().prepareUnavailable;
2574
+ const queryCapacityFallback =
2575
+ candidates.length > 0 && queryUnavailable && queryPool.availablePages === 0;
2576
+ occlusionFallback = queryCapacityFallback
2577
+ ? {
2578
+ active: true as const,
2579
+ reason: 'page-exhausted' as const,
2580
+ error: {
2581
+ code: 'visibility-query-capacity-exhausted',
2582
+ expected: 'one available occlusion-query page for the LOD candidate batch',
2583
+ hint: 'retry the LOD query after an in-flight page completes',
2584
+ detail: {
2585
+ availablePages: queryPool.availablePages,
2586
+ pageCount: queryPool.pageCount,
2587
+ pageIndexLimit: queryPool.pageIndexLimit,
2588
+ },
2589
+ },
2590
+ }
2591
+ : ({ active: false } as const);
2592
+ }
2593
+ // Query transport is best-effort and must fail open. A candidate that
2594
+ // was hidden by an earlier completion becomes visible again when this
2595
+ // device/frame cannot reserve a fresh query page; the next successful
2596
+ // submission can rebuild confidence from a real result.
2597
+ if (
2598
+ occlusionProjection === undefined &&
2599
+ latestCamera !== undefined &&
2600
+ ensureOcclusionRuntime().prepareUnavailable
2601
+ ) {
2602
+ const cameraWorld = worlds[latestCamera.worldId ?? cameraOwner];
2603
+ if (cameraWorld !== undefined) {
2604
+ const fallbackView = viewKey({
2605
+ attachmentId: cameraWorld.identity,
2606
+ cameraEntity: latestCamera.entityKey ?? 0,
2607
+ viewRole: 'main',
2608
+ viewGeneration: latestCamera.historyVersion ?? 0,
2609
+ });
2610
+ for (const renderable of renderables) {
2611
+ if ((renderable.lods?.length ?? 0) === 0) continue;
2612
+ const world = worlds[renderable.worldId];
2613
+ if (world === undefined) continue;
2614
+ const slot = slotByEntity.get(
2615
+ worldEntityKey(
2616
+ worldKeys[renderable.worldId] ?? renderable.worldId,
2617
+ renderable.entityKey,
2618
+ ),
2619
+ );
2620
+ if (slot === undefined) continue;
2621
+ persistentRenderScene.visibilityFacetStore().applyConfidence(
2622
+ fallbackView,
2623
+ primitiveKey({
2624
+ attachmentId: cameraWorld.identity,
2625
+ worldGeneration: worldKeys[renderable.worldId] ?? renderable.worldId,
2626
+ primitiveSlot: slot.slot,
2627
+ slotGeneration: slot.generation,
2628
+ }),
2629
+ { type: 'failure', submissionGeneration: frameState.frameNumber },
2630
+ );
2631
+ }
2632
+ }
2633
+ }
2634
+ const visibilityProjection = persistentRenderScene.projectVisibility(
2635
+ worlds,
2636
+ latestCamera,
2637
+ renderables,
2638
+ dispatch,
2639
+ );
2640
+ const submissionRenderables = visibilityProjection.renderables;
2641
+ const submissionDispatch = visibilityProjection.dispatch;
2642
+ const inspectionCamera = latestCamera;
2643
+ let lodCandidateCountForFrame = 0;
2644
+ if (inspectionCamera !== undefined) {
2645
+ const firstRenderable = renderables[0];
2646
+ const lodCandidateCount =
2647
+ lodCandidateCountCache?.renderables === renderables
2648
+ ? lodCandidateCountCache.count
2649
+ : (() => {
2650
+ const count = renderables.reduce(
2651
+ (total, renderable) => total + ((renderable.lods?.length ?? 0) > 0 ? 1 : 0),
2652
+ 0,
2653
+ );
2654
+ lodCandidateCountCache = { renderables, count };
2655
+ return count;
2656
+ })();
2657
+ lodCandidateCountForFrame = lodCandidateCount;
2658
+ const inspectionRenderables =
2659
+ lodInspectionRenderablesCache?.submissionRenderables === submissionRenderables &&
2660
+ lodInspectionRenderablesCache.lodCandidateCount === lodCandidateCount
2661
+ ? lodInspectionRenderablesCache.values
2662
+ : (() => {
2663
+ const values =
2664
+ lodCandidateCount > 0
2665
+ ? submissionRenderables.filter(
2666
+ (renderable) => (renderable.lods?.length ?? 0) > 0,
2667
+ )
2668
+ : submissionRenderables;
2669
+ lodInspectionRenderablesCache = {
2670
+ submissionRenderables,
2671
+ lodCandidateCount,
2672
+ values,
2673
+ };
2674
+ return values;
2675
+ })();
2676
+ const candidateCount =
2677
+ lodCandidateCount > 0 ? lodCandidateCount : renderables.length + frustumStats.culled;
2678
+ const inspectionQuery = ensureOcclusionRuntime().inspect();
2679
+ const inspectionBudget = persistentRenderScene.visibilityBudgetValue();
2680
+ const inspectionWorlds = createLodWorldInspections(
2681
+ worlds,
2682
+ renderables,
2683
+ submissionRenderables,
2684
+ inspectionCamera,
2685
+ frameState.frameNumber,
2686
+ worldKeys,
2687
+ slotByEntity,
2688
+ {
2689
+ used:
2690
+ inspectionQuery.pageCount * inspectionQuery.pageIndexLimit -
2691
+ inspectionQuery.availablePages * inspectionQuery.pageIndexLimit,
2692
+ capacity: inspectionQuery.pageCount * inspectionQuery.pageIndexLimit,
2693
+ },
2694
+ occlusionFallback,
2695
+ ensureOcclusionRuntime().prepareUnavailable
2696
+ ? { active: true, reason: 'query-unavailable' as const }
2697
+ : { active: false },
2698
+ );
2699
+ const attachmentByWorldKey = new Map<number, string>();
2700
+ for (let worldId = 0; worldId < worlds.length; worldId += 1) {
2701
+ const world = worlds[worldId];
2702
+ if (world === undefined) continue;
2703
+ const worldKey = worldKeys[worldId] ?? worldId;
2704
+ if (attachmentByWorldKey.has(worldKey)) {
2705
+ attachmentByWorldKey.clear();
2706
+ break;
2707
+ }
2708
+ attachmentByWorldKey.set(worldKey, world.identity);
2709
+ }
2710
+ lastLodWorldAttachments = attachmentByWorldKey;
2711
+ lastLodOcclusionInspection = inspectLodOcclusion({
2712
+ root: {
2713
+ guid:
2714
+ firstRenderable === undefined
2715
+ ? 'none'
2716
+ : `asset-handle:${firstRenderable.assetHandle}`,
2717
+ sourceKey: `render-frame:${frameState.frameNumber}`,
2718
+ },
2719
+ view: {
2720
+ attachmentId: worlds[inspectionCamera.worldId ?? cameraOwner]?.identity ?? 'none',
2721
+ cameraEntity: inspectionCamera.entityKey ?? 0,
2722
+ viewRole: 'main',
2723
+ viewGeneration: inspectionCamera.historyVersion ?? 0,
2724
+ },
2725
+ slot: {
2726
+ primitiveSlot: 0,
2727
+ slotGeneration: persistentRenderScene.inspect().topology.revision,
2728
+ },
2729
+ generation: frameState.frameNumber,
2730
+ count: {
2731
+ candidates: candidateCount,
2732
+ visible: inspectionRenderables.length,
2733
+ occluded: visibilityProjection.suppressed,
2734
+ },
2735
+ lodHistogram: [{ level: 0, count: inspectionRenderables.length }],
2736
+ queryLatencyUs: { median: 0, p95: 0, last: 0 },
2737
+ pagePressure: { used: 0, capacity: 3 * 4096 },
2738
+ fallback: occlusionFallback,
2739
+ degradation: ensureOcclusionRuntime().prepareUnavailable
2740
+ ? { active: true, reason: 'query-unavailable' as const }
2741
+ : { active: false },
2742
+ samples: inspectionRenderables.slice(0, 64).map((_, index) => ({
2743
+ primitiveSlot: index,
2744
+ level: 0,
2745
+ visible: true,
2746
+ })),
2747
+ submit: {
2748
+ frameId: frameState.frameNumber,
2749
+ build: resolveInspectionBuild(internals),
2750
+ deviceGeneration: internals.deviceScope.generation,
2751
+ },
2752
+ budget: inspectionBudget,
2753
+ worlds: inspectionWorlds,
2754
+ });
2755
+ } else {
2756
+ lastLodOcclusionInspection = undefined;
2757
+ lastLodWorldAttachments = new Map();
2758
+ }
2719
2759
  const temporalDemanded =
2720
2760
  cameras[0]?.antialias === 'taa' || (cameras[0]?.motionBlur?.shutterAngle ?? 0) > 0;
2721
2761
  if (temporalDemanded) {
2722
- persistentRenderScene.prepareTemporalFrame(renderables);
2762
+ persistentRenderScene.prepareTemporalFrame(submissionRenderables);
2723
2763
  }
2724
2764
  environmentLifecycle.bindScope(internals.deviceScope);
2725
2765
  if (fogFailure !== undefined) {
@@ -2897,9 +2937,31 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2897
2937
  // world that owns skylight / skybox / singleton render state, w5 / D-3).
2898
2938
  // Only the Transparent segment is reordered; queue ordering between
2899
2939
  // segments (sortDispatchByQueue, stable) is preserved.
2900
- const orderedDispatch = runProfiledRenderPhase(profileSession, 'sort', () =>
2901
- sortTransparentDispatch(dispatch, resourceWorld, cameras, renderables),
2902
- );
2940
+ const orderedDispatch = runProfiledRenderPhase(profileSession, 'sort', () => {
2941
+ const cached = orderedDispatchCache;
2942
+ if (
2943
+ cached?.source === submissionDispatch &&
2944
+ cached.world === resourceWorld &&
2945
+ cached.camera === cameras &&
2946
+ cached.renderables === submissionRenderables
2947
+ ) {
2948
+ return cached.value;
2949
+ }
2950
+ const value = sortTransparentDispatch(
2951
+ [...submissionDispatch],
2952
+ resourceWorld,
2953
+ cameras,
2954
+ submissionRenderables,
2955
+ );
2956
+ orderedDispatchCache = {
2957
+ source: submissionDispatch,
2958
+ world: resourceWorld,
2959
+ camera: cameras,
2960
+ renderables: submissionRenderables,
2961
+ value,
2962
+ };
2963
+ return value;
2964
+ });
2903
2965
 
2904
2966
  // M3 / w26: single dispatch list replaces old three-bucket model.
2905
2967
  // Pass dispatch to recordFrame — the record stage iterates dispatch
@@ -2927,13 +2989,16 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2927
2989
  // The same runner is also called by geometry/material helpers;
2928
2990
  // invoking those wrappers would turn a pass probe into the
2929
2991
  // high-overhead per-draw `nested` probe.
2930
- if (
2931
- profileSession.detail === 'passes' &&
2932
- (!phase.startsWith('record/graph-execute/') ||
2933
- (phase.slice('record/graph-execute/'.length).includes('/') &&
2934
- !phase.endsWith('/geometry-loop')))
2935
- ) {
2936
- return action();
2992
+ if (profileSession.detail === 'passes') {
2993
+ const isOcclusionOwner =
2994
+ phase === 'record/occlusion-query-submit' ||
2995
+ phase === 'record/occlusion-global-advance';
2996
+ const isGraphOwner =
2997
+ phase === 'record/graph-execute' ||
2998
+ (phase.startsWith('record/graph-execute/') &&
2999
+ (!phase.slice('record/graph-execute/'.length).includes('/') ||
3000
+ phase.endsWith('/geometry-loop')));
3001
+ if (!isOcclusionOwner && !isGraphOwner) return action();
2937
3002
  }
2938
3003
  return runProfiledRenderPhase(profileSession, phase, action);
2939
3004
  };
@@ -2946,7 +3011,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2946
3011
  resourceWorld,
2947
3012
  cameras,
2948
3013
  lights,
2949
- renderables,
3014
+ [...submissionRenderables],
2950
3015
  orderedDispatch,
2951
3016
  frameState,
2952
3017
  dispatchCounts,
@@ -2956,16 +3021,18 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2956
3021
  skybox,
2957
3022
  skyboxCount,
2958
3023
  postProcessParams,
2959
- fog,
2960
3024
  worlds,
2961
3025
  recordProfilePhase,
2962
3026
  {
2963
3027
  owner: gpuDrivenProduction,
2964
3028
  scene: persistentRenderScene.compositionGpuDrivenState(),
2965
- onCoverage: (ownsAll, clusteredLighting) => {
2966
- const key = `${internals.gpuStore.meshResidencyEpoch}:${clusteredLighting ? 'clustered' : 'standard'}`;
2967
- persistentRenderScene.setCompositionGpuDrivenCoverage(key, ownsAll);
3029
+ onSceneDemand: (sceneRowsRequired) => {
3030
+ persistentRenderScene.setCompositionGpuSceneDemand(sceneRowsRequired);
2968
3031
  },
3032
+ activeEntityKeys: visibilityProjection.activeEntityKeys,
3033
+ activeEntityRevision: visibilityProjection.activeEntityRevision,
3034
+ telemetryCandidateCount: lodCandidateCountForFrame,
3035
+ ...(occlusionProjection === undefined ? {} : { occlusion: occlusionProjection }),
2969
3036
  },
2970
3037
  renderReadLeases,
2971
3038
  featureGraphCandidate,
@@ -2984,6 +3051,22 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2984
3051
  timingCapture,
2985
3052
  ),
2986
3053
  );
3054
+ if (submitted && lastLodOcclusionInspection !== undefined) {
3055
+ const query = ensureOcclusionRuntime().inspect();
3056
+ lastLodOcclusionInspection = {
3057
+ ...lastLodOcclusionInspection,
3058
+ pagePressure: {
3059
+ used:
3060
+ query.pageCount * query.pageIndexLimit -
3061
+ query.availablePages * query.pageIndexLimit,
3062
+ capacity: query.pageCount * query.pageIndexLimit,
3063
+ },
3064
+ fallback: occlusionFallback,
3065
+ degradation: ensureOcclusionRuntime().prepareUnavailable
3066
+ ? { active: true, reason: 'query-unavailable' as const }
3067
+ : { active: false },
3068
+ };
3069
+ }
2987
3070
  const directionalQuality = lights.directionalShadowQuality;
2988
3071
  const directionalShadowReady =
2989
3072
  directionalQuality === undefined ||
@@ -3007,6 +3090,11 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3007
3090
  };
3008
3091
  }
3009
3092
  if (submitted) {
3093
+ if (occlusionProjection === undefined) {
3094
+ runProfiledRenderPhase(profileSession, 'record/occlusion-global-advance', () =>
3095
+ ensureOcclusionRuntime().advanceSuccessfulSubmit(frameState.frameNumber),
3096
+ );
3097
+ }
3010
3098
  const temporalCommit = persistentRenderScene.commitTemporalFrame();
3011
3099
  if (!temporalCommit.ok) {
3012
3100
  internals.errorRegistry.fire(temporalCommit.error);
@@ -3085,6 +3173,21 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3085
3173
  const currentFrameId = frameState.frameNumber - 1;
3086
3174
  return observeCurrentFrame(options, frameState.currentFrameObservationSource, currentFrameId);
3087
3175
  },
3176
+ async observeLodOcclusion(): Promise<void> {
3177
+ await ensureOcclusionRuntime().waitForCompletions();
3178
+ applyLodSelectionTelemetry(await gpuDrivenProduction.readLodSelection());
3179
+ if (lastLodOcclusionInspection !== undefined) {
3180
+ const queryLatency = ensureOcclusionRuntime().inspectQueryLatency();
3181
+ lastLodOcclusionInspection = inspectLodOcclusion({
3182
+ ...lastLodOcclusionInspection,
3183
+ queryLatencyUs: {
3184
+ median: queryLatency.median,
3185
+ p95: queryLatency.p95,
3186
+ last: queryLatency.last,
3187
+ },
3188
+ });
3189
+ }
3190
+ },
3088
3191
  getCurrentGraphTarget(name: string) {
3089
3192
  const graph = frameState.perFrameGraph;
3090
3193
  if (graph === undefined || graph === null) return undefined;
@@ -3236,6 +3339,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3236
3339
  (internals as TimingSessionHost).gpuPassTimingBeginReason = undefined;
3237
3340
  persistentRenderScene.dispose();
3238
3341
  gpuDrivenProduction.dispose();
3342
+ occlusionRuntime?.dispose();
3239
3343
  disposeFeatureGpuWork();
3240
3344
  reflectionProbeOwner.dispose();
3241
3345
  // Bloom generations are renderer-owned rather than graph-owned. Drain
@@ -3304,6 +3408,15 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3304
3408
  // dispose error strategy).
3305
3409
  disposeInstanceBuffers(frameState.instanceBuffers, internals.errorRegistry);
3306
3410
  disposeTransientInstanceBuffers(frameState.transientInstanceBuffers, internals.errorRegistry);
3411
+ if (frameState.probeBlendRecordBuffer !== undefined) {
3412
+ if (!frameState.probeBlendRecordBuffer.isDestroyed) {
3413
+ const result = frameState.probeBlendRecordBuffer.destroy();
3414
+ if (!result.ok) internals.errorRegistry.fire(result.error);
3415
+ }
3416
+ delete frameState.probeBlendRecordBuffer;
3417
+ }
3418
+ frameState.probeBlendRecordBufferCapacity = 0;
3419
+ frameState.probeBlendBuffers.clear();
3307
3420
  if (frameState.morphBuffers !== undefined) {
3308
3421
  for (const entry of frameState.morphBuffers.values()) {
3309
3422
  if (!entry.buffer.isDestroyed) {
@@ -3324,6 +3437,10 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3324
3437
  }
3325
3438
  },
3326
3439
  resetForRecover(): void {
3440
+ ssrFormatReceipt = undefined;
3441
+ ssrFormatProbeGeneration = -1;
3442
+ ssrFormatProbe = undefined;
3443
+ ssrFallbackGeneration = undefined;
3327
3444
  transmissionAdmission.markDeviceLost();
3328
3445
  lastTransmissionKey = '';
3329
3446
  transmissionCapabilityGeneration = -1;
@@ -3341,6 +3458,11 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3341
3458
  persistentRenderScene.resetGpuForRecover();
3342
3459
  gpuDrivenProduction.dispose();
3343
3460
  gpuDrivenProduction = new GpuDrivenProduction(internals.device, gpuDrivenShaderFactory);
3461
+ occlusionRuntime?.dispose();
3462
+ // The replacement device is installed by the host after this hook. Do
3463
+ // not recreate device-bound query resources against the lost device;
3464
+ // the first post-recovery frame lazily binds the current device.
3465
+ occlusionRuntime = undefined;
3344
3466
  disposeFeatureGpuWork();
3345
3467
  reflectionProbeOwner.dispose();
3346
3468
  // A recovered device cannot retain handles from the lost device. The
@@ -3383,6 +3505,12 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3383
3505
  frameState.directionalShadowCacheRecorded = false;
3384
3506
  frameState.currentFrameObservationSource = undefined;
3385
3507
  frameState.lastSuccessfulCameraAntialias = undefined;
3508
+ frameState.temporalFrameTransaction.reset('device-recovery');
3509
+ frameState.temporalFrame = undefined;
3510
+ frameState.temporalFrameInput = undefined;
3511
+ frameState.lastSuccessfulTemporalView = undefined;
3512
+ frameState.successfulTemporalFrameIndex = 0;
3513
+ frameState.pendingTemporalCommit = { kind: 'none' };
3386
3514
  frameState.currentDirectionalShadowView = null;
3387
3515
  frameState.currentSpotShadowView = null;
3388
3516
  if (frameState.temporalGpuState !== undefined) {
@@ -3403,6 +3531,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3403
3531
  }
3404
3532
  frameState.retiredCompiledFrameGraphs.clear();
3405
3533
  frameState.instanceBuffers.clear();
3534
+ delete frameState.probeBlendRecordBuffer;
3535
+ frameState.probeBlendRecordBufferCapacity = 0;
3536
+ frameState.probeBlendBuffers.clear();
3406
3537
  frameState.morphBuffers?.clear();
3407
3538
  frameState.transientInstanceBuffers = [];
3408
3539
  frameState.pointShadowAtlas = null;
@@ -3470,6 +3601,71 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3470
3601
  };
3471
3602
  }
3472
3603
 
3604
+ function buildOcclusionProxyVertices(
3605
+ aabb: ArrayLike<number>,
3606
+ world: ArrayLike<number>,
3607
+ camera: import('./render-contract').CameraSnapshot,
3608
+ ): Float32Array {
3609
+ const view = computeViewMatrix(camera);
3610
+ const projection = computeProjectionMatrix(camera);
3611
+ const viewProjection = mat4.create();
3612
+ mat4.multiply(viewProjection, projection, view);
3613
+ const corners = [
3614
+ [aabb[0] ?? 0, aabb[1] ?? 0, aabb[2] ?? 0],
3615
+ [aabb[3] ?? 0, aabb[1] ?? 0, aabb[2] ?? 0],
3616
+ [aabb[3] ?? 0, aabb[4] ?? 0, aabb[2] ?? 0],
3617
+ [aabb[0] ?? 0, aabb[4] ?? 0, aabb[2] ?? 0],
3618
+ [aabb[0] ?? 0, aabb[1] ?? 0, aabb[5] ?? 0],
3619
+ [aabb[3] ?? 0, aabb[1] ?? 0, aabb[5] ?? 0],
3620
+ [aabb[3] ?? 0, aabb[4] ?? 0, aabb[5] ?? 0],
3621
+ [aabb[0] ?? 0, aabb[4] ?? 0, aabb[5] ?? 0],
3622
+ ];
3623
+ const triangles = [
3624
+ 0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 0, 4, 5, 0, 5, 1, 3, 2, 6, 3, 6, 7, 0, 3, 7, 0, 7, 4, 1, 5,
3625
+ 6, 1, 6, 2,
3626
+ ];
3627
+ const output = new Float32Array(triangles.length * 4);
3628
+ for (let index = 0; index < triangles.length; index += 1) {
3629
+ const corner = corners[triangles[index] ?? 0] ?? [0, 0, 0];
3630
+ const cx = corner[0] ?? 0;
3631
+ const cy = corner[1] ?? 0;
3632
+ const cz = corner[2] ?? 0;
3633
+ const x = (world[0] ?? 0) * cx + (world[4] ?? 0) * cy + (world[8] ?? 0) * cz + (world[12] ?? 0);
3634
+ const y = (world[1] ?? 0) * cx + (world[5] ?? 0) * cy + (world[9] ?? 0) * cz + (world[13] ?? 0);
3635
+ const z =
3636
+ (world[2] ?? 0) * cx + (world[6] ?? 0) * cy + (world[10] ?? 0) * cz + (world[14] ?? 0);
3637
+ const w =
3638
+ (world[3] ?? 0) * cx + (world[7] ?? 0) * cy + (world[11] ?? 0) * cz + (world[15] ?? 1);
3639
+ const base = index * 4;
3640
+ output[base] =
3641
+ (viewProjection[0] ?? 0) * x +
3642
+ (viewProjection[4] ?? 0) * y +
3643
+ (viewProjection[8] ?? 0) * z +
3644
+ (viewProjection[12] ?? 0) * w;
3645
+ output[base + 1] =
3646
+ (viewProjection[1] ?? 0) * x +
3647
+ (viewProjection[5] ?? 0) * y +
3648
+ (viewProjection[9] ?? 0) * z +
3649
+ (viewProjection[13] ?? 0) * w;
3650
+ output[base + 2] =
3651
+ (viewProjection[2] ?? 0) * x +
3652
+ (viewProjection[6] ?? 0) * y +
3653
+ (viewProjection[10] ?? 0) * z +
3654
+ (viewProjection[14] ?? 0) * w;
3655
+ output[base + 3] =
3656
+ (viewProjection[3] ?? 0) * x +
3657
+ (viewProjection[7] ?? 0) * y +
3658
+ (viewProjection[11] ?? 0) * z +
3659
+ (viewProjection[15] ?? 0) * w;
3660
+ }
3661
+ return output;
3662
+ }
3663
+
3664
+ /** Resolve the build identity used by every inspection submit. */
3665
+ export function resolveInspectionBuild(internals: Pick<RenderSystemInternals, 'build'>): string {
3666
+ return internals.build ?? 'render-system';
3667
+ }
3668
+
3473
3669
  // @forgeax/engine-render - built-in record context extension.
3474
3670
  //
3475
3671
  // The public RenderPipelineContext is canonical in render-contract.ts. This