@forgeax/engine-render 0.1.21 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (584) hide show
  1. package/README.md +281 -38
  2. package/dist/assembly/backend-contract.d.ts +13 -0
  3. package/dist/assembly/backend-contract.d.ts.map +1 -1
  4. package/dist/assembly/bundler-contract.d.ts +9 -0
  5. package/dist/assembly/bundler-contract.d.ts.map +1 -0
  6. package/dist/assembly/device-feature-admission.d.ts +8 -0
  7. package/dist/assembly/device-feature-admission.d.ts.map +1 -1
  8. package/dist/assembly/extended-lighting-fallback.d.ts +13 -0
  9. package/dist/assembly/extended-lighting-fallback.d.ts.map +1 -0
  10. package/dist/assembly/factory.d.ts +7 -235
  11. package/dist/assembly/factory.d.ts.map +1 -1
  12. package/dist/assembly/host-contract.d.ts +10 -0
  13. package/dist/assembly/host-contract.d.ts.map +1 -1
  14. package/dist/assembly/material/assembly.d.ts +3 -2
  15. package/dist/assembly/material/assembly.d.ts.map +1 -1
  16. package/dist/assembly/material/standard-layer-projection.d.ts +10 -0
  17. package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -0
  18. package/dist/assembly/material-shader-policy.d.ts +168 -0
  19. package/dist/assembly/material-shader-policy.d.ts.map +1 -0
  20. package/dist/assembly/material-shader-prewarm.d.ts +21 -0
  21. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
  22. package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
  23. package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
  24. package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
  25. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
  26. package/dist/assembly/recovery/generation.d.ts +46 -0
  27. package/dist/assembly/recovery/generation.d.ts.map +1 -0
  28. package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
  29. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
  30. package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
  31. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
  32. package/dist/assembly/renderer-facade.d.ts +10 -0
  33. package/dist/assembly/renderer-facade.d.ts.map +1 -0
  34. package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
  35. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  36. package/dist/assembly/renderer-helpers.d.ts +19 -0
  37. package/dist/assembly/renderer-helpers.d.ts.map +1 -0
  38. package/dist/assembly/renderer-lifecycle.d.ts +34 -0
  39. package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
  40. package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
  41. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
  42. package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
  43. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
  44. package/dist/assembly/webgpu-ready.d.ts +55 -0
  45. package/dist/assembly/webgpu-ready.d.ts.map +1 -0
  46. package/dist/assembly/webgpu-renderer.d.ts +152 -0
  47. package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
  48. package/dist/authoring.mjs +1 -1
  49. package/dist/{chunk-PCHBPYML.mjs → chunk-GE3SDD72.mjs} +37 -5
  50. package/dist/chunk-GE3SDD72.mjs.map +1 -0
  51. package/dist/chunk-KZFTK6DO.mjs +1191 -0
  52. package/dist/chunk-KZFTK6DO.mjs.map +1 -0
  53. package/dist/{chunk-FS2KNUQH.mjs → chunk-Q752SHI5.mjs} +332 -568
  54. package/dist/chunk-Q752SHI5.mjs.map +1 -0
  55. package/dist/{chunk-6OPXDQ2E.mjs → chunk-RR5L4SRS.mjs} +33 -294
  56. package/dist/chunk-RR5L4SRS.mjs.map +1 -0
  57. package/dist/chunk-TFH5I7JJ.mjs +296 -0
  58. package/dist/chunk-TFH5I7JJ.mjs.map +1 -0
  59. package/dist/chunk-UQYS7FRM.mjs +1003 -0
  60. package/dist/chunk-UQYS7FRM.mjs.map +1 -0
  61. package/dist/chunk-YKB5DQV7.mjs +345 -0
  62. package/dist/chunk-YKB5DQV7.mjs.map +1 -0
  63. package/dist/{chunk-5YADUCA7.mjs → chunk-YRUYGA4A.mjs} +1189 -1217
  64. package/dist/chunk-YRUYGA4A.mjs.map +1 -0
  65. package/dist/chunk-Z3YYH2MB.mjs +5082 -0
  66. package/dist/chunk-Z3YYH2MB.mjs.map +1 -0
  67. package/dist/cluster-binner.d.ts +5 -0
  68. package/dist/cluster-binner.d.ts.map +1 -1
  69. package/dist/components/index.d.ts +3 -0
  70. package/dist/components/index.d.ts.map +1 -1
  71. package/dist/components/light-helpers.d.ts +4 -0
  72. package/dist/components/light-helpers.d.ts.map +1 -1
  73. package/dist/components/light-probe.d.ts +9 -0
  74. package/dist/components/light-probe.d.ts.map +1 -0
  75. package/dist/components/rect-area-light.d.ts +12 -0
  76. package/dist/components/rect-area-light.d.ts.map +1 -0
  77. package/dist/components/scene-instance.d.ts +4 -0
  78. package/dist/components/scene-instance.d.ts.map +1 -1
  79. package/dist/components/spot-light.d.ts +3 -0
  80. package/dist/components/spot-light.d.ts.map +1 -1
  81. package/dist/construct-renderer.mjs +27276 -23232
  82. package/dist/construct-renderer.mjs.map +1 -1
  83. package/dist/device/device-scope.d.ts +2 -0
  84. package/dist/device/device-scope.d.ts.map +1 -1
  85. package/dist/device/gpu-residency.d.ts +32 -5
  86. package/dist/device/gpu-residency.d.ts.map +1 -1
  87. package/dist/device/lifecycle-transaction.d.ts +2 -0
  88. package/dist/device/lifecycle-transaction.d.ts.map +1 -0
  89. package/dist/environment/frame.d.ts +5 -0
  90. package/dist/environment/frame.d.ts.map +1 -1
  91. package/dist/environment/inspection.d.ts +7 -0
  92. package/dist/environment/inspection.d.ts.map +1 -1
  93. package/dist/environment/lifecycle.d.ts +20 -2
  94. package/dist/environment/lifecycle.d.ts.map +1 -1
  95. package/dist/errors/gpu-driven.d.ts +26 -0
  96. package/dist/errors/gpu-driven.d.ts.map +1 -0
  97. package/dist/errors/index.d.ts +1 -0
  98. package/dist/errors/index.d.ts.map +1 -1
  99. package/dist/errors/recover.d.ts +67 -3
  100. package/dist/errors/recover.d.ts.map +1 -1
  101. package/dist/errors/render.d.ts +39 -1
  102. package/dist/errors/render.d.ts.map +1 -1
  103. package/dist/extract/contracts.d.ts +899 -0
  104. package/dist/extract/contracts.d.ts.map +1 -0
  105. package/dist/extract/gpu-driven.d.ts +50 -0
  106. package/dist/extract/gpu-driven.d.ts.map +1 -0
  107. package/dist/extract/visibility.d.ts.map +1 -1
  108. package/dist/features/host.d.ts +2 -0
  109. package/dist/features/host.d.ts.map +1 -1
  110. package/dist/features/prepared-gpu-work.d.ts +3 -0
  111. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  112. package/dist/features/prepared-graphics-store.d.ts +2 -0
  113. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  114. package/dist/gpu-driven/batch-topology.d.ts +34 -0
  115. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  116. package/dist/gpu-driven/prepared-draw.d.ts +63 -0
  117. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -0
  118. package/dist/gpu-driven/production-raster.d.ts +126 -2
  119. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  120. package/dist/gpu-driven/view-gpu.d.ts +64 -1
  121. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  122. package/dist/gpu-scene-schema.d.ts +14 -0
  123. package/dist/gpu-scene-schema.d.ts.map +1 -1
  124. package/dist/gpu-scene.d.ts +9 -1
  125. package/dist/gpu-scene.d.ts.map +1 -1
  126. package/dist/gpu-usage.d.ts +0 -1
  127. package/dist/gpu-usage.d.ts.map +1 -1
  128. package/dist/hdrp-buffers.d.ts +3 -3
  129. package/dist/hdrp-buffers.d.ts.map +1 -1
  130. package/dist/ibl/IblPipelineCache.d.ts +7 -6
  131. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  132. package/dist/ibl/skylight-bind-group.d.ts +7 -1
  133. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  134. package/dist/index.d.ts +13 -2
  135. package/dist/index.d.ts.map +1 -1
  136. package/dist/index.mjs +90 -5
  137. package/dist/index.mjs.map +1 -1
  138. package/dist/inspection-types.d.ts +163 -1
  139. package/dist/inspection-types.d.ts.map +1 -1
  140. package/dist/internal.d.ts +9 -0
  141. package/dist/internal.d.ts.map +1 -1
  142. package/dist/internal.mjs +23 -2
  143. package/dist/internal.mjs.map +1 -1
  144. package/dist/light-buffer-layout.d.ts +60 -148
  145. package/dist/light-buffer-layout.d.ts.map +1 -1
  146. package/dist/materials.d.ts +34 -10
  147. package/dist/materials.d.ts.map +1 -1
  148. package/dist/mesh-material-bindings.d.ts +56 -0
  149. package/dist/mesh-material-bindings.d.ts.map +1 -1
  150. package/dist/occlusion-proxy.d.ts +4 -0
  151. package/dist/occlusion-proxy.d.ts.map +1 -0
  152. package/dist/pbr-pipeline.d.ts +58 -21
  153. package/dist/pbr-pipeline.d.ts.map +1 -1
  154. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  155. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -1
  156. package/dist/pipeline/standard-lighting/light-frame.d.ts +3 -3
  157. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -1
  158. package/dist/pipeline/standard-lighting/topology.d.ts +1 -1
  159. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -1
  160. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -1
  161. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  162. package/dist/pipeline-builder.d.ts +2 -0
  163. package/dist/pipeline-builder.d.ts.map +1 -1
  164. package/dist/pipeline-spec.d.ts +1 -1
  165. package/dist/pipeline-spec.d.ts.map +1 -1
  166. package/dist/plugin.d.ts.map +1 -1
  167. package/dist/points-lines/prepare.d.ts +3 -0
  168. package/dist/points-lines/prepare.d.ts.map +1 -1
  169. package/dist/points-lines/standard-owner.d.ts +33 -0
  170. package/dist/points-lines/standard-owner.d.ts.map +1 -0
  171. package/dist/prepare/extended-lighting/ltc-resources.d.ts +31 -0
  172. package/dist/prepare/extended-lighting/ltc-resources.d.ts.map +1 -0
  173. package/dist/prepare/extended-lighting/resources.d.ts +72 -0
  174. package/dist/prepare/extended-lighting/resources.d.ts.map +1 -0
  175. package/dist/prepare/extended-lighting/spot-modifiers.d.ts +42 -0
  176. package/dist/prepare/extended-lighting/spot-modifiers.d.ts.map +1 -0
  177. package/dist/prepare/extended-lighting/state.d.ts +24 -0
  178. package/dist/prepare/extended-lighting/state.d.ts.map +1 -0
  179. package/dist/record/frame-lighting.d.ts +8 -6
  180. package/dist/record/frame-lighting.d.ts.map +1 -1
  181. package/dist/record/frame-snapshot.d.ts +55 -10
  182. package/dist/record/frame-snapshot.d.ts.map +1 -1
  183. package/dist/record/frame-targets.d.ts.map +1 -1
  184. package/dist/record/frame.d.ts +21 -5
  185. package/dist/record/frame.d.ts.map +1 -1
  186. package/dist/record/gpu-timing.d.ts +19 -7
  187. package/dist/record/gpu-timing.d.ts.map +1 -1
  188. package/dist/record/helpers.d.ts +0 -20
  189. package/dist/record/helpers.d.ts.map +1 -1
  190. package/dist/record/main-pass-geometry.d.ts +20 -2
  191. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  192. package/dist/record/main-pass-material.d.ts +10 -18
  193. package/dist/record/main-pass-material.d.ts.map +1 -1
  194. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  195. package/dist/record/main-pass.d.ts.map +1 -1
  196. package/dist/record/mesh-ssbo.d.ts +11 -3
  197. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  198. package/dist/record/recovery-pipeline.d.ts +30 -0
  199. package/dist/record/recovery-pipeline.d.ts.map +1 -0
  200. package/dist/record/render-context.d.ts +82 -9
  201. package/dist/record/render-context.d.ts.map +1 -1
  202. package/dist/record/shadow-pass.d.ts.map +1 -1
  203. package/dist/record/typed-frame-graph.d.ts +49 -2
  204. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  205. package/dist/record/view-ubo.d.ts +1 -2
  206. package/dist/record/view-ubo.d.ts.map +1 -1
  207. package/dist/recovery/render-system-candidate.d.ts +65 -0
  208. package/dist/recovery/render-system-candidate.d.ts.map +1 -0
  209. package/dist/reflection/inspection.d.ts +10 -0
  210. package/dist/reflection/inspection.d.ts.map +1 -0
  211. package/dist/reflection/projection.d.ts +49 -0
  212. package/dist/reflection/projection.d.ts.map +1 -1
  213. package/dist/reflection/record-owner.d.ts +60 -0
  214. package/dist/reflection/record-owner.d.ts.map +1 -0
  215. package/dist/render-contract.d.ts +78 -15
  216. package/dist/render-contract.d.ts.map +1 -1
  217. package/dist/render-pipeline.d.ts +7 -0
  218. package/dist/render-pipeline.d.ts.map +1 -1
  219. package/dist/render-system-extract-tail.d.ts +12 -0
  220. package/dist/render-system-extract-tail.d.ts.map +1 -0
  221. package/dist/render-system-extract.d.ts +252 -30
  222. package/dist/render-system-extract.d.ts.map +1 -1
  223. package/dist/render-system.d.ts +140 -5
  224. package/dist/render-system.d.ts.map +1 -1
  225. package/dist/scene/probe-blend-record.d.ts +22 -0
  226. package/dist/scene/probe-blend-record.d.ts.map +1 -0
  227. package/dist/scene/probe-blend.d.ts +142 -0
  228. package/dist/scene/probe-blend.d.ts.map +1 -0
  229. package/dist/scene/render-scene-types.d.ts +0 -6
  230. package/dist/scene/render-scene-types.d.ts.map +1 -1
  231. package/dist/scene/render-scene.d.ts +122 -9
  232. package/dist/scene/render-scene.d.ts.map +1 -1
  233. package/dist/scene/visibility/bounds.d.ts +20 -0
  234. package/dist/scene/visibility/bounds.d.ts.map +1 -0
  235. package/dist/scene/visibility/budget.d.ts +16 -0
  236. package/dist/scene/visibility/budget.d.ts.map +1 -0
  237. package/dist/scene/visibility/facet.d.ts +73 -0
  238. package/dist/scene/visibility/facet.d.ts.map +1 -0
  239. package/dist/scene/visibility/gpu-frame-timing.d.ts +41 -0
  240. package/dist/scene/visibility/gpu-frame-timing.d.ts.map +1 -0
  241. package/dist/scene/visibility/gpu-lod.d.ts +46 -0
  242. package/dist/scene/visibility/gpu-lod.d.ts.map +1 -0
  243. package/dist/scene/visibility/inspection.d.ts +132 -0
  244. package/dist/scene/visibility/inspection.d.ts.map +1 -0
  245. package/dist/scene/visibility/lod-selector.d.ts +25 -0
  246. package/dist/scene/visibility/lod-selector.d.ts.map +1 -0
  247. package/dist/scene/visibility/occlusion-confidence.d.ts +81 -0
  248. package/dist/scene/visibility/occlusion-confidence.d.ts.map +1 -0
  249. package/dist/scene/visibility/occlusion-pass.d.ts +57 -0
  250. package/dist/scene/visibility/occlusion-pass.d.ts.map +1 -0
  251. package/dist/scene/visibility/occlusion-query-pool.d.ts +57 -0
  252. package/dist/scene/visibility/occlusion-query-pool.d.ts.map +1 -0
  253. package/dist/scene/visibility/occlusion-runtime.d.ts +96 -0
  254. package/dist/scene/visibility/occlusion-runtime.d.ts.map +1 -0
  255. package/dist/scene/visibility/types.d.ts +34 -0
  256. package/dist/scene/visibility/types.d.ts.map +1 -0
  257. package/dist/ssr/admission.d.ts +122 -0
  258. package/dist/ssr/admission.d.ts.map +1 -0
  259. package/dist/ssr/composition.d.ts +32 -0
  260. package/dist/ssr/composition.d.ts.map +1 -0
  261. package/dist/ssr/errors.d.ts +39 -0
  262. package/dist/ssr/errors.d.ts.map +1 -0
  263. package/dist/ssr/identity.d.ts +13 -0
  264. package/dist/ssr/identity.d.ts.map +1 -0
  265. package/dist/systems/skin-palette-allocator.d.ts +1 -1
  266. package/dist/temporal/frame-coordinator.d.ts +13 -0
  267. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  268. package/dist/temporal/frame.d.ts +2 -0
  269. package/dist/temporal/frame.d.ts.map +1 -1
  270. package/dist/temporal/index.mjs +6 -5
  271. package/dist/temporal/taa-history-store.d.ts +15 -1
  272. package/dist/temporal/taa-history-store.d.ts.map +1 -1
  273. package/dist/temporal/transaction.d.ts +5 -0
  274. package/dist/temporal/transaction.d.ts.map +1 -1
  275. package/dist/temporal/view.d.ts +7 -0
  276. package/dist/temporal/view.d.ts.map +1 -1
  277. package/dist/tilemap-chunk-extract-system.d.ts +1 -1
  278. package/dist/typed-render-graph-primitives.d.ts +4 -0
  279. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  280. package/dist/volume/passes.d.ts +8 -4
  281. package/dist/volume/passes.d.ts.map +1 -1
  282. package/package.json +20 -20
  283. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +5 -0
  284. package/src/__tests__/detached-inspection.integration.test.ts +6 -0
  285. package/src/__tests__/device-feature-admission.unit.test.ts +19 -1
  286. package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
  287. package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
  288. package/src/__tests__/direct-light-snapshot.unit.test.ts +59 -3
  289. package/src/__tests__/environment-generation.integration.test.ts +39 -0
  290. package/src/__tests__/evidence-identity.ts +13 -0
  291. package/src/__tests__/evidence-identity.unit.test.ts +24 -0
  292. package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
  293. package/src/__tests__/extended-lighting-recovery.browser.test.ts +14 -0
  294. package/src/__tests__/extended-lighting-recovery.dawn.test.ts +14 -0
  295. package/src/__tests__/extended-lighting-recovery.integration.test.ts +81 -0
  296. package/src/__tests__/extended-lighting-resources.unit.test.ts +51 -0
  297. package/src/__tests__/extended-lighting-zero-feature.integration.test.ts +52 -0
  298. package/src/__tests__/factory-contract.integration.test.ts +154 -6
  299. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
  300. package/src/__tests__/frame-observation.contract.test.ts +15 -0
  301. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +146 -0
  302. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +1 -0
  303. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +1 -0
  304. package/src/__tests__/gpu-driven-production.integration.test.ts +572 -9
  305. package/src/__tests__/gpu-driven-scaling.unit.test.ts +66 -2
  306. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +26 -1
  307. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +89 -2
  308. package/src/__tests__/gpu-driven-view.browser.test.ts +3 -3
  309. package/src/__tests__/gpu-driven-view.dawn.test.ts +3 -3
  310. package/src/__tests__/gpu-driven-view.unit.test.ts +50 -0
  311. package/src/__tests__/gpu-lod-evidence.ts +188 -0
  312. package/src/__tests__/gpu-lod-fallback.integration.test.ts +10 -0
  313. package/src/__tests__/gpu-lod-indirect.integration.test.ts +40 -0
  314. package/src/__tests__/gpu-lod-parity.dawn.test.ts +29 -0
  315. package/src/__tests__/gpu-lod-readiness.integration.test.ts +52 -0
  316. package/src/__tests__/gpu-lod-schema.unit.test.ts +66 -0
  317. package/src/__tests__/gpu-lod-selector.unit.test.ts +28 -0
  318. package/src/__tests__/gpu-lod-silhouette.browser.test.ts +13 -0
  319. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +66 -1
  320. package/src/__tests__/gpu-scene.dawn.test.ts +8 -1
  321. package/src/__tests__/gpu-scene.unit.test.ts +49 -3
  322. package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
  323. package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
  324. package/src/__tests__/light-buffer-layout.unit.test.ts +113 -3
  325. package/src/__tests__/light-inspection.unit.test.ts +55 -0
  326. package/src/__tests__/light-resource-error.unit.test.ts +43 -0
  327. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +109 -0
  328. package/src/__tests__/lod-bounds.unit.test.ts +42 -0
  329. package/src/__tests__/lod-cpu-lane.integration.test.ts +55 -0
  330. package/src/__tests__/lod-cpu-no-change.perf.test.ts +19 -0
  331. package/src/__tests__/lod-gpu-frame-timing.unit.test.ts +49 -0
  332. package/src/__tests__/lod-multi-view.integration.test.ts +63 -0
  333. package/src/__tests__/lod-occlusion-device-loss.integration.test.ts +27 -0
  334. package/src/__tests__/lod-occlusion-final-decision.integration.test.ts +47 -0
  335. package/src/__tests__/lod-occlusion-inspection.integration.test.ts +217 -0
  336. package/src/__tests__/lod-occlusion-inspection.schema.test-d.ts +14 -0
  337. package/src/__tests__/lod-occlusion-inspection.unit.test.ts +72 -0
  338. package/src/__tests__/lod-occlusion-visual-evidence.ts +91 -0
  339. package/src/__tests__/lod-occlusion.browser.test.ts +15 -0
  340. package/src/__tests__/lod-occlusion.dawn.test.ts +20 -0
  341. package/src/__tests__/lod-selector.profile.test.ts +35 -0
  342. package/src/__tests__/lod-selector.unit.test.ts +68 -0
  343. package/src/__tests__/lod-visibility-identity.unit.test.ts +198 -0
  344. package/src/__tests__/lod-visibility-lifecycle.integration.test.ts +132 -0
  345. package/src/__tests__/ltc-resources.unit.test.ts +32 -0
  346. package/src/__tests__/material-contract-inventory.unit.test.ts +41 -9
  347. package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
  348. package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
  349. package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
  350. package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
  351. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
  352. package/src/__tests__/occlusion-confidence.unit.test.ts +168 -0
  353. package/src/__tests__/occlusion-invalidation.integration.test.ts +37 -0
  354. package/src/__tests__/occlusion-pass-routing.unit.test.ts +16 -0
  355. package/src/__tests__/occlusion-pass.dawn.test.ts +20 -0
  356. package/src/__tests__/occlusion-pass.integration.test.ts +43 -0
  357. package/src/__tests__/occlusion-pass.rhi-null.test.ts +22 -0
  358. package/src/__tests__/occlusion-query-pool.rhi-null.test.ts +21 -0
  359. package/src/__tests__/occlusion-query-pool.unit.test.ts +95 -0
  360. package/src/__tests__/occlusion-query-ticket.integration.test.ts +47 -0
  361. package/src/__tests__/occlusion-query.dawn.test.ts +16 -0
  362. package/src/__tests__/occlusion-query.evidence.ts +178 -0
  363. package/src/__tests__/occlusion-query.rhi-null.test.ts +31 -0
  364. package/src/__tests__/occlusion-runtime.unit.test.ts +120 -0
  365. package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
  366. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +196 -0
  367. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
  368. package/src/__tests__/probe-bindings.unit.test.ts +200 -0
  369. package/src/__tests__/probe-blend-numeric.unit.test.ts +67 -0
  370. package/src/__tests__/probe-blend-oracle.unit.test.ts +136 -0
  371. package/src/__tests__/probe-capacity.integration.test.ts +97 -0
  372. package/src/__tests__/probe-recovery.integration.test.ts +104 -0
  373. package/src/__tests__/probe-scene-dirty.integration.test.ts +211 -0
  374. package/src/__tests__/profiler-phase-catalog.test.ts +4 -1
  375. package/src/__tests__/profiler-phase-token.test.ts +1 -0
  376. package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
  377. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
  378. package/src/__tests__/rect-area-boundary.unit.test.ts +24 -0
  379. package/src/__tests__/reflection-fallback-graph.unit.test.ts +67 -0
  380. package/src/__tests__/reflection-fallback-inspection.unit.test.ts +77 -0
  381. package/src/__tests__/reflection-fallback-lifecycle.integration.test.ts +21 -0
  382. package/src/__tests__/reflection-fallback-projection.unit.test.ts +90 -0
  383. package/src/__tests__/reflection-fallback-source.unit.test.ts +32 -0
  384. package/src/__tests__/reflection-probe-owner-placement.unit.test.ts +39 -0
  385. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +120 -0
  386. package/src/__tests__/render-error-code-owner.test-d.ts +1 -0
  387. package/src/__tests__/render-feature-docs.unit.test.ts +12 -7
  388. package/src/__tests__/render-graph-fallback.integration.test.ts +24 -0
  389. package/src/__tests__/render-perf/extended-lighting.bench.test.ts +66 -0
  390. package/src/__tests__/render-scene-projection.unit.test.ts +447 -18
  391. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +160 -17
  392. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
  393. package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
  394. package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
  395. package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
  396. package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
  397. package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
  398. package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
  399. package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
  400. package/src/__tests__/shader-manifest-fixture.ts +13 -16
  401. package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
  402. package/src/__tests__/skin-motion-regression.unit.test.ts +22 -0
  403. package/src/__tests__/skinned-shadow-caster.test.ts +2 -2
  404. package/src/__tests__/spot-modifier-projection.unit.test.ts +70 -0
  405. package/src/__tests__/ssr-admission-matrix.integration.test.ts +206 -0
  406. package/src/__tests__/ssr-composition.unit.test.ts +46 -0
  407. package/src/__tests__/ssr-fallback-zero-work.bench.test.ts +94 -0
  408. package/src/__tests__/ssr-recovery-errors.unit.test.ts +58 -0
  409. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +67 -0
  410. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +7 -0
  411. package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
  412. package/src/__tests__/standard-lighting-inspection.integration.test.ts +1 -1
  413. package/src/__tests__/standard-lighting-write.unit.test.ts +66 -2
  414. package/src/__tests__/temporal-fallback-generation.integration.test.ts +16 -0
  415. package/src/__tests__/temporal-view.unit.test.ts +3 -3
  416. package/src/__tests__/transmission-prewarm.unit.test.ts +111 -32
  417. package/src/__tests__/visibility-budget.integration.test.ts +140 -0
  418. package/src/__tests__/visibility-budget.unit.test.ts +70 -0
  419. package/src/__tests__/visibility-extract.integration.test.ts +11 -6
  420. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +6 -3
  421. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +31 -28
  422. package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
  423. package/src/assembly/backend-contract.ts +13 -0
  424. package/src/assembly/bundler-contract.ts +9 -0
  425. package/src/assembly/device-feature-admission.ts +26 -9
  426. package/src/assembly/extended-lighting-fallback.ts +216 -0
  427. package/src/assembly/factory.ts +81 -9634
  428. package/src/assembly/host-contract.ts +10 -0
  429. package/src/assembly/material/assembly.ts +18 -2
  430. package/src/assembly/material/standard-layer-projection.ts +16 -0
  431. package/src/assembly/material-shader-policy.ts +854 -0
  432. package/src/assembly/material-shader-prewarm.ts +88 -0
  433. package/src/assembly/mesh-ssbo-grow.ts +270 -0
  434. package/src/assembly/recovery/device-loss-fanout.ts +89 -0
  435. package/src/assembly/recovery/generation.ts +141 -0
  436. package/src/assembly/recovery/recovery-attempt.ts +800 -0
  437. package/src/assembly/recovery/renderer-recover.ts +906 -0
  438. package/src/assembly/renderer-facade.ts +342 -0
  439. package/src/assembly/renderer-frame-transaction.ts +45 -0
  440. package/src/assembly/renderer-helpers.ts +99 -0
  441. package/src/assembly/renderer-lifecycle.ts +159 -0
  442. package/src/assembly/shader-prewarm-policy.ts +157 -0
  443. package/src/assembly/webgpu-ready-contract.ts +14 -0
  444. package/src/assembly/webgpu-ready.ts +4079 -0
  445. package/src/assembly/webgpu-renderer.ts +4095 -0
  446. package/src/cluster-binner.ts +31 -0
  447. package/src/components/__tests__/lighting-authoring.unit.test.ts +39 -0
  448. package/src/components/__tests__/rect-area-light.unit.test.ts +33 -0
  449. package/src/components/camera.ts +1 -1
  450. package/src/components/index.ts +3 -0
  451. package/src/components/light-helpers.ts +52 -1
  452. package/src/components/light-probe.ts +13 -0
  453. package/src/components/rect-area-light.ts +15 -0
  454. package/src/components/scene-instance.ts +6 -0
  455. package/src/components/spot-light.ts +3 -0
  456. package/src/debug-draw-glue.ts +1 -1
  457. package/src/device/__tests__/extended-lighting-recovery.unit.test.ts +24 -0
  458. package/src/device/device-scope.ts +35 -6
  459. package/src/device/gpu-residency.ts +334 -35
  460. package/src/device/lifecycle-transaction.ts +12 -0
  461. package/src/environment/frame.ts +6 -0
  462. package/src/environment/inspection.ts +10 -0
  463. package/src/environment/lifecycle.ts +122 -9
  464. package/src/errors/gpu-driven.ts +92 -0
  465. package/src/errors/index.ts +1 -0
  466. package/src/errors/recover.ts +108 -5
  467. package/src/errors/render.ts +72 -1
  468. package/src/extract/camera.ts +3 -3
  469. package/src/extract/contracts.ts +974 -0
  470. package/src/extract/gpu-driven.ts +108 -0
  471. package/src/extract/visibility.ts +0 -23
  472. package/src/features/host.ts +16 -0
  473. package/src/features/prepared-gpu-work.ts +20 -2
  474. package/src/features/prepared-graphics-store.ts +17 -0
  475. package/src/gpu-driven/batch-topology.ts +206 -28
  476. package/src/gpu-driven/prepared-draw.ts +208 -0
  477. package/src/gpu-driven/production-raster.ts +1046 -39
  478. package/src/gpu-driven/view-gpu.ts +411 -29
  479. package/src/gpu-scene-schema.ts +13 -0
  480. package/src/gpu-scene.ts +137 -67
  481. package/src/gpu-usage.ts +0 -1
  482. package/src/hdrp-buffers.ts +6 -6
  483. package/src/ibl/IblPipelineCache.ts +134 -50
  484. package/src/ibl/skylight-bind-group.ts +26 -0
  485. package/src/index.ts +83 -1
  486. package/src/inspection-types.ts +220 -1
  487. package/src/internal.ts +38 -0
  488. package/src/light-buffer-layout.ts +157 -324
  489. package/src/materials.ts +278 -47
  490. package/src/mesh-material-bindings.ts +59 -0
  491. package/src/occlusion-proxy.ts +64 -0
  492. package/src/pbr-pipeline.ts +290 -60
  493. package/src/pipeline/standard-forward-lane.ts +47 -0
  494. package/src/pipeline/standard-lighting/graph.ts +2 -1
  495. package/src/pipeline/standard-lighting/light-frame.ts +3 -2
  496. package/src/pipeline/standard-lighting/topology.ts +1 -1
  497. package/src/pipeline/standard-lighting/transport.ts +2 -1
  498. package/src/pipeline/standard-pipeline.ts +2 -0
  499. package/src/pipeline-builder.ts +5 -2
  500. package/src/pipeline-spec.ts +12 -1
  501. package/src/plugin.ts +4 -0
  502. package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
  503. package/src/points-lines/prepare.ts +9 -0
  504. package/src/points-lines/standard-owner.ts +403 -0
  505. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +56 -0
  506. package/src/prepare/extended-lighting/ltc-resources.ts +136 -0
  507. package/src/prepare/extended-lighting/resources.ts +389 -0
  508. package/src/prepare/extended-lighting/spot-modifiers.ts +194 -0
  509. package/src/prepare/extended-lighting/state.ts +92 -0
  510. package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
  511. package/src/record/__tests__/gpu-timing.unit.test.ts +177 -0
  512. package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
  513. package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
  514. package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
  515. package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
  516. package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
  517. package/src/record/__tests__/temporal-payload.unit.test.ts +29 -28
  518. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +21 -2
  519. package/src/record/frame-lighting.ts +252 -53
  520. package/src/record/frame-snapshot.ts +72 -12
  521. package/src/record/frame-targets.ts +11 -1
  522. package/src/record/frame.ts +416 -95
  523. package/src/record/gpu-timing.ts +108 -44
  524. package/src/record/helpers.ts +1 -59
  525. package/src/record/main-pass-geometry.ts +434 -44
  526. package/src/record/main-pass-material.ts +324 -210
  527. package/src/record/main-pass-sprite-draws.ts +58 -92
  528. package/src/record/main-pass.ts +94 -6
  529. package/src/record/mesh-ssbo.ts +79 -46
  530. package/src/record/recovery-pipeline.ts +695 -0
  531. package/src/record/render-context.ts +99 -7
  532. package/src/record/shadow-pass.ts +66 -40
  533. package/src/record/typed-frame-graph.ts +270 -84
  534. package/src/record/view-ubo.ts +17 -27
  535. package/src/recovery/render-system-candidate.ts +839 -0
  536. package/src/reflection/inspection.ts +67 -0
  537. package/src/reflection/projection.ts +196 -1
  538. package/src/reflection/record-owner.ts +1283 -0
  539. package/src/render-contract.ts +82 -12
  540. package/src/render-pipeline.ts +5 -0
  541. package/src/render-system-extract-tail.ts +2983 -0
  542. package/src/render-system-extract.ts +290 -2784
  543. package/src/render-system.ts +1597 -1192
  544. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +36 -0
  545. package/src/scene/probe-blend-record.ts +54 -0
  546. package/src/scene/probe-blend.ts +813 -0
  547. package/src/scene/render-scene-types.ts +0 -7
  548. package/src/scene/render-scene.ts +972 -184
  549. package/src/scene/visibility/bounds.ts +57 -0
  550. package/src/scene/visibility/budget.ts +40 -0
  551. package/src/scene/visibility/facet.ts +400 -0
  552. package/src/scene/visibility/gpu-frame-timing.ts +101 -0
  553. package/src/scene/visibility/gpu-lod.ts +119 -0
  554. package/src/scene/visibility/inspection.ts +313 -0
  555. package/src/scene/visibility/lod-selector.ts +84 -0
  556. package/src/scene/visibility/occlusion-confidence.ts +478 -0
  557. package/src/scene/visibility/occlusion-pass.ts +165 -0
  558. package/src/scene/visibility/occlusion-query-pool.ts +178 -0
  559. package/src/scene/visibility/occlusion-runtime.ts +672 -0
  560. package/src/scene/visibility/types.ts +50 -0
  561. package/src/ssr/admission.ts +451 -0
  562. package/src/ssr/composition.ts +73 -0
  563. package/src/ssr/errors.ts +76 -0
  564. package/src/ssr/identity.ts +12 -0
  565. package/src/systems/skin-palette-allocator.ts +1 -1
  566. package/src/temporal/frame-coordinator.ts +30 -1
  567. package/src/temporal/frame.ts +14 -1
  568. package/src/temporal/taa-history-store.ts +46 -2
  569. package/src/temporal/temporal-view.ts +1 -1
  570. package/src/temporal/transaction.ts +9 -0
  571. package/src/temporal/view.ts +11 -0
  572. package/src/tilemap-chunk-extract-system.ts +5 -5
  573. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
  574. package/src/typed-render-graph-primitives.ts +48 -0
  575. package/src/volume/passes.ts +46 -49
  576. package/dist/.tsbuildinfo +0 -1
  577. package/dist/chunk-5YADUCA7.mjs.map +0 -1
  578. package/dist/chunk-6OPXDQ2E.mjs.map +0 -1
  579. package/dist/chunk-BXBQQKFQ.mjs +0 -67
  580. package/dist/chunk-BXBQQKFQ.mjs.map +0 -1
  581. package/dist/chunk-FS2KNUQH.mjs.map +0 -1
  582. package/dist/chunk-PCHBPYML.mjs.map +0 -1
  583. package/dist/chunk-VFPLTKCU.mjs +0 -192
  584. package/dist/chunk-VFPLTKCU.mjs.map +0 -1
@@ -16,21 +16,21 @@
16
16
  // .invert` (charter proposition 5: no math reinvention; render-system.test.ts
17
17
  // asserts `/@forgeax\/engine-math/` shows up in render-system.ts source).
18
18
 
19
- import { numMipLevels, resolveAssetHandle } from '@forgeax/engine-assets-runtime';
19
+ import {
20
+ type MipmapEncoderWork,
21
+ numMipLevels,
22
+ resolveAssetHandle,
23
+ } from '@forgeax/engine-assets-runtime';
20
24
  import type { World } from '@forgeax/engine-ecs';
21
25
  import type { RenderReadLease } from '@forgeax/engine-ecs/projection';
22
26
  import type { RecorderSession } from '@forgeax/engine-profiler';
23
27
  import type { CompiledRenderGraphInfo } from '@forgeax/engine-render-graph';
24
28
  import type {
25
- BindGroup,
26
29
  BindGroupEntry,
27
30
  BindGroupLayout,
28
31
  Buffer,
29
32
  RenderPipeline,
30
- Sampler,
31
- Texture,
32
- TextureFormat,
33
- TextureView,
33
+ RhiDevice,
34
34
  } from '@forgeax/engine-rhi';
35
35
  import { err, ok, type Result, RhiError } from '@forgeax/engine-rhi';
36
36
  import {
@@ -39,7 +39,6 @@ import {
39
39
  type Handle,
40
40
  type MaterialAsset,
41
41
  type MaterialTextureValue,
42
- type MeshAsset,
43
42
  type RenderPipelineAsset,
44
43
  RenderQueue,
45
44
  type SamplerAsset,
@@ -52,14 +51,16 @@ import {
52
51
  projectDirectionalShadowInspectionSource,
53
52
  } from './assembly/directional-shadow-inspection';
54
53
  import { createClusterBinScratch } from './cluster-binner';
54
+ import type { DeviceScope, LifecycleResourceSpec } from './device/device-scope';
55
+ import type { EnvironmentGeneration } from './environment/generation';
55
56
  import { EnvironmentLifecycle } from './environment/lifecycle';
56
57
  import {
57
58
  type ObservationUnavailableError,
58
- PointsLinesMaterialUnsupportedError,
59
59
  type RenderError,
60
60
  TransmissionCapabilityMissingError,
61
61
  } from './errors/render';
62
62
  import {
63
+ type RenderFeatureHost,
63
64
  type RenderFeaturePreparedGraphicsResolverInput,
64
65
  runRenderFeatureFrame,
65
66
  settlePreparedGraphicsCompletion,
@@ -82,60 +83,40 @@ import { GpuDrivenProduction } from './gpu-driven/production-raster';
82
83
  import type {
83
84
  BloomInspection,
84
85
  DirectionalShadowInspection,
86
+ GpuDrivenProductionInspection,
87
+ LodOcclusionInspection,
88
+ LodOcclusionInspectionRow,
89
+ LodOcclusionWorldInspection,
85
90
  MotionBlurInspection,
86
91
  ReflectionProbeInspection,
87
- ReflectionProbeSelectionInspection,
88
92
  RenderSceneInspection,
93
+ SsrDependenciesInspection,
89
94
  TemporalTargetInspection,
90
95
  TransmissionInspection,
91
96
  } from './inspection-types';
92
- import { admitPointsLines } from './points-lines/admission';
93
- import { PointsLinesExpansionCache } from './points-lines/expansion-cache';
97
+ import { buildOcclusionProxyVertices } from './occlusion-proxy';
98
+ import type { PointsLinesRetainedSnapshot } from './points-lines/snapshot';
99
+ import { StandardPointsLinesOwner } from './points-lines/standard-owner';
94
100
  import {
95
- inspectPointsLines,
96
- type PointsLinesInspection,
97
- type PointsLinesSourceError,
98
- } from './points-lines/inspection';
101
+ createRenderSystemRecovery,
102
+ materialSamplerHandles,
103
+ materialTextureHandles,
104
+ } from './recovery/render-system-candidate';
105
+ import { ReflectionProbeRecordOwner } from './reflection/record-owner';
99
106
  import {
100
- createPointsLinesLanePreparationAdapter,
101
- type PointsLinesLanePreparationAdapter,
102
- } from './points-lines/prepare';
103
- import { createPointsLinesLaneAdapter, type PointsLinesBackend } from './points-lines/record';
104
- import type { PointsLinesRetainedSnapshot } from './points-lines/snapshot';
105
- import type { PointsLinesRecordOwner, PointsLinesRecordSubmission } from './record/render-context';
107
+ OcclusionRenderRuntime,
108
+ type OcclusionRuntimeCandidate,
109
+ } from './scene/visibility/occlusion-runtime';
110
+ import { primitiveKey, viewKey } from './scene/visibility/types';
106
111
  import { inspectVolumetricFog } from './volume/inspection';
107
112
 
108
113
  export type { RenderSceneInspection } from './inspection-types';
109
114
 
110
- import { deriveVertexLayoutProjection } from '@forgeax/engine-geometry';
111
- import { mat4, vec3 } from '@forgeax/engine-math';
112
- import { buildCubeCameraFaceViews } from './capture/cube-views';
113
115
  import { createCubeCaptureScheduler } from './capture/scheduler';
114
- import {
115
- createFaceUniformsBuffer,
116
- createPrefilterUniformsBuffer,
117
- writeAllFaceUniforms,
118
- writeAllPrefilterUniforms,
119
- } from './device/gpu-residency';
116
+ import type { GpuResidencyCache } from './device/gpu-residency';
120
117
  import type { EnvironmentInspection } from './environment/inspection';
121
- import {
122
- GPU_TEXTURE_USAGE_COPY_SRC,
123
- GPU_TEXTURE_USAGE_RENDER_ATTACHMENT_AND_TEXTURE_BINDING,
124
- } from './gpu-texture-usage';
125
- import {
126
- GPU_BUFFER_USAGE_COPY_DST,
127
- GPU_BUFFER_USAGE_INDEX,
128
- GPU_BUFFER_USAGE_UNIFORM,
129
- GPU_BUFFER_USAGE_VERTEX,
130
- } from './gpu-usage';
118
+ import { GPU_BUFFER_USAGE_COPY_DST, GPU_BUFFER_USAGE_UNIFORM } from './gpu-usage';
131
119
  import { resetHdrpBuffers } from './hdrp-buffers';
132
- import {
133
- CUBEMAP_FACE_VERTICES,
134
- createIblPipelines,
135
- getOrCreateIblCache,
136
- PREFILTER_MIP_LEVELS,
137
- PREFILTER_SIZE,
138
- } from './ibl/IblPipelineCache';
139
120
  import { assembleMaterialWithSkylightEntries } from './ibl/skylight-bind-group';
140
121
  import { disposeInstanceBuffers, disposeTransientInstanceBuffers } from './instance-buffer-cache';
141
122
  import {
@@ -168,7 +149,7 @@ import type {
168
149
  GraphTargetCaptureRequest,
169
150
  RenderFrameState,
170
151
  } from './record/frame-snapshot';
171
- import { getTextureIdentity } from './record/frame-snapshot';
152
+ import { getTextureIdentity, worldEntityKey } from './record/frame-snapshot';
172
153
  import type { GpuPassTimingReason } from './record/gpu-pass-timing/errors';
173
154
  import type {
174
155
  GpuPassTimingCapture,
@@ -177,27 +158,14 @@ import type {
177
158
  } from './record/gpu-pass-timing/session';
178
159
  import type { GpuTimingCapture } from './record/gpu-timing';
179
160
  import { applyParamSnapshotToUbo, residentTextureView } from './record/main-pass-material';
180
- import type { ReflectionProbeRecordState, RenderSystemInternals } from './record/render-context';
181
- import type { CubeCaptureGraphState, ReflectionProbeGraphWork } from './record/typed-frame-graph';
161
+ import type { RecoveryPipelineReadiness } from './record/recovery-pipeline';
162
+ import type { PipelineState, RenderSystemInternals } from './record/render-context';
163
+ import type { CubeCaptureGraphState } from './record/typed-frame-graph';
182
164
  import {
183
165
  type RenderFeatureGraphCandidate,
184
166
  resetRenderFeatureGraphState,
185
167
  settleVolumetricFogGraphCandidate,
186
168
  } from './record/typed-frame-graph';
187
- import { REFLECTION_PROBE_VIEW_SLOT_BASE, VIEW_UNIFORM_SLOT_STRIDE } from './record/view-ubo';
188
- import {
189
- advanceProbeFilter,
190
- commitProbeFilterStep,
191
- createProbeFilterState,
192
- type ProbeFilterState,
193
- } from './reflection/filter';
194
- import { buildReflectionProbeTable } from './reflection/gpu-table';
195
- import {
196
- admitReflectionProbe,
197
- DEFAULT_REFLECTION_PROBE_LIMITS,
198
- type ReflectionProbeFact,
199
- type ReflectionProbeSelectionResult,
200
- } from './reflection/projection';
201
169
  import {
202
170
  type CameraSnapshot,
203
171
  type DrawOwnerOptions,
@@ -214,8 +182,10 @@ import type {
214
182
  RenderableSnapshot,
215
183
  } from './render-system-extract';
216
184
  import { extractFrames } from './render-system-extract';
217
- import { PersistentRenderScene } from './scene/render-scene';
185
+ import { type PersistentGpuDrivenCandidate, PersistentRenderScene } from './scene/render-scene';
186
+ import { inspectLodOcclusion } from './scene/visibility/inspection';
218
187
  import { resolveSsaoParameters } from './ssao-config';
188
+ import { projectSsrDependencies, resolveSsrAdmissionGeneration } from './ssr/admission';
219
189
  import {
220
190
  getTransparentSortConfig,
221
191
  TRANSPARENT_SORT_MODE_DISTANCE,
@@ -239,6 +209,149 @@ import {
239
209
  inspectTransmissionFromAdmission,
240
210
  } from './transmission/inspection';
241
211
 
212
+ function canonicalizeWorldComposition(
213
+ worlds: readonly World[],
214
+ owners: Pick<DrawOwnerOptions, 'cameraOwner' | 'resourceOwner'>,
215
+ leases: readonly RenderReadLease[] | undefined,
216
+ ): {
217
+ readonly worlds: readonly World[];
218
+ readonly owners: Pick<DrawOwnerOptions, 'cameraOwner' | 'resourceOwner'>;
219
+ readonly leases: readonly RenderReadLease[] | undefined;
220
+ } {
221
+ const order = worlds
222
+ .map((world, index) => ({ world, index }))
223
+ .sort((left, right) => left.world.identity.localeCompare(right.world.identity));
224
+ const indexByOriginal = new Map(order.map((entry, index) => [entry.index, index]));
225
+ return {
226
+ worlds: Object.freeze(order.map((entry) => entry.world)),
227
+ owners: {
228
+ cameraOwner: indexByOriginal.get(owners.cameraOwner) ?? owners.cameraOwner,
229
+ resourceOwner: indexByOriginal.get(owners.resourceOwner) ?? owners.resourceOwner,
230
+ },
231
+ leases:
232
+ leases === undefined
233
+ ? undefined
234
+ : Object.freeze(
235
+ order.map((entry) => leases[entry.index]).filter((lease) => lease !== undefined),
236
+ ),
237
+ };
238
+ }
239
+
240
+ interface LodInspectionQuerySummary {
241
+ readonly used: number;
242
+ readonly capacity: number;
243
+ }
244
+
245
+ function createLodWorldInspections(
246
+ worlds: readonly World[],
247
+ renderables: readonly RenderableSnapshot[],
248
+ submittedRenderables: readonly RenderableSnapshot[],
249
+ camera: CameraSnapshot,
250
+ frameId: number,
251
+ worldKeys: readonly number[],
252
+ slots: ReadonlyMap<number, { readonly slot: number; readonly generation: number }>,
253
+ query: LodInspectionQuerySummary,
254
+ fallback: LodOcclusionInspection['fallback'],
255
+ degradation: LodOcclusionInspection['degradation'],
256
+ ): readonly LodOcclusionWorldInspection[] {
257
+ // The CPU projection knows which renderables belong to each World, but it
258
+ // does not receive the GPU selector/query counters split by World from the
259
+ // same submit. Keep these rows available for diagnostics while explicitly
260
+ // marking their attribution unavailable; producers must not promote them
261
+ // to World-reorder evidence by inferring facts from array order.
262
+ const attribution = Object.freeze({
263
+ status: 'unavailable' as const,
264
+ reason: 'projection-only' as const,
265
+ });
266
+ const submitted = new Set(submittedRenderables);
267
+ const sampleLimit = 64;
268
+ const accumulators = worlds.map(() => ({
269
+ allCount: 0,
270
+ allVisible: 0,
271
+ allSamples: [] as RenderableSnapshot[],
272
+ lodCount: 0,
273
+ lodVisible: 0,
274
+ lodSamples: [] as RenderableSnapshot[],
275
+ }));
276
+ // Keep this projection bounded: the old implementation filtered the full
277
+ // 100k renderable list twice per World on every frame and retained another
278
+ // Set-backed traversal just to derive the six counters below. One pass is
279
+ // enough because the inspection only needs counts, the first row identity,
280
+ // and at most 64 detached samples.
281
+ for (const renderable of renderables) {
282
+ const accumulator = accumulators[renderable.worldId];
283
+ if (accumulator === undefined) continue;
284
+ const visible = submitted.has(renderable);
285
+ accumulator.allCount += 1;
286
+ if (visible) accumulator.allVisible += 1;
287
+ if (accumulator.allSamples.length < sampleLimit) accumulator.allSamples.push(renderable);
288
+ if ((renderable.lods?.length ?? 0) > 0) {
289
+ accumulator.lodCount += 1;
290
+ if (visible) accumulator.lodVisible += 1;
291
+ if (accumulator.lodSamples.length < sampleLimit) accumulator.lodSamples.push(renderable);
292
+ }
293
+ }
294
+ const result: LodOcclusionWorldInspection[] = [];
295
+ for (let worldId = 0; worldId < worlds.length; worldId += 1) {
296
+ const world = worlds[worldId];
297
+ if (world === undefined) continue;
298
+ const accumulator = accumulators[worldId];
299
+ if (accumulator === undefined || accumulator.allCount === 0) {
300
+ result.push(
301
+ Object.freeze({ attachmentId: world.identity, rows: Object.freeze([]), attribution }),
302
+ );
303
+ continue;
304
+ }
305
+ const usesLod = accumulator.lodCount > 0;
306
+ const candidateCount = usesLod ? accumulator.lodCount : accumulator.allCount;
307
+ const visible = usesLod ? accumulator.lodVisible : accumulator.allVisible;
308
+ const samples = usesLod ? accumulator.lodSamples : accumulator.allSamples;
309
+ const first = samples[0];
310
+ const slot =
311
+ first === undefined
312
+ ? undefined
313
+ : slots.get(worldEntityKey(worldKeys[first.worldId] ?? first.worldId, first.entityKey));
314
+ const row: LodOcclusionInspectionRow = {
315
+ root: {
316
+ guid: first === undefined ? 'none' : `asset-handle:${first.assetHandle}`,
317
+ sourceKey: `render-world:${world.identity}`,
318
+ },
319
+ view: {
320
+ attachmentId: world.identity,
321
+ cameraEntity: world === worlds[camera.worldId ?? 0] ? (camera.entityKey ?? 0) : 0,
322
+ viewRole: 'main',
323
+ viewGeneration: camera.historyVersion ?? 0,
324
+ },
325
+ slot: { primitiveSlot: slot?.slot ?? 0, slotGeneration: slot?.generation ?? 0 },
326
+ generation: frameId,
327
+ count: { candidates: candidateCount, visible, occluded: candidateCount - visible },
328
+ lodHistogram: [{ level: 0, count: candidateCount }],
329
+ queryLatencyUs: { median: 0, p95: 0, last: 0 },
330
+ pagePressure: query,
331
+ fallback,
332
+ degradation,
333
+ samples: samples.map((candidate, index) => ({
334
+ primitiveSlot:
335
+ slots.get(
336
+ worldEntityKey(worldKeys[candidate.worldId] ?? candidate.worldId, candidate.entityKey),
337
+ )?.slot ?? index,
338
+ level: 0,
339
+ visible: submitted.has(candidate),
340
+ })),
341
+ };
342
+ result.push(
343
+ Object.freeze({
344
+ attachmentId: world.identity,
345
+ rows: Object.freeze([row]),
346
+ attribution,
347
+ }),
348
+ );
349
+ }
350
+ return Object.freeze(
351
+ result.sort((left, right) => left.attachmentId.localeCompare(right.attachmentId)),
352
+ );
353
+ }
354
+
242
355
  type TimingSessionHost = RenderSystemInternals & {
243
356
  gpuPassTimingSession?: GpuPassTimingSession | undefined;
244
357
  gpuPassTimingCapture?: GpuPassTimingCapture | undefined;
@@ -275,6 +388,128 @@ export {
275
388
  selectSwapChainFormat,
276
389
  } from './record/render-context';
277
390
 
391
+ export type RendererProducerRootKind =
392
+ | 'backend-surface'
393
+ | 'shader-material-pipeline'
394
+ | 'mesh-texture-sampler'
395
+ | 'render-scene'
396
+ | 'gpu-driven'
397
+ | 'feature'
398
+ | 'external-source'
399
+ | 'render-graph'
400
+ | 'target-history'
401
+ | 'observation-lease';
402
+
403
+ export interface RendererProducerRoot {
404
+ readonly kind: RendererProducerRootKind;
405
+ readonly owner: string;
406
+ readonly candidateScope: 'device-scope';
407
+ readonly visibility: 'visible-workset' | 'non-visible-lazy';
408
+ readonly disabledWork: 'zero' | 'lazy';
409
+ }
410
+
411
+ /** Device-bound post-process resources prepared off to the side of the active renderer. */
412
+ export interface RecoveryPostProcessResources {
413
+ readonly device: RhiDevice;
414
+ readonly paramsBuffers: Map<string, Buffer>;
415
+ }
416
+
417
+ const PRODUCER_ROOT_MATRIX: readonly RendererProducerRoot[] = Object.freeze([
418
+ {
419
+ kind: 'backend-surface',
420
+ owner: 'backend-surface-owner',
421
+ candidateScope: 'device-scope',
422
+ visibility: 'visible-workset',
423
+ disabledWork: 'zero',
424
+ },
425
+ {
426
+ kind: 'shader-material-pipeline',
427
+ owner: 'shader-material-pipeline-owner',
428
+ candidateScope: 'device-scope',
429
+ visibility: 'visible-workset',
430
+ disabledWork: 'zero',
431
+ },
432
+ {
433
+ kind: 'mesh-texture-sampler',
434
+ owner: 'gpu-residency',
435
+ candidateScope: 'device-scope',
436
+ visibility: 'visible-workset',
437
+ disabledWork: 'lazy',
438
+ },
439
+ {
440
+ kind: 'render-scene',
441
+ owner: 'persistent-render-scene',
442
+ candidateScope: 'device-scope',
443
+ visibility: 'visible-workset',
444
+ disabledWork: 'zero',
445
+ },
446
+ {
447
+ kind: 'gpu-driven',
448
+ owner: 'gpu-driven-production',
449
+ candidateScope: 'device-scope',
450
+ visibility: 'visible-workset',
451
+ disabledWork: 'zero',
452
+ },
453
+ {
454
+ kind: 'feature',
455
+ owner: 'render-feature-host',
456
+ candidateScope: 'device-scope',
457
+ visibility: 'visible-workset',
458
+ disabledWork: 'lazy',
459
+ },
460
+ {
461
+ kind: 'external-source',
462
+ owner: 'external-source-owner',
463
+ candidateScope: 'device-scope',
464
+ visibility: 'non-visible-lazy',
465
+ disabledWork: 'zero',
466
+ },
467
+ {
468
+ kind: 'render-graph',
469
+ owner: 'typed-frame-graph',
470
+ candidateScope: 'device-scope',
471
+ visibility: 'visible-workset',
472
+ disabledWork: 'zero',
473
+ },
474
+ {
475
+ kind: 'target-history',
476
+ owner: 'temporal-target-owner',
477
+ candidateScope: 'device-scope',
478
+ visibility: 'visible-workset',
479
+ disabledWork: 'zero',
480
+ },
481
+ {
482
+ kind: 'observation-lease',
483
+ owner: 'render-observation-owner',
484
+ candidateScope: 'device-scope',
485
+ visibility: 'non-visible-lazy',
486
+ disabledWork: 'zero',
487
+ },
488
+ ]);
489
+
490
+ export function createRendererProducerRootMatrix(): readonly RendererProducerRoot[] {
491
+ return PRODUCER_ROOT_MATRIX;
492
+ }
493
+
494
+ /**
495
+ * Production-owned recovery facts. These values are projections of the live
496
+ * RenderSystem owners; they are not a test ledger or a second graph model.
497
+ */
498
+ export interface RecoveryProductionEvidence {
499
+ readonly producerRoots: readonly RendererProducerRoot[];
500
+ readonly graph: {
501
+ readonly ready: boolean;
502
+ readonly generation: number;
503
+ readonly passCount: number;
504
+ readonly resourceCount: number;
505
+ };
506
+ readonly residency: { readonly meshResidencyEpoch: number };
507
+ readonly submissions: {
508
+ readonly count: number;
509
+ readonly lastGeneration: number | undefined;
510
+ };
511
+ }
512
+
278
513
  /**
279
514
  * Unified transparent-queue sub-sort covering all four
280
515
  * {@link TransparentSortConfig} modes. Reorders the
@@ -323,7 +558,7 @@ function sortTransparentDispatch(
323
558
  const camPos = camera.position;
324
559
 
325
560
  // Squared camera distance per entry (negated → ascending = back-to-front).
326
- // D-3: world position = translation column of Transform.world (m[12,13,14]).
561
+ // D-3: world position = translation column of GlobalTransform.world (m[12,13,14]).
327
562
  const negDistSq = (entry: DispatchEntry): number => {
328
563
  const tx = renderables[entry.renderableIndex]?.transform;
329
564
  if (tx === undefined) return 0;
@@ -422,6 +657,19 @@ export interface RenderSystem {
422
657
  renderReadLeases?: readonly RenderReadLease[],
423
658
  timingCapture?: GpuTimingCapture,
424
659
  ): boolean;
660
+ /** Prepare a detached graph candidate without entering the frame record path. */
661
+ prepareRecoveryGraphCandidate(
662
+ runtime: RecoveryGraphCandidateRuntime,
663
+ ): RecoveryGraphCandidatePreparation;
664
+ /** Finish and submit detached candidate setup, never a frame receipt. */
665
+ submitCandidateSetup(
666
+ candidate: RecoveryGraphCandidate,
667
+ isValid: () => boolean,
668
+ ): Result<RecoveryGraphSetupSubmission, RhiError>;
669
+ /** Publish a detached graph candidate at the generation publication boundary. */
670
+ publishRecoveryGraphCandidate(candidate: RecoveryGraphCandidate): void;
671
+ /** Discard a detached graph candidate that did not reach publication. */
672
+ discardRecoveryGraphCandidate(candidate: RecoveryGraphCandidate): void;
425
673
  /** Release renderer-owned persistent state for one detached World. */
426
674
  detachScene(world: World): void;
427
675
  /** Release the profiler catalog contribution owned by this RenderSystem. */
@@ -429,6 +677,8 @@ export interface RenderSystem {
429
677
  observeCurrentFrame(
430
678
  options: FrameObservationOptions,
431
679
  ): Promise<Result<FrameObservation, ObservationUnavailableError>>;
680
+ /** Resolve the renderer-owned GPU LOD counters for the last accepted submit. */
681
+ observeLodOcclusion(): Promise<void>;
432
682
  /** Internal same-frame graph target access for backend diagnostics. */
433
683
  getCurrentGraphTarget(name: string): CurrentGraphTarget | undefined;
434
684
  /** Internal test capture copied into the draw's command submission. */
@@ -446,6 +696,10 @@ export interface RenderSystem {
446
696
  /** Persistent scene maintenance evidence from the ordinary single-World path. */
447
697
  readonly renderScene: RenderSceneInspection;
448
698
  readonly reflectionProbes: ReflectionProbeInspection;
699
+ /** Live producer/RHI/temporal receipts projected through SSR admission. */
700
+ readonly ssrDependencies: SsrDependenciesInspection;
701
+ /** Producer completion fence included in the public FrameReceipt. */
702
+ readonly reflectionFallbackCompletion: Promise<void> | undefined;
449
703
  /** True while a progressive CubeCamera candidate still owns unfinished faces. */
450
704
  isCubeCapturePending(target: RenderTarget): boolean;
451
705
  readonly environment: EnvironmentInspection;
@@ -453,6 +707,9 @@ export interface RenderSystem {
453
707
  readonly bloom: BloomInspection;
454
708
  readonly motionBlurInspection: MotionBlurInspection | undefined;
455
709
  readonly temporalTargetInspection: TemporalTargetInspection | undefined;
710
+ readonly lodOcclusionInspection: LodOcclusionInspection | undefined;
711
+ /** Bounded GPU-driven counters without materializing the persistent scene table. */
712
+ readonly gpuDrivenInspection: GpuDrivenProductionInspection;
456
713
  /** Last completed transmission candidate facts, detached from GPU handles. */
457
714
  readonly transmission: TransmissionInspection | undefined;
458
715
  /** Single production projection of Directional author/extract/record facts. */
@@ -465,6 +722,8 @@ export interface RenderSystem {
465
722
  readonly pointsLinesSnapshots: readonly PointsLinesRetainedSnapshot[];
466
723
  /** Current mesh-slot provenance and active diagnostics from the last frame. */
467
724
  readonly meshMaterialBindings: readonly MeshMaterialBindingObservation[];
725
+ /** Final-submit IBL binding-chain receipt for the last diagnostic frame. */
726
+ readonly iblBinding: import('./mesh-material-bindings').IblBindingInspection | undefined;
468
727
  /**
469
728
  * feat-20260531-bloom-first-declarative-render-graph-pass M4 fix-up w19:
470
729
  * per-frame render-graph pass names in declaration order. Empty array
@@ -478,6 +737,8 @@ export interface RenderSystem {
478
737
  readonly lastSuccessfulCameraAntialias: CameraSnapshot['antialias'] | undefined;
479
738
  /** Detached compiled-graph facts used by the renderer inspection owner. */
480
739
  readonly perFrameGraphInfo: CompiledRenderGraphInfo | undefined;
740
+ /** Production-owner recovery facts used by Renderer.inspect(). */
741
+ readonly recoveryEvidence: RecoveryProductionEvidence;
481
742
  /**
482
743
  * feat-20260531-per-frame-bind-group-cache M1 / w4: per-frame
483
744
  * createBindGroup counter. Reset to 0 on every `draw([world], { cameraOwner: 0, resourceOwner: 0 })` entry,
@@ -505,6 +766,8 @@ export interface RenderSystem {
505
766
  configureStandard(config: RenderPipelineAsset['config']): void;
506
767
  /** Register one engine-owned post-process shader used by the Standard lane. */
507
768
  registerBuiltinPostProcess(id: string, entry: PostProcessShaderEntry): () => void;
769
+ /** Resolve one active built-in post-process declaration for a detached candidate. */
770
+ readonly lookupPostProcess: (id: string) => PostProcessShaderEntry | undefined;
508
771
  /**
509
772
  * feat-20260612-rhi-destroy-renderer-dispose-gpu-lifecycle / M5 / w21:
510
773
  * release the per-RenderSystem frame-state GPU bookkeeping during
@@ -528,9 +791,19 @@ export interface RenderSystem {
528
791
  * handles and are rebuilt after the replacement device is live.
529
792
  * Idempotent and graph-optional (no-op when nothing has compiled yet).
530
793
  */
531
- resetForRecover(): void;
794
+ resetForRecover(retiringPipelineState?: PipelineState, replacementDevice?: RhiDevice): void;
795
+ /** Build post-process parameter buffers without mutating the active bundle. */
796
+ prepareRecoveryPostProcessResources(
797
+ device: RhiDevice,
798
+ ): Result<RecoveryPostProcessResources, RhiError>;
799
+ /** Publish a previously prepared post-process bundle at the recovery boundary. */
800
+ publishRecoveryPostProcessResources(candidate: RecoveryPostProcessResources): void;
801
+ /** Release a candidate bundle that was never published. */
802
+ discardRecoveryPostProcessResources(candidate: RecoveryPostProcessResources): void;
532
803
  /** Recreate post-process GPU parameter resources after a device rebuild. */
533
804
  restorePostProcessResources(): void;
805
+ /** Build candidate roots from explicit candidate-owned device state. */
806
+ prepareRecoveryRoots(runtime: RecoveryRootRuntime): RecoveryRootBundle;
534
807
  }
535
808
 
536
809
  function isStructuredRendererError(error: unknown): error is RhiError | RenderError {
@@ -586,11 +859,12 @@ function makePreparedPipelinePendingError(): RhiError {
586
859
  });
587
860
  }
588
861
 
589
- type PointsLinesGpuResources = {
590
- readonly vertexBuffer: Buffer;
591
- readonly indexBuffer: Buffer;
592
- };
593
- type PointsLinesPreparationAdapter = PointsLinesLanePreparationAdapter<PointsLinesGpuResources>;
862
+ /** The single Points/Lines owner’s staged device-generation handoff. */
863
+ export interface RecoveryPointsLinesCandidate {
864
+ createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown>;
865
+ publish(): void;
866
+ release(): void;
867
+ }
594
868
 
595
869
  function preparationFailureFrom(error: unknown): MeshMaterialBindingPreparationFailure | undefined {
596
870
  if (typeof error !== 'object' || error === null) return undefined;
@@ -619,30 +893,56 @@ function preparationFailureFrom(error: unknown): MeshMaterialBindingPreparationF
619
893
  };
620
894
  }
621
895
 
622
- function materialTextureHandles(
623
- material: MaterialSnapshot,
624
- ): readonly Handle<'TextureAsset', 'shared'>[] {
625
- const handles = new Map<number, Handle<'TextureAsset', 'shared'>>();
626
- const add = (handle: Handle<'TextureAsset', 'shared'> | undefined): void => {
627
- if (handle !== undefined) handles.set(Number(handle), handle);
628
- };
629
- for (const handle of material.textureHandles?.values() ?? []) add(handle);
630
- add(material.baseColorTexture);
631
- add(material.metallicRoughnessTexture);
632
- add(material.normalTexture);
633
- add(material.emissiveTexture);
634
- add(material.occlusionTexture);
635
- return [...handles.values()];
896
+ /** Candidate-only device/runtime inputs used by the recovery prepare seam. */
897
+ export interface RecoveryGraphCandidateRuntime {
898
+ readonly internals: RenderSystemInternals;
899
+ readonly pipelineState: PipelineState;
636
900
  }
637
901
 
638
- function materialSamplerHandles(
639
- material: MaterialSnapshot,
640
- ): readonly Handle<'SamplerAsset', 'shared'>[] {
641
- const handles = new Map<number, Handle<'SamplerAsset', 'shared'>>();
642
- for (const handle of material.samplerHandles?.values() ?? []) {
643
- handles.set(Number(handle), handle);
644
- }
645
- return [...handles.values()];
902
+ /** Detached graph state prepared on a replacement device, before publication. */
903
+ export interface RecoveryGraphCandidate {
904
+ readonly frameState: RenderFrameState;
905
+ readonly setupWorks: readonly MipmapEncoderWork[];
906
+ readonly device: RhiDevice;
907
+ readonly generation: number;
908
+ readonly gpuDrivenProduction?: GpuDrivenProduction;
909
+ readonly gpuDrivenScene?: PersistentGpuDrivenCandidate;
910
+ /** Staged CPU-expanded Points/Lines buffers owned by the same RenderSystem. */
911
+ readonly pointsLines?: RecoveryPointsLinesCandidate;
912
+ readonly featureHost?: RenderFeatureHost;
913
+ readonly featureGpuWork?: RenderFeatureGpuWorkOwner;
914
+ readonly featureGraphCandidate?: RenderFeatureGraphCandidate;
915
+ readonly postProcessPipelines?: ReadonlyMap<string, RenderPipeline>;
916
+ readonly recoveryReadiness: RecoveryPipelineReadiness;
917
+ /** Release all candidate roots before publication; safe to call repeatedly. */
918
+ readonly release: () => void;
919
+ /** Transfer candidate-root ownership to the published RenderSystem. */
920
+ readonly markPublished: () => void;
921
+ }
922
+
923
+ /** Candidate setup completion; completion is bounded by the recovery owner. */
924
+ export interface RecoveryGraphSetupSubmission {
925
+ readonly completion?: PromiseLike<unknown>;
926
+ }
927
+
928
+ export type RecoveryGraphCandidatePreparation =
929
+ | { readonly kind: 'no-seed' }
930
+ | { readonly kind: 'ready'; readonly candidate: RecoveryGraphCandidate }
931
+ | { readonly kind: 'failed'; readonly reason: string; readonly cause?: unknown };
932
+
933
+ /** Explicit candidate-owned inputs for lifecycle-root preparation. */
934
+ export interface RecoveryRootRuntime {
935
+ readonly scope: DeviceScope;
936
+ readonly device: RhiDevice;
937
+ readonly gpuStore: GpuResidencyCache;
938
+ readonly graphCandidate?: RecoveryGraphCandidate | undefined;
939
+ }
940
+
941
+ /** Candidate roots plus the one synchronous owner switch at publication. */
942
+ export interface RecoveryRootBundle {
943
+ readonly roots: readonly LifecycleResourceSpec<unknown>[];
944
+ publish(): void;
945
+ discard(): void;
646
946
  }
647
947
 
648
948
  /**
@@ -761,812 +1061,6 @@ function observeMaterialResidency(
761
1061
  };
762
1062
  }
763
1063
 
764
- /**
765
- * The Standard renderer's single Points/Lines prepare owner.
766
- *
767
- * The retained snapshot is the identity boundary; this owner only resolves
768
- * the source assets, runs admission, and publishes the existing preparation
769
- * and record contracts to the main geometry loop. The expansion cache is
770
- * shared across lanes and this owner keeps the one prepared vertex/index
771
- * resource pair for each retained identity.
772
- */
773
- class StandardPointsLinesOwner implements PointsLinesRecordOwner {
774
- private readonly cache = new PointsLinesExpansionCache();
775
- private readonly preparations = new Map<string, PointsLinesPreparationAdapter>();
776
- private readonly active = new Map<string, PointsLinesInspection>();
777
- private readonly layoutProjection = deriveVertexLayoutProjection({
778
- position: new Float32Array(0),
779
- normal: new Float32Array(0),
780
- uv: new Float32Array(0),
781
- tangent: new Float32Array(0),
782
- });
783
-
784
- constructor(private readonly internals: RenderSystemInternals) {}
785
-
786
- beginFrame(): void {
787
- this.active.clear();
788
- }
789
-
790
- prepare(
791
- entry: import('./record/frame-snapshot').ValidatedRenderable,
792
- clustered: boolean,
793
- ): PointsLinesRecordSubmission | undefined {
794
- const snapshot = entry.source.pointsLines;
795
- if (snapshot === undefined || snapshot.component === undefined || entry.world === undefined) {
796
- return undefined;
797
- }
798
- const key = `${snapshot.worldId}:${snapshot.entityKey}`;
799
- const topology = snapshot.component === 'Points' ? 'point-list' : 'line-list';
800
- const backend = this.backend();
801
- const lane = backend === 'wgpu-webgl2' ? 'cpu-webgl2' : clustered ? 'clustered' : 'direct';
802
- const meshResult = resolveAssetHandle<MeshAsset>(
803
- entry.world,
804
- toShared<'MeshAsset'>(entry.source.assetHandle),
805
- );
806
- const materialResult = resolveAssetHandle<MaterialAsset>(
807
- entry.world,
808
- toShared<'MaterialAsset'>(snapshot.materialHandle),
809
- );
810
- if (!meshResult.ok || !materialResult.ok) {
811
- this.publishRefusal(
812
- snapshot,
813
- new PointsLinesMaterialUnsupportedError({
814
- entity: snapshot.entityKey,
815
- material: 'unresolved',
816
- pass: 'forward',
817
- module: 'asset-resolution',
818
- reason: 'source MeshAsset or MaterialAsset could not be resolved',
819
- }),
820
- );
821
- return undefined;
822
- }
823
- const admission = admitPointsLines({
824
- entity: snapshot.entityKey,
825
- mesh: meshResult.value,
826
- material: materialResult.value,
827
- ...(snapshot.style?.kind === 'points'
828
- ? {
829
- points: {
830
- sizePx: snapshot.style.sizePx,
831
- shape: snapshot.style.shape === 'circle' ? 1 : 0,
832
- },
833
- }
834
- : { lines: { widthPx: snapshot.style?.widthPx ?? 1 } }),
835
- });
836
- if (!admission.ok) {
837
- this.publishRefusal(snapshot, admission.error);
838
- return undefined;
839
- }
840
- const geometryKey = this.cache.getOrCreate(snapshot, meshResult.value).key;
841
- const prepKey = `${geometryKey}:${lane}:${backend}`;
842
- let preparation = this.preparations.get(prepKey);
843
- if (preparation === undefined) {
844
- preparation = createPointsLinesLanePreparationAdapter(lane, backend, {
845
- cache: this.cache,
846
- adapter: {
847
- create: (geometry) => {
848
- const vertex = this.internals.device.createBuffer({
849
- label: `points-lines-vertices:${geometryKey}`,
850
- size: Math.max(4, geometry.vertices.byteLength),
851
- usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
852
- mappedAtCreation: false,
853
- });
854
- if (!vertex.ok) return err(vertex.error);
855
- const index = this.internals.device.createBuffer({
856
- label: `points-lines-indices:${geometryKey}`,
857
- size: Math.max(4, geometry.indices.byteLength),
858
- usage: GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST,
859
- mappedAtCreation: false,
860
- });
861
- if (!index.ok) {
862
- this.internals.device.destroyBuffer(vertex.value);
863
- return err(index.error);
864
- }
865
- return ok({ vertexBuffer: vertex.value, indexBuffer: index.value });
866
- },
867
- upload: (resource, geometry) => {
868
- if (geometry.vertices.byteLength > 0) {
869
- const vertexWrite = this.internals.device.queue.writeBuffer(
870
- resource.vertexBuffer,
871
- 0,
872
- geometry.vertices,
873
- );
874
- if (!vertexWrite.ok) return err(vertexWrite.error);
875
- }
876
- if (geometry.indices.byteLength > 0) {
877
- const indexWrite = this.internals.device.queue.writeBuffer(
878
- resource.indexBuffer,
879
- 0,
880
- geometry.indices,
881
- );
882
- if (!indexWrite.ok) return err(indexWrite.error);
883
- }
884
- return ok(geometry.derivedBytes);
885
- },
886
- validate: (_resource, geometry) =>
887
- geometry.expandedVertexCount > 0 && geometry.expandedIndexCount > 0
888
- ? ok(undefined)
889
- : err(new Error('Points/Lines expansion contains no drawable triangles')),
890
- destroy: (resource) => {
891
- this.internals.device.destroyBuffer(resource.vertexBuffer);
892
- this.internals.device.destroyBuffer(resource.indexBuffer);
893
- },
894
- },
895
- });
896
- this.preparations.set(prepKey, preparation);
897
- }
898
- const prepared = preparation.prepare(snapshot, meshResult.value);
899
- if (!prepared.ok) {
900
- const lkg = preparation.lastKnownGood();
901
- if (lkg !== undefined) {
902
- const lkgPlan = createPointsLinesLaneAdapter(lane, backend).createRecordPlan(
903
- lkg.snapshot,
904
- lkg.geometry,
905
- );
906
- const state = preparation.inspect();
907
- this.active.set(
908
- key,
909
- inspectPointsLines({
910
- snapshot,
911
- topology,
912
- lane,
913
- pointCount: lkg.geometry.pointCount,
914
- segmentCount: lkg.geometry.segmentCount,
915
- sourceBytes: lkg.geometry.sourceBytes,
916
- derivedBytes: lkg.geometry.derivedBytes,
917
- cache: { hit: true, rebuilds: state.rebuilds, evictions: 0 },
918
- drawCount: lkgPlan.drawCount,
919
- uploadBytes: 0,
920
- lastKnownGood: true,
921
- refusal: {
922
- code: prepared.error.code,
923
- expected: prepared.error.expected,
924
- hint: prepared.error.hint,
925
- detail: prepared.error.detail,
926
- generation: prepared.error.detail.generation,
927
- lastKnownGood: true,
928
- },
929
- }),
930
- );
931
- return {
932
- plan: lkgPlan,
933
- vertexBuffer: lkg.resource.vertexBuffer,
934
- indexBuffer: lkg.resource.indexBuffer,
935
- layoutProjection: this.layoutProjection,
936
- };
937
- }
938
- this.publishRefusal(snapshot, prepared.error);
939
- return undefined;
940
- }
941
- const plan = createPointsLinesLaneAdapter(lane, backend).createRecordPlan(
942
- snapshot,
943
- prepared.value.geometry,
944
- );
945
- const state = preparation.inspect();
946
- this.active.set(
947
- key,
948
- inspectPointsLines({
949
- snapshot,
950
- topology,
951
- lane,
952
- pointCount: prepared.value.geometry.pointCount,
953
- segmentCount: prepared.value.geometry.segmentCount,
954
- sourceBytes: prepared.value.geometry.sourceBytes,
955
- derivedBytes: prepared.value.geometry.derivedBytes,
956
- cache: {
957
- hit: prepared.value.uploadedBytes === 0,
958
- rebuilds: state.rebuilds,
959
- evictions: 0,
960
- },
961
- drawCount: plan.drawCount,
962
- uploadBytes: prepared.value.uploadedBytes,
963
- lastKnownGood: prepared.value.lastKnownGood,
964
- }),
965
- );
966
- return {
967
- plan,
968
- vertexBuffer: prepared.value.resource.vertexBuffer,
969
- indexBuffer: prepared.value.resource.indexBuffer,
970
- layoutProjection: this.layoutProjection,
971
- };
972
- }
973
-
974
- resetForDeviceLoss(): void {
975
- for (const preparation of this.preparations.values()) preparation.resetForDeviceLoss();
976
- this.active.clear();
977
- }
978
-
979
- inspections(): readonly PointsLinesInspection[] {
980
- return [...this.active.values()];
981
- }
982
-
983
- private backend(): PointsLinesBackend {
984
- switch (this.internals.device.caps.backendKind) {
985
- case 'wgpu-webgl2':
986
- return 'wgpu-webgl2';
987
- case 'null':
988
- return 'null';
989
- default:
990
- return 'webgpu';
991
- }
992
- }
993
-
994
- private publishRefusal(
995
- snapshot: PointsLinesRetainedSnapshot,
996
- error: PointsLinesSourceError,
997
- ): void {
998
- this.active.set(
999
- `${snapshot.worldId}:${snapshot.entityKey}`,
1000
- inspectPointsLines({
1001
- snapshot,
1002
- topology: snapshot.component === 'Points' ? 'point-list' : 'line-list',
1003
- lane: 'refused',
1004
- pointCount: 0,
1005
- segmentCount: 0,
1006
- sourceBytes: 0,
1007
- derivedBytes: 0,
1008
- cache: { hit: false, rebuilds: 0, evictions: 0 },
1009
- drawCount: 0,
1010
- uploadBytes: 0,
1011
- lastKnownGood: false,
1012
- refusal: {
1013
- code: error.code,
1014
- expected: error.expected,
1015
- hint: error.hint,
1016
- detail: error.detail,
1017
- generation: snapshot.meshGeneration,
1018
- lastKnownGood: false,
1019
- },
1020
- }),
1021
- );
1022
- }
1023
- }
1024
-
1025
- type ProbeCubeSet = {
1026
- readonly texture: Texture;
1027
- readonly cubeView: TextureView;
1028
- readonly faceViews: readonly TextureView[];
1029
- readonly mipViews: readonly (readonly TextureView[])[];
1030
- readonly depthTexture: Texture;
1031
- readonly depthView: TextureView;
1032
- readonly size: number;
1033
- };
1034
-
1035
- type ProbeGpuResource = {
1036
- fact: ReflectionProbeFact;
1037
- index: number;
1038
- filter: ProbeFilterState;
1039
- rawFaceCursor: number;
1040
- readonly raw: ProbeCubeSet;
1041
- active: ProbeCubeSet | undefined;
1042
- candidate: ProbeCubeSet;
1043
- spare: ProbeCubeSet | undefined;
1044
- readonly sampler: Sampler;
1045
- readonly uniformBuffer: Buffer;
1046
- readonly filterGroup1: BindGroup;
1047
- pending?: {
1048
- readonly rawCaptureFace: number | undefined;
1049
- readonly step: ReturnType<typeof advanceProbeFilter>;
1050
- };
1051
- };
1052
-
1053
- type ProbeSharedResources = {
1054
- readonly faceUniforms: Buffer;
1055
- readonly prefilterUniforms: Buffer;
1056
- readonly vertexBuffer: Buffer;
1057
- readonly sampler: Sampler;
1058
- readonly filterPipeline: RenderPipeline;
1059
- readonly cubeGroup1Bgl: BindGroupLayout;
1060
- readonly prefilterGroup0Bgl: BindGroupLayout;
1061
- };
1062
-
1063
- /** Renderer-owned raw capture and incremental PMREM/LKG promotion owner. */
1064
- class ReflectionProbeRecordOwner {
1065
- private readonly resources = new Map<string, ProbeGpuResource>();
1066
- private shared: ProbeSharedResources | undefined;
1067
- private pipelineWarmup: Promise<void> | undefined;
1068
- private pipelineWarmupFailure: string | undefined;
1069
- private pipelineWarmupAttempts = 0;
1070
- private lastFactCount = 0;
1071
- private lastAcceptedCount = 0;
1072
- private lastScheduledRawFaces = 0;
1073
- private lastScheduledFilteredSteps = 0;
1074
- private lastSelection: ReflectionProbeSelectionInspection = { kind: 'skylight' };
1075
- private readonly outputFormat: TextureFormat;
1076
-
1077
- constructor(private readonly internals: RenderSystemInternals) {
1078
- this.outputFormat = internals.device.caps.rgba16floatRenderable ? 'rgba16float' : 'rgba8unorm';
1079
- }
1080
-
1081
- inspect(): ReflectionProbeInspection {
1082
- let rawFacesCaptured = 0;
1083
- let filteredStepsCompleted = 0;
1084
- const filteredMipLevels = new Set<number>();
1085
- let activeCount = 0;
1086
- for (const resource of this.resources.values()) {
1087
- rawFacesCaptured += resource.rawFaceCursor;
1088
- filteredStepsCompleted += resource.filter.cursor;
1089
- const completedMipCount = Math.min(
1090
- PREFILTER_MIP_LEVELS,
1091
- Math.floor(resource.filter.cursor / resource.filter.faceCount),
1092
- );
1093
- for (let mip = 0; mip < completedMipCount; mip += 1) filteredMipLevels.add(mip);
1094
- if (resource.active !== undefined) activeCount += 1;
1095
- }
1096
- return {
1097
- selection: this.lastSelection,
1098
- factCount: this.lastFactCount,
1099
- acceptedCount: this.lastAcceptedCount,
1100
- activeCount,
1101
- rawFacesCaptured,
1102
- filteredStepsCompleted,
1103
- filteredMipLevels: [...filteredMipLevels].sort((a, b) => a - b),
1104
- scheduledRawFaces: this.lastScheduledRawFaces,
1105
- scheduledFilteredSteps: this.lastScheduledFilteredSteps,
1106
- pipelineReady: this.shared !== undefined,
1107
- pipelineWarmupAttempts: this.pipelineWarmupAttempts,
1108
- ...(this.pipelineWarmupFailure === undefined
1109
- ? {}
1110
- : { pipelineWarmupFailure: this.pipelineWarmupFailure }),
1111
- };
1112
- }
1113
-
1114
- private ensurePipelineWarmup(): void {
1115
- if (this.shared !== undefined || this.pipelineWarmup !== undefined) return;
1116
- const factory = this.internals.shaderModuleFactory;
1117
- if (factory === undefined) return;
1118
- const attempt = this.pipelineWarmupAttempts + 1;
1119
- this.pipelineWarmupAttempts = attempt;
1120
- this.pipelineWarmup = createIblPipelines(
1121
- this.internals.deviceScope,
1122
- this.internals.device,
1123
- (device, descriptor) => {
1124
- const labeled = {
1125
- ...descriptor,
1126
- label: `${descriptor.label ?? 'ibl'}-reflection-probe-${attempt}`,
1127
- };
1128
- return this.internals.createShaderModule === undefined
1129
- ? Promise.resolve(factory.createShaderModule(labeled))
1130
- : this.internals.createShaderModule(device, labeled);
1131
- },
1132
- this.outputFormat,
1133
- ).then((result) => {
1134
- if (!result.ok) {
1135
- this.pipelineWarmupFailure = `${result.error.code}: ${result.error.hint}`;
1136
- console.warn('[forgeax] ReflectionProbe IBL warmup retry', this.pipelineWarmupFailure);
1137
- this.pipelineWarmup = undefined;
1138
- return;
1139
- }
1140
- this.shared = this.createSharedResources(result.value.prefilterPipeline);
1141
- if (this.shared === undefined) {
1142
- this.pipelineWarmupFailure = 'renderer-owned IBL resources could not be allocated';
1143
- console.warn('[forgeax] ReflectionProbe IBL resource allocation retry');
1144
- this.pipelineWarmup = undefined;
1145
- }
1146
- });
1147
- }
1148
-
1149
- prepare(
1150
- facts: readonly ReflectionProbeFact[],
1151
- selections: ReadonlyMap<string, ReflectionProbeSelectionResult>,
1152
- displayCamera: CameraSnapshot | undefined,
1153
- ): ReflectionProbeRecordState {
1154
- this.lastFactCount = facts.length;
1155
- this.lastScheduledRawFaces = 0;
1156
- this.lastScheduledFilteredSteps = 0;
1157
- const selectedProbe = [...selections.values()]
1158
- .filter(
1159
- (selection): selection is Extract<ReflectionProbeSelectionResult, { kind: 'probe' }> =>
1160
- selection.kind === 'probe',
1161
- )
1162
- .sort((a, b) => a.worldId - b.worldId || a.entityKey - b.entityKey)[0];
1163
- this.lastSelection =
1164
- selectedProbe === undefined
1165
- ? { kind: 'skylight' }
1166
- : { kind: 'probe', worldId: selectedProbe.worldId, entityKey: selectedProbe.entityKey };
1167
- if (facts.length > 0) this.ensurePipelineWarmup();
1168
- void this.pipelineWarmup;
1169
- const ordered = facts
1170
- .slice()
1171
- .sort((a, b) => a.worldId - b.worldId || a.entityKey - b.entityKey);
1172
- let acceptedCount = 0;
1173
- let acceptedBytes = 0;
1174
- const accepted = new Map<string, ProbeGpuResource>();
1175
- for (const fact of ordered) {
1176
- const admission = admitReflectionProbe(
1177
- fact,
1178
- { acceptedCount, acceptedBytes },
1179
- DEFAULT_REFLECTION_PROBE_LIMITS,
1180
- );
1181
- if (!admission.ok) continue;
1182
- const key = `${fact.worldId}:${fact.entityKey}`;
1183
- const resource = this.resourceFor(key, fact, acceptedCount);
1184
- if (resource === undefined) continue;
1185
- acceptedCount += 1;
1186
- acceptedBytes = admission.acceptedBytes;
1187
- accepted.set(key, resource);
1188
- }
1189
- for (const [key, resource] of this.resources) {
1190
- if (!accepted.has(key)) {
1191
- this.destroyProbeResource(resource);
1192
- this.resources.delete(key);
1193
- } else resource.index = [...accepted.keys()].indexOf(key);
1194
- }
1195
- const rows = [...accepted.values()]
1196
- .map((resource) => {
1197
- const active = resource.active;
1198
- if (active === undefined) return undefined;
1199
- return {
1200
- primitiveKey: `${resource.fact.worldId}:${resource.fact.entityKey}`,
1201
- index: resource.index,
1202
- worldId: resource.fact.worldId,
1203
- entityKey: resource.fact.entityKey,
1204
- center: resource.fact.center,
1205
- halfExtents: resource.fact.halfExtents,
1206
- intensity: resource.fact.intensity,
1207
- generation: resource.filter.activeGeneration,
1208
- filteredView: active.cubeView,
1209
- sampler: resource.sampler,
1210
- uniformBuffer: resource.uniformBuffer,
1211
- };
1212
- })
1213
- .filter((row): row is NonNullable<typeof row> => row !== undefined);
1214
- const table = buildReflectionProbeTable(rows);
1215
- const work: ReflectionProbeGraphWork[] = [];
1216
- for (const resource of accepted.values()) {
1217
- const rawCaptureFace =
1218
- resource.rawFaceCursor < resource.raw.faceViews.length ? resource.rawFaceCursor : undefined;
1219
- const step =
1220
- resource.rawFaceCursor >= resource.raw.faceViews.length
1221
- ? advanceProbeFilter(resource.filter)
1222
- : undefined;
1223
- if (this.shared === undefined) continue;
1224
- if (rawCaptureFace === undefined && step === undefined) continue;
1225
- const filterGroup0 =
1226
- step === undefined
1227
- ? undefined
1228
- : this.internals.device.createBindGroup({
1229
- label: `reflection-probe-${resource.index}-pmrem-${step.mipLevel}-${step.faceIndex}`,
1230
- layout: this.shared.prefilterGroup0Bgl,
1231
- entries: [
1232
- {
1233
- binding: 0,
1234
- resource: {
1235
- kind: 'buffer',
1236
- value: {
1237
- buffer: this.shared.faceUniforms,
1238
- offset: step.faceIndex * 256,
1239
- size: 64,
1240
- },
1241
- },
1242
- },
1243
- {
1244
- binding: 1,
1245
- resource: {
1246
- kind: 'buffer',
1247
- value: {
1248
- buffer: this.shared.prefilterUniforms,
1249
- offset: (step.mipLevel * 6 + step.faceIndex) * 256,
1250
- size: 16,
1251
- },
1252
- },
1253
- },
1254
- ],
1255
- });
1256
- if (filterGroup0 !== undefined && !filterGroup0.ok) continue;
1257
- if (resource.pending !== undefined) continue;
1258
- resource.pending = { rawCaptureFace, step };
1259
- if (rawCaptureFace !== undefined) this.lastScheduledRawFaces += 1;
1260
- if (step !== undefined) this.lastScheduledFilteredSteps += 1;
1261
- const face =
1262
- rawCaptureFace === undefined
1263
- ? undefined
1264
- : buildCubeCameraFaceViews({
1265
- position: resource.fact.center,
1266
- near: 0.1,
1267
- far: 100,
1268
- })[rawCaptureFace];
1269
- const faceCamera =
1270
- face === undefined || displayCamera === undefined
1271
- ? undefined
1272
- : {
1273
- ...displayCamera,
1274
- position: vec3.create(
1275
- resource.fact.center[0] ?? 0,
1276
- resource.fact.center[1] ?? 0,
1277
- resource.fact.center[2] ?? 0,
1278
- ),
1279
- world: mat4.invert(mat4.create(), face.view),
1280
- fov: Math.PI / 2,
1281
- aspect: 1,
1282
- near: face.near,
1283
- far: face.far,
1284
- projection: 'perspective' as const,
1285
- };
1286
- work.push({
1287
- probeIndex: resource.index,
1288
- rawTexture: resource.raw.texture,
1289
- rawCubeView: resource.raw.cubeView,
1290
- rawFaceViews: resource.raw.faceViews,
1291
- rawDepthTexture: resource.raw.depthTexture,
1292
- filteredTexture: resource.candidate.texture,
1293
- filteredCubeView: resource.candidate.cubeView,
1294
- filteredFaceViewsByMip: resource.candidate.mipViews,
1295
- outputFormat: this.outputFormat,
1296
- sampler: resource.sampler,
1297
- filterPipeline: this.shared.filterPipeline,
1298
- filterGroup0: filterGroup0?.value,
1299
- filterGroup1: resource.filterGroup1,
1300
- cubeVertexBuffer: this.shared.vertexBuffer,
1301
- rawCaptureFace,
1302
- rawDepthView: resource.raw.depthView,
1303
- rawSize: resource.raw.size,
1304
- ...(faceCamera === undefined ? {} : { faceCamera }),
1305
- ...(rawCaptureFace === undefined
1306
- ? {}
1307
- : {
1308
- viewBindGroupDynamicOffset:
1309
- (REFLECTION_PROBE_VIEW_SLOT_BASE + resource.index) * VIEW_UNIFORM_SLOT_STRIDE,
1310
- }),
1311
- filteredSize: resource.candidate.size,
1312
- step,
1313
- });
1314
- }
1315
- return {
1316
- table,
1317
- selections,
1318
- graph: { work },
1319
- completeSubmission: (submitted, completed) => this.completeSubmission(submitted, completed),
1320
- };
1321
- }
1322
-
1323
- private completeSubmission(submitted: boolean, completed?: Promise<unknown>): void {
1324
- if (completed !== undefined) {
1325
- void completed.then(
1326
- () => this.completeSubmission(submitted),
1327
- () => this.completeSubmission(false),
1328
- );
1329
- return;
1330
- }
1331
- for (const resource of this.resources.values()) {
1332
- const pending = resource.pending;
1333
- delete resource.pending;
1334
- if (!submitted || pending === undefined) continue;
1335
- if (pending.rawCaptureFace !== undefined) {
1336
- resource.rawFaceCursor = Math.min(
1337
- resource.rawFaceCursor + 1,
1338
- resource.raw.faceViews.length,
1339
- );
1340
- continue;
1341
- }
1342
- if (pending.step === undefined) continue;
1343
- resource.filter = commitProbeFilterStep(resource.filter, true);
1344
- if (resource.filter.cursor >= resource.filter.faceCount * resource.filter.mipCount) {
1345
- const previous = resource.active;
1346
- resource.active = resource.candidate;
1347
- resource.candidate = resource.spare ?? previous ?? resource.candidate;
1348
- resource.spare = undefined;
1349
- }
1350
- }
1351
- }
1352
-
1353
- dispose(): void {
1354
- for (const resource of this.resources.values()) {
1355
- this.destroyProbeResource(resource);
1356
- }
1357
- this.resources.clear();
1358
- if (this.shared !== undefined) {
1359
- this.internals.device.destroyBuffer(this.shared.faceUniforms);
1360
- this.internals.device.destroyBuffer(this.shared.prefilterUniforms);
1361
- this.internals.device.destroyBuffer(this.shared.vertexBuffer);
1362
- }
1363
- this.shared = undefined;
1364
- this.pipelineWarmup = undefined;
1365
- }
1366
-
1367
- private destroyProbeResource(resource: ProbeGpuResource): void {
1368
- const sets = new Set<ProbeCubeSet>([resource.raw, resource.candidate]);
1369
- if (resource.active !== undefined) sets.add(resource.active);
1370
- if (resource.spare !== undefined) sets.add(resource.spare);
1371
- for (const set of sets) this.destroyCubeSet(set);
1372
- this.internals.device.destroyBuffer(resource.uniformBuffer);
1373
- }
1374
-
1375
- private destroyCubeSet(set: ProbeCubeSet): void {
1376
- this.internals.device.destroyTexture(set.texture);
1377
- this.internals.device.destroyTexture(set.depthTexture);
1378
- }
1379
-
1380
- private resourceFor(
1381
- key: string,
1382
- fact: ReflectionProbeFact,
1383
- index: number,
1384
- ): ProbeGpuResource | undefined {
1385
- const existing = this.resources.get(key);
1386
- if (existing !== undefined) {
1387
- if (existing.fact.revision !== fact.revision) {
1388
- existing.fact = fact;
1389
- existing.filter = createProbeFilterState({
1390
- probeIndex: index,
1391
- mipCount: PREFILTER_MIP_LEVELS,
1392
- activeGeneration: existing.filter.activeGeneration,
1393
- });
1394
- existing.rawFaceCursor = 0;
1395
- } else {
1396
- existing.fact = fact;
1397
- existing.index = index;
1398
- }
1399
- return existing;
1400
- }
1401
- if (this.shared === undefined) return undefined;
1402
- const raw = this.createCubeSet(`reflection-probe-${index}-raw`, fact.resolution);
1403
- const candidate = this.createCubeSet(`reflection-probe-${index}-candidate`, fact.resolution);
1404
- const spare = this.createCubeSet(`reflection-probe-${index}-spare`, fact.resolution);
1405
- if (raw === undefined || candidate === undefined || spare === undefined) return undefined;
1406
- const uniformBuffer = this.internals.device.createBuffer({
1407
- label: `reflection-probe-${index}-uniforms`,
1408
- size: 32,
1409
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
1410
- mappedAtCreation: false,
1411
- });
1412
- if (!uniformBuffer.ok) return undefined;
1413
- const payload = new Float32Array([
1414
- -Math.max(0, fact.intensity),
1415
- fact.center[0],
1416
- fact.center[1],
1417
- fact.center[2],
1418
- fact.halfExtents[0],
1419
- fact.halfExtents[1],
1420
- fact.halfExtents[2],
1421
- 0,
1422
- ]);
1423
- const written = this.internals.device.queue.writeBuffer(uniformBuffer.value, 0, payload);
1424
- if (!written.ok) return undefined;
1425
- const filterGroup1 = this.internals.device.createBindGroup({
1426
- label: `reflection-probe-${index}-filter-input`,
1427
- layout: this.shared.cubeGroup1Bgl,
1428
- entries: [
1429
- { binding: 0, resource: { kind: 'textureView', value: raw.cubeView } },
1430
- { binding: 1, resource: { kind: 'sampler', value: this.shared.sampler } },
1431
- ],
1432
- });
1433
- if (!filterGroup1.ok) return undefined;
1434
- const resource: ProbeGpuResource = {
1435
- fact,
1436
- index,
1437
- filter: createProbeFilterState({ probeIndex: index, mipCount: PREFILTER_MIP_LEVELS }),
1438
- rawFaceCursor: 0,
1439
- raw,
1440
- active: undefined,
1441
- candidate,
1442
- spare,
1443
- sampler: this.shared.sampler,
1444
- uniformBuffer: uniformBuffer.value,
1445
- filterGroup1: filterGroup1.value,
1446
- };
1447
- this.resources.set(key, resource);
1448
- return resource;
1449
- }
1450
-
1451
- private createSharedResources(filterPipeline: RenderPipeline): ProbeSharedResources | undefined {
1452
- const faceUniforms = createFaceUniformsBuffer(this.internals.device);
1453
- const prefilterUniforms = createPrefilterUniformsBuffer(this.internals.device);
1454
- if (!faceUniforms.ok || !prefilterUniforms.ok) return undefined;
1455
- if (!writeAllFaceUniforms(this.internals.device, faceUniforms.value).ok) return undefined;
1456
- if (!writeAllPrefilterUniforms(this.internals.device, prefilterUniforms.value).ok)
1457
- return undefined;
1458
- const vertexBuffer = this.internals.device.createBuffer({
1459
- label: 'reflection-probe-capture-cube-vertices',
1460
- size: CUBEMAP_FACE_VERTICES.byteLength,
1461
- usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
1462
- mappedAtCreation: false,
1463
- });
1464
- if (!vertexBuffer.ok) return undefined;
1465
- const vertexWrite = this.internals.device.queue.writeBuffer(
1466
- vertexBuffer.value,
1467
- 0,
1468
- CUBEMAP_FACE_VERTICES,
1469
- );
1470
- if (!vertexWrite.ok) return undefined;
1471
- const sampler = this.internals.device.createSampler({
1472
- label: 'reflection-probe-pmrem-sampler',
1473
- magFilter: 'linear',
1474
- minFilter: 'linear',
1475
- mipmapFilter: 'linear',
1476
- addressModeU: 'clamp-to-edge',
1477
- addressModeV: 'clamp-to-edge',
1478
- addressModeW: 'clamp-to-edge',
1479
- });
1480
- const cache = getOrCreateIblCache(this.internals.deviceScope);
1481
- if (
1482
- !sampler.ok ||
1483
- cache.cubeGroup1Bgl === undefined ||
1484
- cache.prefilterGroup0Bgl === undefined
1485
- ) {
1486
- return undefined;
1487
- }
1488
- return {
1489
- faceUniforms: faceUniforms.value,
1490
- prefilterUniforms: prefilterUniforms.value,
1491
- vertexBuffer: vertexBuffer.value,
1492
- sampler: sampler.value,
1493
- filterPipeline,
1494
- cubeGroup1Bgl: cache.cubeGroup1Bgl,
1495
- prefilterGroup0Bgl: cache.prefilterGroup0Bgl,
1496
- };
1497
- }
1498
-
1499
- private createCubeSet(label: string, requestedSize: number): ProbeCubeSet | undefined {
1500
- const size = Math.min(Math.max(1, Math.floor(requestedSize)), PREFILTER_SIZE);
1501
- const mipCount = Math.min(PREFILTER_MIP_LEVELS, Math.floor(Math.log2(size)) + 1);
1502
- const texture = this.internals.device.createTexture({
1503
- label,
1504
- size: { width: size, height: size, depthOrArrayLayers: 6 },
1505
- mipLevelCount: mipCount,
1506
- sampleCount: 1,
1507
- dimension: '2d',
1508
- format: this.outputFormat,
1509
- usage: GPU_TEXTURE_USAGE_RENDER_ATTACHMENT_AND_TEXTURE_BINDING | GPU_TEXTURE_USAGE_COPY_SRC,
1510
- viewFormats: [],
1511
- textureBindingViewDimension: undefined,
1512
- });
1513
- if (!texture.ok) return undefined;
1514
- const cubeView = this.internals.device.createTextureView(texture.value, {
1515
- label: `${label}-cube`,
1516
- dimension: 'cube',
1517
- baseMipLevel: 0,
1518
- mipLevelCount: mipCount,
1519
- arrayLayerCount: 6,
1520
- });
1521
- if (!cubeView.ok) return undefined;
1522
- const faceViews: TextureView[] = [];
1523
- const mipViews: TextureView[][] = [];
1524
- for (let mip = 0; mip < mipCount; mip += 1) {
1525
- const faces: TextureView[] = [];
1526
- for (let face = 0; face < 6; face += 1) {
1527
- const view = this.internals.device.createTextureView(texture.value, {
1528
- label: `${label}-mip${mip}-face${face}`,
1529
- dimension: '2d',
1530
- baseMipLevel: mip,
1531
- mipLevelCount: 1,
1532
- baseArrayLayer: face,
1533
- arrayLayerCount: 1,
1534
- });
1535
- if (!view.ok) return undefined;
1536
- if (mip === 0) faceViews.push(view.value);
1537
- faces.push(view.value);
1538
- }
1539
- mipViews.push(faces);
1540
- }
1541
- const depthTexture = this.internals.device.createTexture({
1542
- label: `${label}-depth`,
1543
- size: { width: size, height: size, depthOrArrayLayers: 1 },
1544
- mipLevelCount: 1,
1545
- sampleCount: 1,
1546
- dimension: '2d',
1547
- format: 'depth24plus-stencil8',
1548
- usage: 0x10,
1549
- viewFormats: [],
1550
- textureBindingViewDimension: undefined,
1551
- });
1552
- if (!depthTexture.ok) return undefined;
1553
- const depthView = this.internals.device.createTextureView(depthTexture.value, {
1554
- label: `${label}-depth-view`,
1555
- dimension: '2d',
1556
- });
1557
- if (!depthView.ok) return undefined;
1558
- return {
1559
- texture: texture.value,
1560
- cubeView: cubeView.value,
1561
- faceViews,
1562
- mipViews,
1563
- depthTexture: depthTexture.value,
1564
- depthView: depthView.value,
1565
- size,
1566
- };
1567
- }
1568
- }
1569
-
1570
1064
  export function createRenderSystem(internals: RenderSystemInternals): RenderSystem {
1571
1065
  const phaseCatalogRegistration = internals.profiler?.registerPhaseCatalog(
1572
1066
  'render',
@@ -1578,6 +1072,38 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1578
1072
  let latestCamera: CameraSnapshot | undefined;
1579
1073
  const pointsLinesOwner = new StandardPointsLinesOwner(internals);
1580
1074
  const reflectionProbeOwner = new ReflectionProbeRecordOwner(internals);
1075
+ let ssrFormatReceipt: import('@forgeax/engine-rhi').RhiTextureFormatCapabilityReceipt | undefined;
1076
+ let ssrFormatProbeGeneration = -1;
1077
+ let ssrFormatProbe: Promise<void> | undefined;
1078
+ let ssrFallbackGeneration: number | undefined;
1079
+ const ensureSsrFormatProbe = (): void => {
1080
+ if (internals.ssrIdentity === undefined) return;
1081
+ const generation = internals.deviceScope.generation;
1082
+ if (
1083
+ ssrFormatReceipt?.deviceGeneration === generation ||
1084
+ ssrFormatProbeGeneration === generation ||
1085
+ ssrFormatProbe !== undefined ||
1086
+ typeof internals.device.probeTextureFormatCapability !== 'function'
1087
+ ) {
1088
+ return;
1089
+ }
1090
+ ssrFormatProbeGeneration = generation;
1091
+ const device = internals.device;
1092
+ ssrFormatProbe = device
1093
+ .probeTextureFormatCapability()
1094
+ .then((result) => {
1095
+ if (internals.device !== device || internals.deviceScope.generation !== generation) return;
1096
+ ssrFormatReceipt = result.ok ? result.value : undefined;
1097
+ })
1098
+ .catch(() => {
1099
+ if (internals.device === device && internals.deviceScope.generation === generation) {
1100
+ ssrFormatReceipt = undefined;
1101
+ }
1102
+ })
1103
+ .finally(() => {
1104
+ ssrFormatProbe = undefined;
1105
+ });
1106
+ };
1581
1107
  const cubeCaptureScheduler = createCubeCaptureScheduler({ maxFacesPerFrame: 1 });
1582
1108
  const cubeCaptureState: CubeCaptureGraphState = { work: [] };
1583
1109
  const persistentRenderScene = new PersistentRenderScene({
@@ -1587,7 +1113,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1587
1113
  internals.gpuStore.invalidateMesh(handle, preparedWorlds[worldId] ?? worldId);
1588
1114
  },
1589
1115
  });
1590
- const environmentLifecycle = new EnvironmentLifecycle(internals.deviceScope);
1116
+ let environmentLifecycle = new EnvironmentLifecycle(internals.deviceScope);
1591
1117
  const transmissionAdmission = new TransmissionCandidateAdmission();
1592
1118
  let lastTransmissionInspection: TransmissionInspection | undefined;
1593
1119
  let lastTransmissionKey = '';
@@ -1712,8 +1238,13 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1712
1238
  });
1713
1239
  lastTransmissionKey = key;
1714
1240
  };
1241
+ // GPU-driven production uses the validated adapter for normal frames. The
1242
+ // assembly-supplied closure resolves its device-bound adapter at call time,
1243
+ // so recovery candidate preparation still follows candidateShaderState
1244
+ // rather than retaining a lost-generation module.
1715
1245
  const gpuDrivenShaderFactory =
1716
1246
  internals.shaderModuleFactory ??
1247
+ internals.immediateShaderModuleFactory ??
1717
1248
  ({
1718
1249
  createShaderModule: () =>
1719
1250
  err(
@@ -1724,35 +1255,54 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1724
1255
  }),
1725
1256
  ),
1726
1257
  } satisfies import('./pipeline-builder').PipelineBuilderShaderModuleFactory);
1727
- let gpuDrivenProduction = new GpuDrivenProduction(internals.device, gpuDrivenShaderFactory);
1728
- const featureGpuWork: RenderFeatureGpuWorkOwner = createRenderFeatureGpuWorkOwner({
1729
- getDevice: () => internals.device,
1730
- getShaderModuleFactory: () =>
1731
- internals.shaderModuleFactory ??
1732
- ({
1733
- createShaderModule: () =>
1734
- err(
1735
- new RhiError({
1736
- code: 'rhi-not-available',
1737
- expected: 'the renderer backend exposes shader module creation',
1738
- hint: 'construct the renderer through the backend pack before activating GPU features',
1739
- }),
1740
- ),
1741
- } satisfies import('./pipeline-builder').PipelineBuilderShaderModuleFactory),
1742
- getImmediateShaderModuleFactory: () =>
1743
- internals.immediateShaderModuleFactory ??
1744
- internals.shaderModuleFactory ??
1745
- ({
1746
- createShaderModule: () =>
1747
- err(
1748
- new RhiError({
1749
- code: 'rhi-not-available',
1750
- expected: 'the renderer backend exposes shader module creation',
1751
- hint: 'construct the renderer through the backend pack before activating GPU features',
1752
- }),
1753
- ),
1754
- } satisfies import('./pipeline-builder').PipelineBuilderShaderModuleFactory),
1755
- });
1258
+ let gpuDrivenProduction = GpuDrivenProduction.forDevice(internals.device, gpuDrivenShaderFactory);
1259
+ let occlusionRuntime: OcclusionRenderRuntime | undefined = new OcclusionRenderRuntime(
1260
+ internals.device,
1261
+ persistentRenderScene.visibilityFacetStore(),
1262
+ gpuDrivenShaderFactory,
1263
+ persistentRenderScene.visibilityBudgetValue(),
1264
+ );
1265
+ const ensureOcclusionRuntime = (): OcclusionRenderRuntime => {
1266
+ if (occlusionRuntime === undefined) {
1267
+ occlusionRuntime = new OcclusionRenderRuntime(
1268
+ internals.device,
1269
+ persistentRenderScene.visibilityFacetStore(),
1270
+ gpuDrivenShaderFactory,
1271
+ persistentRenderScene.visibilityBudgetValue(),
1272
+ );
1273
+ }
1274
+ return occlusionRuntime;
1275
+ };
1276
+ const createFeatureGpuWorkOwner = (runtime: RenderSystemInternals): RenderFeatureGpuWorkOwner =>
1277
+ createRenderFeatureGpuWorkOwner({
1278
+ getDevice: () => runtime.device,
1279
+ getShaderModuleFactory: () =>
1280
+ runtime.shaderModuleFactory ??
1281
+ ({
1282
+ createShaderModule: () =>
1283
+ err(
1284
+ new RhiError({
1285
+ code: 'rhi-not-available',
1286
+ expected: 'the renderer backend exposes shader module creation',
1287
+ hint: 'construct the renderer through the backend pack before activating GPU features',
1288
+ }),
1289
+ ),
1290
+ } satisfies import('./pipeline-builder').PipelineBuilderShaderModuleFactory),
1291
+ getImmediateShaderModuleFactory: () =>
1292
+ runtime.immediateShaderModuleFactory ??
1293
+ runtime.shaderModuleFactory ??
1294
+ ({
1295
+ createShaderModule: () =>
1296
+ err(
1297
+ new RhiError({
1298
+ code: 'rhi-not-available',
1299
+ expected: 'the renderer backend exposes shader module creation',
1300
+ hint: 'construct the renderer through the backend pack before activating GPU features',
1301
+ }),
1302
+ ),
1303
+ } satisfies import('./pipeline-builder').PipelineBuilderShaderModuleFactory),
1304
+ });
1305
+ let featureGpuWork = createFeatureGpuWorkOwner(internals);
1756
1306
  const disposeFeatureGpuWork = (): void => {
1757
1307
  const disposed = featureGpuWork.dispose();
1758
1308
  if (!disposed.ok) internals.errorRegistry.fire(disposed.error);
@@ -1772,11 +1322,13 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1772
1322
  // raw (material-handle-based, cross-world collision semantically correct).
1773
1323
  const frameState: RenderFrameState = {
1774
1324
  frameNumber: 0,
1325
+ reflectionFallbackDemand: false,
1775
1326
  graphGeneration: 0,
1776
1327
  successfulTemporalFrameIndex: 0,
1777
1328
  lastSuccessfulBloom: 'off',
1778
1329
  temporalFrameTransaction: createTemporalFrameTransaction({ deviceEpoch: 0 }),
1779
1330
  temporalFrameInput: undefined,
1331
+ reflectionFallbackReadback: undefined,
1780
1332
  directionalShadowCache: null,
1781
1333
  directionalShadowCacheRecorded: false,
1782
1334
  compiledFrameGraph: null,
@@ -1798,6 +1350,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1798
1350
  volumetricFogHistorySignature: null,
1799
1351
  retiredCompiledFrameGraphs: new Set(),
1800
1352
  currentFrameObservationSource: undefined,
1353
+ reflectionFallbackObservationSource: undefined,
1801
1354
  lastSuccessfulCameraAntialias: undefined,
1802
1355
  temporalGpuState: undefined,
1803
1356
  activeTemporalGpuState: undefined,
@@ -1809,6 +1362,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1809
1362
  currentDirectionalShadowView: null,
1810
1363
  currentSpotShadowView: null,
1811
1364
  instanceBuffers: new Map(),
1365
+ probeBlendBuffers: new Map(),
1366
+ probeBlendRecordBufferCapacity: 0,
1812
1367
  morphBuffers: new Map(),
1813
1368
  hdrpClusterBinScratch: createClusterBinScratch(),
1814
1369
  hdrpClusterGridScratch: null,
@@ -1820,8 +1375,6 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1820
1375
  transientInstanceBuffers: [],
1821
1376
  warnedZeroLightStandard: false,
1822
1377
  warnedMultiLightDirectional: false,
1823
- warnedMultiLightPoint: false,
1824
- warnedMultiLightSpot: false,
1825
1378
  // feat-20260630-equirect-kind-internalized-ibl-declarative-skyligh M3 / w19:
1826
1379
  // once-warn latches for >1 Skylight / >1 SkyboxBackground (names winner).
1827
1380
  warnedMultiSkylight: false,
@@ -1891,6 +1444,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1891
1444
  spotShadowSnapshots: [],
1892
1445
  };
1893
1446
  let directFrameId = 0;
1447
+ let submittedFrameCount = 0;
1448
+ let lastSubmittedGeneration: number | undefined;
1894
1449
  let lastDirectionalShadowLights: ExtractedLights | undefined;
1895
1450
  let lastDirectionalShadowCandidate: 'accepted' | 'failed' = 'failed';
1896
1451
  let lastDirectionalShadowError: DirectionalShadowInspectionSource['error'];
@@ -1949,7 +1504,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1949
1504
  // D-3 / D-8: per-shader params UBO resource table (id -> GPU Buffer).
1950
1505
  // Eager-created at register time when entry.params is present (byteSize >= 16,
1951
1506
  // defaultValue.length === byteSize); reused frame-to-frame via queue.writeBuffer.
1952
- const postProcessParamsBuffers = new Map<string, Buffer>();
1507
+ let postProcessParamsBuffers = new Map<string, Buffer>();
1508
+ let postProcessParamsDevice = internals.device;
1953
1509
  const builtinPostProcessEntries = new Map<string, PostProcessShaderEntry>();
1954
1510
  let activeFeaturePostProcessEntries: ReadonlyMap<string, PostProcessShaderEntry> = new Map();
1955
1511
  const lookupPostProcess = (id: string): PostProcessShaderEntry | undefined =>
@@ -1965,6 +1521,76 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1965
1521
  // per-frame snapshot + bind it at group(1) binding(2).
1966
1522
  const getPostProcessParamsBuffer = (id: string): Buffer | undefined =>
1967
1523
  postProcessParamsBuffers.get(id);
1524
+ const postProcessDeclarations = (): readonly (readonly [string, PostProcessShaderEntry])[] => [
1525
+ ...builtinPostProcessEntries.entries(),
1526
+ ...activeFeaturePostProcessEntries.entries(),
1527
+ ];
1528
+ const disposePostProcessResources = (resources: RecoveryPostProcessResources): void => {
1529
+ for (const buffer of resources.paramsBuffers.values()) {
1530
+ const destroyed = resources.device.destroyBuffer(buffer);
1531
+ if (!destroyed.ok) internals.errorRegistry.fire(destroyed.error);
1532
+ }
1533
+ };
1534
+ const prepareRecoveryPostProcessResources = (
1535
+ device: RhiDevice,
1536
+ ): Result<RecoveryPostProcessResources, RhiError> => {
1537
+ const paramsBuffers = new Map<string, Buffer>();
1538
+ try {
1539
+ for (const [id, entry] of postProcessDeclarations()) {
1540
+ if (id === 'forgeax.taa-resolve') continue;
1541
+ let buffer: Buffer | undefined;
1542
+ if (entry.params !== undefined) {
1543
+ const created = device.createBuffer({
1544
+ label: `post-process-params-${id}`,
1545
+ size: entry.params.byteSize,
1546
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
1547
+ mappedAtCreation: false,
1548
+ });
1549
+ if (!created.ok) throw created.error;
1550
+ buffer = created.value;
1551
+ paramsBuffers.set(id, buffer);
1552
+ const written = device.queue.writeBuffer(buffer, 0, entry.params.defaultValue);
1553
+ if (!written.ok) throw written.error;
1554
+ } else if (entryHasDepthRead(entry)) {
1555
+ const created = device.createBuffer({
1556
+ label: `post-process-params-${id}`,
1557
+ size: DEPTH_MIN_PARAMS_BYTE_SIZE,
1558
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
1559
+ mappedAtCreation: false,
1560
+ });
1561
+ if (!created.ok) throw created.error;
1562
+ buffer = created.value;
1563
+ }
1564
+ if (buffer !== undefined) paramsBuffers.set(id, buffer);
1565
+ }
1566
+ } catch (cause) {
1567
+ disposePostProcessResources({ device, paramsBuffers });
1568
+ if (cause instanceof RhiError) return err(cause);
1569
+ return err(
1570
+ new RhiError({
1571
+ code: 'webgpu-runtime-error',
1572
+ expected: 'post-process recovery resources are created on the candidate device',
1573
+ hint: cause instanceof Error ? cause.message : String(cause),
1574
+ }),
1575
+ );
1576
+ }
1577
+ return ok({ device, paramsBuffers });
1578
+ };
1579
+ const publishRecoveryPostProcessResources = (candidate: RecoveryPostProcessResources): void => {
1580
+ if (candidate.paramsBuffers === postProcessParamsBuffers) return;
1581
+ const previous = { device: postProcessParamsDevice, paramsBuffers: postProcessParamsBuffers };
1582
+ postProcessParamsDevice = candidate.device;
1583
+ postProcessParamsBuffers = candidate.paramsBuffers;
1584
+ disposePostProcessResources(previous);
1585
+ };
1586
+ const discardRecoveryPostProcessResources = (candidate: RecoveryPostProcessResources): void => {
1587
+ if (candidate.paramsBuffers === postProcessParamsBuffers) {
1588
+ postProcessParamsBuffers = new Map();
1589
+ disposePostProcessResources(candidate);
1590
+ return;
1591
+ }
1592
+ disposePostProcessResources(candidate);
1593
+ };
1968
1594
  interface PostProcessParamsCandidate {
1969
1595
  readonly buffers: ReadonlyMap<string, Buffer>;
1970
1596
  readonly created: readonly Buffer[];
@@ -2064,6 +1690,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2064
1690
  const key = `${id}|${colorFormat}|${signature}`;
2065
1691
  const cached = postProcessPipelineCache.get(key);
2066
1692
  if (cached !== undefined) return cached;
1693
+ internals.recoveryColdWorkGuard?.notePipelineColdWork();
2067
1694
  const factory = internals.buildPostProcessPipeline;
2068
1695
  if (factory === undefined) return null;
2069
1696
  const built = factory(
@@ -2104,6 +1731,134 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2104
1731
  const dispatchCounts: { unlit: number } = {
2105
1732
  unlit: 0,
2106
1733
  };
1734
+ let lastLodOcclusionInspection: LodOcclusionInspection | undefined;
1735
+ // The GPU selector returns stable numeric World keys with its same-submit
1736
+ // counters. Keep the attachment join from that submit, rather than using
1737
+ // the current worlds[] position when the asynchronous readback completes.
1738
+ let lastLodWorldAttachments: ReadonlyMap<number, string> = new Map();
1739
+ let occlusionCandidateCache:
1740
+ | {
1741
+ readonly renderables: readonly RenderableSnapshot[];
1742
+ readonly camera: CameraSnapshot;
1743
+ readonly cameraWorldIdentity: string;
1744
+ readonly candidates: readonly OcclusionRuntimeCandidate[];
1745
+ }
1746
+ | undefined;
1747
+ let lodCandidateCountCache:
1748
+ | { readonly renderables: readonly RenderableSnapshot[]; readonly count: number }
1749
+ | undefined;
1750
+ let lodInspectionRenderablesCache:
1751
+ | {
1752
+ readonly submissionRenderables: readonly RenderableSnapshot[];
1753
+ readonly lodCandidateCount: number;
1754
+ readonly values: readonly RenderableSnapshot[];
1755
+ }
1756
+ | undefined;
1757
+ let orderedDispatchCache:
1758
+ | {
1759
+ readonly source: readonly DispatchEntry[];
1760
+ readonly world: World;
1761
+ readonly camera: readonly CameraSnapshot[];
1762
+ readonly renderables: readonly RenderableSnapshot[];
1763
+ readonly value: readonly DispatchEntry[];
1764
+ }
1765
+ | undefined;
1766
+ const applyLodSelectionTelemetry = (
1767
+ selection:
1768
+ | {
1769
+ readonly candidateCount: number;
1770
+ readonly visible: number;
1771
+ readonly occluded: number;
1772
+ readonly lodHistogram: readonly { readonly level: number; readonly count: number }[];
1773
+ readonly worldSelections?: readonly {
1774
+ readonly worldKey: number;
1775
+ readonly primitiveSlot: number;
1776
+ readonly slotGeneration: number;
1777
+ readonly candidateCount: number;
1778
+ readonly visible: number;
1779
+ readonly occluded: number;
1780
+ readonly lodHistogram: readonly { readonly level: number; readonly count: number }[];
1781
+ }[];
1782
+ }
1783
+ | undefined,
1784
+ ): void => {
1785
+ if (selection === undefined || lastLodOcclusionInspection === undefined) return;
1786
+ const inspection = lastLodOcclusionInspection;
1787
+ // The GPU readback histogram is the selector fact, while final submitted
1788
+ // counts are owned by the persistent visibility projection. Keeping the
1789
+ // projection count here excludes ordinary non-LOD geometry (for example a
1790
+ // benchmark occluder) from the LOD workload without inventing a second
1791
+ // visibility owner or mixing CPU/GPU admission decisions.
1792
+ let worlds = inspection.worlds;
1793
+ const worldSelections = selection.worldSelections;
1794
+ if (worldSelections !== undefined && worldSelections.length > 0) {
1795
+ const selectionsByAttachment = new Map<string, (typeof worldSelections)[number][]>();
1796
+ let attributionValid = true;
1797
+ for (const selected of worldSelections) {
1798
+ const attachmentId = lastLodWorldAttachments.get(selected.worldKey);
1799
+ if (attachmentId === undefined) {
1800
+ attributionValid = false;
1801
+ break;
1802
+ }
1803
+ const rows = selectionsByAttachment.get(attachmentId);
1804
+ if (rows === undefined) selectionsByAttachment.set(attachmentId, [selected]);
1805
+ else rows.push(selected);
1806
+ }
1807
+ if (attributionValid) {
1808
+ const nextWorlds = worlds.map((world) => {
1809
+ const selected = selectionsByAttachment.get(world.attachmentId);
1810
+ const baseRow = world.rows[0];
1811
+ if (selected === undefined || baseRow === undefined) {
1812
+ if (world.rows.length > 0) attributionValid = false;
1813
+ return world;
1814
+ }
1815
+ const candidateCount = selected.reduce((sum, row) => sum + row.candidateCount, 0);
1816
+ if (candidateCount !== baseRow.count.candidates) {
1817
+ attributionValid = false;
1818
+ return world;
1819
+ }
1820
+ return Object.freeze({
1821
+ ...world,
1822
+ attribution: Object.freeze({
1823
+ status: 'same-submit' as const,
1824
+ submit: inspection.submit,
1825
+ }),
1826
+ rows: Object.freeze(
1827
+ selected.map((row) => ({
1828
+ ...baseRow,
1829
+ slot: {
1830
+ primitiveSlot: row.primitiveSlot,
1831
+ slotGeneration: row.slotGeneration,
1832
+ },
1833
+ count: {
1834
+ candidates: row.candidateCount,
1835
+ visible: row.visible,
1836
+ occluded: row.occluded,
1837
+ },
1838
+ lodHistogram: row.lodHistogram,
1839
+ })),
1840
+ ),
1841
+ });
1842
+ });
1843
+ if (attributionValid) worlds = Object.freeze(nextWorlds);
1844
+ }
1845
+ }
1846
+ lastLodOcclusionInspection = inspectLodOcclusion({
1847
+ ...inspection,
1848
+ count: {
1849
+ ...lastLodOcclusionInspection.count,
1850
+ candidates: selection.candidateCount,
1851
+ visible: selection.visible,
1852
+ occluded: selection.occluded,
1853
+ },
1854
+ lodHistogram: selection.lodHistogram,
1855
+ // Per-primitive samples are intentionally omitted until the GPU readback
1856
+ // carries stable primitive identities; the histogram is the authoritative
1857
+ // producer fact and is copied from the same GPU cull counters.
1858
+ samples: [],
1859
+ worlds,
1860
+ });
1861
+ };
2107
1862
  // feat-20260531-per-frame-bind-group-cache M1 / w4: per-frame
2108
1863
  // createBindGroup counter scaffolding. Reset on every draw([world], { cameraOwner: 0, resourceOwner: 0 }) entry,
2109
1864
  // bumped on cache-miss in render-system-record.ts (M2-M4 bump points).
@@ -2112,44 +1867,60 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2112
1867
  createBindGroup: 0,
2113
1868
  keys: [],
2114
1869
  };
2115
- const preparedPipelineIds = new WeakMap<object, string>();
2116
- const preparedMaterialPipelineShaders = new WeakMap<object, string>();
2117
- const preparedGroup0Pipelines = new WeakSet<object>();
2118
- const preparedViewOnlyPipelines = new WeakSet<object>();
2119
- const preparedRenderMaterialPipelines = new WeakSet<object>();
2120
- const preparedAssetHandles = new WeakMap<World, Map<string, number>>();
1870
+ type PreparedResolverCaches = {
1871
+ readonly preparedPipelineIds: WeakMap<object, string>;
1872
+ readonly preparedMaterialPipelineShaders: WeakMap<object, string>;
1873
+ readonly preparedGroup0Pipelines: WeakSet<object>;
1874
+ readonly preparedViewOnlyPipelines: WeakSet<object>;
1875
+ readonly preparedRenderMaterialPipelines: WeakSet<object>;
1876
+ readonly preparedAssetHandles: WeakMap<World, Map<string, number>>;
1877
+ };
1878
+ const createPreparedResolverCaches = (): PreparedResolverCaches => ({
1879
+ preparedPipelineIds: new WeakMap(),
1880
+ preparedMaterialPipelineShaders: new WeakMap(),
1881
+ preparedGroup0Pipelines: new WeakSet(),
1882
+ preparedViewOnlyPipelines: new WeakSet(),
1883
+ preparedRenderMaterialPipelines: new WeakSet(),
1884
+ preparedAssetHandles: new WeakMap(),
1885
+ });
1886
+ const activePreparedResolverCaches = createPreparedResolverCaches();
2121
1887
  const preparedHandle = <Brand extends string>(
1888
+ runtime: RenderSystemInternals,
1889
+ caches: PreparedResolverCaches,
2122
1890
  world: World,
2123
1891
  guid: string,
2124
1892
  brand: Brand,
2125
1893
  ): Handle<Brand, 'shared'> | undefined => {
2126
- let handles = preparedAssetHandles.get(world);
1894
+ let handles = caches.preparedAssetHandles.get(world);
2127
1895
  if (handles === undefined) {
2128
1896
  handles = new Map();
2129
- preparedAssetHandles.set(world, handles);
1897
+ caches.preparedAssetHandles.set(world, handles);
2130
1898
  }
2131
1899
  const key = `${brand}:${guid.toLowerCase()}`;
2132
1900
  const cached = handles.get(key);
2133
1901
  if (cached !== undefined) return cached as Handle<Brand, 'shared'>;
2134
- const asset = internals.assets.lookup(guid);
1902
+ const asset = runtime.assets.lookup(guid);
2135
1903
  if (asset === undefined) return undefined;
2136
1904
  const handle = world.allocSharedRef(brand, asset);
2137
1905
  handles.set(key, handle as number);
2138
1906
  return handle;
2139
1907
  };
2140
1908
  const preparedMaterialBindings = (
1909
+ runtime: RenderSystemInternals,
1910
+ caches: PreparedResolverCaches,
1911
+ worlds: readonly World[],
2141
1912
  materialShaderId: string,
2142
1913
  worldIndex: number,
2143
1914
  materialGuid: string,
2144
1915
  layout: BindGroupLayout,
2145
1916
  ) => {
2146
- const world = preparedWorlds[worldIndex];
2147
- const material = internals.assets.lookup(materialGuid) as MaterialAsset | undefined;
2148
- const pipelineState = internals.getPipelineState();
1917
+ const world = worlds[worldIndex];
1918
+ const material = runtime.assets.lookup(materialGuid) as MaterialAsset | undefined;
1919
+ const pipelineState = runtime.getPipelineState();
2149
1920
  if (world === undefined || material?.kind !== 'material' || pipelineState === null) {
2150
1921
  return err(new Error('prepared material asset is unavailable'));
2151
1922
  }
2152
- const schema = internals.getParamSchema?.(materialShaderId) ?? [];
1923
+ const schema = runtime.getParamSchema?.(materialShaderId) ?? [];
2153
1924
  const values = material.values ?? {};
2154
1925
  const derived = derive(schema);
2155
1926
  const fields = [...derived.textureFieldNames];
@@ -2162,7 +1933,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2162
1933
  schema,
2163
1934
  values as Readonly<Record<string, number | readonly number[]>>,
2164
1935
  );
2165
- const created = internals.device.createBuffer({
1936
+ const created = runtime.device.createBuffer({
2166
1937
  label: `prepared-material:${materialGuid}`,
2167
1938
  size: payload.byteLength,
2168
1939
  usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
@@ -2170,9 +1941,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2170
1941
  if (!created.ok) return created;
2171
1942
  buffer = created.value;
2172
1943
  payloadByteLength = payload.byteLength;
2173
- const written = internals.device.queue.writeBuffer(buffer, 0, payload);
1944
+ const written = runtime.device.queue.writeBuffer(buffer, 0, payload);
2174
1945
  if (!written.ok) {
2175
- internals.device.destroyBuffer(buffer);
1946
+ runtime.device.destroyBuffer(buffer);
2176
1947
  return written;
2177
1948
  }
2178
1949
  }
@@ -2189,20 +1960,32 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2189
1960
  : undefined;
2190
1961
  let sampler = pipelineState.defaultSampler;
2191
1962
  if (textureValue?.sampler !== undefined) {
2192
- const handle = preparedHandle(world, String(textureValue.sampler), 'SamplerAsset');
1963
+ const handle = preparedHandle(
1964
+ runtime,
1965
+ caches,
1966
+ world,
1967
+ String(textureValue.sampler),
1968
+ 'SamplerAsset',
1969
+ );
2193
1970
  if (handle !== undefined) {
2194
1971
  const asset = resolveAssetHandle<SamplerAsset>(world, handle);
2195
1972
  if (asset.ok) {
2196
- const resident = internals.gpuStore.ensureSamplerResident(handle, asset.value, world);
1973
+ const resident = runtime.gpuStore.ensureSamplerResident(handle, asset.value, world);
2197
1974
  if (resident.ok) sampler = resident.value;
2198
1975
  }
2199
1976
  }
2200
1977
  }
2201
1978
  let view = pipelineState.defaultWhiteTextureView;
2202
1979
  if (textureValue !== undefined) {
2203
- const handle = preparedHandle(world, String(textureValue.texture), 'TextureAsset');
1980
+ const handle = preparedHandle(
1981
+ runtime,
1982
+ caches,
1983
+ world,
1984
+ String(textureValue.texture),
1985
+ 'TextureAsset',
1986
+ );
2204
1987
  if (handle !== undefined) {
2205
- view = residentTextureView(world, internals.gpuStore, internals, handle) ?? view;
1988
+ view = residentTextureView(world, runtime.gpuStore, runtime, handle) ?? view;
2206
1989
  }
2207
1990
  }
2208
1991
  textureResources.push({ sampler, view });
@@ -2238,12 +2021,12 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2238
2021
  textureIndex += 1;
2239
2022
  continue;
2240
2023
  }
2241
- if (buffer !== undefined) internals.device.destroyBuffer(buffer);
2024
+ if (buffer !== undefined) runtime.device.destroyBuffer(buffer);
2242
2025
  return err(new Error('prepared material schema contains unsupported binding kinds'));
2243
2026
  }
2244
2027
  const fallback = pipelineState.skylightFallback;
2245
2028
  if (fallback === null) {
2246
- if (buffer !== undefined) internals.device.destroyBuffer(buffer);
2029
+ if (buffer !== undefined) runtime.device.destroyBuffer(buffer);
2247
2030
  return err(new Error('prepared material skylight fallback is unavailable'));
2248
2031
  }
2249
2032
  const completeEntries = assembleMaterialWithSkylightEntries(entries, {
@@ -2255,206 +2038,261 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2255
2038
  brdfLutSampler: fallback.sampler,
2256
2039
  intensityBuffer: fallback.intensityBuffer,
2257
2040
  });
2258
- const group = internals.device.createBindGroup({ layout, entries: completeEntries });
2041
+ const group = runtime.device.createBindGroup({ layout, entries: completeEntries });
2259
2042
  if (!group.ok) {
2260
- if (buffer !== undefined) internals.device.destroyBuffer(buffer);
2043
+ if (buffer !== undefined) runtime.device.destroyBuffer(buffer);
2261
2044
  return group;
2262
2045
  }
2263
2046
  if (buffer === undefined) return group;
2264
2047
  return ok({
2265
2048
  handle: group.value,
2266
2049
  dynamicOffsets: [0, 0],
2267
- release: () => internals.device.destroyBuffer(buffer),
2050
+ release: () => runtime.device.destroyBuffer(buffer),
2268
2051
  });
2269
2052
  };
2270
2053
  const lastFrustumStats: { culled: number; total: number } = { culled: 0, total: 0 };
2271
2054
  const lastVisibilityStats: { explicitlyHidden: number } = { explicitlyHidden: 0 };
2272
2055
  let lastMeshMaterialBindings: readonly MeshMaterialBindingObservation[] = [];
2273
2056
  let lastMeshMaterialBindingFrame: ExtractedFrame | undefined;
2274
- const preparedResolverFactory = (
2275
- input: RenderFeaturePreparedGraphicsResolverInput,
2276
- ): PreparedGraphicsResolver => {
2277
- return createPreparedGraphicsResolver({
2278
- device: internals.device,
2279
- featureIdentity: input.featureIdentity,
2280
- generation: input.generation,
2281
- capabilityAvailable: true,
2282
- featureOrder: input.order,
2283
- lookup: input.lookup,
2284
- resolveGpuBuffer: (reference) =>
2285
- featureGpuWork.resolveBuffer(input.featureIdentity, reference),
2286
- resolvePipeline: (descriptor) => {
2287
- const postProcessEntry =
2288
- input.fullscreenEffects.get(descriptor.shader) ??
2289
- builtinPostProcessEntries.get(descriptor.shader);
2290
- if (postProcessEntry !== undefined) {
2291
- const fullscreen = buildFullscreenPostProcessPass(
2292
- { device: internals.device, errorRegistry: internals.errorRegistry },
2293
- postProcessEntry,
2294
- );
2295
- if (fullscreen === null) return err(new Error('prepared post-process layout failed'));
2296
- const pipeline = internals.getPostProcessPipeline?.(
2057
+ const createPreparedResolverFactory =
2058
+ (
2059
+ runtime: RenderSystemInternals,
2060
+ gpuWork: RenderFeatureGpuWorkOwner,
2061
+ worlds: readonly World[],
2062
+ caches: PreparedResolverCaches,
2063
+ ) =>
2064
+ (input: RenderFeaturePreparedGraphicsResolverInput): PreparedGraphicsResolver => {
2065
+ const {
2066
+ preparedPipelineIds,
2067
+ preparedMaterialPipelineShaders,
2068
+ preparedGroup0Pipelines,
2069
+ preparedViewOnlyPipelines,
2070
+ preparedRenderMaterialPipelines,
2071
+ } = caches;
2072
+ return createPreparedGraphicsResolver({
2073
+ device: runtime.device,
2074
+ featureIdentity: input.featureIdentity,
2075
+ generation: input.generation,
2076
+ capabilityAvailable: true,
2077
+ featureOrder: input.order,
2078
+ lookup: input.lookup,
2079
+ resolveGpuBuffer: (reference) => gpuWork.resolveBuffer(input.featureIdentity, reference),
2080
+ resolvePipeline: (descriptor) => {
2081
+ const postProcessEntry =
2082
+ input.fullscreenEffects.get(descriptor.shader) ??
2083
+ builtinPostProcessEntries.get(descriptor.shader);
2084
+ if (postProcessEntry !== undefined) {
2085
+ const fullscreen = buildFullscreenPostProcessPass(
2086
+ { device: runtime.device, errorRegistry: runtime.errorRegistry },
2087
+ postProcessEntry,
2088
+ );
2089
+ if (fullscreen === null) return err(new Error('prepared post-process layout failed'));
2090
+ const pipeline = runtime.getPostProcessPipeline?.(
2091
+ descriptor.shader,
2092
+ fullscreen.bindGroupLayout,
2093
+ descriptor.colorFormats[0] as GPUTextureFormat,
2094
+ postProcessEntry,
2095
+ );
2096
+ if (pipeline !== null && pipeline !== undefined) {
2097
+ preparedPipelineIds.set(pipeline as object, descriptor.shader);
2098
+ if (runtime.getMaterialShaderBindingContract?.(descriptor.shader) === 'group-0') {
2099
+ preparedGroup0Pipelines.add(pipeline as object);
2100
+ }
2101
+ }
2102
+ return pipeline === null || pipeline === undefined
2103
+ ? err(makePreparedPipelinePendingError())
2104
+ : ok(pipeline);
2105
+ }
2106
+ const preparedPipeline = runtime.getMaterialShaderPipeline?.(
2297
2107
  descriptor.shader,
2298
- fullscreen.bindGroupLayout,
2108
+ descriptor.colorFormats[0] === 'rgba16float',
2109
+ descriptor.renderState,
2110
+ descriptor.topology,
2111
+ descriptor.indexFormat,
2112
+ undefined,
2113
+ 'forward',
2114
+ undefined,
2115
+ descriptor.sampleCount ?? 1,
2116
+ // Prepared graphics own their declared attachment format. Passing
2117
+ // it through keeps the PSO compatible with feature-target views
2118
+ // (the ordinary material path intentionally defaults to the
2119
+ // swap-chain view format).
2299
2120
  descriptor.colorFormats[0] as GPUTextureFormat,
2300
- postProcessEntry,
2121
+ undefined,
2122
+ descriptor.depthFormat === undefined
2123
+ ? null
2124
+ : (descriptor.depthFormat as GPUTextureFormat),
2125
+ descriptor.vertexLayout,
2126
+ undefined,
2127
+ input.shaderModuleMode,
2301
2128
  );
2302
- if (pipeline !== null && pipeline !== undefined) {
2303
- preparedPipelineIds.set(pipeline as object, descriptor.shader);
2304
- if (internals.getMaterialShaderBindingContract?.(descriptor.shader) === 'group-0') {
2129
+ // A requested material shader is an exact pipeline contract. During
2130
+ // async shader warmup, substituting the generic unlit pipeline can
2131
+ // mismatch vertex layouts and HDR attachment formats, turning a
2132
+ // retryable next-frame prepare into an invalid GPU command buffer.
2133
+ const pipeline = preparedPipeline ?? null;
2134
+ if (pipeline !== null) {
2135
+ preparedMaterialPipelineShaders.set(pipeline as object, descriptor.shader);
2136
+ const bindingContract = runtime.getMaterialShaderBindingContract?.(descriptor.shader);
2137
+ if (bindingContract === 'group-0') {
2305
2138
  preparedGroup0Pipelines.add(pipeline as object);
2139
+ } else if (bindingContract === 'view-only') {
2140
+ preparedViewOnlyPipelines.add(pipeline as object);
2141
+ } else {
2142
+ preparedRenderMaterialPipelines.add(pipeline as object);
2306
2143
  }
2307
2144
  }
2308
- return pipeline === null || pipeline === undefined
2309
- ? err(makePreparedPipelinePendingError())
2310
- : ok(pipeline);
2311
- }
2312
- const preparedPipeline = internals.getMaterialShaderPipeline?.(
2313
- descriptor.shader,
2314
- descriptor.colorFormats[0] === 'rgba16float',
2315
- descriptor.renderState,
2316
- descriptor.topology,
2317
- descriptor.indexFormat,
2318
- undefined,
2319
- 'forward',
2320
- undefined,
2321
- descriptor.sampleCount ?? 1,
2322
- // Prepared graphics own their declared attachment format. Passing
2323
- // it through keeps the PSO compatible with feature-target views
2324
- // (the ordinary material path intentionally defaults to the
2325
- // swap-chain view format).
2326
- descriptor.colorFormats[0] as GPUTextureFormat,
2327
- undefined,
2328
- descriptor.depthFormat === undefined
2329
- ? null
2330
- : (descriptor.depthFormat as GPUTextureFormat),
2331
- descriptor.vertexLayout,
2332
- undefined,
2333
- input.shaderModuleMode,
2334
- );
2335
- // A requested material shader is an exact pipeline contract. During
2336
- // async shader warmup, substituting the generic unlit pipeline can
2337
- // mismatch vertex layouts and HDR attachment formats, turning a
2338
- // retryable next-frame prepare into an invalid GPU command buffer.
2339
- const pipeline = preparedPipeline ?? null;
2340
- if (pipeline !== null) {
2341
- preparedMaterialPipelineShaders.set(pipeline as object, descriptor.shader);
2342
- const bindingContract = internals.getMaterialShaderBindingContract?.(descriptor.shader);
2343
- if (bindingContract === 'group-0') {
2344
- preparedGroup0Pipelines.add(pipeline as object);
2345
- } else if (bindingContract === 'view-only') {
2346
- preparedViewOnlyPipelines.add(pipeline as object);
2347
- } else {
2348
- preparedRenderMaterialPipelines.add(pipeline as object);
2145
+ return pipeline === null ? err(makePreparedPipelinePendingError()) : ok(pipeline);
2146
+ },
2147
+ resolveBindings: (descriptor, pipeline) => {
2148
+ const materialShaderId = preparedMaterialPipelineShaders.get(pipeline as object);
2149
+ const bindingContract =
2150
+ materialShaderId === undefined
2151
+ ? undefined
2152
+ : runtime.getMaterialShaderBindingContract?.(materialShaderId);
2153
+ if (bindingContract === 'group-0-resource') {
2154
+ return descriptor.values.sceneDepth === undefined
2155
+ ? err(new Error('prepared group-0 resource pipeline requires a scene target'))
2156
+ : ok(undefined);
2349
2157
  }
2350
- }
2351
- return pipeline === null ? err(makePreparedPipelinePendingError()) : ok(pipeline);
2352
- },
2353
- resolveBindings: (descriptor, pipeline) => {
2354
- const materialShaderId = preparedMaterialPipelineShaders.get(pipeline as object);
2355
- const bindingContract =
2356
- materialShaderId === undefined
2357
- ? undefined
2358
- : internals.getMaterialShaderBindingContract?.(materialShaderId);
2359
- if (bindingContract === 'group-0-resource') {
2360
- return descriptor.values.sceneDepth === undefined
2361
- ? err(new Error('prepared group-0 resource pipeline requires a scene target'))
2362
- : ok(undefined);
2363
- }
2364
- if (bindingContract === 'view-and-scene-depth') {
2365
- return ok(undefined);
2366
- }
2367
- if (preparedGroup0Pipelines.has(pipeline as object)) {
2158
+ if (bindingContract === 'view-and-scene-depth') {
2159
+ return ok(undefined);
2160
+ }
2161
+ if (preparedGroup0Pipelines.has(pipeline as object)) {
2162
+ const layout =
2163
+ (materialShaderId === undefined
2164
+ ? undefined
2165
+ : runtime.getMaterialBindGroupLayout?.(materialShaderId)) ??
2166
+ (
2167
+ pipeline as RenderPipeline & {
2168
+ getBindGroupLayout?: (index: number) => BindGroupLayout;
2169
+ }
2170
+ ).getBindGroupLayout?.(0);
2171
+ return layout === undefined
2172
+ ? err(new Error('prepared group-0 pipeline bind group layout is unavailable'))
2173
+ : runtime.device.createBindGroup({ layout, entries: [] });
2174
+ }
2175
+ if (
2176
+ preparedViewOnlyPipelines.has(pipeline as object) ||
2177
+ (preparedRenderMaterialPipelines.has(pipeline as object) &&
2178
+ descriptor.values.group === 0) ||
2179
+ pipeline === runtime.getPipelineState()?.unlitPipeline
2180
+ ) {
2181
+ return ok(undefined);
2182
+ }
2183
+ const group = descriptor.values.group;
2368
2184
  const layout =
2369
- (materialShaderId === undefined
2370
- ? undefined
2371
- : internals.getMaterialBindGroupLayout?.(materialShaderId)) ??
2185
+ (materialShaderId !== undefined
2186
+ ? (runtime.getMaterialBindGroupLayout?.(materialShaderId) ??
2187
+ (group === 0 ? runtime.getPipelineState()?.materialBindGroupLayout : undefined))
2188
+ : undefined) ??
2372
2189
  (
2373
2190
  pipeline as RenderPipeline & {
2374
2191
  getBindGroupLayout?: (index: number) => BindGroupLayout;
2375
2192
  }
2376
- ).getBindGroupLayout?.(0);
2377
- return layout === undefined
2378
- ? err(new Error('prepared group-0 pipeline bind group layout is unavailable'))
2379
- : internals.device.createBindGroup({ layout, entries: [] });
2380
- }
2381
- if (
2382
- preparedViewOnlyPipelines.has(pipeline as object) ||
2383
- (preparedRenderMaterialPipelines.has(pipeline as object) &&
2384
- descriptor.values.group === 0) ||
2385
- pipeline === internals.getPipelineState()?.unlitPipeline
2386
- ) {
2387
- return ok(undefined);
2388
- }
2389
- const group = descriptor.values.group;
2390
- const layout =
2391
- (materialShaderId !== undefined
2392
- ? (internals.getMaterialBindGroupLayout?.(materialShaderId) ??
2393
- (group === 0 ? internals.getPipelineState()?.materialBindGroupLayout : undefined))
2394
- : undefined) ??
2395
- (
2396
- pipeline as RenderPipeline & {
2397
- getBindGroupLayout?: (index: number) => BindGroupLayout;
2398
- }
2399
- ).getBindGroupLayout?.(group === 1 ? 1 : 0);
2400
- const material = descriptor.values.material;
2401
- if (
2402
- group === 1 &&
2403
- layout !== undefined &&
2404
- materialShaderId !== undefined &&
2405
- typeof material === 'object' &&
2406
- material !== null &&
2407
- 'world' in material &&
2408
- 'guid' in material &&
2409
- typeof material.world === 'number' &&
2410
- typeof material.guid === 'string'
2411
- ) {
2412
- return preparedMaterialBindings(materialShaderId, material.world, material.guid, layout);
2413
- }
2414
- return layout === undefined
2415
- ? err(new Error('prepared pipeline bind group layout is unavailable'))
2416
- : internals.device.createBindGroup({
2193
+ ).getBindGroupLayout?.(group === 1 ? 1 : 0);
2194
+ const material = descriptor.values.material;
2195
+ if (
2196
+ group === 1 &&
2197
+ layout !== undefined &&
2198
+ materialShaderId !== undefined &&
2199
+ typeof material === 'object' &&
2200
+ material !== null &&
2201
+ 'world' in material &&
2202
+ 'guid' in material &&
2203
+ typeof material.world === 'number' &&
2204
+ typeof material.guid === 'string'
2205
+ ) {
2206
+ return preparedMaterialBindings(
2207
+ runtime,
2208
+ caches,
2209
+ worlds,
2210
+ materialShaderId,
2211
+ material.world,
2212
+ material.guid,
2417
2213
  layout,
2418
- entries:
2419
- group === 1 && preparedPipelineIds.has(pipeline as object)
2420
- ? [
2421
- {
2422
- binding: 0,
2423
- resource: {
2424
- kind: 'textureView',
2425
- value: internals.getPipelineState()?.fallbackTextureView as never,
2214
+ );
2215
+ }
2216
+ return layout === undefined
2217
+ ? err(new Error('prepared pipeline bind group layout is unavailable'))
2218
+ : runtime.device.createBindGroup({
2219
+ layout,
2220
+ entries:
2221
+ group === 1 && preparedPipelineIds.has(pipeline as object)
2222
+ ? [
2223
+ {
2224
+ binding: 0,
2225
+ resource: {
2226
+ kind: 'textureView',
2227
+ value: runtime.getPipelineState()?.fallbackTextureView as never,
2228
+ },
2426
2229
  },
2427
- },
2428
- {
2429
- binding: 1,
2430
- resource: {
2431
- kind: 'sampler',
2432
- value: internals.getPipelineState()?.defaultSampler as never,
2230
+ {
2231
+ binding: 1,
2232
+ resource: {
2233
+ kind: 'sampler',
2234
+ value: runtime.getPipelineState()?.defaultSampler as never,
2235
+ },
2433
2236
  },
2434
- },
2435
- ...(internals.getPostProcessParamsBuffer?.(
2436
- preparedPipelineIds.get(pipeline as object) ?? '',
2437
- ) === undefined
2438
- ? []
2439
- : [
2440
- {
2441
- binding: 2,
2442
- resource: {
2443
- kind: 'buffer' as const,
2444
- value: {
2445
- buffer: internals.getPostProcessParamsBuffer?.(
2446
- preparedPipelineIds.get(pipeline as object) ?? '',
2447
- ) as never,
2237
+ ...(runtime.getPostProcessParamsBuffer?.(
2238
+ preparedPipelineIds.get(pipeline as object) ?? '',
2239
+ ) === undefined
2240
+ ? []
2241
+ : [
2242
+ {
2243
+ binding: 2,
2244
+ resource: {
2245
+ kind: 'buffer' as const,
2246
+ value: {
2247
+ buffer: runtime.getPostProcessParamsBuffer?.(
2248
+ preparedPipelineIds.get(pipeline as object) ?? '',
2249
+ ) as never,
2250
+ },
2448
2251
  },
2449
2252
  },
2450
- },
2451
- ]),
2452
- ]
2453
- : [],
2454
- });
2455
- },
2456
- });
2457
- };
2253
+ ]),
2254
+ ]
2255
+ : [],
2256
+ });
2257
+ },
2258
+ });
2259
+ };
2260
+ const preparedResolverFactory = (
2261
+ input: RenderFeaturePreparedGraphicsResolverInput,
2262
+ ): PreparedGraphicsResolver =>
2263
+ createPreparedResolverFactory(
2264
+ internals,
2265
+ featureGpuWork,
2266
+ preparedWorlds,
2267
+ activePreparedResolverCaches,
2268
+ )(input);
2269
+ const recoveryOwner = createRenderSystemRecovery({
2270
+ frameState,
2271
+ internals,
2272
+ getEnvironmentLifecycle: () => environmentLifecycle,
2273
+ getGpuDrivenProduction: () => gpuDrivenProduction,
2274
+ setGpuDrivenProduction: (production) => {
2275
+ gpuDrivenProduction = production;
2276
+ },
2277
+ disposeFeatureGpuWork,
2278
+ setFeatureGpuWork: (owner) => {
2279
+ featureGpuWork = owner;
2280
+ },
2281
+ getActiveFeaturePostProcessEntries: () => activeFeaturePostProcessEntries,
2282
+ setActiveFeaturePostProcessEntries: (entries) => {
2283
+ activeFeaturePostProcessEntries = entries;
2284
+ },
2285
+ postProcessPipelineCache,
2286
+ clearPostProcessPipelineCache,
2287
+ clearPostProcessPipelineEntry,
2288
+ invalidatePostProcessModule,
2289
+ persistentRenderScene,
2290
+ gpuDrivenShaderFactory,
2291
+ createFeatureGpuWorkOwner,
2292
+ pointsLinesOwner,
2293
+ createPreparedResolverFactory,
2294
+ createPreparedResolverCaches,
2295
+ });
2458
2296
  return {
2459
2297
  releaseProfilerCatalog(): void {
2460
2298
  releaseProfilerCatalog?.();
@@ -2466,9 +2304,44 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2466
2304
  gpuDriven: gpuDrivenProduction.inspect(),
2467
2305
  };
2468
2306
  },
2307
+ get gpuDrivenInspection(): GpuDrivenProductionInspection {
2308
+ return gpuDrivenProduction.inspect();
2309
+ },
2469
2310
  get reflectionProbes(): ReflectionProbeInspection {
2470
2311
  return reflectionProbeOwner.inspect();
2471
2312
  },
2313
+ get ssrDependencies(): SsrDependenciesInspection {
2314
+ ensureSsrFormatProbe();
2315
+ const owner = reflectionProbeOwner.inspect();
2316
+ const nextGeneration = owner.reflectionFallback.projectionGeneration;
2317
+ const reset = resolveSsrAdmissionGeneration(ssrFallbackGeneration, nextGeneration);
2318
+ if (reset.changed) {
2319
+ frameState.temporalFrameTransaction.reset('signature-change');
2320
+ frameState.temporalFrame = undefined;
2321
+ frameState.temporalFrameInput = undefined;
2322
+ frameState.lastSuccessfulTemporalView = undefined;
2323
+ frameState.successfulTemporalFrameIndex = 0;
2324
+ frameState.pendingTemporalCommit = { kind: 'none' };
2325
+ }
2326
+ ssrFallbackGeneration = reset.generation;
2327
+ const temporal =
2328
+ frameState.lastSuccessfulTemporalView === undefined
2329
+ ? undefined
2330
+ : ({
2331
+ successfulSubmit: true,
2332
+ generation: frameState.lastSuccessfulTemporalView.input.deviceGeneration,
2333
+ } as const);
2334
+ return projectSsrDependencies({
2335
+ requested: frameState.reflectionFallbackDemand === true,
2336
+ identity: internals.ssrIdentity,
2337
+ reflectionFallback: owner.reflectionFallback,
2338
+ format: ssrFormatReceipt,
2339
+ temporal,
2340
+ });
2341
+ },
2342
+ get reflectionFallbackCompletion(): Promise<void> | undefined {
2343
+ return frameState.reflectionFallbackCompletion;
2344
+ },
2472
2345
  isCubeCapturePending(target: RenderTarget): boolean {
2473
2346
  return cubeCaptureScheduler.inspect(target).candidateGeneration !== undefined;
2474
2347
  },
@@ -2538,6 +2411,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2538
2411
  }),
2539
2412
  });
2540
2413
  },
2414
+ get lodOcclusionInspection(): LodOcclusionInspection | undefined {
2415
+ return lastLodOcclusionInspection;
2416
+ },
2541
2417
  get bloom(): BloomInspection {
2542
2418
  const pipelineState = internals.getPipelineState();
2543
2419
  const resources = pipelineState?.perPassResources;
@@ -2619,7 +2495,10 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2619
2495
  renderReadLeases?: readonly RenderReadLease[],
2620
2496
  timingCapture?: GpuTimingCapture,
2621
2497
  ): boolean {
2622
- preparedWorlds = worlds;
2498
+ const composition = canonicalizeWorldComposition(worlds, opts, renderReadLeases);
2499
+ const compositionWorlds = composition.worlds;
2500
+ const compositionLeases = composition.leases;
2501
+ preparedWorlds = compositionWorlds;
2623
2502
  const profileSession = internals.profiler?.activeSession();
2624
2503
  let ownsProfileFrame = false;
2625
2504
  let submitted = false;
@@ -2635,7 +2514,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2635
2514
  // cameraOwner drives the surfaced cameras + frustum
2636
2515
  // cull; resourceOwner drives skylight/skybox/postProcess + per-world
2637
2516
  // record config.
2638
- const { cameraOwner, resourceOwner } = opts;
2517
+ const { cameraOwner, resourceOwner } = composition.owners;
2639
2518
  // Keep the compiled graph as last-known-good until the candidate topology
2640
2519
  // compiles. The failed candidate frame does not execute it; successful
2641
2520
  // replacement retires it atomically in ensureCompiledFrameGraph.
@@ -2670,22 +2549,28 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2670
2549
  // the dedicated resourceOwner index. The variable is named
2671
2550
  // `resourceWorld` so the record-stage reads are self-describing (they
2672
2551
  // are resource-owner reads, not camera reads).
2673
- const resourceWorld = worlds[resourceOwner] as World;
2552
+ const resourceWorld = compositionWorlds[resourceOwner] as World;
2674
2553
  const frame = runProfiledRenderPhase(profileSession, 'extract', () => {
2675
2554
  return persistentRenderScene.extractComposition(
2676
- worlds,
2555
+ compositionWorlds,
2677
2556
  { cameraOwner, resourceOwner },
2678
2557
  internals.assets.catalogEpoch,
2679
- () =>
2558
+ (renderables) =>
2680
2559
  extractFrames(
2681
- worlds,
2560
+ compositionWorlds,
2682
2561
  { cameraOwner, resourceOwner },
2683
2562
  internals.assets,
2684
2563
  internals.getPipelineState(),
2685
2564
  persistentRenderScene.materialSnapshotCacheStore(),
2686
- { cull: 'none' },
2565
+ {
2566
+ cull: 'none',
2567
+ renderables,
2568
+ ...(internals.getMaterialShaderArtifact === undefined
2569
+ ? {}
2570
+ : { getMaterialShaderArtifact: internals.getMaterialShaderArtifact }),
2571
+ },
2687
2572
  ),
2688
- renderReadLeases,
2573
+ compositionLeases,
2689
2574
  );
2690
2575
  });
2691
2576
  const {
@@ -2701,7 +2586,6 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2701
2586
  skylightCount,
2702
2587
  skybox,
2703
2588
  skyboxCount,
2704
- fog,
2705
2589
  fogFailure,
2706
2590
  frustumStats,
2707
2591
  visibilityStats,
@@ -2714,12 +2598,280 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2714
2598
  frame.reflectionProbes ?? [],
2715
2599
  persistentRenderScene.reflectionProbeProjection().selected,
2716
2600
  cameras[0],
2601
+ frameState.frameNumber,
2602
+ skylight,
2717
2603
  );
2718
2604
  latestCamera = cameras[0];
2605
+ persistentRenderScene.updateVisibilityFacet(compositionWorlds, latestCamera, renderables);
2606
+ let occlusionProjection:
2607
+ | import('./scene/visibility/occlusion-runtime').OcclusionFrameProjection
2608
+ | undefined;
2609
+ let occlusionFallback: LodOcclusionInspection['fallback'] = { active: false };
2610
+ const worldKeys = persistentRenderScene.visibilityWorldKeysFor(compositionWorlds);
2611
+ const slotByEntity = persistentRenderScene.compositionSlotByStableEntity();
2612
+ if (latestCamera !== undefined) {
2613
+ const camera = latestCamera;
2614
+ const cameraWorld = compositionWorlds[camera.worldId ?? cameraOwner];
2615
+ const view = viewKey({
2616
+ attachmentId: cameraWorld?.identity ?? 'missing-camera-world',
2617
+ cameraEntity: camera.entityKey ?? 0,
2618
+ viewRole: 'main',
2619
+ viewGeneration: camera.historyVersion ?? 0,
2620
+ });
2621
+ const cachedCandidates = occlusionCandidateCache;
2622
+ const candidates =
2623
+ cachedCandidates?.renderables === renderables &&
2624
+ cachedCandidates.camera === camera &&
2625
+ cachedCandidates.cameraWorldIdentity === (cameraWorld?.identity ?? '')
2626
+ ? cachedCandidates.candidates
2627
+ : renderables.flatMap((renderable) => {
2628
+ const world = compositionWorlds[renderable.worldId];
2629
+ const aabb = renderable.localAabb;
2630
+ // Occlusion is a LOD transport concern. A plain mesh has no
2631
+ // lower-detail range to select, so keep it on the ordinary CPU
2632
+ // visibility path and do not allocate query resources for it.
2633
+ if (
2634
+ world === undefined ||
2635
+ cameraWorld === undefined ||
2636
+ aabb === undefined ||
2637
+ aabb.length < 6 ||
2638
+ renderable.lods === undefined ||
2639
+ renderable.lods.length === 0
2640
+ )
2641
+ return [];
2642
+ const slot = slotByEntity.get(
2643
+ worldEntityKey(
2644
+ worldKeys[renderable.worldId] ?? renderable.worldId,
2645
+ renderable.entityKey,
2646
+ ),
2647
+ );
2648
+ if (slot === undefined) return [];
2649
+ const primitive = primitiveKey({
2650
+ attachmentId: cameraWorld.identity,
2651
+ worldGeneration: worldKeys[renderable.worldId] ?? renderable.worldId,
2652
+ primitiveSlot: slot.slot,
2653
+ slotGeneration: slot.generation,
2654
+ });
2655
+ return [
2656
+ {
2657
+ view,
2658
+ primitive,
2659
+ epoch: 0,
2660
+ bounds: {
2661
+ min: [aabb[0] ?? 0, aabb[1] ?? 0, aabb[2] ?? 0] as [number, number, number],
2662
+ max: [aabb[3] ?? 0, aabb[4] ?? 0, aabb[5] ?? 0] as [number, number, number],
2663
+ },
2664
+ candidate: { level: 0, confidence: 1 },
2665
+ deviceGeneration: internals.deviceScope.generation,
2666
+ proxyVertices: buildOcclusionProxyVertices(
2667
+ aabb,
2668
+ renderable.transform.world,
2669
+ camera,
2670
+ ),
2671
+ },
2672
+ ];
2673
+ });
2674
+ occlusionCandidateCache = {
2675
+ renderables,
2676
+ camera,
2677
+ cameraWorldIdentity: cameraWorld?.identity ?? '',
2678
+ candidates,
2679
+ };
2680
+ occlusionProjection = runProfiledRenderPhase(profileSession, 'occlusion-prepare', () =>
2681
+ ensureOcclusionRuntime().prepareBatch(candidates, camera.antialias === 'msaa' ? 4 : 1),
2682
+ );
2683
+ const queryPool = ensureOcclusionRuntime().inspect();
2684
+ const queryUnavailable = ensureOcclusionRuntime().prepareUnavailable;
2685
+ const queryCapacityFallback =
2686
+ candidates.length > 0 && queryUnavailable && queryPool.availablePages === 0;
2687
+ occlusionFallback = queryCapacityFallback
2688
+ ? {
2689
+ active: true as const,
2690
+ reason: 'page-exhausted' as const,
2691
+ error: {
2692
+ code: 'visibility-query-capacity-exhausted',
2693
+ expected: 'one available occlusion-query page for the LOD candidate batch',
2694
+ hint: 'retry the LOD query after an in-flight page completes',
2695
+ detail: {
2696
+ availablePages: queryPool.availablePages,
2697
+ pageCount: queryPool.pageCount,
2698
+ pageIndexLimit: queryPool.pageIndexLimit,
2699
+ },
2700
+ },
2701
+ }
2702
+ : ({ active: false } as const);
2703
+ }
2704
+ // Query transport is best-effort and must fail open. A candidate that
2705
+ // was hidden by an earlier completion becomes visible again when this
2706
+ // device/frame cannot reserve a fresh query page; the next successful
2707
+ // submission can rebuild confidence from a real result.
2708
+ if (
2709
+ occlusionProjection === undefined &&
2710
+ latestCamera !== undefined &&
2711
+ ensureOcclusionRuntime().prepareUnavailable
2712
+ ) {
2713
+ const cameraWorld = compositionWorlds[latestCamera.worldId ?? cameraOwner];
2714
+ if (cameraWorld !== undefined) {
2715
+ const fallbackView = viewKey({
2716
+ attachmentId: cameraWorld.identity,
2717
+ cameraEntity: latestCamera.entityKey ?? 0,
2718
+ viewRole: 'main',
2719
+ viewGeneration: latestCamera.historyVersion ?? 0,
2720
+ });
2721
+ for (const renderable of renderables) {
2722
+ if ((renderable.lods?.length ?? 0) === 0) continue;
2723
+ const world = compositionWorlds[renderable.worldId];
2724
+ if (world === undefined) continue;
2725
+ const slot = slotByEntity.get(
2726
+ worldEntityKey(
2727
+ worldKeys[renderable.worldId] ?? renderable.worldId,
2728
+ renderable.entityKey,
2729
+ ),
2730
+ );
2731
+ if (slot === undefined) continue;
2732
+ persistentRenderScene.visibilityFacetStore().applyConfidence(
2733
+ fallbackView,
2734
+ primitiveKey({
2735
+ attachmentId: cameraWorld.identity,
2736
+ worldGeneration: worldKeys[renderable.worldId] ?? renderable.worldId,
2737
+ primitiveSlot: slot.slot,
2738
+ slotGeneration: slot.generation,
2739
+ }),
2740
+ { type: 'failure', submissionGeneration: frameState.frameNumber },
2741
+ );
2742
+ }
2743
+ }
2744
+ }
2745
+ const visibilityProjection = persistentRenderScene.projectVisibility(
2746
+ compositionWorlds,
2747
+ latestCamera,
2748
+ renderables,
2749
+ dispatch,
2750
+ );
2751
+ const submissionRenderables = visibilityProjection.renderables;
2752
+ const submissionDispatch = visibilityProjection.dispatch;
2753
+ const inspectionCamera = latestCamera;
2754
+ let lodCandidateCountForFrame = 0;
2755
+ if (inspectionCamera !== undefined) {
2756
+ const firstRenderable = renderables[0];
2757
+ const lodCandidateCount =
2758
+ lodCandidateCountCache?.renderables === renderables
2759
+ ? lodCandidateCountCache.count
2760
+ : (() => {
2761
+ const count = renderables.reduce(
2762
+ (total, renderable) => total + ((renderable.lods?.length ?? 0) > 0 ? 1 : 0),
2763
+ 0,
2764
+ );
2765
+ lodCandidateCountCache = { renderables, count };
2766
+ return count;
2767
+ })();
2768
+ lodCandidateCountForFrame = lodCandidateCount;
2769
+ const inspectionRenderables =
2770
+ lodInspectionRenderablesCache?.submissionRenderables === submissionRenderables &&
2771
+ lodInspectionRenderablesCache.lodCandidateCount === lodCandidateCount
2772
+ ? lodInspectionRenderablesCache.values
2773
+ : (() => {
2774
+ const values =
2775
+ lodCandidateCount > 0
2776
+ ? submissionRenderables.filter(
2777
+ (renderable) => (renderable.lods?.length ?? 0) > 0,
2778
+ )
2779
+ : submissionRenderables;
2780
+ lodInspectionRenderablesCache = {
2781
+ submissionRenderables,
2782
+ lodCandidateCount,
2783
+ values,
2784
+ };
2785
+ return values;
2786
+ })();
2787
+ const candidateCount =
2788
+ lodCandidateCount > 0 ? lodCandidateCount : renderables.length + frustumStats.culled;
2789
+ const inspectionQuery = ensureOcclusionRuntime().inspect();
2790
+ const inspectionBudget = persistentRenderScene.visibilityBudgetValue();
2791
+ const inspectionWorlds = createLodWorldInspections(
2792
+ compositionWorlds,
2793
+ renderables,
2794
+ submissionRenderables,
2795
+ inspectionCamera,
2796
+ frameState.frameNumber,
2797
+ worldKeys,
2798
+ slotByEntity,
2799
+ {
2800
+ used:
2801
+ inspectionQuery.pageCount * inspectionQuery.pageIndexLimit -
2802
+ inspectionQuery.availablePages * inspectionQuery.pageIndexLimit,
2803
+ capacity: inspectionQuery.pageCount * inspectionQuery.pageIndexLimit,
2804
+ },
2805
+ occlusionFallback,
2806
+ ensureOcclusionRuntime().prepareUnavailable
2807
+ ? { active: true, reason: 'query-unavailable' as const }
2808
+ : { active: false },
2809
+ );
2810
+ const attachmentByWorldKey = new Map<number, string>();
2811
+ for (let worldId = 0; worldId < compositionWorlds.length; worldId += 1) {
2812
+ const world = compositionWorlds[worldId];
2813
+ if (world === undefined) continue;
2814
+ const worldKey = worldKeys[worldId] ?? worldId;
2815
+ if (attachmentByWorldKey.has(worldKey)) {
2816
+ attachmentByWorldKey.clear();
2817
+ break;
2818
+ }
2819
+ attachmentByWorldKey.set(worldKey, world.identity);
2820
+ }
2821
+ lastLodWorldAttachments = attachmentByWorldKey;
2822
+ lastLodOcclusionInspection = inspectLodOcclusion({
2823
+ root: {
2824
+ guid:
2825
+ firstRenderable === undefined
2826
+ ? 'none'
2827
+ : `asset-handle:${firstRenderable.assetHandle}`,
2828
+ sourceKey: `render-frame:${frameState.frameNumber}`,
2829
+ },
2830
+ view: {
2831
+ attachmentId:
2832
+ compositionWorlds[inspectionCamera.worldId ?? cameraOwner]?.identity ?? 'none',
2833
+ cameraEntity: inspectionCamera.entityKey ?? 0,
2834
+ viewRole: 'main',
2835
+ viewGeneration: inspectionCamera.historyVersion ?? 0,
2836
+ },
2837
+ slot: {
2838
+ primitiveSlot: 0,
2839
+ slotGeneration: persistentRenderScene.inspect().topology.revision,
2840
+ },
2841
+ generation: frameState.frameNumber,
2842
+ count: {
2843
+ candidates: candidateCount,
2844
+ visible: inspectionRenderables.length,
2845
+ occluded: visibilityProjection.suppressed,
2846
+ },
2847
+ lodHistogram: [{ level: 0, count: inspectionRenderables.length }],
2848
+ queryLatencyUs: { median: 0, p95: 0, last: 0 },
2849
+ pagePressure: { used: 0, capacity: 3 * 4096 },
2850
+ fallback: occlusionFallback,
2851
+ degradation: ensureOcclusionRuntime().prepareUnavailable
2852
+ ? { active: true, reason: 'query-unavailable' as const }
2853
+ : { active: false },
2854
+ samples: inspectionRenderables.slice(0, 64).map((_, index) => ({
2855
+ primitiveSlot: index,
2856
+ level: 0,
2857
+ visible: true,
2858
+ })),
2859
+ submit: {
2860
+ frameId: frameState.frameNumber,
2861
+ build: resolveInspectionBuild(internals),
2862
+ deviceGeneration: internals.deviceScope.generation,
2863
+ },
2864
+ budget: inspectionBudget,
2865
+ worlds: inspectionWorlds,
2866
+ });
2867
+ } else {
2868
+ lastLodOcclusionInspection = undefined;
2869
+ lastLodWorldAttachments = new Map();
2870
+ }
2719
2871
  const temporalDemanded =
2720
2872
  cameras[0]?.antialias === 'taa' || (cameras[0]?.motionBlur?.shutterAngle ?? 0) > 0;
2721
2873
  if (temporalDemanded) {
2722
- persistentRenderScene.prepareTemporalFrame(renderables);
2874
+ persistentRenderScene.prepareTemporalFrame(submissionRenderables);
2723
2875
  }
2724
2876
  environmentLifecycle.bindScope(internals.deviceScope);
2725
2877
  if (fogFailure !== undefined) {
@@ -2819,7 +2971,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2819
2971
  const preparedResourceBatches = runProfiledRenderPhase(profileSession, 'features', () => {
2820
2972
  if (internals.featureHost === undefined) return [];
2821
2973
  const featureFrame = runRenderFeatureFrame(internals.featureHost, {
2822
- worlds,
2974
+ worlds: compositionWorlds,
2823
2975
  owner: resourceOwner,
2824
2976
  frameNumber: frameState.frameNumber,
2825
2977
  visibilitySnapshots: frame.featureVisibilitySnapshots,
@@ -2897,9 +3049,31 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2897
3049
  // world that owns skylight / skybox / singleton render state, w5 / D-3).
2898
3050
  // Only the Transparent segment is reordered; queue ordering between
2899
3051
  // segments (sortDispatchByQueue, stable) is preserved.
2900
- const orderedDispatch = runProfiledRenderPhase(profileSession, 'sort', () =>
2901
- sortTransparentDispatch(dispatch, resourceWorld, cameras, renderables),
2902
- );
3052
+ const orderedDispatch = runProfiledRenderPhase(profileSession, 'sort', () => {
3053
+ const cached = orderedDispatchCache;
3054
+ if (
3055
+ cached?.source === submissionDispatch &&
3056
+ cached.world === resourceWorld &&
3057
+ cached.camera === cameras &&
3058
+ cached.renderables === submissionRenderables
3059
+ ) {
3060
+ return cached.value;
3061
+ }
3062
+ const value = sortTransparentDispatch(
3063
+ [...submissionDispatch],
3064
+ resourceWorld,
3065
+ cameras,
3066
+ submissionRenderables,
3067
+ );
3068
+ orderedDispatchCache = {
3069
+ source: submissionDispatch,
3070
+ world: resourceWorld,
3071
+ camera: cameras,
3072
+ renderables: submissionRenderables,
3073
+ value,
3074
+ };
3075
+ return value;
3076
+ });
2903
3077
 
2904
3078
  // M3 / w26: single dispatch list replaces old three-bucket model.
2905
3079
  // Pass dispatch to recordFrame — the record stage iterates dispatch
@@ -2927,13 +3101,16 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2927
3101
  // The same runner is also called by geometry/material helpers;
2928
3102
  // invoking those wrappers would turn a pass probe into the
2929
3103
  // high-overhead per-draw `nested` probe.
2930
- if (
2931
- profileSession.detail === 'passes' &&
2932
- (!phase.startsWith('record/graph-execute/') ||
2933
- (phase.slice('record/graph-execute/'.length).includes('/') &&
2934
- !phase.endsWith('/geometry-loop')))
2935
- ) {
2936
- return action();
3104
+ if (profileSession.detail === 'passes') {
3105
+ const isOcclusionOwner =
3106
+ phase === 'record/occlusion-query-submit' ||
3107
+ phase === 'record/occlusion-global-advance';
3108
+ const isGraphOwner =
3109
+ phase === 'record/graph-execute' ||
3110
+ (phase.startsWith('record/graph-execute/') &&
3111
+ (!phase.slice('record/graph-execute/'.length).includes('/') ||
3112
+ phase.endsWith('/geometry-loop')));
3113
+ if (!isOcclusionOwner && !isGraphOwner) return action();
2937
3114
  }
2938
3115
  return runProfiledRenderPhase(profileSession, phase, action);
2939
3116
  };
@@ -2946,7 +3123,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2946
3123
  resourceWorld,
2947
3124
  cameras,
2948
3125
  lights,
2949
- renderables,
3126
+ [...submissionRenderables],
2950
3127
  orderedDispatch,
2951
3128
  frameState,
2952
3129
  dispatchCounts,
@@ -2956,18 +3133,20 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2956
3133
  skybox,
2957
3134
  skyboxCount,
2958
3135
  postProcessParams,
2959
- fog,
2960
- worlds,
3136
+ compositionWorlds,
2961
3137
  recordProfilePhase,
2962
3138
  {
2963
3139
  owner: gpuDrivenProduction,
2964
3140
  scene: persistentRenderScene.compositionGpuDrivenState(),
2965
- onCoverage: (ownsAll, clusteredLighting) => {
2966
- const key = `${internals.gpuStore.meshResidencyEpoch}:${clusteredLighting ? 'clustered' : 'standard'}`;
2967
- persistentRenderScene.setCompositionGpuDrivenCoverage(key, ownsAll);
3141
+ onSceneDemand: (sceneRowsRequired) => {
3142
+ persistentRenderScene.setCompositionGpuSceneDemand(sceneRowsRequired);
2968
3143
  },
3144
+ activeEntityKeys: visibilityProjection.activeEntityKeys,
3145
+ activeEntityRevision: visibilityProjection.activeEntityRevision,
3146
+ telemetryCandidateCount: lodCandidateCountForFrame,
3147
+ ...(occlusionProjection === undefined ? {} : { occlusion: occlusionProjection }),
2969
3148
  },
2970
- renderReadLeases,
3149
+ compositionLeases,
2971
3150
  featureGraphCandidate,
2972
3151
  pointsLinesOwner,
2973
3152
  {
@@ -2984,6 +3163,30 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2984
3163
  timingCapture,
2985
3164
  ),
2986
3165
  );
3166
+ if (submitted) {
3167
+ try {
3168
+ internals.recoveryColdWorkGuard?.finish();
3169
+ } catch (cause) {
3170
+ submitted = false;
3171
+ throw cause;
3172
+ }
3173
+ }
3174
+ if (submitted && lastLodOcclusionInspection !== undefined) {
3175
+ const query = ensureOcclusionRuntime().inspect();
3176
+ lastLodOcclusionInspection = {
3177
+ ...lastLodOcclusionInspection,
3178
+ pagePressure: {
3179
+ used:
3180
+ query.pageCount * query.pageIndexLimit -
3181
+ query.availablePages * query.pageIndexLimit,
3182
+ capacity: query.pageCount * query.pageIndexLimit,
3183
+ },
3184
+ fallback: occlusionFallback,
3185
+ degradation: ensureOcclusionRuntime().prepareUnavailable
3186
+ ? { active: true, reason: 'query-unavailable' as const }
3187
+ : { active: false },
3188
+ };
3189
+ }
2987
3190
  const directionalQuality = lights.directionalShadowQuality;
2988
3191
  const directionalShadowReady =
2989
3192
  directionalQuality === undefined ||
@@ -3007,6 +3210,21 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3007
3210
  };
3008
3211
  }
3009
3212
  if (submitted) {
3213
+ recoveryOwner.setLastSuccessfulFrameSeed({
3214
+ frame,
3215
+ worlds: compositionWorlds,
3216
+ cameraOwner: composition.owners.cameraOwner,
3217
+ resourceOwner: composition.owners.resourceOwner,
3218
+ width: Math.max(1, internals.canvas.width),
3219
+ height: Math.max(1, internals.canvas.height),
3220
+ });
3221
+ submittedFrameCount += 1;
3222
+ lastSubmittedGeneration = internals.deviceScope?.generation;
3223
+ if (occlusionProjection === undefined) {
3224
+ runProfiledRenderPhase(profileSession, 'record/occlusion-global-advance', () =>
3225
+ ensureOcclusionRuntime().advanceSuccessfulSubmit(frameState.frameNumber),
3226
+ );
3227
+ }
3010
3228
  const temporalCommit = persistentRenderScene.commitTemporalFrame();
3011
3229
  if (!temporalCommit.ok) {
3012
3230
  internals.errorRegistry.fire(temporalCommit.error);
@@ -3085,6 +3303,21 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3085
3303
  const currentFrameId = frameState.frameNumber - 1;
3086
3304
  return observeCurrentFrame(options, frameState.currentFrameObservationSource, currentFrameId);
3087
3305
  },
3306
+ async observeLodOcclusion(): Promise<void> {
3307
+ await ensureOcclusionRuntime().waitForCompletions();
3308
+ applyLodSelectionTelemetry(await gpuDrivenProduction.readLodSelection());
3309
+ if (lastLodOcclusionInspection !== undefined) {
3310
+ const queryLatency = ensureOcclusionRuntime().inspectQueryLatency();
3311
+ lastLodOcclusionInspection = inspectLodOcclusion({
3312
+ ...lastLodOcclusionInspection,
3313
+ queryLatencyUs: {
3314
+ median: queryLatency.median,
3315
+ p95: queryLatency.p95,
3316
+ last: queryLatency.last,
3317
+ },
3318
+ });
3319
+ }
3320
+ },
3088
3321
  getCurrentGraphTarget(name: string) {
3089
3322
  const graph = frameState.perFrameGraph;
3090
3323
  if (graph === undefined || graph === null) return undefined;
@@ -3123,6 +3356,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3123
3356
  get meshMaterialBindings(): readonly MeshMaterialBindingObservation[] {
3124
3357
  return lastMeshMaterialBindings;
3125
3358
  },
3359
+ get iblBinding(): import('./mesh-material-bindings').IblBindingInspection | undefined {
3360
+ return frameState.iblBindingInspection;
3361
+ },
3126
3362
  get perFramePassNames(): readonly string[] {
3127
3363
  return frameState.compiledFrameGraph?.inspect().passes.map((pass) => pass.name) ?? [];
3128
3364
  },
@@ -3132,6 +3368,25 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3132
3368
  get perFrameGraphInfo(): CompiledRenderGraphInfo | undefined {
3133
3369
  return frameState.compiledFrameGraph?.inspect();
3134
3370
  },
3371
+ get recoveryEvidence(): RecoveryProductionEvidence {
3372
+ const graph = frameState.compiledFrameGraph?.inspect();
3373
+ return Object.freeze({
3374
+ producerRoots: createRendererProducerRootMatrix(),
3375
+ graph: Object.freeze({
3376
+ ready: graph !== undefined,
3377
+ generation: frameState.graphGeneration,
3378
+ passCount: graph?.passes.length ?? 0,
3379
+ resourceCount: graph?.resources.length ?? 0,
3380
+ }),
3381
+ residency: Object.freeze({
3382
+ meshResidencyEpoch: internals.gpuStore.meshResidencyEpoch,
3383
+ }),
3384
+ submissions: Object.freeze({
3385
+ count: submittedFrameCount,
3386
+ lastGeneration: lastSubmittedGeneration,
3387
+ }),
3388
+ });
3389
+ },
3135
3390
  get volumetricFog(): import('./volume/inspection').VolumetricFogInspection {
3136
3391
  return frameState.volumetricFogInspection;
3137
3392
  },
@@ -3227,6 +3482,11 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3227
3482
  throw cause;
3228
3483
  }
3229
3484
  },
3485
+ lookupPostProcess,
3486
+ prepareRecoveryGraphCandidate: recoveryOwner.prepareRecoveryGraphCandidate,
3487
+ submitCandidateSetup: recoveryOwner.submitCandidateSetup,
3488
+ publishRecoveryGraphCandidate: recoveryOwner.publishRecoveryGraphCandidate,
3489
+ discardRecoveryGraphCandidate: recoveryOwner.discardRecoveryGraphCandidate,
3230
3490
  disposeFrameState(): void {
3231
3491
  (internals as TimingSessionHost).gpuPassTimingSession?.dispose();
3232
3492
  (internals as TimingSessionHost).gpuPassTimingSession = undefined;
@@ -3234,8 +3494,10 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3234
3494
  (internals as TimingSessionHost).gpuPassTimingSubmittedWork = undefined;
3235
3495
  (internals as TimingSessionHost).gpuPassTimingFrameIdentity = undefined;
3236
3496
  (internals as TimingSessionHost).gpuPassTimingBeginReason = undefined;
3497
+ pointsLinesOwner.dispose();
3237
3498
  persistentRenderScene.dispose();
3238
3499
  gpuDrivenProduction.dispose();
3500
+ occlusionRuntime?.dispose();
3239
3501
  disposeFeatureGpuWork();
3240
3502
  reflectionProbeOwner.dispose();
3241
3503
  // Bloom generations are renderer-owned rather than graph-owned. Drain
@@ -3304,6 +3566,15 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3304
3566
  // dispose error strategy).
3305
3567
  disposeInstanceBuffers(frameState.instanceBuffers, internals.errorRegistry);
3306
3568
  disposeTransientInstanceBuffers(frameState.transientInstanceBuffers, internals.errorRegistry);
3569
+ if (frameState.probeBlendRecordBuffer !== undefined) {
3570
+ if (!frameState.probeBlendRecordBuffer.isDestroyed) {
3571
+ const result = frameState.probeBlendRecordBuffer.destroy();
3572
+ if (!result.ok) internals.errorRegistry.fire(result.error);
3573
+ }
3574
+ delete frameState.probeBlendRecordBuffer;
3575
+ }
3576
+ frameState.probeBlendRecordBufferCapacity = 0;
3577
+ frameState.probeBlendBuffers.clear();
3307
3578
  if (frameState.morphBuffers !== undefined) {
3308
3579
  for (const entry of frameState.morphBuffers.values()) {
3309
3580
  if (!entry.buffer.isDestroyed) {
@@ -3323,7 +3594,18 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3323
3594
  frameState.pointShadowAtlas = null;
3324
3595
  }
3325
3596
  },
3326
- resetForRecover(): void {
3597
+ resetForRecover(retiringPipelineState?: PipelineState, replacementDevice?: RhiDevice): void {
3598
+ frameState.lastSuccessfulTemporalView = undefined;
3599
+ frameState.successfulTemporalFrameIndex = 0;
3600
+ frameState.pendingTemporalCommit = { kind: 'none' };
3601
+ // The active expansion buffers belong to the lost generation. Abandon
3602
+ // their handles without invoking destroy on a device that can no longer
3603
+ // service cleanup; the staged candidate owns the replacement buffers.
3604
+ pointsLinesOwner.abandonForDeviceLoss();
3605
+ ssrFormatReceipt = undefined;
3606
+ ssrFormatProbeGeneration = -1;
3607
+ ssrFormatProbe = undefined;
3608
+ ssrFallbackGeneration = undefined;
3327
3609
  transmissionAdmission.markDeviceLost();
3328
3610
  lastTransmissionKey = '';
3329
3611
  transmissionCapabilityGeneration = -1;
@@ -3340,13 +3622,28 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3340
3622
  updateTransmissionInspection(lastTransmissionAntialias, false);
3341
3623
  persistentRenderScene.resetGpuForRecover();
3342
3624
  gpuDrivenProduction.dispose();
3343
- gpuDrivenProduction = new GpuDrivenProduction(internals.device, gpuDrivenShaderFactory);
3625
+ // The active renderer still points at the lost device until the caller
3626
+ // completes the synchronous publication boundary. Bind the replacement
3627
+ // explicitly so the first published draw never routes through an owner
3628
+ // recreated against the old physical device.
3629
+ gpuDrivenProduction = GpuDrivenProduction.forDevice(
3630
+ replacementDevice ?? internals.device,
3631
+ gpuDrivenShaderFactory,
3632
+ );
3633
+ occlusionRuntime?.dispose();
3634
+ // The replacement device is installed by the host after this hook. Do
3635
+ // not recreate device-bound query resources against the lost device;
3636
+ // the first post-recovery frame lazily binds the current device.
3637
+ occlusionRuntime = undefined;
3344
3638
  disposeFeatureGpuWork();
3639
+ featureGpuWork = createFeatureGpuWorkOwner(internals);
3345
3640
  reflectionProbeOwner.dispose();
3346
3641
  // A recovered device cannot retain handles from the lost device. The
3347
3642
  // Bloom owner abandons candidates and retires every committed generation
3348
3643
  // before the new PipelineState is rebuilt.
3349
- internals.getPipelineState()?.perPassResources.drainBloomResources?.();
3644
+ (
3645
+ retiringPipelineState ?? internals.getPipelineState()
3646
+ )?.perPassResources.drainBloomResources?.();
3350
3647
  // feat-20260622-s5 M3 / B-2 / w18: recover() rebuild drops device-bound
3351
3648
  // state minted by the lost device. The active graph and per-entity caches
3352
3649
  // must be discarded, not merely marked for destruction: their opaque
@@ -3383,6 +3680,12 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3383
3680
  frameState.directionalShadowCacheRecorded = false;
3384
3681
  frameState.currentFrameObservationSource = undefined;
3385
3682
  frameState.lastSuccessfulCameraAntialias = undefined;
3683
+ frameState.temporalFrameTransaction.reset('device-recovery');
3684
+ frameState.temporalFrame = undefined;
3685
+ frameState.temporalFrameInput = undefined;
3686
+ frameState.lastSuccessfulTemporalView = undefined;
3687
+ frameState.successfulTemporalFrameIndex = 0;
3688
+ frameState.pendingTemporalCommit = { kind: 'none' };
3386
3689
  frameState.currentDirectionalShadowView = null;
3387
3690
  frameState.currentSpotShadowView = null;
3388
3691
  if (frameState.temporalGpuState !== undefined) {
@@ -3403,6 +3706,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3403
3706
  }
3404
3707
  frameState.retiredCompiledFrameGraphs.clear();
3405
3708
  frameState.instanceBuffers.clear();
3709
+ delete frameState.probeBlendRecordBuffer;
3710
+ frameState.probeBlendRecordBufferCapacity = 0;
3711
+ frameState.probeBlendBuffers.clear();
3406
3712
  frameState.morphBuffers?.clear();
3407
3713
  frameState.transientInstanceBuffers = [];
3408
3714
  frameState.pointShadowAtlas = null;
@@ -3423,53 +3729,152 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3423
3729
  // the lost device, so recovery must invalidate this cache alongside the
3424
3730
  // feature-host declarations and UBOs below.
3425
3731
  postProcessPipelineCache.clear();
3426
- // Logical declarations stay live in the feature host while a replacement
3427
- // device is being requested.
3428
- postProcessParamsBuffers.clear();
3732
+ // Logical declarations and the active params bundle stay live until the
3733
+ // candidate post-process bundle crosses the publication boundary.
3429
3734
  resetRenderFeatureGraphState(internals);
3430
3735
  },
3736
+ prepareRecoveryPostProcessResources(device: RhiDevice) {
3737
+ return prepareRecoveryPostProcessResources(device);
3738
+ },
3739
+ publishRecoveryPostProcessResources(candidate: RecoveryPostProcessResources): void {
3740
+ publishRecoveryPostProcessResources(candidate);
3741
+ },
3742
+ discardRecoveryPostProcessResources(candidate: RecoveryPostProcessResources): void {
3743
+ discardRecoveryPostProcessResources(candidate);
3744
+ },
3431
3745
  restorePostProcessResources(): void {
3432
- const declarations = [
3433
- ...builtinPostProcessEntries.entries(),
3434
- ...activeFeaturePostProcessEntries.entries(),
3435
- ];
3436
- for (const [id, entry] of declarations) {
3437
- if (id === 'forgeax.taa-resolve') continue;
3438
- let buffer: Buffer | undefined;
3439
- if (entry.params !== undefined) {
3440
- const created = internals.device.createBuffer({
3441
- label: `post-process-params-${id}`,
3442
- size: entry.params.byteSize,
3443
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
3444
- mappedAtCreation: false,
3445
- });
3446
- if (!created.ok) throw created.error;
3447
- buffer = created.value;
3448
- const written = internals.device.queue.writeBuffer(buffer, 0, entry.params.defaultValue);
3449
- if (!written.ok) {
3450
- internals.device.destroyBuffer(buffer);
3451
- throw written.error;
3452
- }
3453
- } else if (entryHasDepthRead(entry)) {
3454
- const created = internals.device.createBuffer({
3455
- label: `post-process-params-${id}`,
3456
- size: DEPTH_MIN_PARAMS_BYTE_SIZE,
3457
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
3458
- mappedAtCreation: false,
3746
+ const prepared = prepareRecoveryPostProcessResources(internals.device);
3747
+ if (!prepared.ok) throw prepared.error;
3748
+ publishRecoveryPostProcessResources(prepared.value);
3749
+ },
3750
+ prepareRecoveryRoots(runtime: RecoveryRootRuntime): RecoveryRootBundle {
3751
+ // Snapshot only CPU projections/declarations here. The root create
3752
+ // callbacks below must never close over a mutable active owner while
3753
+ // LifecycleTransaction is awaiting candidate construction.
3754
+ const candidateEnvironmentLifecycle = environmentLifecycle.createRecoveryCandidate(
3755
+ runtime.scope,
3756
+ );
3757
+ const roots: LifecycleResourceSpec<unknown>[] = [];
3758
+ let candidateEnvironmentGeneration: EnvironmentGeneration | undefined;
3759
+ let environmentRootCleaned = false;
3760
+ let published = false;
3761
+ if (candidateEnvironmentLifecycle.hasRecoveryFrame()) {
3762
+ const environmentRoot = candidateEnvironmentLifecycle.createRecoveryRoot(runtime.scope);
3763
+ roots.push({
3764
+ ...environmentRoot,
3765
+ create: async () => {
3766
+ const generation = (await environmentRoot.create()) as EnvironmentGeneration;
3767
+ candidateEnvironmentGeneration = generation;
3768
+ return generation;
3769
+ },
3770
+ cleanup: (value) => {
3771
+ if (environmentRootCleaned) return;
3772
+ environmentRootCleaned = true;
3773
+ if (candidateEnvironmentGeneration === value) {
3774
+ candidateEnvironmentGeneration = undefined;
3775
+ }
3776
+ if (published) {
3777
+ candidateEnvironmentLifecycle.retirePublishedGeneration(
3778
+ value as EnvironmentGeneration,
3779
+ );
3780
+ } else {
3781
+ environmentRoot.cleanup(value);
3782
+ }
3783
+ },
3784
+ });
3785
+ }
3786
+ const graphCandidate = runtime.graphCandidate;
3787
+ if (graphCandidate !== undefined) {
3788
+ const releaseGraphCandidate = (): void => graphCandidate.release();
3789
+ const compiledGraph = graphCandidate.frameState.compiledFrameGraph;
3790
+ if (compiledGraph !== null) {
3791
+ roots.push({
3792
+ kind: 'pipeline',
3793
+ create: () => {
3794
+ if (!runtime.scope.isAlive()) {
3795
+ throw new Error('Recovery graph candidate scope is not active.');
3796
+ }
3797
+ if (graphCandidate.frameState.compiledFrameGraph !== compiledGraph) {
3798
+ throw new Error('Recovery graph candidate was changed before aggregate assembly.');
3799
+ }
3800
+ return compiledGraph;
3801
+ },
3802
+ // All graph/feature/GPU-driven candidate resources share one
3803
+ // owner. The root is a real release seam, not a marker; the
3804
+ // candidate latch makes every root cleanup call harmless after
3805
+ // the first one.
3806
+ cleanup: releaseGraphCandidate,
3459
3807
  });
3460
- if (!created.ok) throw created.error;
3461
- buffer = created.value;
3462
3808
  }
3463
- if (buffer !== undefined && lookupPostProcess(id) === entry) {
3464
- postProcessParamsBuffers.set(id, buffer);
3465
- } else if (buffer !== undefined) {
3466
- internals.device.destroyBuffer(buffer);
3809
+ if (graphCandidate.featureHost !== undefined) {
3810
+ const featureRoot = graphCandidate.featureHost.createRecoveryRoot(runtime.scope);
3811
+ roots.push({ ...featureRoot, cleanup: releaseGraphCandidate });
3812
+ }
3813
+ if (graphCandidate.featureGpuWork !== undefined) {
3814
+ const featureWorkRoot = graphCandidate.featureGpuWork.createRecoveryRoot(runtime.scope);
3815
+ roots.push({ ...featureWorkRoot, cleanup: releaseGraphCandidate });
3816
+ }
3817
+ if (graphCandidate.gpuDrivenProduction !== undefined) {
3818
+ const productionRoot = graphCandidate.gpuDrivenProduction.createRecoveryRoot(
3819
+ runtime.scope,
3820
+ );
3821
+ roots.push({ ...productionRoot, cleanup: releaseGraphCandidate });
3822
+ }
3823
+ if (graphCandidate.gpuDrivenScene?.state !== undefined) {
3824
+ const sceneRoot = graphCandidate.gpuDrivenScene.createRecoveryRoot(runtime.scope);
3825
+ roots.push({ ...sceneRoot, cleanup: releaseGraphCandidate });
3826
+ }
3827
+ if (graphCandidate.pointsLines !== undefined) {
3828
+ const pointsLinesRoot = graphCandidate.pointsLines.createRecoveryRoot(runtime.scope);
3829
+ roots.push({ ...pointsLinesRoot, cleanup: releaseGraphCandidate });
3830
+ }
3831
+ if (runtime.gpuStore.recoveryResourceCount() > 0) {
3832
+ const gpuStoreRoot = runtime.gpuStore.createRecoveryRoot(runtime.scope);
3833
+ roots.push({ ...gpuStoreRoot, cleanup: releaseGraphCandidate });
3467
3834
  }
3468
3835
  }
3836
+ let discarded = false;
3837
+ return {
3838
+ roots: Object.freeze(roots),
3839
+ publish: () => {
3840
+ if (published || discarded) return;
3841
+ if (
3842
+ candidateEnvironmentGeneration !== undefined &&
3843
+ !candidateEnvironmentGeneration.retired
3844
+ ) {
3845
+ environmentLifecycle.retireActiveForReplacement();
3846
+ candidateEnvironmentLifecycle.publish(candidateEnvironmentGeneration);
3847
+ }
3848
+ published = true;
3849
+ environmentLifecycle = candidateEnvironmentLifecycle;
3850
+ Object.assign(frameState, { environmentLifecycle });
3851
+ },
3852
+ discard: () => {
3853
+ if (published || discarded) return;
3854
+ discarded = true;
3855
+ if (candidateEnvironmentGeneration !== undefined) {
3856
+ candidateEnvironmentLifecycle.discard(candidateEnvironmentGeneration);
3857
+ candidateEnvironmentGeneration = undefined;
3858
+ }
3859
+ candidateEnvironmentLifecycle.discardRecoveryCandidates();
3860
+ // The transaction can fail before it reaches any graph root (for
3861
+ // example while the environment root is created). Keep the graph,
3862
+ // feature, GPU-driven, and residency resources on the same real
3863
+ // candidate owner in that case; later scope/factory cleanup is a
3864
+ // harmless repeat through the candidate latch.
3865
+ graphCandidate?.release();
3866
+ runtime.scope.abandon();
3867
+ },
3868
+ };
3469
3869
  },
3470
3870
  };
3471
3871
  }
3472
3872
 
3873
+ /** Resolve the build identity used by every inspection submit. */
3874
+ export function resolveInspectionBuild(internals: Pick<RenderSystemInternals, 'build'>): string {
3875
+ return internals.build ?? 'render-system';
3876
+ }
3877
+
3473
3878
  // @forgeax/engine-render - built-in record context extension.
3474
3879
  //
3475
3880
  // The public RenderPipelineContext is canonical in render-contract.ts. This