@forgeax/engine-render 0.1.20 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (645) hide show
  1. package/README.md +365 -29
  2. package/dist/assembly/backend-contract.d.ts +13 -0
  3. package/dist/assembly/backend-contract.d.ts.map +1 -1
  4. package/dist/assembly/device-feature-admission.d.ts +18 -0
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -0
  6. package/dist/assembly/directional-shadow-inspection.d.ts +38 -0
  7. package/dist/assembly/directional-shadow-inspection.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +30 -19
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/host-contract.d.ts +19 -0
  11. package/dist/assembly/host-contract.d.ts.map +1 -1
  12. package/dist/assembly/render-target-host.d.ts +46 -0
  13. package/dist/assembly/render-target-host.d.ts.map +1 -0
  14. package/dist/assets/render-asset-port.d.ts.map +1 -1
  15. package/dist/authoring.mjs +4 -1
  16. package/dist/authoring.mjs.map +1 -1
  17. package/dist/capture/cube-views.d.ts +21 -0
  18. package/dist/capture/cube-views.d.ts.map +1 -0
  19. package/dist/capture/scheduler.d.ts +54 -0
  20. package/dist/capture/scheduler.d.ts.map +1 -0
  21. package/dist/{chunk-PCHBPYML.mjs → chunk-4L3P6DT5.mjs} +2 -2
  22. package/dist/chunk-4L3P6DT5.mjs.map +1 -0
  23. package/dist/{chunk-VFPLTKCU.mjs → chunk-4NPUASZR.mjs} +15 -4
  24. package/dist/chunk-4NPUASZR.mjs.map +1 -0
  25. package/dist/chunk-DIYW3M5D.mjs +5085 -0
  26. package/dist/chunk-DIYW3M5D.mjs.map +1 -0
  27. package/dist/chunk-FBC4R44J.mjs +1003 -0
  28. package/dist/chunk-FBC4R44J.mjs.map +1 -0
  29. package/dist/{chunk-CDTWTHZR.mjs → chunk-FF7K3FSO.mjs} +474 -168
  30. package/dist/chunk-FF7K3FSO.mjs.map +1 -0
  31. package/dist/chunk-HZUPSYW7.mjs +1104 -0
  32. package/dist/chunk-HZUPSYW7.mjs.map +1 -0
  33. package/dist/{chunk-TBQJOK2L.mjs → chunk-QODA5UKU.mjs} +321 -269
  34. package/dist/chunk-QODA5UKU.mjs.map +1 -0
  35. package/dist/chunk-YALPS44A.mjs +296 -0
  36. package/dist/chunk-YALPS44A.mjs.map +1 -0
  37. package/dist/{chunk-4XINDLAY.mjs → chunk-ZUWKAZ2I.mjs} +1963 -2178
  38. package/dist/chunk-ZUWKAZ2I.mjs.map +1 -0
  39. package/dist/cluster-binner.d.ts +5 -0
  40. package/dist/cluster-binner.d.ts.map +1 -1
  41. package/dist/components/camera.d.ts +1 -0
  42. package/dist/components/camera.d.ts.map +1 -1
  43. package/dist/components/cube-camera.d.ts +20 -0
  44. package/dist/components/cube-camera.d.ts.map +1 -0
  45. package/dist/components/directional-light.d.ts +11 -2
  46. package/dist/components/directional-light.d.ts.map +1 -1
  47. package/dist/components/directional-shadow-filter.d.ts +25 -0
  48. package/dist/components/directional-shadow-filter.d.ts.map +1 -0
  49. package/dist/components/index.d.ts +5 -0
  50. package/dist/components/index.d.ts.map +1 -1
  51. package/dist/components/light-helpers.d.ts +4 -0
  52. package/dist/components/light-helpers.d.ts.map +1 -1
  53. package/dist/components/light-probe.d.ts +9 -0
  54. package/dist/components/light-probe.d.ts.map +1 -0
  55. package/dist/components/rect-area-light.d.ts +12 -0
  56. package/dist/components/rect-area-light.d.ts.map +1 -0
  57. package/dist/components/reflection-probe.d.ts +17 -0
  58. package/dist/components/reflection-probe.d.ts.map +1 -0
  59. package/dist/components/scene-instance.d.ts +4 -0
  60. package/dist/components/scene-instance.d.ts.map +1 -1
  61. package/dist/components/spot-light.d.ts +3 -0
  62. package/dist/components/spot-light.d.ts.map +1 -1
  63. package/dist/construct-renderer.mjs +14836 -9620
  64. package/dist/construct-renderer.mjs.map +1 -1
  65. package/dist/device/gpu-residency.d.ts +8 -1
  66. package/dist/device/gpu-residency.d.ts.map +1 -1
  67. package/dist/device/lifecycle-transaction.d.ts +2 -0
  68. package/dist/device/lifecycle-transaction.d.ts.map +1 -0
  69. package/dist/environment/frame.d.ts.map +1 -1
  70. package/dist/environment/lifecycle.d.ts +5 -0
  71. package/dist/environment/lifecycle.d.ts.map +1 -1
  72. package/dist/errors/gpu-driven.d.ts +26 -0
  73. package/dist/errors/gpu-driven.d.ts.map +1 -0
  74. package/dist/errors/index.d.ts +1 -0
  75. package/dist/errors/index.d.ts.map +1 -1
  76. package/dist/errors/recover.d.ts +9 -1
  77. package/dist/errors/recover.d.ts.map +1 -1
  78. package/dist/errors/render.d.ts +166 -93
  79. package/dist/errors/render.d.ts.map +1 -1
  80. package/dist/extract/directional-shadow-projection.d.ts +23 -0
  81. package/dist/extract/directional-shadow-projection.d.ts.map +1 -0
  82. package/dist/extract/environment.d.ts +14 -0
  83. package/dist/extract/environment.d.ts.map +1 -1
  84. package/dist/extract/gpu-driven.d.ts +50 -0
  85. package/dist/extract/gpu-driven.d.ts.map +1 -0
  86. package/dist/extract/visibility.d.ts.map +1 -1
  87. package/dist/features/fullscreen.d.ts.map +1 -1
  88. package/dist/features/targets.d.ts.map +1 -1
  89. package/dist/features/types.d.ts +10 -0
  90. package/dist/features/types.d.ts.map +1 -1
  91. package/dist/fullscreen-post-process-pass.d.ts +13 -2
  92. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  93. package/dist/gpu-driven/batch-topology.d.ts +34 -0
  94. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  95. package/dist/gpu-driven/prepared-draw.d.ts +63 -0
  96. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -0
  97. package/dist/gpu-driven/production-raster.d.ts +97 -2
  98. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  99. package/dist/gpu-driven/view-gpu.d.ts +64 -1
  100. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  101. package/dist/gpu-scene-schema.d.ts +14 -0
  102. package/dist/gpu-scene-schema.d.ts.map +1 -1
  103. package/dist/gpu-scene.d.ts +8 -1
  104. package/dist/gpu-scene.d.ts.map +1 -1
  105. package/dist/gpu-usage.d.ts +0 -1
  106. package/dist/gpu-usage.d.ts.map +1 -1
  107. package/dist/hdrp-buffers.d.ts +21 -19
  108. package/dist/hdrp-buffers.d.ts.map +1 -1
  109. package/dist/ibl/IblPipelineCache.d.ts +4 -0
  110. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  111. package/dist/ibl/kernel-cache.d.ts +36 -0
  112. package/dist/ibl/kernel-cache.d.ts.map +1 -0
  113. package/dist/index.d.ts +39 -3
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.mjs +91 -6
  116. package/dist/index.mjs.map +1 -1
  117. package/dist/inspection-types.d.ts +214 -1
  118. package/dist/inspection-types.d.ts.map +1 -1
  119. package/dist/instances-derived-bounds.d.ts +50 -0
  120. package/dist/instances-derived-bounds.d.ts.map +1 -0
  121. package/dist/internal.d.ts +9 -0
  122. package/dist/internal.d.ts.map +1 -1
  123. package/dist/internal.mjs +23 -2
  124. package/dist/internal.mjs.map +1 -1
  125. package/dist/light-buffer-layout.d.ts +58 -140
  126. package/dist/light-buffer-layout.d.ts.map +1 -1
  127. package/dist/materials.d.ts.map +1 -1
  128. package/dist/mesh-material-bindings.d.ts +54 -0
  129. package/dist/mesh-material-bindings.d.ts.map +1 -1
  130. package/dist/pbr-pipeline.d.ts +38 -14
  131. package/dist/pbr-pipeline.d.ts.map +1 -1
  132. package/dist/pipeline/standard-forward-lane.d.ts +2 -1
  133. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  134. package/dist/pipeline/standard-lighting/errors.d.ts +4 -0
  135. package/dist/pipeline/standard-lighting/errors.d.ts.map +1 -0
  136. package/dist/pipeline/standard-lighting/graph.d.ts +24 -0
  137. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -0
  138. package/dist/pipeline/standard-lighting/inspection.d.ts +38 -0
  139. package/dist/pipeline/standard-lighting/inspection.d.ts.map +1 -0
  140. package/dist/pipeline/standard-lighting/layout.d.ts +28 -0
  141. package/dist/pipeline/standard-lighting/layout.d.ts.map +1 -0
  142. package/dist/pipeline/standard-lighting/light-frame.d.ts +33 -0
  143. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -0
  144. package/dist/pipeline/standard-lighting/membership.d.ts +14 -0
  145. package/dist/pipeline/standard-lighting/membership.d.ts.map +1 -0
  146. package/dist/pipeline/standard-lighting/prepare.d.ts +16 -0
  147. package/dist/pipeline/standard-lighting/prepare.d.ts.map +1 -0
  148. package/dist/pipeline/standard-lighting/topology.d.ts +45 -0
  149. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -0
  150. package/dist/pipeline/standard-lighting/transport.d.ts +36 -0
  151. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -0
  152. package/dist/pipeline/standard-pipeline.d.ts +4 -13
  153. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  154. package/dist/pipeline/standard-post.d.ts.map +1 -1
  155. package/dist/pipeline/standard-profile.d.ts +5 -26
  156. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  157. package/dist/pipeline-builder.d.ts +2 -0
  158. package/dist/pipeline-builder.d.ts.map +1 -1
  159. package/dist/pipeline-spec.d.ts +40 -7
  160. package/dist/pipeline-spec.d.ts.map +1 -1
  161. package/dist/plugin.d.ts.map +1 -1
  162. package/dist/point-shadow-inspection.d.ts +21 -0
  163. package/dist/point-shadow-inspection.d.ts.map +1 -0
  164. package/dist/prepare/extended-lighting/ltc-resources.d.ts +31 -0
  165. package/dist/prepare/extended-lighting/ltc-resources.d.ts.map +1 -0
  166. package/dist/prepare/extended-lighting/resources.d.ts +72 -0
  167. package/dist/prepare/extended-lighting/resources.d.ts.map +1 -0
  168. package/dist/prepare/extended-lighting/spot-modifiers.d.ts +42 -0
  169. package/dist/prepare/extended-lighting/spot-modifiers.d.ts.map +1 -0
  170. package/dist/prepare/extended-lighting/state.d.ts +24 -0
  171. package/dist/prepare/extended-lighting/state.d.ts.map +1 -0
  172. package/dist/record/frame-lighting.d.ts +33 -36
  173. package/dist/record/frame-lighting.d.ts.map +1 -1
  174. package/dist/record/frame-snapshot.d.ts +78 -24
  175. package/dist/record/frame-snapshot.d.ts.map +1 -1
  176. package/dist/record/frame-targets.d.ts +5 -0
  177. package/dist/record/frame-targets.d.ts.map +1 -1
  178. package/dist/record/frame.d.ts +16 -6
  179. package/dist/record/frame.d.ts.map +1 -1
  180. package/dist/record/gpu-pass-timing/contract.d.ts +96 -0
  181. package/dist/record/gpu-pass-timing/contract.d.ts.map +1 -0
  182. package/dist/record/gpu-pass-timing/errors.d.ts +18 -0
  183. package/dist/record/gpu-pass-timing/errors.d.ts.map +1 -0
  184. package/dist/record/gpu-pass-timing/index.d.ts +8 -0
  185. package/dist/record/gpu-pass-timing/index.d.ts.map +1 -0
  186. package/dist/record/gpu-pass-timing/parser.d.ts +11 -0
  187. package/dist/record/gpu-pass-timing/parser.d.ts.map +1 -0
  188. package/dist/record/gpu-pass-timing/session.d.ts +43 -0
  189. package/dist/record/gpu-pass-timing/session.d.ts.map +1 -0
  190. package/dist/record/gpu-timing.d.ts +20 -3
  191. package/dist/record/gpu-timing.d.ts.map +1 -1
  192. package/dist/record/helpers.d.ts +0 -20
  193. package/dist/record/helpers.d.ts.map +1 -1
  194. package/dist/record/main-pass-geometry.d.ts +6 -1
  195. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  196. package/dist/record/main-pass-material.d.ts +25 -2
  197. package/dist/record/main-pass-material.d.ts.map +1 -1
  198. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  199. package/dist/record/main-pass.d.ts +2 -0
  200. package/dist/record/main-pass.d.ts.map +1 -1
  201. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  202. package/dist/record/render-context.d.ts +98 -6
  203. package/dist/record/render-context.d.ts.map +1 -1
  204. package/dist/record/shadow-pass.d.ts.map +1 -1
  205. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  206. package/dist/record/typed-frame-graph.d.ts +119 -4
  207. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  208. package/dist/record/view-ubo.d.ts +7 -5
  209. package/dist/record/view-ubo.d.ts.map +1 -1
  210. package/dist/reflection/filter.d.ts +22 -0
  211. package/dist/reflection/filter.d.ts.map +1 -0
  212. package/dist/reflection/gpu-table.d.ts +25 -0
  213. package/dist/reflection/gpu-table.d.ts.map +1 -0
  214. package/dist/reflection/inspection.d.ts +10 -0
  215. package/dist/reflection/inspection.d.ts.map +1 -0
  216. package/dist/reflection/projection.d.ts +140 -0
  217. package/dist/reflection/projection.d.ts.map +1 -0
  218. package/dist/reflection/record-owner.d.ts +60 -0
  219. package/dist/reflection/record-owner.d.ts.map +1 -0
  220. package/dist/render-contract.d.ts +62 -14
  221. package/dist/render-contract.d.ts.map +1 -1
  222. package/dist/render-graph-primitives.d.ts +1 -0
  223. package/dist/render-graph-primitives.d.ts.map +1 -1
  224. package/dist/render-pipeline.d.ts +49 -2
  225. package/dist/render-pipeline.d.ts.map +1 -1
  226. package/dist/render-system-extract.d.ts +160 -31
  227. package/dist/render-system-extract.d.ts.map +1 -1
  228. package/dist/render-system.d.ts +28 -2
  229. package/dist/render-system.d.ts.map +1 -1
  230. package/dist/renderer.d.ts +43 -0
  231. package/dist/renderer.d.ts.map +1 -0
  232. package/dist/scene/probe-blend-record.d.ts +22 -0
  233. package/dist/scene/probe-blend-record.d.ts.map +1 -0
  234. package/dist/scene/probe-blend.d.ts +142 -0
  235. package/dist/scene/probe-blend.d.ts.map +1 -0
  236. package/dist/scene/render-scene-types.d.ts +0 -6
  237. package/dist/scene/render-scene-types.d.ts.map +1 -1
  238. package/dist/scene/render-scene.d.ts +117 -9
  239. package/dist/scene/render-scene.d.ts.map +1 -1
  240. package/dist/scene/visibility/bounds.d.ts +20 -0
  241. package/dist/scene/visibility/bounds.d.ts.map +1 -0
  242. package/dist/scene/visibility/budget.d.ts +16 -0
  243. package/dist/scene/visibility/budget.d.ts.map +1 -0
  244. package/dist/scene/visibility/facet.d.ts +73 -0
  245. package/dist/scene/visibility/facet.d.ts.map +1 -0
  246. package/dist/scene/visibility/gpu-frame-timing.d.ts +41 -0
  247. package/dist/scene/visibility/gpu-frame-timing.d.ts.map +1 -0
  248. package/dist/scene/visibility/gpu-lod.d.ts +46 -0
  249. package/dist/scene/visibility/gpu-lod.d.ts.map +1 -0
  250. package/dist/scene/visibility/inspection.d.ts +132 -0
  251. package/dist/scene/visibility/inspection.d.ts.map +1 -0
  252. package/dist/scene/visibility/lod-selector.d.ts +25 -0
  253. package/dist/scene/visibility/lod-selector.d.ts.map +1 -0
  254. package/dist/scene/visibility/occlusion-confidence.d.ts +81 -0
  255. package/dist/scene/visibility/occlusion-confidence.d.ts.map +1 -0
  256. package/dist/scene/visibility/occlusion-pass.d.ts +57 -0
  257. package/dist/scene/visibility/occlusion-pass.d.ts.map +1 -0
  258. package/dist/scene/visibility/occlusion-query-pool.d.ts +57 -0
  259. package/dist/scene/visibility/occlusion-query-pool.d.ts.map +1 -0
  260. package/dist/scene/visibility/occlusion-runtime.d.ts +96 -0
  261. package/dist/scene/visibility/occlusion-runtime.d.ts.map +1 -0
  262. package/dist/scene/visibility/types.d.ts +34 -0
  263. package/dist/scene/visibility/types.d.ts.map +1 -0
  264. package/dist/shadow-atlas.d.ts +3 -3
  265. package/dist/shadow-atlas.d.ts.map +1 -1
  266. package/dist/ssao-buffers.d.ts +3 -0
  267. package/dist/ssao-buffers.d.ts.map +1 -1
  268. package/dist/ssr/admission.d.ts +122 -0
  269. package/dist/ssr/admission.d.ts.map +1 -0
  270. package/dist/ssr/composition.d.ts +32 -0
  271. package/dist/ssr/composition.d.ts.map +1 -0
  272. package/dist/ssr/errors.d.ts +39 -0
  273. package/dist/ssr/errors.d.ts.map +1 -0
  274. package/dist/ssr/identity.d.ts +13 -0
  275. package/dist/ssr/identity.d.ts.map +1 -0
  276. package/dist/systems/skin-palette-allocator.d.ts +1 -1
  277. package/dist/targets/contracts.d.ts +66 -0
  278. package/dist/targets/contracts.d.ts.map +1 -0
  279. package/dist/targets/graph-projection.d.ts +29 -0
  280. package/dist/targets/graph-projection.d.ts.map +1 -0
  281. package/dist/targets/material-source.d.ts +34 -0
  282. package/dist/targets/material-source.d.ts.map +1 -0
  283. package/dist/targets/owner.d.ts +32 -0
  284. package/dist/targets/owner.d.ts.map +1 -0
  285. package/dist/targets/physical.d.ts +18 -0
  286. package/dist/targets/physical.d.ts.map +1 -0
  287. package/dist/targets/readback.d.ts +39 -0
  288. package/dist/targets/readback.d.ts.map +1 -0
  289. package/dist/temporal/frame-coordinator.d.ts +13 -0
  290. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  291. package/dist/temporal/frame.d.ts +2 -0
  292. package/dist/temporal/frame.d.ts.map +1 -1
  293. package/dist/temporal/index.mjs +6 -5
  294. package/dist/temporal/view.d.ts +7 -0
  295. package/dist/temporal/view.d.ts.map +1 -1
  296. package/dist/tilemap-chunk-extract-system.d.ts +1 -1
  297. package/dist/typed-render-graph-primitives.d.ts +5 -1
  298. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  299. package/dist/typed-shadow-passes.d.ts +1 -1
  300. package/dist/typed-shadow-passes.d.ts.map +1 -1
  301. package/dist/volume/passes.d.ts +9 -5
  302. package/dist/volume/passes.d.ts.map +1 -1
  303. package/package.json +20 -20
  304. package/src/__tests__/bloom-exact-zero.unit.test.ts +48 -19
  305. package/src/__tests__/bloom-lifecycle.integration.test.ts +41 -13
  306. package/src/__tests__/camera-target-consumption.unit.test.ts +53 -0
  307. package/src/__tests__/cluster-binner.unit.test.ts +35 -0
  308. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +12 -2
  309. package/src/__tests__/cube-camera-scheduler.unit.test.ts +70 -0
  310. package/src/__tests__/cube-camera-single-submit.perf.test.ts +75 -0
  311. package/src/__tests__/cube-camera-view-matrices.unit.test.ts +41 -0
  312. package/src/__tests__/cube-camera-vocabulary.unit.test.ts +47 -0
  313. package/src/__tests__/cube-camera.integration.test.ts +42 -0
  314. package/src/__tests__/detached-inspection.integration.test.ts +6 -0
  315. package/src/__tests__/device-feature-admission.unit.test.ts +85 -0
  316. package/src/__tests__/direct-light-snapshot.unit.test.ts +62 -6
  317. package/src/__tests__/docs-gate-green.unit.test.ts +38 -0
  318. package/src/__tests__/environment-generation.integration.test.ts +16 -0
  319. package/src/__tests__/evidence-identity.ts +13 -0
  320. package/src/__tests__/evidence-identity.unit.test.ts +24 -0
  321. package/src/__tests__/extended-lighting-recovery.browser.test.ts +14 -0
  322. package/src/__tests__/extended-lighting-recovery.dawn.test.ts +14 -0
  323. package/src/__tests__/extended-lighting-recovery.integration.test.ts +81 -0
  324. package/src/__tests__/extended-lighting-resources.unit.test.ts +51 -0
  325. package/src/__tests__/extended-lighting-zero-feature.integration.test.ts +52 -0
  326. package/src/__tests__/factory-contract.integration.test.ts +288 -29
  327. package/src/__tests__/fog-component.unit.test.ts +67 -0
  328. package/src/__tests__/fog.integration.test.ts +86 -0
  329. package/src/__tests__/frame-observation.contract.test.ts +15 -0
  330. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +37 -0
  331. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +146 -0
  332. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +10 -8
  333. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +18 -1
  334. package/src/__tests__/gpu-driven-production.integration.test.ts +579 -17
  335. package/src/__tests__/gpu-driven-scaling.unit.test.ts +66 -2
  336. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +26 -1
  337. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +101 -6
  338. package/src/__tests__/gpu-driven-view.browser.test.ts +3 -3
  339. package/src/__tests__/gpu-driven-view.dawn.test.ts +3 -3
  340. package/src/__tests__/gpu-driven-view.unit.test.ts +50 -0
  341. package/src/__tests__/gpu-lod-evidence.ts +188 -0
  342. package/src/__tests__/gpu-lod-fallback.integration.test.ts +10 -0
  343. package/src/__tests__/gpu-lod-indirect.integration.test.ts +40 -0
  344. package/src/__tests__/gpu-lod-parity.dawn.test.ts +29 -0
  345. package/src/__tests__/gpu-lod-readiness.integration.test.ts +52 -0
  346. package/src/__tests__/gpu-lod-schema.unit.test.ts +66 -0
  347. package/src/__tests__/gpu-lod-selector.unit.test.ts +28 -0
  348. package/src/__tests__/gpu-lod-silhouette.browser.test.ts +13 -0
  349. package/src/__tests__/gpu-pass-timing-browser-runner.ts +281 -0
  350. package/src/__tests__/gpu-pass-timing-coexistence.integration.test.ts +80 -0
  351. package/src/__tests__/gpu-pass-timing-contract.unit.test.ts +243 -0
  352. package/src/__tests__/gpu-pass-timing-exact-zero.unit.test.ts +131 -0
  353. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +230 -0
  354. package/src/__tests__/gpu-pass-timing-observation.unit.test.ts +233 -0
  355. package/src/__tests__/gpu-pass-timing-session.unit.test.ts +438 -0
  356. package/src/__tests__/gpu-pass-timing.browser.test.ts +26 -0
  357. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +125 -0
  358. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +66 -1
  359. package/src/__tests__/gpu-scene.dawn.test.ts +8 -1
  360. package/src/__tests__/gpu-scene.unit.test.ts +49 -3
  361. package/src/__tests__/hdrp-group2-visibility.unit.test.ts +24 -0
  362. package/src/__tests__/instances-culling.integration.test.ts +94 -0
  363. package/src/__tests__/instances-derived-bounds.unit.test.ts +118 -0
  364. package/src/__tests__/light-buffer-layout.unit.test.ts +113 -3
  365. package/src/__tests__/light-inspection.unit.test.ts +55 -0
  366. package/src/__tests__/light-resource-error.unit.test.ts +43 -0
  367. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +109 -0
  368. package/src/__tests__/lod-bounds.unit.test.ts +42 -0
  369. package/src/__tests__/lod-cpu-lane.integration.test.ts +55 -0
  370. package/src/__tests__/lod-cpu-no-change.perf.test.ts +19 -0
  371. package/src/__tests__/lod-gpu-frame-timing.unit.test.ts +49 -0
  372. package/src/__tests__/lod-multi-view.integration.test.ts +63 -0
  373. package/src/__tests__/lod-occlusion-device-loss.integration.test.ts +27 -0
  374. package/src/__tests__/lod-occlusion-final-decision.integration.test.ts +47 -0
  375. package/src/__tests__/lod-occlusion-inspection.integration.test.ts +217 -0
  376. package/src/__tests__/lod-occlusion-inspection.schema.test-d.ts +14 -0
  377. package/src/__tests__/lod-occlusion-inspection.unit.test.ts +72 -0
  378. package/src/__tests__/lod-occlusion-visual-evidence.ts +91 -0
  379. package/src/__tests__/lod-occlusion.browser.test.ts +15 -0
  380. package/src/__tests__/lod-occlusion.dawn.test.ts +20 -0
  381. package/src/__tests__/lod-selector.profile.test.ts +35 -0
  382. package/src/__tests__/lod-selector.unit.test.ts +68 -0
  383. package/src/__tests__/lod-visibility-identity.unit.test.ts +198 -0
  384. package/src/__tests__/lod-visibility-lifecycle.integration.test.ts +132 -0
  385. package/src/__tests__/ltc-resources.unit.test.ts +32 -0
  386. package/src/__tests__/material-contract-inventory.unit.test.ts +17 -0
  387. package/src/__tests__/material-observation.integration.test.ts +101 -0
  388. package/src/__tests__/occlusion-confidence.unit.test.ts +168 -0
  389. package/src/__tests__/occlusion-invalidation.integration.test.ts +37 -0
  390. package/src/__tests__/occlusion-pass-routing.unit.test.ts +16 -0
  391. package/src/__tests__/occlusion-pass.dawn.test.ts +20 -0
  392. package/src/__tests__/occlusion-pass.integration.test.ts +43 -0
  393. package/src/__tests__/occlusion-pass.rhi-null.test.ts +22 -0
  394. package/src/__tests__/occlusion-query-pool.rhi-null.test.ts +21 -0
  395. package/src/__tests__/occlusion-query-pool.unit.test.ts +95 -0
  396. package/src/__tests__/occlusion-query-ticket.integration.test.ts +47 -0
  397. package/src/__tests__/occlusion-query.dawn.test.ts +16 -0
  398. package/src/__tests__/occlusion-query.evidence.ts +178 -0
  399. package/src/__tests__/occlusion-query.rhi-null.test.ts +31 -0
  400. package/src/__tests__/occlusion-runtime.unit.test.ts +120 -0
  401. package/src/__tests__/pbr-view-layout.unit.test.ts +16 -0
  402. package/src/__tests__/point-shadow-inspection.unit.test.ts +60 -0
  403. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +196 -0
  404. package/src/__tests__/probe-bindings.unit.test.ts +200 -0
  405. package/src/__tests__/probe-blend-numeric.unit.test.ts +67 -0
  406. package/src/__tests__/probe-blend-oracle.unit.test.ts +136 -0
  407. package/src/__tests__/probe-capacity.integration.test.ts +97 -0
  408. package/src/__tests__/probe-recovery.integration.test.ts +104 -0
  409. package/src/__tests__/probe-scene-dirty.integration.test.ts +211 -0
  410. package/src/__tests__/profiler-phase-catalog.test.ts +4 -1
  411. package/src/__tests__/profiler-phase-token.test.ts +1 -0
  412. package/src/__tests__/public-surface.test-d.ts +10 -0
  413. package/src/__tests__/rect-area-boundary.unit.test.ts +24 -0
  414. package/src/__tests__/reflection-fallback-graph.unit.test.ts +67 -0
  415. package/src/__tests__/reflection-fallback-inspection.unit.test.ts +77 -0
  416. package/src/__tests__/reflection-fallback-lifecycle.integration.test.ts +21 -0
  417. package/src/__tests__/reflection-fallback-projection.unit.test.ts +90 -0
  418. package/src/__tests__/reflection-fallback-source.unit.test.ts +32 -0
  419. package/src/__tests__/reflection-probe-authoring.unit.test.ts +71 -0
  420. package/src/__tests__/reflection-probe-filter.unit.test.ts +54 -0
  421. package/src/__tests__/reflection-probe-ibl-topology.unit.test.ts +45 -0
  422. package/src/__tests__/reflection-probe-owner-placement.unit.test.ts +39 -0
  423. package/src/__tests__/reflection-probe-pass-encode.integration.test.ts +101 -0
  424. package/src/__tests__/reflection-probe-performance.perf.test.ts +60 -0
  425. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +120 -0
  426. package/src/__tests__/reflection-probe-recovery.integration.test.ts +55 -0
  427. package/src/__tests__/reflection-probe-selection.unit.test.ts +46 -0
  428. package/src/__tests__/render-error-code-owner.test-d.ts +19 -3
  429. package/src/__tests__/render-feature-docs.unit.test.ts +12 -7
  430. package/src/__tests__/render-graph-fallback.integration.test.ts +24 -0
  431. package/src/__tests__/render-perf/extended-lighting.bench.test.ts +66 -0
  432. package/src/__tests__/render-scene-projection.unit.test.ts +447 -18
  433. package/src/__tests__/render-target-admission.unit.test.ts +80 -0
  434. package/src/__tests__/render-target-graph-projection.unit.test.ts +102 -0
  435. package/src/__tests__/render-target-inspection.unit.test.ts +52 -0
  436. package/src/__tests__/render-target-material-source.test-d.ts +30 -0
  437. package/src/__tests__/render-target-material-source.unit.test.ts +97 -0
  438. package/src/__tests__/render-target-owner-placement.unit.test.ts +203 -0
  439. package/src/__tests__/render-target-owner.unit.test.ts +122 -0
  440. package/src/__tests__/render-target-probe-soak.perf.test.ts +94 -0
  441. package/src/__tests__/render-target-public-schema.json +107 -0
  442. package/src/__tests__/render-target-public-schema.unit.test.ts +91 -0
  443. package/src/__tests__/render-target-public-surface.test-d.ts +83 -0
  444. package/src/__tests__/render-target-readback.integration.test.ts +90 -0
  445. package/src/__tests__/render-target-recovery.integration.test.ts +85 -0
  446. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +124 -0
  447. package/src/__tests__/renderer-lifecycle.integration.test.ts +14 -0
  448. package/src/__tests__/shader-stage-surface.unit.test.ts +19 -4
  449. package/src/__tests__/skin-motion-regression.unit.test.ts +154 -0
  450. package/src/__tests__/skinned-shadow-caster.test.ts +13 -4
  451. package/src/__tests__/spot-modifier-projection.unit.test.ts +70 -0
  452. package/src/__tests__/ssr-admission-matrix.integration.test.ts +206 -0
  453. package/src/__tests__/ssr-composition.unit.test.ts +46 -0
  454. package/src/__tests__/ssr-fallback-zero-work.bench.test.ts +94 -0
  455. package/src/__tests__/ssr-recovery-errors.unit.test.ts +58 -0
  456. package/src/__tests__/standard-cluster-capability.integration.test.ts +63 -0
  457. package/src/__tests__/standard-cluster-transport.unit.test.ts +121 -0
  458. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +299 -0
  459. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +15 -5
  460. package/src/__tests__/standard-lighting-inspection.integration.test.ts +138 -0
  461. package/src/__tests__/standard-lighting-ssot.unit.test.ts +18 -0
  462. package/src/__tests__/standard-lighting-write.unit.test.ts +441 -0
  463. package/src/__tests__/standard-lighting.integration.test.ts +67 -0
  464. package/src/__tests__/standard-pipeline.dawn.test.ts +206 -5
  465. package/src/__tests__/standard-pipeline.integration.test.ts +181 -150
  466. package/src/__tests__/standard-probe-binding.unit.test.ts +25 -0
  467. package/src/__tests__/standard-profile-oracle.unit.test.ts +23 -38
  468. package/src/__tests__/temporal-fallback-generation.integration.test.ts +16 -0
  469. package/src/__tests__/temporal-view.unit.test.ts +3 -3
  470. package/src/__tests__/transmission-prewarm.unit.test.ts +69 -0
  471. package/src/__tests__/typed-pipeline-topology.unit.test.ts +395 -14
  472. package/src/__tests__/urp-pipeline-targets.unit.test.ts +2 -2
  473. package/src/__tests__/visibility-budget.integration.test.ts +140 -0
  474. package/src/__tests__/visibility-budget.unit.test.ts +70 -0
  475. package/src/__tests__/visibility-extract.integration.test.ts +11 -6
  476. package/src/__tests__/visibility-resolver.unit.test.ts +15 -0
  477. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +6 -3
  478. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +31 -28
  479. package/src/assembly/backend-contract.ts +13 -0
  480. package/src/assembly/device-feature-admission.ts +80 -0
  481. package/src/assembly/directional-shadow-inspection.ts +122 -0
  482. package/src/assembly/factory.ts +1411 -366
  483. package/src/assembly/host-contract.ts +35 -0
  484. package/src/assembly/render-target-host.ts +847 -0
  485. package/src/assets/asset-decoders.ts +1 -1
  486. package/src/assets/render-asset-port.ts +7 -1
  487. package/src/capture/cube-views.ts +69 -0
  488. package/src/capture/scheduler.ts +265 -0
  489. package/src/cluster-binner.ts +31 -0
  490. package/src/components/__tests__/lighting-authoring.unit.test.ts +39 -0
  491. package/src/components/__tests__/rect-area-light.unit.test.ts +33 -0
  492. package/src/components/__tests__/schema-contract.unit.test.ts +12 -0
  493. package/src/components/__tests__/schema-parity.unit.test.ts +9 -0
  494. package/src/components/camera.ts +4 -0
  495. package/src/components/cube-camera.ts +52 -0
  496. package/src/components/directional-light.ts +22 -2
  497. package/src/components/directional-shadow-filter.ts +57 -0
  498. package/src/components/index.ts +5 -0
  499. package/src/components/light-helpers.ts +133 -8
  500. package/src/components/light-probe.ts +13 -0
  501. package/src/components/rect-area-light.ts +15 -0
  502. package/src/components/reflection-probe.ts +49 -0
  503. package/src/components/scene-instance.ts +6 -0
  504. package/src/components/spot-light.ts +3 -0
  505. package/src/device/__tests__/extended-lighting-recovery.unit.test.ts +24 -0
  506. package/src/device/gpu-residency.ts +140 -9
  507. package/src/device/lifecycle-transaction.ts +12 -0
  508. package/src/environment/frame.ts +6 -24
  509. package/src/environment/lifecycle.ts +20 -1
  510. package/src/errors/gpu-driven.ts +92 -0
  511. package/src/errors/index.ts +1 -0
  512. package/src/errors/recover.ts +32 -0
  513. package/src/errors/render.ts +341 -143
  514. package/src/extract/camera.ts +3 -3
  515. package/src/extract/directional-shadow-projection.ts +61 -0
  516. package/src/extract/environment.ts +57 -0
  517. package/src/extract/gpu-driven.ts +108 -0
  518. package/src/extract/visibility.ts +2 -1
  519. package/src/features/fullscreen.ts +3 -0
  520. package/src/features/targets.ts +4 -1
  521. package/src/features/types.ts +10 -0
  522. package/src/fullscreen-post-process-pass.ts +43 -2
  523. package/src/gpu-driven/batch-topology.ts +207 -28
  524. package/src/gpu-driven/prepared-draw.ts +208 -0
  525. package/src/gpu-driven/production-raster.ts +953 -43
  526. package/src/gpu-driven/view-gpu.ts +411 -29
  527. package/src/gpu-scene-schema.ts +13 -0
  528. package/src/gpu-scene.ts +140 -64
  529. package/src/gpu-usage.ts +0 -1
  530. package/src/hdrp-buffers.ts +169 -63
  531. package/src/ibl/IblPipelineCache.ts +11 -0
  532. package/src/ibl/kernel-cache.ts +88 -0
  533. package/src/index.ts +222 -0
  534. package/src/inspection-types.ts +276 -1
  535. package/src/instances-derived-bounds.ts +171 -0
  536. package/src/internal.ts +38 -0
  537. package/src/light-buffer-layout.ts +155 -310
  538. package/src/materials.ts +16 -2
  539. package/src/mesh-material-bindings.ts +132 -0
  540. package/src/pbr-pipeline.ts +255 -60
  541. package/src/pipeline/standard-forward-lane.ts +93 -16
  542. package/src/pipeline/standard-lighting/errors.ts +6 -0
  543. package/src/pipeline/standard-lighting/graph.ts +158 -0
  544. package/src/pipeline/standard-lighting/inspection.ts +88 -0
  545. package/src/pipeline/standard-lighting/layout.ts +31 -0
  546. package/src/pipeline/standard-lighting/light-frame.ts +35 -0
  547. package/src/pipeline/standard-lighting/membership.ts +43 -0
  548. package/src/pipeline/standard-lighting/prepare.ts +37 -0
  549. package/src/pipeline/standard-lighting/topology.ts +120 -0
  550. package/src/pipeline/standard-lighting/transport.ts +78 -0
  551. package/src/pipeline/standard-pipeline.ts +124 -193
  552. package/src/pipeline/standard-post.ts +26 -7
  553. package/src/pipeline/standard-profile.ts +17 -47
  554. package/src/pipeline-builder.ts +5 -2
  555. package/src/pipeline-spec.ts +95 -21
  556. package/src/plugin.ts +4 -0
  557. package/src/point-shadow-inspection.ts +55 -0
  558. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +56 -0
  559. package/src/prepare/extended-lighting/ltc-resources.ts +136 -0
  560. package/src/prepare/extended-lighting/resources.ts +389 -0
  561. package/src/prepare/extended-lighting/spot-modifiers.ts +194 -0
  562. package/src/prepare/extended-lighting/state.ts +92 -0
  563. package/src/record/__tests__/gpu-timing.unit.test.ts +177 -0
  564. package/src/record/__tests__/temporal-payload.unit.test.ts +64 -1
  565. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +73 -0
  566. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +20 -1
  567. package/src/record/__tests__/view-ubo-layout.unit.test.ts +40 -0
  568. package/src/record/frame-lighting.ts +591 -452
  569. package/src/record/frame-snapshot.ts +92 -39
  570. package/src/record/frame-targets.ts +17 -0
  571. package/src/record/frame.ts +662 -158
  572. package/src/record/gpu-pass-timing/contract.ts +180 -0
  573. package/src/record/gpu-pass-timing/errors.ts +34 -0
  574. package/src/record/gpu-pass-timing/index.ts +37 -0
  575. package/src/record/gpu-pass-timing/parser.ts +97 -0
  576. package/src/record/gpu-pass-timing/session.ts +938 -0
  577. package/src/record/gpu-timing.ts +112 -39
  578. package/src/record/helpers.ts +0 -58
  579. package/src/record/main-pass-geometry.ts +324 -119
  580. package/src/record/main-pass-material.ts +103 -10
  581. package/src/record/main-pass-sprite-draws.ts +99 -103
  582. package/src/record/main-pass.ts +71 -15
  583. package/src/record/mesh-ssbo.ts +47 -36
  584. package/src/record/render-context.ts +136 -3
  585. package/src/record/shadow-pass.ts +83 -68
  586. package/src/record/skybox-post-pass.ts +20 -4
  587. package/src/record/typed-frame-graph.ts +1034 -43
  588. package/src/record/view-ubo.ts +72 -47
  589. package/src/reflection/filter.ts +81 -0
  590. package/src/reflection/gpu-table.ts +44 -0
  591. package/src/reflection/inspection.ts +67 -0
  592. package/src/reflection/projection.ts +476 -0
  593. package/src/reflection/record-owner.ts +1283 -0
  594. package/src/render-contract.ts +93 -10
  595. package/src/render-graph-primitives.ts +9 -4
  596. package/src/render-pipeline.ts +122 -2
  597. package/src/render-system-extract.ts +1041 -225
  598. package/src/render-system.ts +1310 -85
  599. package/src/renderer.ts +222 -0
  600. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +36 -0
  601. package/src/scene/probe-blend-record.ts +54 -0
  602. package/src/scene/probe-blend.ts +813 -0
  603. package/src/scene/render-scene-types.ts +0 -7
  604. package/src/scene/render-scene.ts +942 -171
  605. package/src/scene/visibility/bounds.ts +57 -0
  606. package/src/scene/visibility/budget.ts +40 -0
  607. package/src/scene/visibility/facet.ts +400 -0
  608. package/src/scene/visibility/gpu-frame-timing.ts +101 -0
  609. package/src/scene/visibility/gpu-lod.ts +119 -0
  610. package/src/scene/visibility/inspection.ts +313 -0
  611. package/src/scene/visibility/lod-selector.ts +84 -0
  612. package/src/scene/visibility/occlusion-confidence.ts +478 -0
  613. package/src/scene/visibility/occlusion-pass.ts +165 -0
  614. package/src/scene/visibility/occlusion-query-pool.ts +178 -0
  615. package/src/scene/visibility/occlusion-runtime.ts +672 -0
  616. package/src/scene/visibility/types.ts +50 -0
  617. package/src/shadow-atlas.ts +5 -5
  618. package/src/ssao-buffers.ts +9 -2
  619. package/src/ssr/admission.ts +451 -0
  620. package/src/ssr/composition.ts +73 -0
  621. package/src/ssr/errors.ts +76 -0
  622. package/src/ssr/identity.ts +12 -0
  623. package/src/systems/skin-palette-allocator.ts +1 -1
  624. package/src/targets/contracts.ts +182 -0
  625. package/src/targets/graph-projection.ts +169 -0
  626. package/src/targets/material-source.ts +143 -0
  627. package/src/targets/owner.ts +214 -0
  628. package/src/targets/physical.ts +18 -0
  629. package/src/targets/readback.ts +153 -0
  630. package/src/temporal/frame-coordinator.ts +26 -0
  631. package/src/temporal/frame.ts +14 -1
  632. package/src/temporal/view.ts +11 -0
  633. package/src/tilemap-chunk-extract-system.ts +4 -4
  634. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +59 -20
  635. package/src/typed-render-graph-primitives.ts +53 -3
  636. package/src/typed-shadow-passes.ts +46 -40
  637. package/src/volume/passes.ts +61 -58
  638. package/dist/.tsbuildinfo +0 -1
  639. package/dist/chunk-4XINDLAY.mjs.map +0 -1
  640. package/dist/chunk-CDTWTHZR.mjs.map +0 -1
  641. package/dist/chunk-M2MP4ZOU.mjs +0 -73
  642. package/dist/chunk-M2MP4ZOU.mjs.map +0 -1
  643. package/dist/chunk-PCHBPYML.mjs.map +0 -1
  644. package/dist/chunk-TBQJOK2L.mjs.map +0 -1
  645. package/dist/chunk-VFPLTKCU.mjs.map +0 -1
@@ -19,12 +19,14 @@
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
25
  import type { BindGroupEntry, BindGroupLayout, Buffer, RenderPipeline } from '@forgeax/engine-rhi';
25
26
  import { err, ok, type Result, RhiError } from '@forgeax/engine-rhi';
26
27
  import {
27
28
  derive,
29
+ deriveTextureLayout,
28
30
  type Handle,
29
31
  type MaterialAsset,
30
32
  type MaterialTextureValue,
@@ -32,8 +34,14 @@ import {
32
34
  type RenderPipelineAsset,
33
35
  RenderQueue,
34
36
  type SamplerAsset,
37
+ type TextureAsset,
35
38
  toShared,
36
39
  } from '@forgeax/engine-types';
40
+ import {
41
+ type DirectionalShadowInspectionSource,
42
+ directionalShadowProfileFromQuality,
43
+ projectDirectionalShadowInspectionSource,
44
+ } from './assembly/directional-shadow-inspection';
37
45
  import { createClusterBinScratch } from './cluster-binner';
38
46
  import { EnvironmentLifecycle } from './environment/lifecycle';
39
47
  import {
@@ -57,12 +65,22 @@ import {
57
65
  DEPTH_MIN_PARAMS_BYTE_SIZE,
58
66
  entryHasDepthRead,
59
67
  type PostProcessShaderEntry,
68
+ postProcessShaderEntrySignature,
69
+ postProcessShaderModuleLabel,
70
+ postProcessShaderPipelineLabel,
60
71
  } from './fullscreen-post-process-pass';
61
72
  import { GpuDrivenProduction } from './gpu-driven/production-raster';
62
73
  import type {
63
74
  BloomInspection,
75
+ DirectionalShadowInspection,
76
+ GpuDrivenProductionInspection,
77
+ LodOcclusionInspection,
78
+ LodOcclusionInspectionRow,
79
+ LodOcclusionWorldInspection,
64
80
  MotionBlurInspection,
81
+ ReflectionProbeInspection,
65
82
  RenderSceneInspection,
83
+ SsrDependenciesInspection,
66
84
  TemporalTargetInspection,
67
85
  TransmissionInspection,
68
86
  } from './inspection-types';
@@ -80,11 +98,18 @@ import {
80
98
  import { createPointsLinesLaneAdapter, type PointsLinesBackend } from './points-lines/record';
81
99
  import type { PointsLinesRetainedSnapshot } from './points-lines/snapshot';
82
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';
83
107
  import { inspectVolumetricFog } from './volume/inspection';
84
108
 
85
109
  export type { RenderSceneInspection } from './inspection-types';
86
110
 
87
111
  import { deriveVertexLayoutProjection } from '@forgeax/engine-geometry';
112
+ import { createCubeCaptureScheduler } from './capture/scheduler';
88
113
  import type { EnvironmentInspection } from './environment/inspection';
89
114
  import {
90
115
  GPU_BUFFER_USAGE_COPY_DST,
@@ -92,22 +117,27 @@ import {
92
117
  GPU_BUFFER_USAGE_UNIFORM,
93
118
  GPU_BUFFER_USAGE_VERTEX,
94
119
  } from './gpu-usage';
120
+ import { resetHdrpBuffers } from './hdrp-buffers';
95
121
  import { assembleMaterialWithSkylightEntries } from './ibl/skylight-bind-group';
96
122
  import { disposeInstanceBuffers, disposeTransientInstanceBuffers } from './instance-buffer-cache';
97
- import type { MeshMaterialBindingObservation } from './mesh-material-bindings';
98
- import { validateClusterGrid } from './pipeline/standard-pipeline';
99
123
  import {
100
- DEFAULT_CLUSTER_GRID,
101
- DEFAULT_STANDARD_PROFILE,
102
- resolveStandardLane,
103
- } from './pipeline/standard-profile';
124
+ type MeshMaterialBindingObservation,
125
+ type MeshMaterialBindingPreparationFailure,
126
+ type MeshMaterialBindingResidency,
127
+ projectMeshMaterialBindingObservation,
128
+ } from './mesh-material-bindings';
129
+ import type { StandardLightingInspection } from './pipeline/standard-lighting/inspection';
130
+ import { validateClusterGrid } from './pipeline/standard-pipeline';
131
+ import { DEFAULT_CLUSTER_GRID } from './pipeline/standard-profile';
104
132
  import { PipelineSpecError } from './pipeline-spec';
133
+ import type { PointShadowInspection } from './point-shadow-inspection';
105
134
  import { PostProcessError } from './post-process-errors';
106
135
  import {
107
136
  createPreparedGraphicsResolver,
108
137
  type PreparedGraphicsResolver,
109
138
  } from './prepare/prepared-graphics-resolver';
110
139
  import {
140
+ type CubeCaptureFrameInput,
111
141
  type FrameObservation,
112
142
  type FrameObservationOptions,
113
143
  observeCurrentFrame,
@@ -120,10 +150,18 @@ import type {
120
150
  GraphTargetCaptureRequest,
121
151
  RenderFrameState,
122
152
  } from './record/frame-snapshot';
123
- import { getTextureIdentity } from './record/frame-snapshot';
153
+ import { getTextureIdentity, worldEntityKey } from './record/frame-snapshot';
154
+ import type { GpuPassTimingReason } from './record/gpu-pass-timing/errors';
155
+ import type {
156
+ GpuPassTimingCapture,
157
+ GpuPassTimingFrameIdentity,
158
+ GpuPassTimingSession,
159
+ } from './record/gpu-pass-timing/session';
124
160
  import type { GpuTimingCapture } from './record/gpu-timing';
161
+ import { computeProjectionMatrix, computeViewMatrix } from './record/helpers';
125
162
  import { applyParamSnapshotToUbo, residentTextureView } from './record/main-pass-material';
126
163
  import type { RenderSystemInternals } from './record/render-context';
164
+ import type { CubeCaptureGraphState } from './record/typed-frame-graph';
127
165
  import {
128
166
  type RenderFeatureGraphCandidate,
129
167
  resetRenderFeatureGraphState,
@@ -137,10 +175,18 @@ import {
137
175
  type RenderPhaseSkipReason,
138
176
  type RenderRecordPhase,
139
177
  } from './render-contract';
140
- import type { DispatchEntry, ExtractedFrame, RenderableSnapshot } from './render-system-extract';
178
+ import type {
179
+ DispatchEntry,
180
+ ExtractedFrame,
181
+ ExtractedLights,
182
+ MaterialSnapshot,
183
+ RenderableSnapshot,
184
+ } from './render-system-extract';
141
185
  import { extractFrames } from './render-system-extract';
142
186
  import { PersistentRenderScene } from './scene/render-scene';
187
+ import { inspectLodOcclusion } from './scene/visibility/inspection';
143
188
  import { resolveSsaoParameters } from './ssao-config';
189
+ import { projectSsrDependencies, resolveSsrAdmissionGeneration } from './ssr/admission';
144
190
  import {
145
191
  getTransparentSortConfig,
146
192
  TRANSPARENT_SORT_MODE_DISTANCE,
@@ -148,6 +194,7 @@ import {
148
194
  TRANSPARENT_SORT_MODE_LAYER_YZ,
149
195
  TRANSPARENT_SORT_MODE_LAYER_Z,
150
196
  } from './systems/transparent-sort-config';
197
+ import type { RenderTarget } from './targets/contracts';
151
198
  import { createTemporalFrameTransaction, type TemporalFrame } from './temporal/frame';
152
199
  import { retireTemporalGpuState } from './temporal/gpu';
153
200
  import { inspectTemporal, type TemporalInspection } from './temporal/inspection';
@@ -163,6 +210,136 @@ import {
163
210
  inspectTransmissionFromAdmission,
164
211
  } from './transmission/inspection';
165
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
+
328
+ type TimingSessionHost = RenderSystemInternals & {
329
+ gpuPassTimingSession?: GpuPassTimingSession | undefined;
330
+ gpuPassTimingCapture?: GpuPassTimingCapture | undefined;
331
+ gpuPassTimingFrameIdentity?: GpuPassTimingFrameIdentity | undefined;
332
+ gpuPassTimingBeginReason?: GpuPassTimingReason | undefined;
333
+ };
334
+
335
+ /** Attach the Render-owned timing session without widening the public renderer surface. */
336
+ export function attachGpuPassTimingSession(
337
+ internals: object,
338
+ session: GpuPassTimingSession | undefined,
339
+ ): void {
340
+ (internals as TimingSessionHost).gpuPassTimingSession = session;
341
+ }
342
+
166
343
  export type {
167
344
  _InternalRenderPipelineContext,
168
345
  _StandardForwardSceneView,
@@ -232,7 +409,7 @@ function sortTransparentDispatch(
232
409
  const camPos = camera.position;
233
410
 
234
411
  // Squared camera distance per entry (negated → ascending = back-to-front).
235
- // 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]).
236
413
  const negDistSq = (entry: DispatchEntry): number => {
237
414
  const tx = renderables[entry.renderableIndex]?.transform;
238
415
  if (tx === undefined) return 0;
@@ -338,6 +515,8 @@ export interface RenderSystem {
338
515
  observeCurrentFrame(
339
516
  options: FrameObservationOptions,
340
517
  ): Promise<Result<FrameObservation, ObservationUnavailableError>>;
518
+ /** Resolve the renderer-owned GPU LOD counters for the last accepted submit. */
519
+ observeLodOcclusion(): Promise<void>;
341
520
  /** Internal same-frame graph target access for backend diagnostics. */
342
521
  getCurrentGraphTarget(name: string): CurrentGraphTarget | undefined;
343
522
  /** Internal test capture copied into the draw's command submission. */
@@ -354,13 +533,29 @@ export interface RenderSystem {
354
533
  readonly visibilityStats: { explicitlyHidden: number };
355
534
  /** Persistent scene maintenance evidence from the ordinary single-World path. */
356
535
  readonly renderScene: RenderSceneInspection;
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;
541
+ /** True while a progressive CubeCamera candidate still owns unfinished faces. */
542
+ isCubeCapturePending(target: RenderTarget): boolean;
357
543
  readonly environment: EnvironmentInspection;
358
544
  readonly temporal: TemporalInspection;
359
545
  readonly bloom: BloomInspection;
360
546
  readonly motionBlurInspection: MotionBlurInspection | undefined;
361
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;
362
551
  /** Last completed transmission candidate facts, detached from GPU handles. */
363
552
  readonly transmission: TransmissionInspection | undefined;
553
+ /** Single production projection of Directional author/extract/record facts. */
554
+ readonly directionalShadow: DirectionalShadowInspection;
555
+ /** Last prepared Standard transport facts, detached from GPU handles. */
556
+ readonly standardLightingInspection: StandardLightingInspection | undefined;
557
+ /** Last submitted point-shadow atlas budget facts, detached from GPU handles. */
558
+ readonly pointShadowInspection: PointShadowInspection | undefined;
364
559
  /** Retained Points/Lines authoring facts from the single scene projection. */
365
560
  readonly pointsLinesSnapshots: readonly PointsLinesRetainedSnapshot[];
366
561
  /** Current mesh-slot provenance and active diagnostics from the last frame. */
@@ -492,6 +687,175 @@ type PointsLinesGpuResources = {
492
687
  };
493
688
  type PointsLinesPreparationAdapter = PointsLinesLanePreparationAdapter<PointsLinesGpuResources>;
494
689
 
690
+ function preparationFailureFrom(error: unknown): MeshMaterialBindingPreparationFailure | undefined {
691
+ if (typeof error !== 'object' || error === null) return undefined;
692
+ const value = error as {
693
+ readonly code?: unknown;
694
+ readonly expected?: unknown;
695
+ readonly hint?: unknown;
696
+ readonly detail?: unknown;
697
+ };
698
+ if (
699
+ typeof value.code !== 'string' ||
700
+ typeof value.expected !== 'string' ||
701
+ typeof value.hint !== 'string'
702
+ ) {
703
+ return undefined;
704
+ }
705
+ const detail =
706
+ typeof value.detail === 'object' && value.detail !== null
707
+ ? { ...(value.detail as Record<string, unknown>) }
708
+ : undefined;
709
+ return {
710
+ code: value.code,
711
+ expected: value.expected,
712
+ hint: value.hint,
713
+ ...(detail === undefined ? {} : { detail }),
714
+ };
715
+ }
716
+
717
+ function materialTextureHandles(
718
+ material: MaterialSnapshot,
719
+ ): readonly Handle<'TextureAsset', 'shared'>[] {
720
+ const handles = new Map<number, Handle<'TextureAsset', 'shared'>>();
721
+ const add = (handle: Handle<'TextureAsset', 'shared'> | undefined): void => {
722
+ if (handle !== undefined) handles.set(Number(handle), handle);
723
+ };
724
+ for (const handle of material.textureHandles?.values() ?? []) add(handle);
725
+ add(material.baseColorTexture);
726
+ add(material.metallicRoughnessTexture);
727
+ add(material.normalTexture);
728
+ add(material.emissiveTexture);
729
+ add(material.occlusionTexture);
730
+ return [...handles.values()];
731
+ }
732
+
733
+ function materialSamplerHandles(
734
+ material: MaterialSnapshot,
735
+ ): readonly Handle<'SamplerAsset', 'shared'>[] {
736
+ const handles = new Map<number, Handle<'SamplerAsset', 'shared'>>();
737
+ for (const handle of material.samplerHandles?.values() ?? []) {
738
+ handles.set(Number(handle), handle);
739
+ }
740
+ return [...handles.values()];
741
+ }
742
+
743
+ /**
744
+ * Project the producer-owned material and GPU store state into the one
745
+ * renderer observation surface. This is intentionally recomputed from the
746
+ * current frame's detached material snapshots; no readiness map is retained
747
+ * by RenderSystem. A producer failure with an already resident resource is
748
+ * reported as last-known-good, preserving recovery context without copying
749
+ * the producer's lifecycle state.
750
+ */
751
+ function observeMaterialResidency(
752
+ world: World | undefined,
753
+ material: MaterialSnapshot,
754
+ internals: RenderSystemInternals,
755
+ ): MeshMaterialBindingResidency {
756
+ const textures: Array<{ readonly handle: number; readonly mipLevelCount: number }> = [];
757
+ const samplers: Array<{ readonly handle: number; readonly resident: boolean }> = [];
758
+ let pending = false;
759
+ let resident = false;
760
+ let failure: MeshMaterialBindingPreparationFailure | undefined;
761
+ const keepFailure = (error: unknown): void => {
762
+ if (failure === undefined) failure = preparationFailureFrom(error);
763
+ };
764
+
765
+ if (world === undefined) {
766
+ pending = true;
767
+ } else {
768
+ const materialHandle = material.materialHandle;
769
+ if (materialHandle !== undefined && materialHandle !== 0) {
770
+ const materialResult = resolveAssetHandle<MaterialAsset>(
771
+ world,
772
+ toShared<'MaterialAsset'>(materialHandle),
773
+ );
774
+ if (!materialResult.ok) {
775
+ keepFailure(materialResult.error);
776
+ } else {
777
+ const guid = internals.assets.guidOf(materialResult.value);
778
+ if (guid !== undefined) {
779
+ const readiness = internals.assets.getMaterialReadiness(guid);
780
+ if (readiness?.status === 'Error') keepFailure(readiness.error);
781
+ const load = internals.assets.loadState.get(guid);
782
+ if (load?.status === 'provisional') pending = true;
783
+ }
784
+ }
785
+ }
786
+
787
+ for (const handle of materialTextureHandles(material)) {
788
+ const existing = internals.gpuStore.getTextureGpuView(handle, world);
789
+ const pod = resolveAssetHandle<TextureAsset>(world, handle);
790
+ if (!pod.ok) {
791
+ keepFailure(pod.error);
792
+ continue;
793
+ }
794
+ let view = existing;
795
+ if (view === undefined) {
796
+ const prepared = internals.gpuStore.ensureResident(handle, pod.value, world);
797
+ if (!prepared.ok) keepFailure(prepared.error);
798
+ view = internals.gpuStore.getTextureGpuView(handle, world);
799
+ }
800
+ if (view === undefined) {
801
+ pending = failure === undefined;
802
+ continue;
803
+ }
804
+ const textureLayout = deriveTextureLayout({
805
+ shape: pod.value.shape,
806
+ format: pod.value.format,
807
+ mips: pod.value.mips,
808
+ });
809
+ if (!textureLayout.ok) {
810
+ keepFailure(textureLayout.error);
811
+ continue;
812
+ }
813
+ resident = true;
814
+ textures.push({
815
+ handle: Number(handle),
816
+ mipLevelCount: Math.max(1, textureLayout.value.levels.length),
817
+ });
818
+ }
819
+
820
+ for (const handle of materialSamplerHandles(material)) {
821
+ const pod = resolveAssetHandle<SamplerAsset>(world, handle);
822
+ if (!pod.ok) {
823
+ keepFailure(pod.error);
824
+ samplers.push({ handle: Number(handle), resident: false });
825
+ continue;
826
+ }
827
+ const prepared = internals.gpuStore.ensureSamplerResident(handle, pod.value, world);
828
+ if (!prepared.ok) {
829
+ keepFailure(prepared.error);
830
+ samplers.push({ handle: Number(handle), resident: false });
831
+ } else {
832
+ resident = true;
833
+ samplers.push({ handle: Number(handle), resident: true });
834
+ }
835
+ }
836
+ // Video views are supplied by the dynamic host store at record time. They
837
+ // are not static TextureAsset residents, so this bounded projection stays
838
+ // pending until a dynamic observation contract exists rather than guessing
839
+ // from the VideoAsset GUID.
840
+ if ((material.videoTextureFields?.size ?? 0) > 0) pending = true;
841
+ }
842
+
843
+ const readiness =
844
+ failure === undefined
845
+ ? pending
846
+ ? 'pending'
847
+ : 'ready'
848
+ : resident
849
+ ? 'last-known-good'
850
+ : 'failed';
851
+ return {
852
+ readiness,
853
+ samplers,
854
+ textures,
855
+ ...(failure === undefined ? {} : { preparationFailure: failure }),
856
+ };
857
+ }
858
+
495
859
  /**
496
860
  * The Standard renderer's single Points/Lines prepare owner.
497
861
  *
@@ -763,6 +1127,41 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
763
1127
  let preparedWorlds: readonly World[] = [];
764
1128
  let latestCamera: CameraSnapshot | undefined;
765
1129
  const pointsLinesOwner = new StandardPointsLinesOwner(internals);
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
+ };
1163
+ const cubeCaptureScheduler = createCubeCaptureScheduler({ maxFacesPerFrame: 1 });
1164
+ const cubeCaptureState: CubeCaptureGraphState = { work: [] };
766
1165
  const persistentRenderScene = new PersistentRenderScene({
767
1166
  getDevice: () => internals.device,
768
1167
  onGpuError: (error) => internals.errorRegistry.fire(error),
@@ -908,6 +1307,23 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
908
1307
  ),
909
1308
  } satisfies import('./pipeline-builder').PipelineBuilderShaderModuleFactory);
910
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
+ };
911
1327
  const featureGpuWork: RenderFeatureGpuWorkOwner = createRenderFeatureGpuWorkOwner({
912
1328
  getDevice: () => internals.device,
913
1329
  getShaderModuleFactory: () =>
@@ -955,15 +1371,18 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
955
1371
  // raw (material-handle-based, cross-world collision semantically correct).
956
1372
  const frameState: RenderFrameState = {
957
1373
  frameNumber: 0,
1374
+ reflectionFallbackDemand: false,
958
1375
  graphGeneration: 0,
959
1376
  successfulTemporalFrameIndex: 0,
960
1377
  lastSuccessfulBloom: 'off',
961
1378
  temporalFrameTransaction: createTemporalFrameTransaction({ deviceEpoch: 0 }),
962
1379
  temporalFrameInput: undefined,
1380
+ reflectionFallbackReadback: undefined,
963
1381
  directionalShadowCache: null,
964
1382
  directionalShadowCacheRecorded: false,
965
1383
  compiledFrameGraph: null,
966
1384
  compiledFrameGraphTopologyKey: null,
1385
+ compiledFrameGraphGeneration: 0,
967
1386
  volumetricFogPreviousGraph: null,
968
1387
  volumetricFogPreviousGraphKey: null,
969
1388
  volumetricFogCandidateGraph: null,
@@ -980,6 +1399,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
980
1399
  volumetricFogHistorySignature: null,
981
1400
  retiredCompiledFrameGraphs: new Set(),
982
1401
  currentFrameObservationSource: undefined,
1402
+ reflectionFallbackObservationSource: undefined,
983
1403
  lastSuccessfulCameraAntialias: undefined,
984
1404
  temporalGpuState: undefined,
985
1405
  activeTemporalGpuState: undefined,
@@ -991,16 +1411,19 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
991
1411
  currentDirectionalShadowView: null,
992
1412
  currentSpotShadowView: null,
993
1413
  instanceBuffers: new Map(),
1414
+ probeBlendBuffers: new Map(),
1415
+ probeBlendRecordBufferCapacity: 0,
994
1416
  morphBuffers: new Map(),
995
1417
  hdrpClusterBinScratch: createClusterBinScratch(),
996
1418
  hdrpClusterGridScratch: null,
997
1419
  hdrpLightIndexListScratch: null,
998
- hdrpClusterMembershipBindGroup: null,
1420
+ hdrpClusterMembership: null,
1421
+ standardLightingGraphSignature: '',
1422
+ standardLightingInspection: undefined,
1423
+ pointShadowInspection: undefined,
999
1424
  transientInstanceBuffers: [],
1000
1425
  warnedZeroLightStandard: false,
1001
1426
  warnedMultiLightDirectional: false,
1002
- warnedMultiLightPoint: false,
1003
- warnedMultiLightSpot: false,
1004
1427
  // feat-20260630-equirect-kind-internalized-ibl-declarative-skyligh M3 / w19:
1005
1428
  // once-warn latches for >1 Skylight / >1 SkyboxBackground (names winner).
1006
1429
  warnedMultiSkylight: false,
@@ -1051,10 +1474,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1051
1474
  // (its topology is frame-invariant). Standard configuration overwrites this with the
1052
1475
  // resolved asset config on every swap so the active graph reads its current config.
1053
1476
  installedPipelineConfig: undefined,
1054
- // feat-20260608-cluster-lighting M2 / w10 + M5 / w20: HDRP active flag + once-per-frame
1055
- // warn dedup set for hdrp-light-budget-exceeded / hdrp-index-list-overflow.
1056
- isHdrpActive: false,
1057
- hdrpOncePerFrameFired: new Set(),
1477
+ standardOncePerFrameFired: new Set(),
1058
1478
  // feat-20260612-point-light-shadows-urp-hdrp M3 / T-M3-2 (plan-strategy §D-1):
1059
1479
  // cube_array shadow atlas + per-frame snapshot list. Atlas is null until the
1060
1480
  // first frame whose extracted lights.pointShadow is non-empty (zero-shadow
@@ -1073,6 +1493,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1073
1493
  spotShadowSnapshots: [],
1074
1494
  };
1075
1495
  let directFrameId = 0;
1496
+ let lastDirectionalShadowLights: ExtractedLights | undefined;
1497
+ let lastDirectionalShadowCandidate: 'accepted' | 'failed' = 'failed';
1498
+ let lastDirectionalShadowError: DirectionalShadowInspectionSource['error'];
1076
1499
 
1077
1500
  function beginProfilePhase(session: RecorderSession | undefined, phase: RenderPhase): boolean {
1078
1501
  if (session === undefined) return false;
@@ -1133,66 +1556,124 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1133
1556
  let activeFeaturePostProcessEntries: ReadonlyMap<string, PostProcessShaderEntry> = new Map();
1134
1557
  const lookupPostProcess = (id: string): PostProcessShaderEntry | undefined =>
1135
1558
  builtinPostProcessEntries.get(id) ?? activeFeaturePostProcessEntries.get(id);
1559
+ const invalidatePostProcessModule = (id: string): void => {
1560
+ const entry = lookupPostProcess(id);
1561
+ if (entry !== undefined) {
1562
+ internals.invalidateShaderModule?.(postProcessShaderModuleLabel(id, entry.source));
1563
+ }
1564
+ };
1136
1565
  // feat-20260621 M-A2 / w8: expose the eager-created per-id params UBO through
1137
1566
  // the narrow runtime surface so dispatchFullscreenPass can writeBuffer the
1138
1567
  // per-frame snapshot + bind it at group(1) binding(2).
1139
1568
  const getPostProcessParamsBuffer = (id: string): Buffer | undefined =>
1140
1569
  postProcessParamsBuffers.get(id);
1141
- const ensurePostProcessParamsResources = (
1570
+ interface PostProcessParamsCandidate {
1571
+ readonly buffers: ReadonlyMap<string, Buffer>;
1572
+ readonly created: readonly Buffer[];
1573
+ }
1574
+ const postProcessParamsByteSize = (entry: PostProcessShaderEntry): number =>
1575
+ entry.params?.byteSize ?? (entryHasDepthRead(entry) ? DEPTH_MIN_PARAMS_BYTE_SIZE : 0);
1576
+ const destroyPostProcessParams = (buffers: readonly Buffer[]): void => {
1577
+ for (const buffer of buffers) {
1578
+ const destroyed = internals.device.destroyBuffer(buffer);
1579
+ if (!destroyed.ok) internals.errorRegistry.fire(destroyed.error);
1580
+ }
1581
+ };
1582
+ const preparePostProcessParamsResources = (
1142
1583
  featureEntries: ReadonlyMap<string, PostProcessShaderEntry>,
1143
- ): void => {
1584
+ ): Result<PostProcessParamsCandidate, RhiError> => {
1144
1585
  const declarations = new Map<string, PostProcessShaderEntry>([
1145
1586
  ...builtinPostProcessEntries.entries(),
1146
1587
  ...featureEntries.entries(),
1147
1588
  ]);
1589
+ const next = new Map<string, Buffer>();
1590
+ const allocated: Buffer[] = [];
1591
+ const discardCreated = (): void => destroyPostProcessParams(allocated);
1148
1592
  for (const [id, entry] of declarations) {
1149
1593
  if (id === 'forgeax.taa-resolve') continue;
1150
- const byteSize =
1151
- entry.params?.byteSize ?? (entryHasDepthRead(entry) ? DEPTH_MIN_PARAMS_BYTE_SIZE : 0);
1152
- if (byteSize === 0 || postProcessParamsBuffers.has(id)) continue;
1594
+ const byteSize = postProcessParamsByteSize(entry);
1595
+ if (byteSize === 0) continue;
1596
+ const existing = postProcessParamsBuffers.get(id);
1597
+ const accepted = lookupPostProcess(id);
1598
+ if (
1599
+ existing !== undefined &&
1600
+ accepted !== undefined &&
1601
+ postProcessShaderEntrySignature(accepted) === postProcessShaderEntrySignature(entry)
1602
+ ) {
1603
+ next.set(id, existing);
1604
+ continue;
1605
+ }
1153
1606
  const created = internals.device.createBuffer({
1154
1607
  label: `post-process-params-${id}`,
1155
1608
  size: byteSize,
1156
1609
  usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
1157
1610
  mappedAtCreation: false,
1158
1611
  });
1159
- if (!created.ok) throw created.error;
1612
+ if (!created.ok) {
1613
+ discardCreated();
1614
+ return err(created.error);
1615
+ }
1160
1616
  const buffer = created.value;
1617
+ allocated.push(buffer);
1161
1618
  if (entry.params !== undefined) {
1162
1619
  const written = internals.device.queue.writeBuffer(buffer, 0, entry.params.defaultValue);
1163
1620
  if (!written.ok) {
1164
- internals.device.destroyBuffer(buffer);
1165
- throw written.error;
1621
+ discardCreated();
1622
+ return err(written.error);
1166
1623
  }
1167
1624
  }
1168
- postProcessParamsBuffers.set(id, buffer);
1625
+ next.set(id, buffer);
1169
1626
  }
1170
- for (const [id, buffer] of postProcessParamsBuffers) {
1171
- if (declarations.has(id)) continue;
1172
- internals.device.destroyBuffer(buffer);
1173
- postProcessParamsBuffers.delete(id);
1627
+ return ok({ buffers: next, created: Object.freeze([...allocated]) });
1628
+ };
1629
+ const discardPostProcessParamsCandidate = (candidate: PostProcessParamsCandidate): void => {
1630
+ destroyPostProcessParams(candidate.created);
1631
+ };
1632
+ const retirePostProcessParams = (buffers: readonly Buffer[]): void => {
1633
+ if (buffers.length === 0) return;
1634
+ const release = (): void => destroyPostProcessParams(buffers);
1635
+ try {
1636
+ void internals.device.queue.onSubmittedWorkDone().then(release, release);
1637
+ } catch {
1638
+ release();
1174
1639
  }
1175
1640
  };
1176
- // feat-20260609 M4 / T-10-a: post-process pipeline cache (id|colorFormat -> RhiRenderPipeline).
1641
+ const acceptPostProcessParamsCandidate = (candidate: PostProcessParamsCandidate): void => {
1642
+ const previous = [...postProcessParamsBuffers.values()];
1643
+ const next = new Set(candidate.buffers.values());
1644
+ postProcessParamsBuffers.clear();
1645
+ for (const [id, buffer] of candidate.buffers) {
1646
+ postProcessParamsBuffers.set(id, buffer);
1647
+ }
1648
+ retirePostProcessParams(previous.filter((buffer) => !next.has(buffer)));
1649
+ };
1650
+ // feat-20260609 M4 / T-10-a: post-process pipeline cache (declaration|colorFormat -> RhiRenderPipeline).
1177
1651
  // Solves CONCERN-1: dispatcher previously passed `pipeline=null` to
1178
1652
  // built.createHandle because per-frame execute closures cannot await async
1179
1653
  // shader compile. The cache here delegates the actual build to
1180
1654
  // `internals.buildPostProcessPipeline` (sync wrapper over the shared shader
1181
- // adapter; 1-frame warmup), then memoizes by `id|colorFormat`.
1655
+ // adapter; 1-frame warmup), then memoizes by declaration identity + format.
1182
1656
  const postProcessPipelineCache = new Map<string, RenderPipeline>();
1183
1657
  const getPostProcessPipeline = (
1184
1658
  id: string,
1185
1659
  bgl: BindGroupLayout,
1186
1660
  colorFormat: GPUTextureFormat,
1661
+ entryOverride?: PostProcessShaderEntry,
1187
1662
  ): RenderPipeline | null => {
1188
- const key = `${id}|${colorFormat}`;
1663
+ const entry = entryOverride ?? lookupPostProcess(id);
1664
+ if (entry === undefined) return null;
1665
+ const signature = postProcessShaderEntrySignature(entry);
1666
+ const key = `${id}|${colorFormat}|${signature}`;
1189
1667
  const cached = postProcessPipelineCache.get(key);
1190
1668
  if (cached !== undefined) return cached;
1191
- const entry = lookupPostProcess(id);
1192
- if (entry === undefined) return null;
1193
1669
  const factory = internals.buildPostProcessPipeline;
1194
1670
  if (factory === undefined) return null;
1195
- const built = factory(entry, bgl, colorFormat, `post-process-${id}`);
1671
+ const built = factory(
1672
+ entry,
1673
+ bgl,
1674
+ colorFormat,
1675
+ postProcessShaderPipelineLabel(id, entry.source),
1676
+ );
1196
1677
  if (built === null) return null;
1197
1678
  postProcessPipelineCache.set(key, built);
1198
1679
  return built;
@@ -1202,6 +1683,14 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1202
1683
  if (key.startsWith(`${id}|`)) postProcessPipelineCache.delete(key);
1203
1684
  }
1204
1685
  };
1686
+ const clearPostProcessPipelineEntry = (id: string, entry: PostProcessShaderEntry): void => {
1687
+ const suffix = `|${postProcessShaderEntrySignature(entry)}`;
1688
+ for (const key of postProcessPipelineCache.keys()) {
1689
+ if (key.startsWith(`${id}|`) && key.endsWith(suffix)) {
1690
+ postProcessPipelineCache.delete(key);
1691
+ }
1692
+ }
1693
+ };
1205
1694
  Object.assign(internals, {
1206
1695
  lookupPostProcess,
1207
1696
  getPostProcessParamsBuffer,
@@ -1217,6 +1706,134 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1217
1706
  const dispatchCounts: { unlit: number } = {
1218
1707
  unlit: 0,
1219
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
+ };
1220
1837
  // feat-20260531-per-frame-bind-group-cache M1 / w4: per-frame
1221
1838
  // createBindGroup counter scaffolding. Reset on every draw([world], { cameraOwner: 0, resourceOwner: 0 }) entry,
1222
1839
  // bumped on cache-miss in render-system-record.ts (M2-M4 bump points).
@@ -1376,7 +1993,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1376
1993
  if (buffer === undefined) return group;
1377
1994
  return ok({
1378
1995
  handle: group.value,
1379
- dynamicOffsets: [0],
1996
+ dynamicOffsets: [0, 0],
1380
1997
  release: () => internals.device.destroyBuffer(buffer),
1381
1998
  });
1382
1999
  };
@@ -1387,8 +2004,6 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1387
2004
  const preparedResolverFactory = (
1388
2005
  input: RenderFeaturePreparedGraphicsResolverInput,
1389
2006
  ): PreparedGraphicsResolver => {
1390
- activeFeaturePostProcessEntries = input.fullscreenEffects;
1391
- ensurePostProcessParamsResources(input.fullscreenEffects);
1392
2007
  return createPreparedGraphicsResolver({
1393
2008
  device: internals.device,
1394
2009
  featureIdentity: input.featureIdentity,
@@ -1412,6 +2027,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1412
2027
  descriptor.shader,
1413
2028
  fullscreen.bindGroupLayout,
1414
2029
  descriptor.colorFormats[0] as GPUTextureFormat,
2030
+ postProcessEntry,
1415
2031
  );
1416
2032
  if (pipeline !== null && pipeline !== undefined) {
1417
2033
  preparedPipelineIds.set(pipeline as object, descriptor.shader);
@@ -1580,6 +2196,47 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1580
2196
  gpuDriven: gpuDrivenProduction.inspect(),
1581
2197
  };
1582
2198
  },
2199
+ get gpuDrivenInspection(): GpuDrivenProductionInspection {
2200
+ return gpuDrivenProduction.inspect();
2201
+ },
2202
+ get reflectionProbes(): ReflectionProbeInspection {
2203
+ return reflectionProbeOwner.inspect();
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
+ },
2237
+ isCubeCapturePending(target: RenderTarget): boolean {
2238
+ return cubeCaptureScheduler.inspect(target).candidateGeneration !== undefined;
2239
+ },
1583
2240
  get environment(): EnvironmentInspection {
1584
2241
  return environmentLifecycle.inspect();
1585
2242
  },
@@ -1646,6 +2303,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1646
2303
  }),
1647
2304
  });
1648
2305
  },
2306
+ get lodOcclusionInspection(): LodOcclusionInspection | undefined {
2307
+ return lastLodOcclusionInspection;
2308
+ },
1649
2309
  get bloom(): BloomInspection {
1650
2310
  const pipelineState = internals.getPipelineState();
1651
2311
  const resources = pipelineState?.perPassResources;
@@ -1672,6 +2332,49 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1672
2332
  get transmission(): TransmissionInspection | undefined {
1673
2333
  return lastTransmissionInspection;
1674
2334
  },
2335
+ get directionalShadow(): DirectionalShadowInspection {
2336
+ const lights = lastDirectionalShadowLights;
2337
+ const pipelineState = internals.getPipelineState();
2338
+ const graph = frameState.compiledFrameGraph?.inspect();
2339
+ const requested = directionalShadowProfileFromQuality(lights?.directionalShadowQuality);
2340
+ const mapSize = pipelineState?.perPassResources.shadowMapSize ?? 0;
2341
+ const cascadeCount = lights?.cascadeCount ?? 0;
2342
+ const shadowReady = frameState.currentDirectionalShadowView !== null;
2343
+ const atlasBytes =
2344
+ shadowReady && mapSize > 0 && cascadeCount > 0
2345
+ ? mapSize * mapSize * Math.ceil(Math.sqrt(cascadeCount)) ** 2 * 4
2346
+ : 0;
2347
+ return projectDirectionalShadowInspectionSource({
2348
+ requested,
2349
+ candidate: lastDirectionalShadowCandidate,
2350
+ ...(frameState.directionalShadowCache === null
2351
+ ? {}
2352
+ : {
2353
+ lastKnownGood: directionalShadowProfileFromQuality(
2354
+ frameState.directionalShadowCache.directionalShadowQuality,
2355
+ ),
2356
+ }),
2357
+ cascadeCount,
2358
+ mapSize,
2359
+ atlasBytes,
2360
+ writerPasses:
2361
+ graph?.passes.filter((pass) => pass.name.startsWith('shadowCascade')).length ?? 0,
2362
+ shadowAngularRadius: lights?.directionalCsmConfig?.shadowAngularRadius,
2363
+ maxPenumbraTexels: lights?.directionalCsmConfig?.maxPenumbraTexels,
2364
+ deviceGeneration: internals.deviceScope.generation,
2365
+ graphGeneration: frameState.perFrameGraph?.graphGeneration ?? frameState.graphGeneration,
2366
+ shadowReady,
2367
+ ...(lastDirectionalShadowError === undefined ? {} : { error: lastDirectionalShadowError }),
2368
+ });
2369
+ },
2370
+ get standardLightingInspection(): StandardLightingInspection | undefined {
2371
+ const inspection = frameState.standardLightingInspection;
2372
+ return inspection === undefined ? undefined : Object.freeze({ ...inspection });
2373
+ },
2374
+ get pointShadowInspection(): PointShadowInspection | undefined {
2375
+ const inspection = frameState.pointShadowInspection;
2376
+ return inspection === undefined ? undefined : Object.freeze({ ...inspection });
2377
+ },
1675
2378
  get pointsLinesSnapshots(): readonly PointsLinesRetainedSnapshot[] {
1676
2379
  return persistentRenderScene.pointsLinesSnapshots();
1677
2380
  },
@@ -1741,20 +2444,27 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1741
2444
  worlds,
1742
2445
  { cameraOwner, resourceOwner },
1743
2446
  internals.assets.catalogEpoch,
1744
- () =>
2447
+ (renderables) =>
1745
2448
  extractFrames(
1746
2449
  worlds,
1747
2450
  { cameraOwner, resourceOwner },
1748
2451
  internals.assets,
1749
2452
  internals.getPipelineState(),
1750
2453
  persistentRenderScene.materialSnapshotCacheStore(),
1751
- { cull: 'none' },
2454
+ {
2455
+ cull: 'none',
2456
+ renderables,
2457
+ ...(internals.getMaterialShaderArtifact === undefined
2458
+ ? {}
2459
+ : { getMaterialShaderArtifact: internals.getMaterialShaderArtifact }),
2460
+ },
1752
2461
  ),
1753
2462
  renderReadLeases,
1754
2463
  );
1755
2464
  });
1756
2465
  const {
1757
2466
  cameras,
2467
+ cubeCameras,
1758
2468
  lights,
1759
2469
  environment,
1760
2470
  environmentReady,
@@ -1765,18 +2475,296 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1765
2475
  skylightCount,
1766
2476
  skybox,
1767
2477
  skyboxCount,
1768
- fog,
2478
+ fogFailure,
1769
2479
  frustumStats,
1770
2480
  visibilityStats,
1771
2481
  postProcessParams,
1772
2482
  } = frame;
2483
+ lastDirectionalShadowLights = lights;
2484
+ lastDirectionalShadowCandidate = 'failed';
2485
+ lastDirectionalShadowError = lights.directionalShadowError;
2486
+ const reflectionProbeRecord = reflectionProbeOwner.prepare(
2487
+ frame.reflectionProbes ?? [],
2488
+ persistentRenderScene.reflectionProbeProjection().selected,
2489
+ cameras[0],
2490
+ frameState.frameNumber,
2491
+ skylight,
2492
+ );
1773
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
+ }
1774
2759
  const temporalDemanded =
1775
2760
  cameras[0]?.antialias === 'taa' || (cameras[0]?.motionBlur?.shutterAngle ?? 0) > 0;
1776
2761
  if (temporalDemanded) {
1777
- persistentRenderScene.prepareTemporalFrame(renderables);
2762
+ persistentRenderScene.prepareTemporalFrame(submissionRenderables);
1778
2763
  }
1779
2764
  environmentLifecycle.bindScope(internals.deviceScope);
2765
+ if (fogFailure !== undefined) {
2766
+ environmentLifecycle.recordSelectionFailure(fogFailure);
2767
+ }
1780
2768
  if (!environmentReady || environment === undefined) {
1781
2769
  frameState.pendingTemporalCommit = { kind: 'none' };
1782
2770
  // A world with no camera still has a valid clear-only render path.
@@ -1805,15 +2793,21 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1805
2793
  }
1806
2794
  if (lastMeshMaterialBindingFrame !== frame) {
1807
2795
  lastMeshMaterialBindingFrame = frame;
1808
- lastMeshMaterialBindings = renderables.map((renderable) => ({
1809
- worldId: renderable.worldId,
1810
- entityKey: renderable.entityKey,
1811
- bindings: renderable.materials.map((material, slotIndex) => ({
1812
- handle: material.materialHandle ?? 0,
1813
- source: renderable.materialBindingSources[slotIndex] ?? 'engine-default',
1814
- })),
1815
- diagnostics: renderable.materialBindingDiagnostics ?? [],
1816
- }));
2796
+ lastMeshMaterialBindings = renderables.map((renderable) => {
2797
+ const materialWorld = preparedWorlds[renderable.worldId];
2798
+ return projectMeshMaterialBindingObservation({
2799
+ worldId: renderable.worldId,
2800
+ entityKey: renderable.entityKey,
2801
+ bindings: renderable.materials.map((material, slotIndex) => ({
2802
+ handle: material.materialHandle ?? 0,
2803
+ source: renderable.materialBindingSources[slotIndex] ?? 'engine-default',
2804
+ })),
2805
+ diagnostics: renderable.materialBindingDiagnostics ?? [],
2806
+ residency: renderable.materials.map((material) =>
2807
+ observeMaterialResidency(materialWorld, material, internals),
2808
+ ),
2809
+ });
2810
+ });
1817
2811
  }
1818
2812
 
1819
2813
  bindGroupCounts.createBindGroup = 0;
@@ -1879,14 +2873,23 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1879
2873
  createPreparedGraphicsResolver: preparedResolverFactory,
1880
2874
  gpuWork: featureGpuWork,
1881
2875
  });
1882
- activeFeaturePostProcessEntries = featureFrame.fullscreenEffects;
1883
- ensurePostProcessParamsResources(featureFrame.fullscreenEffects);
1884
2876
  lastVisibilityStats.explicitlyHidden = featureFrame.hiddenEntityReports.length;
1885
2877
  for (const featureError of featureFrame.errors) {
1886
2878
  if (!isPendingRenderFeaturePreparation(featureError)) {
1887
2879
  internals.errorRegistry.fire(featureError);
1888
2880
  }
1889
2881
  }
2882
+ const postProcessParamsCandidate = preparePostProcessParamsResources(
2883
+ featureFrame.fullscreenEffects,
2884
+ );
2885
+ if (!postProcessParamsCandidate.ok) {
2886
+ internals.errorRegistry.fire(postProcessParamsCandidate.error);
2887
+ for (const batch of featureFrame.preparedResourceBatches) {
2888
+ const released = batch.release();
2889
+ if (!released.ok) internals.errorRegistry.fire(released.error);
2890
+ }
2891
+ return [];
2892
+ }
1890
2893
  featureGraphCandidate = {
1891
2894
  plans: featureFrame.plans,
1892
2895
  fullscreenEffects: featureFrame.fullscreenEffects,
@@ -1900,6 +2903,29 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1900
2903
  internals.errorRegistry.fire(released.error);
1901
2904
  }
1902
2905
  }
2906
+ discardPostProcessParamsCandidate(postProcessParamsCandidate.value);
2907
+ },
2908
+ onAbandoned: () => {
2909
+ discardPostProcessParamsCandidate(postProcessParamsCandidate.value);
2910
+ },
2911
+ onAccepted: () => {
2912
+ for (const [id, previous] of activeFeaturePostProcessEntries) {
2913
+ const next = featureFrame.fullscreenEffects.get(id);
2914
+ if (next === undefined) {
2915
+ clearPostProcessPipelineCache(id);
2916
+ invalidatePostProcessModule(id);
2917
+ } else if (
2918
+ postProcessShaderEntrySignature(previous) !==
2919
+ postProcessShaderEntrySignature(next)
2920
+ ) {
2921
+ clearPostProcessPipelineEntry(id, previous);
2922
+ if (previous.source !== next.source) {
2923
+ invalidatePostProcessModule(id);
2924
+ }
2925
+ }
2926
+ }
2927
+ acceptPostProcessParamsCandidate(postProcessParamsCandidate.value);
2928
+ activeFeaturePostProcessEntries = featureFrame.fullscreenEffects;
1903
2929
  },
1904
2930
  };
1905
2931
  return featureFrame.preparedResourceBatches;
@@ -1911,9 +2937,31 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1911
2937
  // world that owns skylight / skybox / singleton render state, w5 / D-3).
1912
2938
  // Only the Transparent segment is reordered; queue ordering between
1913
2939
  // segments (sortDispatchByQueue, stable) is preserved.
1914
- const orderedDispatch = runProfiledRenderPhase(profileSession, 'sort', () =>
1915
- sortTransparentDispatch(dispatch, resourceWorld, cameras, renderables),
1916
- );
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
+ });
1917
2965
 
1918
2966
  // M3 / w26: single dispatch list replaces old three-bucket model.
1919
2967
  // Pass dispatch to recordFrame — the record stage iterates dispatch
@@ -1941,13 +2989,16 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1941
2989
  // The same runner is also called by geometry/material helpers;
1942
2990
  // invoking those wrappers would turn a pass probe into the
1943
2991
  // high-overhead per-draw `nested` probe.
1944
- if (
1945
- profileSession.detail === 'passes' &&
1946
- (!phase.startsWith('record/graph-execute/') ||
1947
- (phase.slice('record/graph-execute/'.length).includes('/') &&
1948
- !phase.endsWith('/geometry-loop')))
1949
- ) {
1950
- 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();
1951
3002
  }
1952
3003
  return runProfiledRenderPhase(profileSession, phase, action);
1953
3004
  };
@@ -1960,7 +3011,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1960
3011
  resourceWorld,
1961
3012
  cameras,
1962
3013
  lights,
1963
- renderables,
3014
+ [...submissionRenderables],
1964
3015
  orderedDispatch,
1965
3016
  frameState,
1966
3017
  dispatchCounts,
@@ -1970,20 +3021,28 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1970
3021
  skybox,
1971
3022
  skyboxCount,
1972
3023
  postProcessParams,
1973
- fog,
1974
3024
  worlds,
1975
3025
  recordProfilePhase,
1976
3026
  {
1977
3027
  owner: gpuDrivenProduction,
1978
3028
  scene: persistentRenderScene.compositionGpuDrivenState(),
1979
- onCoverage: (ownsAll) => {
1980
- const key = `${internals.gpuStore.meshResidencyEpoch}:${frameState.isHdrpActive ? 'hdrp' : 'urp'}`;
1981
- persistentRenderScene.setCompositionGpuDrivenCoverage(key, ownsAll);
3029
+ onSceneDemand: (sceneRowsRequired) => {
3030
+ persistentRenderScene.setCompositionGpuSceneDemand(sceneRowsRequired);
1982
3031
  },
3032
+ activeEntityKeys: visibilityProjection.activeEntityKeys,
3033
+ activeEntityRevision: visibilityProjection.activeEntityRevision,
3034
+ telemetryCandidateCount: lodCandidateCountForFrame,
3035
+ ...(occlusionProjection === undefined ? {} : { occlusion: occlusionProjection }),
1983
3036
  },
1984
3037
  renderReadLeases,
1985
3038
  featureGraphCandidate,
1986
3039
  pointsLinesOwner,
3040
+ {
3041
+ snapshots: cubeCameras,
3042
+ scheduler: cubeCaptureScheduler,
3043
+ state: cubeCaptureState,
3044
+ reflectionProbes: reflectionProbeRecord,
3045
+ } satisfies CubeCaptureFrameInput,
1987
3046
  environment?.environmentSignature ?? '',
1988
3047
  environment?.fogSignature ?? '',
1989
3048
  environmentReady && environment !== undefined,
@@ -1992,7 +3051,50 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1992
3051
  timingCapture,
1993
3052
  ),
1994
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
+ }
3070
+ const directionalQuality = lights.directionalShadowQuality;
3071
+ const directionalShadowReady =
3072
+ directionalQuality === undefined ||
3073
+ (frameState.currentDirectionalShadowView !== null &&
3074
+ (lights.lightViewProj?.length ?? 0) > 0 &&
3075
+ (internals.getPipelineState()?.perPassResources.shadowMapSize ?? 0) > 0);
3076
+ if (submitted && directionalShadowReady) {
3077
+ lastDirectionalShadowCandidate = 'accepted';
3078
+ } else if (directionalQuality !== undefined) {
3079
+ lastDirectionalShadowError = {
3080
+ code: 'directional-shadow-candidate-failed',
3081
+ expected:
3082
+ 'the extracted Directional CSM candidate has a valid graph target and fitted matrices',
3083
+ hint: 'inspect the retained LKG and retry after the graph or resource owner recovers',
3084
+ detail: {
3085
+ submitted,
3086
+ hasShadowView: frameState.currentDirectionalShadowView !== null,
3087
+ matrixCount: lights.lightViewProj?.length ?? 0,
3088
+ shadowMapSize: internals.getPipelineState()?.perPassResources.shadowMapSize ?? 0,
3089
+ },
3090
+ };
3091
+ }
1995
3092
  if (submitted) {
3093
+ if (occlusionProjection === undefined) {
3094
+ runProfiledRenderPhase(profileSession, 'record/occlusion-global-advance', () =>
3095
+ ensureOcclusionRuntime().advanceSuccessfulSubmit(frameState.frameNumber),
3096
+ );
3097
+ }
1996
3098
  const temporalCommit = persistentRenderScene.commitTemporalFrame();
1997
3099
  if (!temporalCommit.ok) {
1998
3100
  internals.errorRegistry.fire(temporalCommit.error);
@@ -2071,6 +3173,21 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2071
3173
  const currentFrameId = frameState.frameNumber - 1;
2072
3174
  return observeCurrentFrame(options, frameState.currentFrameObservationSource, currentFrameId);
2073
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
+ },
2074
3191
  getCurrentGraphTarget(name: string) {
2075
3192
  const graph = frameState.perFrameGraph;
2076
3193
  if (graph === undefined || graph === null) return undefined;
@@ -2127,9 +3244,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2127
3244
  profileConfig === undefined
2128
3245
  ? config
2129
3246
  : {
2130
- ...(profileConfig.lighting === 'clustered'
2131
- ? { clusterGrid: DEFAULT_CLUSTER_GRID }
2132
- : {}),
3247
+ clusterGrid: DEFAULT_CLUSTER_GRID,
2133
3248
  ...(profileConfig.ssao ? { ssao: { enabled: true } } : {}),
2134
3249
  ...config,
2135
3250
  };
@@ -2154,16 +3269,6 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2154
3269
  // is a rare boot/swap event, never a per-frame cost.
2155
3270
  installEpoch += 1;
2156
3271
  frameState.installedPipelineHandle = installEpoch;
2157
- // The installed profile may request clustered lighting, but the actual
2158
- // graph lane is capability-gated. On WebGL2 (including WebKit's wgpu
2159
- // fallback) the forward lane is selected when compute/storage buffers
2160
- // are unavailable; keep the frame bindings on that same lane instead of
2161
- // advertising HDRP's clustered bind-group layout to URP pipelines.
2162
- frameState.isHdrpActive =
2163
- resolveStandardLane(internals.standardProfile ?? DEFAULT_STANDARD_PROFILE, {
2164
- compute: internals.device.caps.compute,
2165
- storageBuffer: internals.device.caps.storageBuffer,
2166
- }) === 'clustered';
2167
3272
  frameState.installedPipelineConfig = resolvedConfig;
2168
3273
  },
2169
3274
  registerBuiltinPostProcess(id: string, entry: PostProcessShaderEntry): () => void {
@@ -2207,6 +3312,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2207
3312
  }
2208
3313
  builtinPostProcessEntries.set(id, entry);
2209
3314
  if (paramsBuffer !== undefined) postProcessParamsBuffers.set(id, paramsBuffer);
3315
+ const moduleLabel = postProcessShaderModuleLabel(id, entry.source);
2210
3316
  return () => {
2211
3317
  if (builtinPostProcessEntries.get(id) === entry) {
2212
3318
  builtinPostProcessEntries.delete(id);
@@ -2216,9 +3322,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2216
3322
  internals.device.destroyBuffer(current);
2217
3323
  postProcessParamsBuffers.delete(id);
2218
3324
  }
2219
- for (const key of postProcessPipelineCache.keys()) {
2220
- if (key.startsWith(`${id}|`)) postProcessPipelineCache.delete(key);
2221
- }
3325
+ clearPostProcessPipelineCache(id);
3326
+ internals.invalidateShaderModule?.(moduleLabel);
2222
3327
  };
2223
3328
  } catch (cause) {
2224
3329
  if (paramsBuffer !== undefined) internals.device.destroyBuffer(paramsBuffer);
@@ -2226,16 +3331,25 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2226
3331
  }
2227
3332
  },
2228
3333
  disposeFrameState(): void {
3334
+ (internals as TimingSessionHost).gpuPassTimingSession?.dispose();
3335
+ (internals as TimingSessionHost).gpuPassTimingSession = undefined;
3336
+ (internals as TimingSessionHost).gpuPassTimingCapture = undefined;
3337
+ (internals as TimingSessionHost).gpuPassTimingSubmittedWork = undefined;
3338
+ (internals as TimingSessionHost).gpuPassTimingFrameIdentity = undefined;
3339
+ (internals as TimingSessionHost).gpuPassTimingBeginReason = undefined;
2229
3340
  persistentRenderScene.dispose();
2230
3341
  gpuDrivenProduction.dispose();
3342
+ occlusionRuntime?.dispose();
2231
3343
  disposeFeatureGpuWork();
3344
+ reflectionProbeOwner.dispose();
2232
3345
  // Bloom generations are renderer-owned rather than graph-owned. Drain
2233
3346
  // candidate, active, and fence-retiring bundles before the rest of the
2234
3347
  // frame state is torn down; the owner callback is idempotent so the
2235
3348
  // outer Renderer.dispose cascade can safely call this once only.
2236
3349
  internals.getPipelineState()?.perPassResources.drainBloomResources?.();
2237
3350
  internals.clearPostProcessPipelineCache?.('forgeax.taa-resolve');
2238
- internals.invalidateShaderModule?.('post-process-forgeax.taa-resolve-module');
3351
+ invalidatePostProcessModule('forgeax.taa-resolve');
3352
+ resetHdrpBuffers(internals);
2239
3353
  // Retire graph-owned resources and dispose instance-buffer caches.
2240
3354
  // Both calls are idempotent + tolerate per-handle errors silently;
2241
3355
  // the Renderer.dispose() cascade owns the surrounding try/catch
@@ -2276,9 +3390,14 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2276
3390
  frameState.volumetricFogHistoryGraph = null;
2277
3391
  frameState.volumetricFogHistorySlot = null;
2278
3392
  frameState.volumetricFogHistorySignature = null;
3393
+ frameState.hdrpClusterMembership = null;
2279
3394
  const compiled = frameState.compiledFrameGraph;
2280
3395
  frameState.compiledFrameGraph = null;
2281
3396
  frameState.compiledFrameGraphTopologyKey = null;
3397
+ frameState.perFrameGraph = null;
3398
+ frameState.standardLightingGraphSignature = '';
3399
+ frameState.standardLightingInspection = undefined;
3400
+ frameState.pointShadowInspection = undefined;
2282
3401
  if (compiled !== null) compiled.retire().catch(() => undefined);
2283
3402
  for (const retired of frameState.retiredCompiledFrameGraphs) {
2284
3403
  retired.retire().catch(() => undefined);
@@ -2289,6 +3408,15 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2289
3408
  // dispose error strategy).
2290
3409
  disposeInstanceBuffers(frameState.instanceBuffers, internals.errorRegistry);
2291
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();
2292
3420
  if (frameState.morphBuffers !== undefined) {
2293
3421
  for (const entry of frameState.morphBuffers.values()) {
2294
3422
  if (!entry.buffer.isDestroyed) {
@@ -2309,10 +3437,19 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2309
3437
  }
2310
3438
  },
2311
3439
  resetForRecover(): void {
3440
+ ssrFormatReceipt = undefined;
3441
+ ssrFormatProbeGeneration = -1;
3442
+ ssrFormatProbe = undefined;
3443
+ ssrFallbackGeneration = undefined;
2312
3444
  transmissionAdmission.markDeviceLost();
2313
3445
  lastTransmissionKey = '';
2314
3446
  transmissionCapabilityGeneration = -1;
2315
3447
  transmissionCapability = undefined;
3448
+ // The replacement device cannot consume buffers minted by the lost
3449
+ // device. Drop the shared Standard Cluster bundle before the next
3450
+ // frame asks for a new generation; resetHdrpBuffers fences the bundle
3451
+ // using its own device/queue instead of the replacement runtime device.
3452
+ resetHdrpBuffers(internals);
2316
3453
  // Keep the persistent material demand observable across device loss.
2317
3454
  // The physical resource has already been fenced by markDeviceLost(); the
2318
3455
  // detached inspection now exposes the rebuild-required lifecycle until a
@@ -2321,7 +3458,13 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2321
3458
  persistentRenderScene.resetGpuForRecover();
2322
3459
  gpuDrivenProduction.dispose();
2323
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;
2324
3466
  disposeFeatureGpuWork();
3467
+ reflectionProbeOwner.dispose();
2325
3468
  // A recovered device cannot retain handles from the lost device. The
2326
3469
  // Bloom owner abandons candidates and retires every committed generation
2327
3470
  // before the new PipelineState is rebuilt.
@@ -2351,10 +3494,23 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2351
3494
  frameState.volumetricFogParamsAcceptedSlot = null;
2352
3495
  frameState.volumetricFogAcceptedParams = undefined;
2353
3496
  frameState.volumetricFogPendingParams = undefined;
3497
+ // Do not expose graph-owned texture/descriptor accessors from the lost
3498
+ // device while recovery is between generations. Inspection and capture
3499
+ // must remain detached until the replacement graph is accepted.
3500
+ frameState.perFrameGraph = null;
3501
+ frameState.standardLightingGraphSignature = '';
3502
+ frameState.standardLightingInspection = undefined;
3503
+ frameState.pointShadowInspection = undefined;
2354
3504
  frameState.directionalShadowCache = null;
2355
3505
  frameState.directionalShadowCacheRecorded = false;
2356
3506
  frameState.currentFrameObservationSource = undefined;
2357
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' };
2358
3514
  frameState.currentDirectionalShadowView = null;
2359
3515
  frameState.currentSpotShadowView = null;
2360
3516
  if (frameState.temporalGpuState !== undefined) {
@@ -2369,11 +3525,15 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2369
3525
  retireTemporalGpuState(retiring);
2370
3526
  }
2371
3527
  frameState.retiringTemporalGpuStates.clear();
3528
+ frameState.hdrpClusterMembership = null;
2372
3529
  for (const retired of frameState.retiredCompiledFrameGraphs) {
2373
3530
  retired.retire().catch(() => undefined);
2374
3531
  }
2375
3532
  frameState.retiredCompiledFrameGraphs.clear();
2376
3533
  frameState.instanceBuffers.clear();
3534
+ delete frameState.probeBlendRecordBuffer;
3535
+ frameState.probeBlendRecordBufferCapacity = 0;
3536
+ frameState.probeBlendBuffers.clear();
2377
3537
  frameState.morphBuffers?.clear();
2378
3538
  frameState.transientInstanceBuffers = [];
2379
3539
  frameState.pointShadowAtlas = null;
@@ -2441,6 +3601,71 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2441
3601
  };
2442
3602
  }
2443
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
+
2444
3669
  // @forgeax/engine-render - built-in record context extension.
2445
3670
  //
2446
3671
  // The public RenderPipelineContext is canonical in render-contract.ts. This