@forgeax/engine-render 0.1.21 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (584) hide show
  1. package/README.md +281 -38
  2. package/dist/assembly/backend-contract.d.ts +13 -0
  3. package/dist/assembly/backend-contract.d.ts.map +1 -1
  4. package/dist/assembly/bundler-contract.d.ts +9 -0
  5. package/dist/assembly/bundler-contract.d.ts.map +1 -0
  6. package/dist/assembly/device-feature-admission.d.ts +8 -0
  7. package/dist/assembly/device-feature-admission.d.ts.map +1 -1
  8. package/dist/assembly/extended-lighting-fallback.d.ts +13 -0
  9. package/dist/assembly/extended-lighting-fallback.d.ts.map +1 -0
  10. package/dist/assembly/factory.d.ts +7 -235
  11. package/dist/assembly/factory.d.ts.map +1 -1
  12. package/dist/assembly/host-contract.d.ts +10 -0
  13. package/dist/assembly/host-contract.d.ts.map +1 -1
  14. package/dist/assembly/material/assembly.d.ts +3 -2
  15. package/dist/assembly/material/assembly.d.ts.map +1 -1
  16. package/dist/assembly/material/standard-layer-projection.d.ts +10 -0
  17. package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -0
  18. package/dist/assembly/material-shader-policy.d.ts +168 -0
  19. package/dist/assembly/material-shader-policy.d.ts.map +1 -0
  20. package/dist/assembly/material-shader-prewarm.d.ts +21 -0
  21. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
  22. package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
  23. package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
  24. package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
  25. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
  26. package/dist/assembly/recovery/generation.d.ts +46 -0
  27. package/dist/assembly/recovery/generation.d.ts.map +1 -0
  28. package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
  29. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
  30. package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
  31. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
  32. package/dist/assembly/renderer-facade.d.ts +10 -0
  33. package/dist/assembly/renderer-facade.d.ts.map +1 -0
  34. package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
  35. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  36. package/dist/assembly/renderer-helpers.d.ts +19 -0
  37. package/dist/assembly/renderer-helpers.d.ts.map +1 -0
  38. package/dist/assembly/renderer-lifecycle.d.ts +34 -0
  39. package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
  40. package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
  41. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
  42. package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
  43. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
  44. package/dist/assembly/webgpu-ready.d.ts +55 -0
  45. package/dist/assembly/webgpu-ready.d.ts.map +1 -0
  46. package/dist/assembly/webgpu-renderer.d.ts +152 -0
  47. package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
  48. package/dist/authoring.mjs +1 -1
  49. package/dist/{chunk-PCHBPYML.mjs → chunk-GE3SDD72.mjs} +37 -5
  50. package/dist/chunk-GE3SDD72.mjs.map +1 -0
  51. package/dist/chunk-KZFTK6DO.mjs +1191 -0
  52. package/dist/chunk-KZFTK6DO.mjs.map +1 -0
  53. package/dist/{chunk-FS2KNUQH.mjs → chunk-Q752SHI5.mjs} +332 -568
  54. package/dist/chunk-Q752SHI5.mjs.map +1 -0
  55. package/dist/{chunk-6OPXDQ2E.mjs → chunk-RR5L4SRS.mjs} +33 -294
  56. package/dist/chunk-RR5L4SRS.mjs.map +1 -0
  57. package/dist/chunk-TFH5I7JJ.mjs +296 -0
  58. package/dist/chunk-TFH5I7JJ.mjs.map +1 -0
  59. package/dist/chunk-UQYS7FRM.mjs +1003 -0
  60. package/dist/chunk-UQYS7FRM.mjs.map +1 -0
  61. package/dist/chunk-YKB5DQV7.mjs +345 -0
  62. package/dist/chunk-YKB5DQV7.mjs.map +1 -0
  63. package/dist/{chunk-5YADUCA7.mjs → chunk-YRUYGA4A.mjs} +1189 -1217
  64. package/dist/chunk-YRUYGA4A.mjs.map +1 -0
  65. package/dist/chunk-Z3YYH2MB.mjs +5082 -0
  66. package/dist/chunk-Z3YYH2MB.mjs.map +1 -0
  67. package/dist/cluster-binner.d.ts +5 -0
  68. package/dist/cluster-binner.d.ts.map +1 -1
  69. package/dist/components/index.d.ts +3 -0
  70. package/dist/components/index.d.ts.map +1 -1
  71. package/dist/components/light-helpers.d.ts +4 -0
  72. package/dist/components/light-helpers.d.ts.map +1 -1
  73. package/dist/components/light-probe.d.ts +9 -0
  74. package/dist/components/light-probe.d.ts.map +1 -0
  75. package/dist/components/rect-area-light.d.ts +12 -0
  76. package/dist/components/rect-area-light.d.ts.map +1 -0
  77. package/dist/components/scene-instance.d.ts +4 -0
  78. package/dist/components/scene-instance.d.ts.map +1 -1
  79. package/dist/components/spot-light.d.ts +3 -0
  80. package/dist/components/spot-light.d.ts.map +1 -1
  81. package/dist/construct-renderer.mjs +27276 -23232
  82. package/dist/construct-renderer.mjs.map +1 -1
  83. package/dist/device/device-scope.d.ts +2 -0
  84. package/dist/device/device-scope.d.ts.map +1 -1
  85. package/dist/device/gpu-residency.d.ts +32 -5
  86. package/dist/device/gpu-residency.d.ts.map +1 -1
  87. package/dist/device/lifecycle-transaction.d.ts +2 -0
  88. package/dist/device/lifecycle-transaction.d.ts.map +1 -0
  89. package/dist/environment/frame.d.ts +5 -0
  90. package/dist/environment/frame.d.ts.map +1 -1
  91. package/dist/environment/inspection.d.ts +7 -0
  92. package/dist/environment/inspection.d.ts.map +1 -1
  93. package/dist/environment/lifecycle.d.ts +20 -2
  94. package/dist/environment/lifecycle.d.ts.map +1 -1
  95. package/dist/errors/gpu-driven.d.ts +26 -0
  96. package/dist/errors/gpu-driven.d.ts.map +1 -0
  97. package/dist/errors/index.d.ts +1 -0
  98. package/dist/errors/index.d.ts.map +1 -1
  99. package/dist/errors/recover.d.ts +67 -3
  100. package/dist/errors/recover.d.ts.map +1 -1
  101. package/dist/errors/render.d.ts +39 -1
  102. package/dist/errors/render.d.ts.map +1 -1
  103. package/dist/extract/contracts.d.ts +899 -0
  104. package/dist/extract/contracts.d.ts.map +1 -0
  105. package/dist/extract/gpu-driven.d.ts +50 -0
  106. package/dist/extract/gpu-driven.d.ts.map +1 -0
  107. package/dist/extract/visibility.d.ts.map +1 -1
  108. package/dist/features/host.d.ts +2 -0
  109. package/dist/features/host.d.ts.map +1 -1
  110. package/dist/features/prepared-gpu-work.d.ts +3 -0
  111. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  112. package/dist/features/prepared-graphics-store.d.ts +2 -0
  113. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  114. package/dist/gpu-driven/batch-topology.d.ts +34 -0
  115. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  116. package/dist/gpu-driven/prepared-draw.d.ts +63 -0
  117. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -0
  118. package/dist/gpu-driven/production-raster.d.ts +126 -2
  119. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  120. package/dist/gpu-driven/view-gpu.d.ts +64 -1
  121. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  122. package/dist/gpu-scene-schema.d.ts +14 -0
  123. package/dist/gpu-scene-schema.d.ts.map +1 -1
  124. package/dist/gpu-scene.d.ts +9 -1
  125. package/dist/gpu-scene.d.ts.map +1 -1
  126. package/dist/gpu-usage.d.ts +0 -1
  127. package/dist/gpu-usage.d.ts.map +1 -1
  128. package/dist/hdrp-buffers.d.ts +3 -3
  129. package/dist/hdrp-buffers.d.ts.map +1 -1
  130. package/dist/ibl/IblPipelineCache.d.ts +7 -6
  131. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  132. package/dist/ibl/skylight-bind-group.d.ts +7 -1
  133. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  134. package/dist/index.d.ts +13 -2
  135. package/dist/index.d.ts.map +1 -1
  136. package/dist/index.mjs +90 -5
  137. package/dist/index.mjs.map +1 -1
  138. package/dist/inspection-types.d.ts +163 -1
  139. package/dist/inspection-types.d.ts.map +1 -1
  140. package/dist/internal.d.ts +9 -0
  141. package/dist/internal.d.ts.map +1 -1
  142. package/dist/internal.mjs +23 -2
  143. package/dist/internal.mjs.map +1 -1
  144. package/dist/light-buffer-layout.d.ts +60 -148
  145. package/dist/light-buffer-layout.d.ts.map +1 -1
  146. package/dist/materials.d.ts +34 -10
  147. package/dist/materials.d.ts.map +1 -1
  148. package/dist/mesh-material-bindings.d.ts +56 -0
  149. package/dist/mesh-material-bindings.d.ts.map +1 -1
  150. package/dist/occlusion-proxy.d.ts +4 -0
  151. package/dist/occlusion-proxy.d.ts.map +1 -0
  152. package/dist/pbr-pipeline.d.ts +58 -21
  153. package/dist/pbr-pipeline.d.ts.map +1 -1
  154. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  155. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -1
  156. package/dist/pipeline/standard-lighting/light-frame.d.ts +3 -3
  157. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -1
  158. package/dist/pipeline/standard-lighting/topology.d.ts +1 -1
  159. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -1
  160. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -1
  161. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  162. package/dist/pipeline-builder.d.ts +2 -0
  163. package/dist/pipeline-builder.d.ts.map +1 -1
  164. package/dist/pipeline-spec.d.ts +1 -1
  165. package/dist/pipeline-spec.d.ts.map +1 -1
  166. package/dist/plugin.d.ts.map +1 -1
  167. package/dist/points-lines/prepare.d.ts +3 -0
  168. package/dist/points-lines/prepare.d.ts.map +1 -1
  169. package/dist/points-lines/standard-owner.d.ts +33 -0
  170. package/dist/points-lines/standard-owner.d.ts.map +1 -0
  171. package/dist/prepare/extended-lighting/ltc-resources.d.ts +31 -0
  172. package/dist/prepare/extended-lighting/ltc-resources.d.ts.map +1 -0
  173. package/dist/prepare/extended-lighting/resources.d.ts +72 -0
  174. package/dist/prepare/extended-lighting/resources.d.ts.map +1 -0
  175. package/dist/prepare/extended-lighting/spot-modifiers.d.ts +42 -0
  176. package/dist/prepare/extended-lighting/spot-modifiers.d.ts.map +1 -0
  177. package/dist/prepare/extended-lighting/state.d.ts +24 -0
  178. package/dist/prepare/extended-lighting/state.d.ts.map +1 -0
  179. package/dist/record/frame-lighting.d.ts +8 -6
  180. package/dist/record/frame-lighting.d.ts.map +1 -1
  181. package/dist/record/frame-snapshot.d.ts +55 -10
  182. package/dist/record/frame-snapshot.d.ts.map +1 -1
  183. package/dist/record/frame-targets.d.ts.map +1 -1
  184. package/dist/record/frame.d.ts +21 -5
  185. package/dist/record/frame.d.ts.map +1 -1
  186. package/dist/record/gpu-timing.d.ts +19 -7
  187. package/dist/record/gpu-timing.d.ts.map +1 -1
  188. package/dist/record/helpers.d.ts +0 -20
  189. package/dist/record/helpers.d.ts.map +1 -1
  190. package/dist/record/main-pass-geometry.d.ts +20 -2
  191. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  192. package/dist/record/main-pass-material.d.ts +10 -18
  193. package/dist/record/main-pass-material.d.ts.map +1 -1
  194. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  195. package/dist/record/main-pass.d.ts.map +1 -1
  196. package/dist/record/mesh-ssbo.d.ts +11 -3
  197. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  198. package/dist/record/recovery-pipeline.d.ts +30 -0
  199. package/dist/record/recovery-pipeline.d.ts.map +1 -0
  200. package/dist/record/render-context.d.ts +82 -9
  201. package/dist/record/render-context.d.ts.map +1 -1
  202. package/dist/record/shadow-pass.d.ts.map +1 -1
  203. package/dist/record/typed-frame-graph.d.ts +49 -2
  204. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  205. package/dist/record/view-ubo.d.ts +1 -2
  206. package/dist/record/view-ubo.d.ts.map +1 -1
  207. package/dist/recovery/render-system-candidate.d.ts +65 -0
  208. package/dist/recovery/render-system-candidate.d.ts.map +1 -0
  209. package/dist/reflection/inspection.d.ts +10 -0
  210. package/dist/reflection/inspection.d.ts.map +1 -0
  211. package/dist/reflection/projection.d.ts +49 -0
  212. package/dist/reflection/projection.d.ts.map +1 -1
  213. package/dist/reflection/record-owner.d.ts +60 -0
  214. package/dist/reflection/record-owner.d.ts.map +1 -0
  215. package/dist/render-contract.d.ts +78 -15
  216. package/dist/render-contract.d.ts.map +1 -1
  217. package/dist/render-pipeline.d.ts +7 -0
  218. package/dist/render-pipeline.d.ts.map +1 -1
  219. package/dist/render-system-extract-tail.d.ts +12 -0
  220. package/dist/render-system-extract-tail.d.ts.map +1 -0
  221. package/dist/render-system-extract.d.ts +252 -30
  222. package/dist/render-system-extract.d.ts.map +1 -1
  223. package/dist/render-system.d.ts +140 -5
  224. package/dist/render-system.d.ts.map +1 -1
  225. package/dist/scene/probe-blend-record.d.ts +22 -0
  226. package/dist/scene/probe-blend-record.d.ts.map +1 -0
  227. package/dist/scene/probe-blend.d.ts +142 -0
  228. package/dist/scene/probe-blend.d.ts.map +1 -0
  229. package/dist/scene/render-scene-types.d.ts +0 -6
  230. package/dist/scene/render-scene-types.d.ts.map +1 -1
  231. package/dist/scene/render-scene.d.ts +122 -9
  232. package/dist/scene/render-scene.d.ts.map +1 -1
  233. package/dist/scene/visibility/bounds.d.ts +20 -0
  234. package/dist/scene/visibility/bounds.d.ts.map +1 -0
  235. package/dist/scene/visibility/budget.d.ts +16 -0
  236. package/dist/scene/visibility/budget.d.ts.map +1 -0
  237. package/dist/scene/visibility/facet.d.ts +73 -0
  238. package/dist/scene/visibility/facet.d.ts.map +1 -0
  239. package/dist/scene/visibility/gpu-frame-timing.d.ts +41 -0
  240. package/dist/scene/visibility/gpu-frame-timing.d.ts.map +1 -0
  241. package/dist/scene/visibility/gpu-lod.d.ts +46 -0
  242. package/dist/scene/visibility/gpu-lod.d.ts.map +1 -0
  243. package/dist/scene/visibility/inspection.d.ts +132 -0
  244. package/dist/scene/visibility/inspection.d.ts.map +1 -0
  245. package/dist/scene/visibility/lod-selector.d.ts +25 -0
  246. package/dist/scene/visibility/lod-selector.d.ts.map +1 -0
  247. package/dist/scene/visibility/occlusion-confidence.d.ts +81 -0
  248. package/dist/scene/visibility/occlusion-confidence.d.ts.map +1 -0
  249. package/dist/scene/visibility/occlusion-pass.d.ts +57 -0
  250. package/dist/scene/visibility/occlusion-pass.d.ts.map +1 -0
  251. package/dist/scene/visibility/occlusion-query-pool.d.ts +57 -0
  252. package/dist/scene/visibility/occlusion-query-pool.d.ts.map +1 -0
  253. package/dist/scene/visibility/occlusion-runtime.d.ts +96 -0
  254. package/dist/scene/visibility/occlusion-runtime.d.ts.map +1 -0
  255. package/dist/scene/visibility/types.d.ts +34 -0
  256. package/dist/scene/visibility/types.d.ts.map +1 -0
  257. package/dist/ssr/admission.d.ts +122 -0
  258. package/dist/ssr/admission.d.ts.map +1 -0
  259. package/dist/ssr/composition.d.ts +32 -0
  260. package/dist/ssr/composition.d.ts.map +1 -0
  261. package/dist/ssr/errors.d.ts +39 -0
  262. package/dist/ssr/errors.d.ts.map +1 -0
  263. package/dist/ssr/identity.d.ts +13 -0
  264. package/dist/ssr/identity.d.ts.map +1 -0
  265. package/dist/systems/skin-palette-allocator.d.ts +1 -1
  266. package/dist/temporal/frame-coordinator.d.ts +13 -0
  267. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  268. package/dist/temporal/frame.d.ts +2 -0
  269. package/dist/temporal/frame.d.ts.map +1 -1
  270. package/dist/temporal/index.mjs +6 -5
  271. package/dist/temporal/taa-history-store.d.ts +15 -1
  272. package/dist/temporal/taa-history-store.d.ts.map +1 -1
  273. package/dist/temporal/transaction.d.ts +5 -0
  274. package/dist/temporal/transaction.d.ts.map +1 -1
  275. package/dist/temporal/view.d.ts +7 -0
  276. package/dist/temporal/view.d.ts.map +1 -1
  277. package/dist/tilemap-chunk-extract-system.d.ts +1 -1
  278. package/dist/typed-render-graph-primitives.d.ts +4 -0
  279. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  280. package/dist/volume/passes.d.ts +8 -4
  281. package/dist/volume/passes.d.ts.map +1 -1
  282. package/package.json +20 -20
  283. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +5 -0
  284. package/src/__tests__/detached-inspection.integration.test.ts +6 -0
  285. package/src/__tests__/device-feature-admission.unit.test.ts +19 -1
  286. package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
  287. package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
  288. package/src/__tests__/direct-light-snapshot.unit.test.ts +59 -3
  289. package/src/__tests__/environment-generation.integration.test.ts +39 -0
  290. package/src/__tests__/evidence-identity.ts +13 -0
  291. package/src/__tests__/evidence-identity.unit.test.ts +24 -0
  292. package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
  293. package/src/__tests__/extended-lighting-recovery.browser.test.ts +14 -0
  294. package/src/__tests__/extended-lighting-recovery.dawn.test.ts +14 -0
  295. package/src/__tests__/extended-lighting-recovery.integration.test.ts +81 -0
  296. package/src/__tests__/extended-lighting-resources.unit.test.ts +51 -0
  297. package/src/__tests__/extended-lighting-zero-feature.integration.test.ts +52 -0
  298. package/src/__tests__/factory-contract.integration.test.ts +154 -6
  299. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
  300. package/src/__tests__/frame-observation.contract.test.ts +15 -0
  301. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +146 -0
  302. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +1 -0
  303. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +1 -0
  304. package/src/__tests__/gpu-driven-production.integration.test.ts +572 -9
  305. package/src/__tests__/gpu-driven-scaling.unit.test.ts +66 -2
  306. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +26 -1
  307. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +89 -2
  308. package/src/__tests__/gpu-driven-view.browser.test.ts +3 -3
  309. package/src/__tests__/gpu-driven-view.dawn.test.ts +3 -3
  310. package/src/__tests__/gpu-driven-view.unit.test.ts +50 -0
  311. package/src/__tests__/gpu-lod-evidence.ts +188 -0
  312. package/src/__tests__/gpu-lod-fallback.integration.test.ts +10 -0
  313. package/src/__tests__/gpu-lod-indirect.integration.test.ts +40 -0
  314. package/src/__tests__/gpu-lod-parity.dawn.test.ts +29 -0
  315. package/src/__tests__/gpu-lod-readiness.integration.test.ts +52 -0
  316. package/src/__tests__/gpu-lod-schema.unit.test.ts +66 -0
  317. package/src/__tests__/gpu-lod-selector.unit.test.ts +28 -0
  318. package/src/__tests__/gpu-lod-silhouette.browser.test.ts +13 -0
  319. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +66 -1
  320. package/src/__tests__/gpu-scene.dawn.test.ts +8 -1
  321. package/src/__tests__/gpu-scene.unit.test.ts +49 -3
  322. package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
  323. package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
  324. package/src/__tests__/light-buffer-layout.unit.test.ts +113 -3
  325. package/src/__tests__/light-inspection.unit.test.ts +55 -0
  326. package/src/__tests__/light-resource-error.unit.test.ts +43 -0
  327. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +109 -0
  328. package/src/__tests__/lod-bounds.unit.test.ts +42 -0
  329. package/src/__tests__/lod-cpu-lane.integration.test.ts +55 -0
  330. package/src/__tests__/lod-cpu-no-change.perf.test.ts +19 -0
  331. package/src/__tests__/lod-gpu-frame-timing.unit.test.ts +49 -0
  332. package/src/__tests__/lod-multi-view.integration.test.ts +63 -0
  333. package/src/__tests__/lod-occlusion-device-loss.integration.test.ts +27 -0
  334. package/src/__tests__/lod-occlusion-final-decision.integration.test.ts +47 -0
  335. package/src/__tests__/lod-occlusion-inspection.integration.test.ts +217 -0
  336. package/src/__tests__/lod-occlusion-inspection.schema.test-d.ts +14 -0
  337. package/src/__tests__/lod-occlusion-inspection.unit.test.ts +72 -0
  338. package/src/__tests__/lod-occlusion-visual-evidence.ts +91 -0
  339. package/src/__tests__/lod-occlusion.browser.test.ts +15 -0
  340. package/src/__tests__/lod-occlusion.dawn.test.ts +20 -0
  341. package/src/__tests__/lod-selector.profile.test.ts +35 -0
  342. package/src/__tests__/lod-selector.unit.test.ts +68 -0
  343. package/src/__tests__/lod-visibility-identity.unit.test.ts +198 -0
  344. package/src/__tests__/lod-visibility-lifecycle.integration.test.ts +132 -0
  345. package/src/__tests__/ltc-resources.unit.test.ts +32 -0
  346. package/src/__tests__/material-contract-inventory.unit.test.ts +41 -9
  347. package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
  348. package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
  349. package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
  350. package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
  351. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
  352. package/src/__tests__/occlusion-confidence.unit.test.ts +168 -0
  353. package/src/__tests__/occlusion-invalidation.integration.test.ts +37 -0
  354. package/src/__tests__/occlusion-pass-routing.unit.test.ts +16 -0
  355. package/src/__tests__/occlusion-pass.dawn.test.ts +20 -0
  356. package/src/__tests__/occlusion-pass.integration.test.ts +43 -0
  357. package/src/__tests__/occlusion-pass.rhi-null.test.ts +22 -0
  358. package/src/__tests__/occlusion-query-pool.rhi-null.test.ts +21 -0
  359. package/src/__tests__/occlusion-query-pool.unit.test.ts +95 -0
  360. package/src/__tests__/occlusion-query-ticket.integration.test.ts +47 -0
  361. package/src/__tests__/occlusion-query.dawn.test.ts +16 -0
  362. package/src/__tests__/occlusion-query.evidence.ts +178 -0
  363. package/src/__tests__/occlusion-query.rhi-null.test.ts +31 -0
  364. package/src/__tests__/occlusion-runtime.unit.test.ts +120 -0
  365. package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
  366. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +196 -0
  367. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
  368. package/src/__tests__/probe-bindings.unit.test.ts +200 -0
  369. package/src/__tests__/probe-blend-numeric.unit.test.ts +67 -0
  370. package/src/__tests__/probe-blend-oracle.unit.test.ts +136 -0
  371. package/src/__tests__/probe-capacity.integration.test.ts +97 -0
  372. package/src/__tests__/probe-recovery.integration.test.ts +104 -0
  373. package/src/__tests__/probe-scene-dirty.integration.test.ts +211 -0
  374. package/src/__tests__/profiler-phase-catalog.test.ts +4 -1
  375. package/src/__tests__/profiler-phase-token.test.ts +1 -0
  376. package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
  377. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
  378. package/src/__tests__/rect-area-boundary.unit.test.ts +24 -0
  379. package/src/__tests__/reflection-fallback-graph.unit.test.ts +67 -0
  380. package/src/__tests__/reflection-fallback-inspection.unit.test.ts +77 -0
  381. package/src/__tests__/reflection-fallback-lifecycle.integration.test.ts +21 -0
  382. package/src/__tests__/reflection-fallback-projection.unit.test.ts +90 -0
  383. package/src/__tests__/reflection-fallback-source.unit.test.ts +32 -0
  384. package/src/__tests__/reflection-probe-owner-placement.unit.test.ts +39 -0
  385. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +120 -0
  386. package/src/__tests__/render-error-code-owner.test-d.ts +1 -0
  387. package/src/__tests__/render-feature-docs.unit.test.ts +12 -7
  388. package/src/__tests__/render-graph-fallback.integration.test.ts +24 -0
  389. package/src/__tests__/render-perf/extended-lighting.bench.test.ts +66 -0
  390. package/src/__tests__/render-scene-projection.unit.test.ts +447 -18
  391. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +160 -17
  392. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
  393. package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
  394. package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
  395. package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
  396. package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
  397. package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
  398. package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
  399. package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
  400. package/src/__tests__/shader-manifest-fixture.ts +13 -16
  401. package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
  402. package/src/__tests__/skin-motion-regression.unit.test.ts +22 -0
  403. package/src/__tests__/skinned-shadow-caster.test.ts +2 -2
  404. package/src/__tests__/spot-modifier-projection.unit.test.ts +70 -0
  405. package/src/__tests__/ssr-admission-matrix.integration.test.ts +206 -0
  406. package/src/__tests__/ssr-composition.unit.test.ts +46 -0
  407. package/src/__tests__/ssr-fallback-zero-work.bench.test.ts +94 -0
  408. package/src/__tests__/ssr-recovery-errors.unit.test.ts +58 -0
  409. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +67 -0
  410. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +7 -0
  411. package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
  412. package/src/__tests__/standard-lighting-inspection.integration.test.ts +1 -1
  413. package/src/__tests__/standard-lighting-write.unit.test.ts +66 -2
  414. package/src/__tests__/temporal-fallback-generation.integration.test.ts +16 -0
  415. package/src/__tests__/temporal-view.unit.test.ts +3 -3
  416. package/src/__tests__/transmission-prewarm.unit.test.ts +111 -32
  417. package/src/__tests__/visibility-budget.integration.test.ts +140 -0
  418. package/src/__tests__/visibility-budget.unit.test.ts +70 -0
  419. package/src/__tests__/visibility-extract.integration.test.ts +11 -6
  420. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +6 -3
  421. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +31 -28
  422. package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
  423. package/src/assembly/backend-contract.ts +13 -0
  424. package/src/assembly/bundler-contract.ts +9 -0
  425. package/src/assembly/device-feature-admission.ts +26 -9
  426. package/src/assembly/extended-lighting-fallback.ts +216 -0
  427. package/src/assembly/factory.ts +81 -9634
  428. package/src/assembly/host-contract.ts +10 -0
  429. package/src/assembly/material/assembly.ts +18 -2
  430. package/src/assembly/material/standard-layer-projection.ts +16 -0
  431. package/src/assembly/material-shader-policy.ts +854 -0
  432. package/src/assembly/material-shader-prewarm.ts +88 -0
  433. package/src/assembly/mesh-ssbo-grow.ts +270 -0
  434. package/src/assembly/recovery/device-loss-fanout.ts +89 -0
  435. package/src/assembly/recovery/generation.ts +141 -0
  436. package/src/assembly/recovery/recovery-attempt.ts +800 -0
  437. package/src/assembly/recovery/renderer-recover.ts +906 -0
  438. package/src/assembly/renderer-facade.ts +342 -0
  439. package/src/assembly/renderer-frame-transaction.ts +45 -0
  440. package/src/assembly/renderer-helpers.ts +99 -0
  441. package/src/assembly/renderer-lifecycle.ts +159 -0
  442. package/src/assembly/shader-prewarm-policy.ts +157 -0
  443. package/src/assembly/webgpu-ready-contract.ts +14 -0
  444. package/src/assembly/webgpu-ready.ts +4079 -0
  445. package/src/assembly/webgpu-renderer.ts +4095 -0
  446. package/src/cluster-binner.ts +31 -0
  447. package/src/components/__tests__/lighting-authoring.unit.test.ts +39 -0
  448. package/src/components/__tests__/rect-area-light.unit.test.ts +33 -0
  449. package/src/components/camera.ts +1 -1
  450. package/src/components/index.ts +3 -0
  451. package/src/components/light-helpers.ts +52 -1
  452. package/src/components/light-probe.ts +13 -0
  453. package/src/components/rect-area-light.ts +15 -0
  454. package/src/components/scene-instance.ts +6 -0
  455. package/src/components/spot-light.ts +3 -0
  456. package/src/debug-draw-glue.ts +1 -1
  457. package/src/device/__tests__/extended-lighting-recovery.unit.test.ts +24 -0
  458. package/src/device/device-scope.ts +35 -6
  459. package/src/device/gpu-residency.ts +334 -35
  460. package/src/device/lifecycle-transaction.ts +12 -0
  461. package/src/environment/frame.ts +6 -0
  462. package/src/environment/inspection.ts +10 -0
  463. package/src/environment/lifecycle.ts +122 -9
  464. package/src/errors/gpu-driven.ts +92 -0
  465. package/src/errors/index.ts +1 -0
  466. package/src/errors/recover.ts +108 -5
  467. package/src/errors/render.ts +72 -1
  468. package/src/extract/camera.ts +3 -3
  469. package/src/extract/contracts.ts +974 -0
  470. package/src/extract/gpu-driven.ts +108 -0
  471. package/src/extract/visibility.ts +0 -23
  472. package/src/features/host.ts +16 -0
  473. package/src/features/prepared-gpu-work.ts +20 -2
  474. package/src/features/prepared-graphics-store.ts +17 -0
  475. package/src/gpu-driven/batch-topology.ts +206 -28
  476. package/src/gpu-driven/prepared-draw.ts +208 -0
  477. package/src/gpu-driven/production-raster.ts +1046 -39
  478. package/src/gpu-driven/view-gpu.ts +411 -29
  479. package/src/gpu-scene-schema.ts +13 -0
  480. package/src/gpu-scene.ts +137 -67
  481. package/src/gpu-usage.ts +0 -1
  482. package/src/hdrp-buffers.ts +6 -6
  483. package/src/ibl/IblPipelineCache.ts +134 -50
  484. package/src/ibl/skylight-bind-group.ts +26 -0
  485. package/src/index.ts +83 -1
  486. package/src/inspection-types.ts +220 -1
  487. package/src/internal.ts +38 -0
  488. package/src/light-buffer-layout.ts +157 -324
  489. package/src/materials.ts +278 -47
  490. package/src/mesh-material-bindings.ts +59 -0
  491. package/src/occlusion-proxy.ts +64 -0
  492. package/src/pbr-pipeline.ts +290 -60
  493. package/src/pipeline/standard-forward-lane.ts +47 -0
  494. package/src/pipeline/standard-lighting/graph.ts +2 -1
  495. package/src/pipeline/standard-lighting/light-frame.ts +3 -2
  496. package/src/pipeline/standard-lighting/topology.ts +1 -1
  497. package/src/pipeline/standard-lighting/transport.ts +2 -1
  498. package/src/pipeline/standard-pipeline.ts +2 -0
  499. package/src/pipeline-builder.ts +5 -2
  500. package/src/pipeline-spec.ts +12 -1
  501. package/src/plugin.ts +4 -0
  502. package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
  503. package/src/points-lines/prepare.ts +9 -0
  504. package/src/points-lines/standard-owner.ts +403 -0
  505. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +56 -0
  506. package/src/prepare/extended-lighting/ltc-resources.ts +136 -0
  507. package/src/prepare/extended-lighting/resources.ts +389 -0
  508. package/src/prepare/extended-lighting/spot-modifiers.ts +194 -0
  509. package/src/prepare/extended-lighting/state.ts +92 -0
  510. package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
  511. package/src/record/__tests__/gpu-timing.unit.test.ts +177 -0
  512. package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
  513. package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
  514. package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
  515. package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
  516. package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
  517. package/src/record/__tests__/temporal-payload.unit.test.ts +29 -28
  518. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +21 -2
  519. package/src/record/frame-lighting.ts +252 -53
  520. package/src/record/frame-snapshot.ts +72 -12
  521. package/src/record/frame-targets.ts +11 -1
  522. package/src/record/frame.ts +416 -95
  523. package/src/record/gpu-timing.ts +108 -44
  524. package/src/record/helpers.ts +1 -59
  525. package/src/record/main-pass-geometry.ts +434 -44
  526. package/src/record/main-pass-material.ts +324 -210
  527. package/src/record/main-pass-sprite-draws.ts +58 -92
  528. package/src/record/main-pass.ts +94 -6
  529. package/src/record/mesh-ssbo.ts +79 -46
  530. package/src/record/recovery-pipeline.ts +695 -0
  531. package/src/record/render-context.ts +99 -7
  532. package/src/record/shadow-pass.ts +66 -40
  533. package/src/record/typed-frame-graph.ts +270 -84
  534. package/src/record/view-ubo.ts +17 -27
  535. package/src/recovery/render-system-candidate.ts +839 -0
  536. package/src/reflection/inspection.ts +67 -0
  537. package/src/reflection/projection.ts +196 -1
  538. package/src/reflection/record-owner.ts +1283 -0
  539. package/src/render-contract.ts +82 -12
  540. package/src/render-pipeline.ts +5 -0
  541. package/src/render-system-extract-tail.ts +2983 -0
  542. package/src/render-system-extract.ts +290 -2784
  543. package/src/render-system.ts +1597 -1192
  544. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +36 -0
  545. package/src/scene/probe-blend-record.ts +54 -0
  546. package/src/scene/probe-blend.ts +813 -0
  547. package/src/scene/render-scene-types.ts +0 -7
  548. package/src/scene/render-scene.ts +972 -184
  549. package/src/scene/visibility/bounds.ts +57 -0
  550. package/src/scene/visibility/budget.ts +40 -0
  551. package/src/scene/visibility/facet.ts +400 -0
  552. package/src/scene/visibility/gpu-frame-timing.ts +101 -0
  553. package/src/scene/visibility/gpu-lod.ts +119 -0
  554. package/src/scene/visibility/inspection.ts +313 -0
  555. package/src/scene/visibility/lod-selector.ts +84 -0
  556. package/src/scene/visibility/occlusion-confidence.ts +478 -0
  557. package/src/scene/visibility/occlusion-pass.ts +165 -0
  558. package/src/scene/visibility/occlusion-query-pool.ts +178 -0
  559. package/src/scene/visibility/occlusion-runtime.ts +672 -0
  560. package/src/scene/visibility/types.ts +50 -0
  561. package/src/ssr/admission.ts +451 -0
  562. package/src/ssr/composition.ts +73 -0
  563. package/src/ssr/errors.ts +76 -0
  564. package/src/ssr/identity.ts +12 -0
  565. package/src/systems/skin-palette-allocator.ts +1 -1
  566. package/src/temporal/frame-coordinator.ts +30 -1
  567. package/src/temporal/frame.ts +14 -1
  568. package/src/temporal/taa-history-store.ts +46 -2
  569. package/src/temporal/temporal-view.ts +1 -1
  570. package/src/temporal/transaction.ts +9 -0
  571. package/src/temporal/view.ts +11 -0
  572. package/src/tilemap-chunk-extract-system.ts +5 -5
  573. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
  574. package/src/typed-render-graph-primitives.ts +48 -0
  575. package/src/volume/passes.ts +46 -49
  576. package/dist/.tsbuildinfo +0 -1
  577. package/dist/chunk-5YADUCA7.mjs.map +0 -1
  578. package/dist/chunk-6OPXDQ2E.mjs.map +0 -1
  579. package/dist/chunk-BXBQQKFQ.mjs +0 -67
  580. package/dist/chunk-BXBQQKFQ.mjs.map +0 -1
  581. package/dist/chunk-FS2KNUQH.mjs.map +0 -1
  582. package/dist/chunk-PCHBPYML.mjs.map +0 -1
  583. package/dist/chunk-VFPLTKCU.mjs +0 -192
  584. package/dist/chunk-VFPLTKCU.mjs.map +0 -1
@@ -1,9 +1,9 @@
1
- import type { EntityHandle, World } from '@forgeax/engine-ecs';
1
+ import type { Query, World } from '@forgeax/engine-ecs';
2
2
  import { componentId } from '@forgeax/engine-ecs/internal';
3
- import type { RenderReadLease } from '@forgeax/engine-ecs/projection';
3
+ import type { RenderReadLease, RenderReadVersion } from '@forgeax/engine-ecs/projection';
4
4
  import { box3, frustum, mat4 } from '@forgeax/engine-math';
5
5
  import { type RhiDevice, RhiError } from '@forgeax/engine-rhi';
6
- import { ChildOf, Transform } from '@forgeax/engine-scene';
6
+ import { ChildOf, GlobalTransform, Transform } from '@forgeax/engine-scene';
7
7
  import { Skin } from '@forgeax/engine-skinning';
8
8
  import { err, ok, type Result } from '@forgeax/engine-types';
9
9
  import {
@@ -11,8 +11,10 @@ import {
11
11
  DirectionalLight,
12
12
  Instances,
13
13
  Layer,
14
+ LightProbe,
14
15
  MeshFilter,
15
16
  MeshRenderer,
17
+ MotionBlur,
16
18
  PointLight,
17
19
  PointLightShadow,
18
20
  PostProcessParams,
@@ -24,6 +26,7 @@ import {
24
26
  SpriteRegionOverride,
25
27
  Visibility,
26
28
  } from '../components';
29
+ import type { DeviceScope, LifecycleResourceSpec } from '../device/device-scope';
27
30
  import {
28
31
  BatchTopology,
29
32
  type BatchTopologyInspection,
@@ -34,6 +37,7 @@ import type { PersistentRenderSceneInspection } from '../inspection-types';
34
37
  import { fingerprintNumericArray, InstanceBoundsCache } from '../instances-derived-bounds';
35
38
  import type { PointsLinesInspection } from '../points-lines/inspection';
36
39
  import type { PointsLinesRetainedSnapshot } from '../points-lines/snapshot';
40
+ import { worldEntityKey } from '../record/frame-snapshot';
37
41
  import {
38
42
  type ReflectionProbeFact,
39
43
  ReflectionProbeProjection,
@@ -55,21 +59,30 @@ import {
55
59
  type TransmissionDemandOperation,
56
60
  } from '../transmission/projection';
57
61
  import { VolumetricFog } from '../volume/component';
62
+ import { ProbeBlendSceneProjection, type ProbeSceneObjectInput } from './probe-blend';
58
63
  import type {
59
64
  RenderSceneApplyResult,
60
65
  RenderSceneBounds,
61
- RenderSceneDelta,
62
66
  RenderSceneInspection,
63
67
  RenderSceneOperation,
64
68
  RenderSceneRecord,
65
69
  RenderSceneResyncReason,
66
70
  RenderSceneSlot,
67
71
  } from './render-scene-types';
72
+ import { createVisibilityBudget, type VisibilityBudget } from './visibility/budget';
73
+ import {
74
+ primitiveKey,
75
+ primitiveKeyId,
76
+ VisibilityFacetStore,
77
+ viewKey,
78
+ viewKeyId,
79
+ } from './visibility/facet';
80
+ import { decideVisibility } from './visibility/occlusion-confidence';
81
+ import type { PrimitiveKey, ViewKey, VisibilityCandidate } from './visibility/types';
68
82
 
69
83
  export type {
70
84
  RenderSceneApplyResult,
71
85
  RenderSceneBounds,
72
- RenderSceneDelta,
73
86
  RenderSceneIdentity,
74
87
  RenderSceneInspection,
75
88
  RenderSceneOperation,
@@ -86,6 +99,14 @@ interface PendingIdentity {
86
99
  removed: boolean;
87
100
  }
88
101
 
102
+ interface WorldTransformSpan {
103
+ readonly worldId: number;
104
+ readonly entities: Readonly<Uint32Array>;
105
+ readonly worlds: Readonly<Float32Array>;
106
+ }
107
+
108
+ type GlobalTransformChangeQuery = Query<readonly [typeof GlobalTransform]>;
109
+
89
110
  export interface RenderSceneSubmissionCapture {
90
111
  readonly revision: number;
91
112
  readonly slots: readonly {
@@ -200,31 +221,55 @@ function intersects(left: RenderSceneBounds, right: RenderSceneBounds): boolean
200
221
  );
201
222
  }
202
223
 
203
- function worldBounds(snapshot: RenderableSnapshot): RenderSceneBounds | undefined {
224
+ function worldBounds(
225
+ snapshot: RenderableSnapshot,
226
+ target?: RenderSceneBounds,
227
+ ): RenderSceneBounds | undefined {
204
228
  const local = snapshot.localAabb;
205
229
  if (local === undefined || local.length < 6) return undefined;
206
230
  const world = snapshot.transform.world;
207
- const min: [number, number, number] = [Infinity, Infinity, Infinity];
208
- const max: [number, number, number] = [-Infinity, -Infinity, -Infinity];
209
- for (const x of [local[0] ?? 0, local[3] ?? 0]) {
210
- for (const y of [local[1] ?? 0, local[4] ?? 0]) {
211
- for (const z of [local[2] ?? 0, local[5] ?? 0]) {
212
- const tx =
213
- (world[0] ?? 0) * x + (world[4] ?? 0) * y + (world[8] ?? 0) * z + (world[12] ?? 0);
214
- const ty =
215
- (world[1] ?? 0) * x + (world[5] ?? 0) * y + (world[9] ?? 0) * z + (world[13] ?? 0);
216
- const tz =
217
- (world[2] ?? 0) * x + (world[6] ?? 0) * y + (world[10] ?? 0) * z + (world[14] ?? 0);
218
- min[0] = Math.min(min[0], tx);
219
- min[1] = Math.min(min[1], ty);
220
- min[2] = Math.min(min[2], tz);
221
- max[0] = Math.max(max[0], tx);
222
- max[1] = Math.max(max[1], ty);
223
- max[2] = Math.max(max[2], tz);
224
- }
225
- }
226
- }
227
- return { min, max };
231
+ const min = (target?.min ?? [0, 0, 0]) as [number, number, number];
232
+ const max = (target?.max ?? [0, 0, 0]) as [number, number, number];
233
+ const centerX = ((local[0] ?? 0) + (local[3] ?? 0)) * 0.5;
234
+ const centerY = ((local[1] ?? 0) + (local[4] ?? 0)) * 0.5;
235
+ const centerZ = ((local[2] ?? 0) + (local[5] ?? 0)) * 0.5;
236
+ const extentX = ((local[3] ?? 0) - (local[0] ?? 0)) * 0.5;
237
+ const extentY = ((local[4] ?? 0) - (local[1] ?? 0)) * 0.5;
238
+ const extentZ = ((local[5] ?? 0) - (local[2] ?? 0)) * 0.5;
239
+ const worldCenterX =
240
+ (world[0] ?? 0) * centerX +
241
+ (world[4] ?? 0) * centerY +
242
+ (world[8] ?? 0) * centerZ +
243
+ (world[12] ?? 0);
244
+ const worldCenterY =
245
+ (world[1] ?? 0) * centerX +
246
+ (world[5] ?? 0) * centerY +
247
+ (world[9] ?? 0) * centerZ +
248
+ (world[13] ?? 0);
249
+ const worldCenterZ =
250
+ (world[2] ?? 0) * centerX +
251
+ (world[6] ?? 0) * centerY +
252
+ (world[10] ?? 0) * centerZ +
253
+ (world[14] ?? 0);
254
+ const worldExtentX =
255
+ Math.abs(world[0] ?? 0) * extentX +
256
+ Math.abs(world[4] ?? 0) * extentY +
257
+ Math.abs(world[8] ?? 0) * extentZ;
258
+ const worldExtentY =
259
+ Math.abs(world[1] ?? 0) * extentX +
260
+ Math.abs(world[5] ?? 0) * extentY +
261
+ Math.abs(world[9] ?? 0) * extentZ;
262
+ const worldExtentZ =
263
+ Math.abs(world[2] ?? 0) * extentX +
264
+ Math.abs(world[6] ?? 0) * extentY +
265
+ Math.abs(world[10] ?? 0) * extentZ;
266
+ min[0] = worldCenterX - worldExtentX;
267
+ min[1] = worldCenterY - worldExtentY;
268
+ min[2] = worldCenterZ - worldExtentZ;
269
+ max[0] = worldCenterX + worldExtentX;
270
+ max[1] = worldCenterY + worldExtentY;
271
+ max[2] = worldCenterZ + worldExtentZ;
272
+ return target ?? { min, max };
228
273
  }
229
274
 
230
275
  function boundsFromArray(bounds: ArrayLike<number>): RenderSceneBounds | undefined {
@@ -253,20 +298,23 @@ function boundsFromArray(bounds: ArrayLike<number>): RenderSceneBounds | undefin
253
298
  export class RenderScene {
254
299
  private readonly slots: Array<RenderSceneSlot | undefined> = [];
255
300
  private readonly lastSubmittedSlots: Array<RenderSceneSlot | undefined> = [];
301
+ private readonly previousWorldBySlot: Array<Float32Array | undefined> = [];
256
302
  private readonly generations: number[] = [];
257
303
  private readonly freeSlots: number[] = [];
258
304
  private readonly slotsByWorld = new Map<number, Map<number, number>>();
259
305
  private readonly slotsByMaterial = new Map<number, Set<number>>();
260
306
  private readonly dirtySinceSubmitted = new Set<number>();
307
+ private readonly transformUpdatedSlots: RenderSceneSlot[] = [];
308
+ private temporalTracking = false;
261
309
  private lastSubmittedVisible = new Set<string>();
262
310
  private submittedEpoch = 0;
263
- // World bounds are a pure projection of each immutable snapshot. Cache the
264
- // derived value by snapshot identity so repeated view queries do not redo
265
- // the eight-corner transform for every view in the same frame.
266
- private readonly worldBoundsCache = new WeakMap<RenderableSnapshot, RenderSceneBounds | null>();
311
+ // Bounds are a derived column of the stable render slot, not metadata of a
312
+ // transient snapshot wrapper. Undefined means dirty; null means that the
313
+ // retained row has no usable bounds and must be conservatively visible.
314
+ private readonly worldBoundsBySlot: Array<RenderSceneBounds | null | undefined> = [];
267
315
  private readonly instanceBoundsCache = new InstanceBoundsCache();
268
316
  private orderedSlots: number[] = [];
269
- private materialized: readonly RenderableSnapshot[] | undefined;
317
+ private materialized: RenderableSnapshot[] | undefined;
270
318
  private slotSnapshot: readonly RenderSceneSlot[] | undefined;
271
319
  private revision = 0;
272
320
  private noChangeFrames = 0;
@@ -374,6 +422,7 @@ export class RenderScene {
374
422
  };
375
423
  this.unindexMaterials(pending.initial);
376
424
  this.slots[pending.initial.slot] = updatedSlot;
425
+ this.worldBoundsBySlot[updatedSlot.slot] = undefined;
377
426
  this.indexMaterials(updatedSlot);
378
427
  this.dirtySinceSubmitted.add(updatedSlot.slot);
379
428
  updatedSlots.push(updatedSlot);
@@ -399,19 +448,55 @@ export class RenderScene {
399
448
  };
400
449
  }
401
450
 
402
- applyDelta(delta: RenderSceneDelta): RenderSceneApplyResult {
403
- if (delta.overflowed || delta.resync !== undefined) {
404
- const snapshots = delta.resync ?? [];
405
- this.reset(snapshots);
406
- this.resyncs += 1;
407
- this.lastResyncReason = delta.reason ?? 'journal-overflow';
408
- return {
409
- ...this.emptyResult(),
410
- created: snapshots.length,
411
- resynced: 1,
412
- };
451
+ /** Apply version-selected GlobalTransform columns without allocating per-entity operations. */
452
+ applyTransformSpans(spans: readonly WorldTransformSpan[]): RenderSceneApplyResult {
453
+ this.deltaFrames += 1;
454
+ let scanned = 0;
455
+ let updated = 0;
456
+ let ignoredLateUpdates = 0;
457
+ const updatedSlots = this.transformUpdatedSlots;
458
+ updatedSlots.length = 0;
459
+ for (const span of spans) {
460
+ scanned += span.entities.length;
461
+ for (let row = 0; row < span.entities.length; row += 1) {
462
+ const entity = span.entities[row];
463
+ if (entity === undefined) continue;
464
+ const slot = this.lookup(span.worldId, entity);
465
+ if (slot === undefined) {
466
+ ignoredLateUpdates += 1;
467
+ continue;
468
+ }
469
+ const target = slot.snapshot.transform.world;
470
+ const sourceStart = row * 16;
471
+ for (let index = 0; index < 16; index += 1) {
472
+ target[index] = span.worlds[sourceStart + index] ?? target[index] ?? 0;
473
+ }
474
+ if (slot.snapshot.instances === undefined) {
475
+ const cached = this.worldBoundsBySlot[slot.slot];
476
+ const bounds = worldBounds(slot.snapshot, cached ?? undefined);
477
+ this.worldBoundsBySlot[slot.slot] = bounds ?? null;
478
+ } else {
479
+ this.worldBoundsBySlot[slot.slot] = undefined;
480
+ }
481
+ if (this.temporalTracking) this.dirtySinceSubmitted.add(slot.slot);
482
+ updatedSlots.push(slot);
483
+ updated += 1;
484
+ }
413
485
  }
414
- return this.apply(delta.operations);
486
+ this.renderableScans = scanned;
487
+ if (updated > 0) this.revision += 1;
488
+ return {
489
+ created: 0,
490
+ updated,
491
+ removed: 0,
492
+ recreated: 0,
493
+ ignoredLateUpdates,
494
+ createdSlots: [],
495
+ updatedSlots,
496
+ removedSlots: [],
497
+ recreatedSlots: [],
498
+ resynced: 0,
499
+ };
415
500
  }
416
501
 
417
502
  /** Reconcile the current identity set while retaining slots for survivors. */
@@ -446,6 +531,7 @@ export class RenderScene {
446
531
  };
447
532
  this.unindexMaterials(existing);
448
533
  this.slots[existing.slot] = updated;
534
+ this.worldBoundsBySlot[updated.slot] = undefined;
449
535
  this.indexMaterials(updated);
450
536
  this.dirtySinceSubmitted.add(updated.slot);
451
537
  this.orderedSlots.push(existing.slot);
@@ -460,14 +546,25 @@ export class RenderScene {
460
546
  this.reset(snapshots);
461
547
  }
462
548
 
463
- materialize(): readonly RenderableSnapshot[] {
549
+ /** Enable previous-frame publication only while a temporal feature consumes it. */
550
+ setTemporalTracking(enabled: boolean): void {
551
+ if (enabled === this.temporalTracking) return;
552
+ this.temporalTracking = enabled;
553
+ if (!enabled) {
554
+ this.dirtySinceSubmitted.clear();
555
+ return;
556
+ }
557
+ for (const slot of this.orderedSlots) this.dirtySinceSubmitted.add(slot);
558
+ }
559
+
560
+ materialize(): RenderableSnapshot[] {
464
561
  if (this.materialized !== undefined) return this.materialized;
465
562
  const snapshots: RenderableSnapshot[] = [];
466
563
  for (const slot of this.orderedSlots) {
467
564
  const record = this.slots[slot];
468
565
  if (record !== undefined) snapshots.push(record.snapshot);
469
566
  }
470
- this.materialized = Object.freeze(snapshots);
567
+ this.materialized = snapshots;
471
568
  return this.materialized;
472
569
  }
473
570
 
@@ -513,6 +610,15 @@ export class RenderScene {
513
610
  }
514
611
  for (const update of capture.slots) {
515
612
  this.lastSubmittedSlots[update.slot] = update.current;
613
+ const currentWorld = update.current?.snapshot.transform.world;
614
+ if (currentWorld !== undefined) {
615
+ let previousWorld = this.previousWorldBySlot[update.slot];
616
+ if (previousWorld === undefined) {
617
+ previousWorld = new Float32Array(16);
618
+ this.previousWorldBySlot[update.slot] = previousWorld;
619
+ }
620
+ previousWorld.set(currentWorld);
621
+ }
516
622
  this.dirtySinceSubmitted.delete(update.slot);
517
623
  }
518
624
  this.lastSubmittedVisible = new Set(capture.visible);
@@ -575,8 +681,7 @@ export class RenderScene {
575
681
  for (const slot of this.orderedSlots) {
576
682
  const record = this.slots[slot];
577
683
  if (record === undefined) continue;
578
- const snapshot = record.snapshot;
579
- const candidate = this.cullingWorldBounds(snapshot);
684
+ const candidate = this.cullingWorldBoundsAt(record);
580
685
  if (candidate === null || candidate === undefined) continue;
581
686
  if (intersects(candidate, bounds)) records.push(record);
582
687
  }
@@ -634,8 +739,15 @@ export class RenderScene {
634
739
  entityKey: snapshot.entityKey,
635
740
  snapshot: ownSnapshot(snapshot),
636
741
  };
742
+ let previousWorld = this.previousWorldBySlot[slot];
743
+ if (previousWorld === undefined) {
744
+ previousWorld = new Float32Array(16);
745
+ this.previousWorldBySlot[slot] = previousWorld;
746
+ }
747
+ previousWorld.set(record.snapshot.transform.world);
637
748
  this.generations[slot] = generation;
638
749
  this.slots[slot] = record;
750
+ this.worldBoundsBySlot[slot] = undefined;
639
751
  let entities = this.slotsByWorld.get(snapshot.worldId);
640
752
  if (entities === undefined) {
641
753
  entities = new Map<number, number>();
@@ -649,6 +761,7 @@ export class RenderScene {
649
761
 
650
762
  private release(record: RenderSceneSlot): void {
651
763
  this.slots[record.slot] = undefined;
764
+ this.worldBoundsBySlot[record.slot] = undefined;
652
765
  this.instanceBoundsCache.invalidate(record.entityKey, record.worldId);
653
766
  this.unindexMaterials(record);
654
767
  const entities = this.slotsByWorld.get(record.worldId);
@@ -711,12 +824,16 @@ export class RenderScene {
711
824
  reasons.includes('geometry-revision') ||
712
825
  reasons.includes('material-revision');
713
826
  const previous = seedCurrent || submitted === undefined ? slot.snapshot : submitted.snapshot;
827
+ const previousWorld = this.previousWorldBySlot[slot.slot];
714
828
  return {
715
829
  previousEpoch: seedCurrent ? undefined : this.submittedEpoch,
716
830
  previousSource: seedCurrent ? 'current-seed' : 'last-submitted',
717
831
  reactive: reasons.length > 0,
718
832
  reactiveReasons: reasons,
719
- previousTransform: previous.transform,
833
+ previousTransform:
834
+ seedCurrent || previousWorld === undefined
835
+ ? previous.transform
836
+ : { ...previous.transform, world: previousWorld },
720
837
  previousInstances: previous.instances,
721
838
  previousSkin: previous.skin,
722
839
  previousMorphWeights: previous.morph?.weights,
@@ -734,15 +851,14 @@ export class RenderScene {
734
851
  * bounds for its independent per-instance visibility pass. Unknown or
735
852
  * empty instance facts deliberately return undefined (conservative no-cull).
736
853
  */
737
- cullingWorldBounds(snapshot: RenderableSnapshot): RenderSceneBounds | undefined {
738
- let candidate = this.worldBoundsCache.get(snapshot);
739
- if (candidate !== undefined || this.worldBoundsCache.has(snapshot)) {
740
- return candidate ?? undefined;
741
- }
854
+ cullingWorldBoundsAt(slot: RenderSceneSlot): RenderSceneBounds | undefined {
855
+ let candidate = this.worldBoundsBySlot[slot.slot];
856
+ if (candidate !== undefined) return candidate ?? undefined;
857
+ const snapshot = slot.snapshot;
742
858
  if (snapshot.instances !== undefined) {
743
859
  const instanceCount = snapshot.instances.instanceCount;
744
860
  if (instanceCount === 0 || snapshot.localAabb === undefined) {
745
- this.worldBoundsCache.set(snapshot, null);
861
+ this.worldBoundsBySlot[slot.slot] = null;
746
862
  return undefined;
747
863
  }
748
864
  const derived = this.instanceBoundsCache.get({
@@ -757,28 +873,92 @@ export class RenderScene {
757
873
  });
758
874
  candidate = boundsFromArray(derived ?? []);
759
875
  } else {
760
- candidate = worldBounds(snapshot);
876
+ candidate = worldBounds(snapshot, candidate ?? undefined);
761
877
  }
762
- this.worldBoundsCache.set(snapshot, candidate ?? null);
878
+ this.worldBoundsBySlot[slot.slot] = candidate ?? null;
763
879
  return candidate;
764
880
  }
765
- }
766
881
 
767
- function readTransformWorld(world: World, entity: EntityHandle): Float32Array | undefined {
768
- const transform = world.get(entity, Transform);
769
- return transform.ok ? new Float32Array(transform.value.world) : undefined;
882
+ cullingWorldBounds(snapshot: RenderableSnapshot): RenderSceneBounds | undefined {
883
+ const slot = this.lookup(snapshot.worldId, snapshot.entityKey);
884
+ return slot === undefined ? worldBounds(snapshot) : this.cullingWorldBoundsAt(slot);
885
+ }
770
886
  }
771
887
 
772
888
  export interface PersistentRenderSceneOptions {
773
889
  readonly getDevice?: (() => RhiDevice) | undefined;
774
890
  readonly onGpuError?: ((error: RhiError) => void) | undefined;
775
891
  readonly onSharedRefMutation?: ((worldId: number, handle: number) => void) | undefined;
892
+ readonly configuredQueryBudget?: number | undefined;
776
893
  }
777
894
 
778
895
  export interface PersistentGpuDrivenState {
779
896
  readonly scene: GpuScene;
780
897
  readonly plan: SubmissionPlan;
781
898
  readonly slots: readonly RenderSceneSlot[];
899
+ /** Stable renderer-local World keys aligned with RenderSceneSlot.worldId. */
900
+ readonly worldKeys?: readonly number[];
901
+ }
902
+
903
+ export interface CpuDirectViewInput {
904
+ readonly view: ViewKey;
905
+ readonly primitives: readonly PrimitiveKey[];
906
+ }
907
+
908
+ export interface CpuDirectCandidate {
909
+ readonly view: ViewKey;
910
+ readonly primitive: PrimitiveKey;
911
+ readonly candidate: VisibilityCandidate;
912
+ }
913
+
914
+ /**
915
+ * The one renderer-owned submission projection for a display view.
916
+ *
917
+ * Occlusion completion updates the facet asynchronously, so extraction must
918
+ * retain every query candidate while record/prepare consume this projection.
919
+ * Keeping the dispatch reindex here prevents CPU direct and GPU indirect
920
+ * lanes from rebuilding a second visibility map with different identity
921
+ * semantics.
922
+ */
923
+ export interface PersistentVisibilityProjection {
924
+ readonly renderables: readonly RenderableSnapshot[];
925
+ readonly dispatch: readonly DispatchEntry[];
926
+ readonly activeEntityKeys: ReadonlySet<number>;
927
+ /** Monotonic owner revision for exactly this active-key projection. */
928
+ readonly activeEntityRevision: number;
929
+ readonly suppressed: number;
930
+ }
931
+
932
+ /** Project one selected candidate per view and primitive from the facet owner. */
933
+ export function projectCpuDirectCandidates(
934
+ facets: VisibilityFacetStore,
935
+ views: readonly CpuDirectViewInput[],
936
+ ): readonly CpuDirectCandidate[] {
937
+ const candidates: CpuDirectCandidate[] = [];
938
+ for (const { view, primitives } of views) {
939
+ for (const primitive of primitives) {
940
+ const candidate = facets.getCandidate(view, primitive);
941
+ if (candidate !== undefined) candidates.push({ view, primitive, candidate });
942
+ }
943
+ }
944
+ return candidates;
945
+ }
946
+
947
+ /** Detached GPU scene prepared from the retained CPU composition. */
948
+ export interface PersistentGpuDrivenCandidate {
949
+ readonly state: PersistentGpuDrivenState | undefined;
950
+ /** Candidate-owned scene root; never references the active GPU scene. */
951
+ createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown>;
952
+ publish(): void;
953
+ discard(): void;
954
+ /** Release the candidate scene even after it has crossed publication. */
955
+ release(): void;
956
+ }
957
+
958
+ export interface RenderSceneRecoveryRoot {
959
+ readonly generation: number;
960
+ readonly revision: number;
961
+ readonly visibleSlots: number;
782
962
  }
783
963
 
784
964
  type PersistentGpuSceneInspection = PersistentRenderSceneInspection['gpu'];
@@ -791,16 +971,29 @@ interface PersistentCompositionEntry {
791
971
  readonly projection: RenderScene;
792
972
  readonly topology: BatchTopology;
793
973
  readonly transmissionDemand: PersistentTransmissionDemandProjection;
794
- readonly readCursors: number[];
974
+ readonly structure: Pick<
975
+ ExtractedFrame,
976
+ | 'dispatch'
977
+ | 'visibilityStats'
978
+ | 'visibilitySnapshots'
979
+ | 'featureVisibilitySnapshots'
980
+ | 'hiddenEntityReports'
981
+ >;
982
+ /** Stable renderer-local World keys; unlike RenderableSnapshot.worldId they survive reorder. */
983
+ readonly worldKeys: readonly number[];
984
+ readonly readVersions: RenderReadVersion[];
795
985
  readonly leaseIdentities: readonly string[];
986
+ readonly transformQueries: readonly GlobalTransformChangeQuery[];
796
987
  catalogEpoch: number;
797
988
  readonly rigidOnly: boolean;
798
- gpuDrivenCoverageKey: string | undefined;
799
- gpuDrivenOwnsAll: boolean;
989
+ readonly probeProjection: ProbeBlendSceneProjection;
990
+ temporalDemanded: boolean;
991
+ gpuDrivenSceneRequired: boolean;
800
992
  }
801
993
 
802
994
  const RENDER_RELEVANT_COMPONENT_IDS = new Set([
803
995
  componentId(Transform),
996
+ componentId(GlobalTransform),
804
997
  componentId(ChildOf),
805
998
  componentId(MeshFilter),
806
999
  componentId(MeshRenderer),
@@ -811,7 +1004,9 @@ const RENDER_RELEVANT_COMPONENT_IDS = new Set([
811
1004
  componentId(Layer),
812
1005
  componentId(Visibility),
813
1006
  componentId(Camera),
1007
+ componentId(MotionBlur),
814
1008
  componentId(DirectionalLight),
1009
+ componentId(LightProbe),
815
1010
  componentId(PointLight),
816
1011
  componentId(PointLightShadow),
817
1012
  componentId(SpotLight),
@@ -822,12 +1017,20 @@ const RENDER_RELEVANT_COMPONENT_IDS = new Set([
822
1017
  componentId(VolumetricFog),
823
1018
  ]);
824
1019
 
825
- const NON_RENDERABLE_TRANSFORM_CONSUMERS = [
826
- Camera,
827
- DirectionalLight,
828
- PointLight,
829
- SpotLight,
830
- ] as const;
1020
+ function createGlobalTransformChangeQuery(world: World): GlobalTransformChangeQuery {
1021
+ const result = world.query({ read: [GlobalTransform], changed: [GlobalTransform] });
1022
+ if (!result.ok) throw result.error;
1023
+ const query = result.value as GlobalTransformChangeQuery;
1024
+ for (const _span of query.spans().unwrap()) {
1025
+ // The full rebuild consumed these values; establish the observation baseline.
1026
+ }
1027
+ return query;
1028
+ }
1029
+
1030
+ function frameDemandsTemporal(frame: ExtractedFrame): boolean {
1031
+ const camera = frame.cameras[0];
1032
+ return camera?.antialias === 'taa' || (camera?.motionBlur?.shutterAngle ?? 0) > 0;
1033
+ }
831
1034
 
832
1035
  function cameraFrusta(cameras: readonly CameraSnapshot[]): readonly Float32Array[] {
833
1036
  const planes: Float32Array[] = [];
@@ -845,8 +1048,8 @@ function cameraFrusta(cameras: readonly CameraSnapshot[]): readonly Float32Array
845
1048
  projection,
846
1049
  camera.orthoLeft,
847
1050
  camera.orthoRight,
848
- camera.orthoBottom,
849
1051
  camera.orthoTop,
1052
+ camera.orthoBottom,
850
1053
  camera.near,
851
1054
  camera.far,
852
1055
  );
@@ -864,15 +1067,23 @@ function cameraFrusta(cameras: readonly CameraSnapshot[]): readonly Float32Array
864
1067
  return planes;
865
1068
  }
866
1069
 
1070
+ let cullProjectedIndexScratch = new Int32Array(0);
1071
+
867
1072
  function cullPersistentFrame(
868
1073
  frame: ExtractedFrame,
869
1074
  candidates: readonly RenderableSnapshot[],
870
- boundsOf: (snapshot: RenderableSnapshot) => RenderSceneBounds | undefined = worldBounds,
1075
+ boundsOf: (snapshot: RenderableSnapshot, index: number) => RenderSceneBounds | undefined = (
1076
+ snapshot,
1077
+ ) => worldBounds(snapshot),
871
1078
  ): ExtractedFrame {
872
1079
  const planes = cameraFrusta(frame.cameras);
873
1080
  const visible: RenderableSnapshot[] = [];
874
- const projectedIndex = new Int32Array(candidates.length);
875
- projectedIndex.fill(-1);
1081
+ if (cullProjectedIndexScratch.length < candidates.length) {
1082
+ cullProjectedIndexScratch = new Int32Array(candidates.length);
1083
+ }
1084
+ const projectedIndex = cullProjectedIndexScratch;
1085
+ projectedIndex.fill(-1, 0, candidates.length);
1086
+ const worldAabb = box3.create();
876
1087
  let total = 0;
877
1088
  let culled = 0;
878
1089
 
@@ -880,10 +1091,9 @@ function cullPersistentFrame(
880
1091
  const candidate = candidates[index];
881
1092
  if (candidate === undefined) continue;
882
1093
  let isVisible = true;
883
- const candidateBounds = boundsOf(candidate);
1094
+ const candidateBounds = boundsOf(candidate, index);
884
1095
  if (candidateBounds !== undefined) {
885
1096
  total += 1;
886
- const worldAabb = box3.create();
887
1097
  worldAabb[0] = candidateBounds.min[0] ?? 0;
888
1098
  worldAabb[1] = candidateBounds.min[1] ?? 0;
889
1099
  worldAabb[2] = candidateBounds.min[2] ?? 0;
@@ -906,7 +1116,12 @@ function cullPersistentFrame(
906
1116
  continue;
907
1117
  }
908
1118
  projectedIndex[index] = visible.length;
909
- visible.push(candidate);
1119
+ // The culling candidate is the persistent projection snapshot used for
1120
+ // bounds, while `frame.renderables[index]` may carry frame-owned
1121
+ // attachments (for example the probe blend record). Preserve that
1122
+ // attached renderable in the output; pushing the bare candidate here
1123
+ // silently drops the object-level Probe ABI before record selection.
1124
+ visible.push(frame.renderables[index] ?? candidate);
910
1125
  }
911
1126
 
912
1127
  const dispatch: DispatchEntry[] = [];
@@ -924,8 +1139,33 @@ function cullPersistentFrame(
924
1139
  };
925
1140
  }
926
1141
 
1142
+ function attachProbeRecords(
1143
+ frame: ExtractedFrame,
1144
+ candidates: RenderableSnapshot[],
1145
+ slots: readonly RenderSceneSlot[],
1146
+ projection: ProbeBlendSceneProjection,
1147
+ ): ExtractedFrame {
1148
+ if (!projection.hasRecords()) {
1149
+ return frame.renderables === candidates ? frame : { ...frame, renderables: candidates };
1150
+ }
1151
+ const objectKeys = new Map<string, number>();
1152
+ for (const slot of slots)
1153
+ objectKeys.set(identityKey(slot.worldId, slot.snapshot.entityKey), slot.slot);
1154
+ const renderables = candidates.map((candidate) => {
1155
+ const slot = objectKeys.get(identityKey(candidate.worldId, candidate.entityKey));
1156
+ const record = slot === undefined ? undefined : projection.getRecord(slot);
1157
+ return record === undefined ? candidate : { ...candidate, probeBlendRecord: record };
1158
+ });
1159
+ return { ...frame, renderables };
1160
+ }
1161
+
927
1162
  /** Persistent scene projection used by the renderer's multi-World composition path. */
928
1163
  export class PersistentRenderScene {
1164
+ private readonly visibilityBudget: VisibilityBudget;
1165
+ private readonly visibilityFacets: VisibilityFacetStore;
1166
+ /** World identity is the author/runtime identity; array position is only a frame routing detail. */
1167
+ private readonly visibilityWorldKeys = new Map<string, number>();
1168
+ private nextVisibilityWorldKey = 0;
929
1169
  private readonly materialSnapshotCaches: MaterialSnapshotCachesByWorld = new WeakMap();
930
1170
  private fullRebuilds = 0;
931
1171
  private worldEntitiesScanned = 0;
@@ -942,28 +1182,300 @@ export class PersistentRenderScene {
942
1182
  private temporalCapture:
943
1183
  | { readonly projection: RenderScene; readonly capture: RenderSceneSubmissionCapture }
944
1184
  | undefined;
1185
+ /** Monotonic token for camera/dynamic frame facts consumed by visibility caches. */
1186
+ private frameEpoch = 0;
1187
+ private visibilityFacetFrame:
1188
+ | {
1189
+ readonly frameEpoch: number;
1190
+ readonly viewId: string;
1191
+ readonly renderables: readonly RenderableSnapshot[];
1192
+ }
1193
+ | undefined;
1194
+ private visibilityProjectionCache:
1195
+ | {
1196
+ readonly frameEpoch: number;
1197
+ readonly drawRevision: number;
1198
+ readonly viewId: string;
1199
+ readonly renderables: readonly RenderableSnapshot[];
1200
+ readonly dispatch: readonly DispatchEntry[];
1201
+ readonly projection: PersistentVisibilityProjection;
1202
+ }
1203
+ | undefined;
1204
+ private visibilityProjectionRevision = 0;
1205
+ private lastActiveEntityKeys: ReadonlySet<number> | undefined;
1206
+ /** Stable renderer-world/entity lookup; rebuilt only with composition topology. */
1207
+ private readonly stableSlotByEntity = new Map<number, RenderSceneSlot>();
945
1208
  private pointsLinesInspections: readonly PointsLinesInspection[] = [];
946
1209
  private readonly reflectionProbes = new ReflectionProbeProjection();
947
1210
 
948
- constructor(private readonly options: PersistentRenderSceneOptions = {}) {}
1211
+ constructor(private readonly options: PersistentRenderSceneOptions = {}) {
1212
+ this.visibilityBudget = createVisibilityBudget(options.configuredQueryBudget);
1213
+ this.visibilityFacets = new VisibilityFacetStore(this.visibilityBudget);
1214
+ }
949
1215
 
950
1216
  /** RenderScene owns the per-World material fact cache used during extraction. */
951
1217
  materialSnapshotCacheStore(): MaterialSnapshotCachesByWorld {
952
1218
  return this.materialSnapshotCaches;
953
1219
  }
954
1220
 
1221
+ /** The sole renderer-owned ViewKey x PrimitiveKey visibility state. */
1222
+ visibilityFacetStore(): VisibilityFacetStore {
1223
+ return this.visibilityFacets;
1224
+ }
1225
+
1226
+ visibilityBudgetValue(): VisibilityBudget {
1227
+ return this.visibilityBudget;
1228
+ }
1229
+
1230
+ private visibilityWorldKey(world: World | undefined, fallback: number): number {
1231
+ if (world === undefined) return fallback;
1232
+ const existing = this.visibilityWorldKeys.get(world.identity);
1233
+ if (existing !== undefined) return existing;
1234
+ const key = this.nextVisibilityWorldKey;
1235
+ this.nextVisibilityWorldKey += 1;
1236
+ this.visibilityWorldKeys.set(world.identity, key);
1237
+ return key;
1238
+ }
1239
+
1240
+ private worldKeyAt(worlds: readonly World[], worldId: number): number {
1241
+ return this.visibilityWorldKey(worlds[worldId], worldId);
1242
+ }
1243
+
1244
+ /** Stable World routing keys for the current worlds[] frame projection. */
1245
+ visibilityWorldKeysFor(worlds: readonly World[]): readonly number[] {
1246
+ return worlds.map((world, worldId) => this.visibilityWorldKey(world, worldId));
1247
+ }
1248
+
1249
+ /** Current retained slots, available to CPU query transport as well as GPU. */
1250
+ compositionSlots(): readonly RenderSceneSlot[] {
1251
+ return this.composition?.projection.slotsSnapshot() ?? [];
1252
+ }
1253
+
1254
+ /** Stable renderer-world/entity lookup shared by query and final projection paths. */
1255
+ compositionSlotByStableEntity(): ReadonlyMap<number, RenderSceneSlot> {
1256
+ return this.stableSlotByEntity;
1257
+ }
1258
+
1259
+ private stableEntityKey(
1260
+ worlds: readonly World[],
1261
+ renderable: Pick<RenderableSnapshot, 'worldId' | 'entityKey'>,
1262
+ ): number {
1263
+ return worldEntityKey(this.worldKeyAt(worlds, renderable.worldId), renderable.entityKey);
1264
+ }
1265
+
1266
+ /** Feed the facet from extracted renderables using persistent slot identity. */
1267
+ updateVisibilityFacet(
1268
+ worlds: readonly World[],
1269
+ camera: CameraSnapshot | undefined,
1270
+ renderables: readonly RenderableSnapshot[],
1271
+ ): void {
1272
+ const world = worlds[camera?.worldId ?? 0];
1273
+ if (world === undefined || camera === undefined) return;
1274
+ const view = viewKey({
1275
+ attachmentId: world.identity,
1276
+ cameraEntity: camera.entityKey ?? 0,
1277
+ viewRole: 'main',
1278
+ viewGeneration: camera.historyVersion ?? 0,
1279
+ });
1280
+ const viewId = viewKeyId(view);
1281
+ const previous = this.visibilityFacetFrame;
1282
+ if (
1283
+ previous?.frameEpoch === this.frameEpoch &&
1284
+ previous.viewId === viewId &&
1285
+ previous.renderables === renderables
1286
+ ) {
1287
+ return;
1288
+ }
1289
+ this.visibilityFacets.activateView(view);
1290
+ const activePrimitiveIds = new Set<string>();
1291
+ for (const renderable of renderables) {
1292
+ const bounds = renderable.localAabb;
1293
+ if (
1294
+ renderable.lods === undefined ||
1295
+ renderable.lods.length === 0 ||
1296
+ bounds === undefined ||
1297
+ bounds.length < 6 ||
1298
+ [...bounds].some((value) => !Number.isFinite(value))
1299
+ ) {
1300
+ continue;
1301
+ }
1302
+ const slot = this.stableSlotByEntity.get(this.stableEntityKey(worlds, renderable));
1303
+ if (slot === undefined) continue;
1304
+ const primitive = primitiveKey({
1305
+ attachmentId: world.identity,
1306
+ worldGeneration: this.worldKeyAt(worlds, renderable.worldId),
1307
+ primitiveSlot: slot.slot,
1308
+ slotGeneration: slot.generation,
1309
+ });
1310
+ activePrimitiveIds.add(primitiveKeyId(primitive));
1311
+ this.visibilityFacets.setCandidate(view, primitive, { level: 0, confidence: 1 });
1312
+ }
1313
+ this.visibilityFacets.pruneCandidates(view, activePrimitiveIds);
1314
+ this.visibilityFacetFrame = { frameEpoch: this.frameEpoch, viewId, renderables };
1315
+ }
1316
+
1317
+ /**
1318
+ * Project the current facet into the final primary-raster submission.
1319
+ *
1320
+ * Only renderables carrying authored lower-detail levels are queryable. A
1321
+ * plain mesh therefore remains on the ordinary visibility path, while an
1322
+ * LOD candidate with two accepted zero-sample results is suppressed until
1323
+ * the confidence scheduler requests its bounded re-test. Unknown identity,
1324
+ * invalid bounds, and non-queryable confidence all remain conservatively
1325
+ * visible through `decideVisibility`.
1326
+ */
1327
+ projectVisibility(
1328
+ worlds: readonly World[],
1329
+ camera: CameraSnapshot | undefined,
1330
+ renderables: readonly RenderableSnapshot[],
1331
+ dispatch: readonly DispatchEntry[],
1332
+ ): PersistentVisibilityProjection {
1333
+ if (camera === undefined) {
1334
+ const activeEntityKeys = new Set(
1335
+ renderables.map((renderable) => this.stableEntityKey(worlds, renderable)),
1336
+ );
1337
+ return {
1338
+ renderables,
1339
+ dispatch,
1340
+ activeEntityKeys,
1341
+ activeEntityRevision: this.activeEntityRevision(activeEntityKeys),
1342
+ suppressed: 0,
1343
+ };
1344
+ }
1345
+ const cameraWorld = worlds[camera.worldId ?? 0];
1346
+ if (cameraWorld === undefined) {
1347
+ const activeEntityKeys = new Set(
1348
+ renderables.map((renderable) => this.stableEntityKey(worlds, renderable)),
1349
+ );
1350
+ return {
1351
+ renderables,
1352
+ dispatch,
1353
+ activeEntityKeys,
1354
+ activeEntityRevision: this.activeEntityRevision(activeEntityKeys),
1355
+ suppressed: 0,
1356
+ };
1357
+ }
1358
+ const view = viewKey({
1359
+ attachmentId: cameraWorld.identity,
1360
+ cameraEntity: camera.entityKey ?? 0,
1361
+ viewRole: 'main',
1362
+ viewGeneration: camera.historyVersion ?? 0,
1363
+ });
1364
+ const viewId = viewKeyId(view);
1365
+ const cached = this.visibilityProjectionCache;
1366
+ if (
1367
+ cached?.frameEpoch === this.frameEpoch &&
1368
+ cached.drawRevision === this.visibilityFacets.drawRevisionValue &&
1369
+ cached.viewId === viewId &&
1370
+ cached.renderables === renderables &&
1371
+ cached.dispatch === dispatch
1372
+ ) {
1373
+ return cached.projection;
1374
+ }
1375
+ const projectedIndex = new Int32Array(renderables.length);
1376
+ projectedIndex.fill(-1);
1377
+ const projected: RenderableSnapshot[] = [];
1378
+ const activeEntityKeys = new Set<number>();
1379
+ let suppressed = 0;
1380
+ for (let index = 0; index < renderables.length; index += 1) {
1381
+ const renderable = renderables[index];
1382
+ if (renderable === undefined) continue;
1383
+ const slot = this.stableSlotByEntity.get(this.stableEntityKey(worlds, renderable));
1384
+ let draw = true;
1385
+ if (slot !== undefined && (renderable.lods?.length ?? 0) > 0) {
1386
+ const primitive = primitiveKey({
1387
+ attachmentId: cameraWorld.identity,
1388
+ worldGeneration: this.worldKeyAt(worlds, renderable.worldId),
1389
+ primitiveSlot: slot.slot,
1390
+ slotGeneration: slot.generation,
1391
+ });
1392
+ const bounds = renderable.localAabb;
1393
+ const validBounds =
1394
+ bounds !== undefined &&
1395
+ bounds.length >= 6 &&
1396
+ [...bounds].every((value) => Number.isFinite(value));
1397
+ draw = decideVisibility({
1398
+ authorVisible: true,
1399
+ validBounds,
1400
+ frustumVisible: true,
1401
+ lodReady: true,
1402
+ occlusion: this.visibilityFacets.getConfidence(view, primitive),
1403
+ lane: 'gpu',
1404
+ }).draw;
1405
+ }
1406
+ if (!draw) {
1407
+ suppressed += 1;
1408
+ continue;
1409
+ }
1410
+ projectedIndex[index] = projected.length;
1411
+ projected.push(renderable);
1412
+ activeEntityKeys.add(this.stableEntityKey(worlds, renderable));
1413
+ }
1414
+ const projectedDispatch: DispatchEntry[] = [];
1415
+ for (const entry of dispatch) {
1416
+ const renderableIndex = projectedIndex[entry.renderableIndex];
1417
+ if (renderableIndex === undefined || renderableIndex < 0) continue;
1418
+ projectedDispatch.push({ ...entry, renderableIndex });
1419
+ }
1420
+ const projection = {
1421
+ renderables: Object.freeze(projected),
1422
+ dispatch: Object.freeze(projectedDispatch),
1423
+ activeEntityKeys,
1424
+ activeEntityRevision: this.activeEntityRevision(activeEntityKeys),
1425
+ suppressed,
1426
+ };
1427
+ this.visibilityProjectionCache = {
1428
+ frameEpoch: this.frameEpoch,
1429
+ drawRevision: this.visibilityFacets.drawRevisionValue,
1430
+ viewId,
1431
+ renderables,
1432
+ dispatch,
1433
+ projection,
1434
+ };
1435
+ return projection;
1436
+ }
1437
+
1438
+ /**
1439
+ * Advance the projection revision only when the active stable-entity set
1440
+ * changes. Camera fingerprints and scene-plan identity cover view/topology
1441
+ * changes; keeping this token set-stable preserves the GPU filtered-plan
1442
+ * cache on ordinary no-change frames.
1443
+ */
1444
+ private activeEntityRevision(activeEntityKeys: ReadonlySet<number>): number {
1445
+ const previous = this.lastActiveEntityKeys;
1446
+ if (
1447
+ previous === undefined ||
1448
+ previous.size !== activeEntityKeys.size ||
1449
+ [...previous].some((key) => !activeEntityKeys.has(key))
1450
+ ) {
1451
+ this.lastActiveEntityKeys = new Set(activeEntityKeys);
1452
+ this.visibilityProjectionRevision += 1;
1453
+ }
1454
+ return this.visibilityProjectionRevision;
1455
+ }
1456
+
1457
+ private invalidateActiveEntityRevision(): void {
1458
+ this.lastActiveEntityKeys = undefined;
1459
+ this.visibilityProjectionRevision += 1;
1460
+ }
1461
+
955
1462
  /** Project probe facts and per-primitive selection into the persistent scene owner. */
956
1463
  projectReflectionProbes(probes: readonly ReflectionProbeFact[]): void {
957
1464
  const projection = this.composition?.projection;
958
1465
  if (projection === undefined) return;
1466
+ if (probes.length === 0) {
1467
+ this.reflectionProbes.update([], []);
1468
+ return;
1469
+ }
959
1470
  const primitives = projection
960
1471
  .slotsSnapshot()
961
1472
  .map((slot) => {
962
- const resolved = worldBounds(slot.snapshot);
1473
+ const resolved = projection.cullingWorldBoundsAt(slot);
963
1474
  if (resolved === undefined || resolved === null) return undefined;
964
1475
  return {
965
1476
  worldId: slot.worldId,
966
1477
  entityKey: slot.entityKey,
1478
+ renderableKey: `${slot.worldId}:${slot.entityKey}`,
967
1479
  center: [
968
1480
  (resolved.min[0] + resolved.max[0]) / 2,
969
1481
  (resolved.min[1] + resolved.max[1]) / 2,
@@ -987,11 +1499,32 @@ export class PersistentRenderScene {
987
1499
  worlds: readonly World[],
988
1500
  owner: { readonly cameraOwner: number; readonly resourceOwner: number },
989
1501
  catalogEpoch: number,
990
- buildCandidateFrame: () => ExtractedFrame,
1502
+ buildCandidateFrame: (renderables: 'full' | 'none') => ExtractedFrame,
991
1503
  leases?: readonly RenderReadLease[],
992
1504
  ): ExtractedFrame {
993
1505
  this.worldEntitiesScanned = 0;
994
1506
  const entry = this.composition;
1507
+ const sameWorldSet =
1508
+ entry !== undefined &&
1509
+ entry.worlds.length === worlds.length &&
1510
+ entry.worlds.every((world) => worlds.includes(world));
1511
+ const worldOrderChanged =
1512
+ entry !== undefined &&
1513
+ sameWorldSet &&
1514
+ entry.worlds.some((world, index) => world !== worlds[index]);
1515
+ const leaseSetPreserved =
1516
+ entry !== undefined &&
1517
+ leases !== undefined &&
1518
+ leases.length === worlds.length &&
1519
+ entry.leaseIdentities.length === leases.length &&
1520
+ entry.leaseIdentities.every((identity) =>
1521
+ leases.some((lease) => lease.worldIdentity === identity),
1522
+ );
1523
+ const preserveVisibilityOnReorder =
1524
+ entry !== undefined &&
1525
+ worldOrderChanged &&
1526
+ entry.catalogEpoch === catalogEpoch &&
1527
+ leaseSetPreserved;
995
1528
  if (
996
1529
  entry === undefined ||
997
1530
  entry.catalogEpoch !== catalogEpoch ||
@@ -1002,13 +1535,19 @@ export class PersistentRenderScene {
1002
1535
  leases === undefined ||
1003
1536
  leases.length !== worlds.length
1004
1537
  ) {
1005
- return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
1538
+ return this.rebuildComposition(
1539
+ worlds,
1540
+ owner,
1541
+ catalogEpoch,
1542
+ buildCandidateFrame,
1543
+ leases,
1544
+ preserveVisibilityOnReorder,
1545
+ );
1006
1546
  }
1007
1547
  if (!entry.rigidOnly) {
1008
1548
  return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
1009
1549
  }
1010
1550
 
1011
- const operations: RenderSceneOperation[] = [];
1012
1551
  let requiresRebuild = false;
1013
1552
  for (let worldId = 0; worldId < worlds.length; worldId += 1) {
1014
1553
  const world = worlds[worldId];
@@ -1018,14 +1557,19 @@ export class PersistentRenderScene {
1018
1557
  requiresRebuild = true;
1019
1558
  break;
1020
1559
  }
1560
+ const version = entry.readVersions[worldId];
1561
+ if (version === undefined) {
1562
+ requiresRebuild = true;
1563
+ break;
1564
+ }
1021
1565
  let read: ReturnType<RenderReadLease['readChanges']>;
1022
1566
  try {
1023
- read = lease.readChanges(entry.readCursors[worldId] ?? 0);
1567
+ read = lease.readChanges(version);
1024
1568
  } catch {
1025
1569
  requiresRebuild = true;
1026
1570
  break;
1027
1571
  }
1028
- if (read.status === 'overflow') {
1572
+ if (read.status !== 'ok') {
1029
1573
  requiresRebuild = true;
1030
1574
  break;
1031
1575
  }
@@ -1038,119 +1582,86 @@ export class PersistentRenderScene {
1038
1582
  requiresRebuild = true;
1039
1583
  break;
1040
1584
  }
1041
- const derivedTransforms = new Set<EntityHandle>();
1042
- for (const record of changes.records) {
1585
+ for (const changedComponentId of changes.changedComponentIds) {
1586
+ if (!RENDER_RELEVANT_COMPONENT_IDS.has(changedComponentId)) continue;
1043
1587
  if (
1044
- record.kind === 'derived-component-changed' &&
1045
- record.componentId === componentId(Transform)
1588
+ changedComponentId !== componentId(Transform) &&
1589
+ changedComponentId !== componentId(GlobalTransform)
1046
1590
  ) {
1047
- derivedTransforms.add(record.entity as EntityHandle);
1048
- }
1049
- }
1050
- for (const record of changes.records) {
1051
- if (
1052
- record.kind !== 'entity-removed' &&
1053
- record.componentId !== undefined &&
1054
- !RENDER_RELEVANT_COMPONENT_IDS.has(record.componentId)
1055
- ) {
1056
- continue;
1057
- }
1058
- const authoredTransform =
1059
- record.kind === 'component-changed' && record.componentId === componentId(Transform);
1060
- const derivedTransform =
1061
- record.kind === 'derived-component-changed' &&
1062
- record.componentId === componentId(Transform);
1063
- if (derivedTransform) continue;
1064
- if (authoredTransform && derivedTransforms.has(record.entity as EntityHandle)) continue;
1065
- if (
1066
- (authoredTransform || record.componentId === componentId(Transform)) &&
1067
- entry.projection.has(worldId, record.entity)
1068
- ) {
1069
- const worldView = readTransformWorld(world, record.entity as EntityHandle);
1070
- if (worldView === undefined) {
1071
- requiresRebuild = true;
1072
- break;
1073
- }
1074
- operations.push({
1075
- kind: 'update-transform',
1076
- worldId,
1077
- entityKey: record.entity,
1078
- world: new Float32Array(worldView),
1079
- });
1080
- continue;
1591
+ requiresRebuild = true;
1592
+ break;
1081
1593
  }
1082
- requiresRebuild = true;
1083
- break;
1084
1594
  }
1085
- if (!requiresRebuild) {
1086
- for (const entity of derivedTransforms) {
1087
- if (entry.projection.has(worldId, entity)) {
1088
- const worldView = readTransformWorld(world, entity);
1089
- if (worldView === undefined) {
1090
- requiresRebuild = true;
1091
- break;
1092
- }
1093
- operations.push({
1094
- kind: 'update-transform',
1095
- worldId,
1096
- entityKey: entity,
1097
- world: new Float32Array(worldView),
1098
- });
1099
- continue;
1100
- }
1101
- // Camera snapshots are rebuilt from the current candidate frame and
1102
- // drive culling there; moving a camera must not invalidate the
1103
- // retained scene projection or recreate its GPU tables.
1104
- if (
1105
- !world.hasComponent(entity, Camera) &&
1106
- NON_RENDERABLE_TRANSFORM_CONSUMERS.some((component) =>
1107
- world.hasComponent(entity, component),
1108
- )
1109
- ) {
1110
- requiresRebuild = true;
1111
- break;
1112
- }
1595
+ if (requiresRebuild) break;
1596
+ entry.readVersions[worldId] = read.version;
1597
+ }
1598
+ if (requiresRebuild) {
1599
+ return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
1600
+ }
1601
+ const transformSpans: WorldTransformSpan[] = [];
1602
+ for (let worldId = 0; worldId < entry.transformQueries.length; worldId += 1) {
1603
+ const query = entry.transformQueries[worldId];
1604
+ if (query === undefined) continue;
1605
+ for (const span of query.spans().unwrap()) {
1606
+ const changedWorlds = span.get(GlobalTransform).world;
1607
+ if (!(changedWorlds instanceof Float32Array)) {
1608
+ requiresRebuild = true;
1609
+ break;
1113
1610
  }
1611
+ transformSpans.push({ worldId, entities: span.entities, worlds: changedWorlds });
1114
1612
  }
1115
1613
  if (requiresRebuild) break;
1116
- entry.readCursors[worldId] = read.cursor;
1117
1614
  }
1118
1615
  if (requiresRebuild) {
1119
1616
  return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
1120
1617
  }
1121
- if (operations.length === 0) {
1618
+ if (transformSpans.length === 0) {
1122
1619
  this.noChangeFrames += 1;
1123
1620
  this.syncGpuOwner(entry.token, entry.projection);
1124
1621
  return this.deriveFramePlan(entry, buildCandidateFrame);
1125
1622
  }
1126
- const delta = entry.projection.apply(operations);
1623
+ const delta = entry.projection.applyTransformSpans(transformSpans);
1127
1624
  entry.topology.apply(delta);
1128
1625
  this.syncGpuOwner(entry.token, entry.projection, delta);
1129
1626
  this.deltaFrames += 1;
1130
- this.transformUpdates += operations.length;
1627
+ this.transformUpdates += delta.updated;
1131
1628
  return this.deriveFramePlan(entry, buildCandidateFrame);
1132
1629
  }
1133
1630
 
1134
1631
  invalidate(): void {
1135
1632
  this.composition = undefined;
1136
1633
  this.temporalCapture = undefined;
1634
+ this.visibilityFacets.clear();
1635
+ this.invalidateActiveEntityRevision();
1636
+ this.visibilityFacetFrame = undefined;
1637
+ this.visibilityProjectionCache = undefined;
1638
+ this.stableSlotByEntity.clear();
1137
1639
  this.lastResyncReason = 'explicit-invalidate';
1138
1640
  }
1139
1641
 
1140
- setCompositionGpuDrivenCoverage(key: string, ownsAll: boolean): void {
1642
+ setCompositionGpuSceneDemand(sceneRowsRequired: boolean): void {
1141
1643
  const entry = this.composition;
1142
1644
  if (entry === undefined) return;
1143
- entry.gpuDrivenCoverageKey = key;
1144
- entry.gpuDrivenOwnsAll = ownsAll;
1645
+ entry.gpuDrivenSceneRequired = sceneRowsRequired;
1145
1646
  }
1146
1647
 
1147
1648
  detach(world: World): void {
1649
+ this.visibilityFacets.detachAttachment(world.identity);
1148
1650
  this.materialSnapshotCaches.delete(world);
1149
1651
  const detachedComposition = this.composition?.worlds.includes(world)
1150
1652
  ? this.composition
1151
1653
  : undefined;
1152
1654
  if (detachedComposition !== undefined) this.composition = undefined;
1153
1655
  if (detachedComposition !== undefined) this.temporalCapture = undefined;
1656
+ if (detachedComposition !== undefined) {
1657
+ this.visibilityFacets.clear();
1658
+ this.invalidateActiveEntityRevision();
1659
+ }
1660
+ if (detachedComposition !== undefined) {
1661
+ this.visibilityFacetFrame = undefined;
1662
+ this.visibilityProjectionCache = undefined;
1663
+ this.stableSlotByEntity.clear();
1664
+ }
1154
1665
  if (this.gpuOwner !== world && this.gpuOwner !== detachedComposition?.token) return;
1155
1666
  this.gpuScene?.dispose();
1156
1667
  this.gpuScene = undefined;
@@ -1161,13 +1672,118 @@ export class PersistentRenderScene {
1161
1672
 
1162
1673
  /** Drop only device-owned tables; the CPU projection remains the recovery authority. */
1163
1674
  resetGpuForRecover(): void {
1164
- if (this.composition !== undefined) this.composition.gpuDrivenOwnsAll = false;
1675
+ if (this.composition !== undefined) this.composition.gpuDrivenSceneRequired = true;
1165
1676
  this.gpuScene = undefined;
1166
1677
  this.gpuOwner = undefined;
1167
1678
  this.gpuDevice = undefined;
1168
1679
  this.gpuStatus = this.options.getDevice === undefined ? 'inactive' : 'rebuild-pending';
1169
1680
  }
1170
1681
 
1682
+ /** Prepare a detached GPU scene without changing the active scene owner. */
1683
+ prepareRecoveryGpuDrivenCandidate(
1684
+ device: RhiDevice,
1685
+ ): Result<PersistentGpuDrivenCandidate, RhiError> {
1686
+ const composition = this.composition;
1687
+ if (composition === undefined) {
1688
+ return ok({
1689
+ state: undefined,
1690
+ createRecoveryRoot: (scope) => ({
1691
+ kind: 'scene-table',
1692
+ create: () => {
1693
+ if (!scope.isAlive()) throw new Error('GPU-driven candidate scope is not active.');
1694
+ throw new Error('GPU-driven candidate has no scene resource.');
1695
+ },
1696
+ cleanup: () => undefined,
1697
+ }),
1698
+ publish: () => undefined,
1699
+ discard: () => undefined,
1700
+ release: () => undefined,
1701
+ });
1702
+ }
1703
+ const created = GpuScene.create(
1704
+ device,
1705
+ Math.max(256, composition.projection.inspect().slotCapacity),
1706
+ );
1707
+ if (!created.ok) return created;
1708
+ if (created.value.status === 'unavailable') {
1709
+ return ok({
1710
+ state: undefined,
1711
+ createRecoveryRoot: (scope) => ({
1712
+ kind: 'scene-table',
1713
+ create: () => {
1714
+ if (!scope.isAlive()) throw new Error('GPU-driven candidate scope is not active.');
1715
+ throw new Error('GPU-driven candidate has no scene resource.');
1716
+ },
1717
+ cleanup: () => undefined,
1718
+ }),
1719
+ publish: () => undefined,
1720
+ discard: () => undefined,
1721
+ release: () => undefined,
1722
+ });
1723
+ }
1724
+ const scene = created.value.scene;
1725
+ const slots = composition.projection.slotsSnapshot();
1726
+ const rebuilt = scene.rebuild(slots);
1727
+ if (!rebuilt.ok) {
1728
+ scene.dispose();
1729
+ return rebuilt;
1730
+ }
1731
+ const state: PersistentGpuDrivenState = Object.freeze({
1732
+ scene,
1733
+ plan: composition.topology.plan(),
1734
+ slots,
1735
+ });
1736
+ let published = false;
1737
+ let released = false;
1738
+ return ok({
1739
+ state,
1740
+ createRecoveryRoot: (scope) => ({
1741
+ kind: 'scene-table',
1742
+ create: () => {
1743
+ if (!scope.isAlive()) throw new Error('GPU-driven candidate scope is not active.');
1744
+ return scene;
1745
+ },
1746
+ cleanup: () => undefined,
1747
+ }),
1748
+ publish: () => {
1749
+ if (published || released) return;
1750
+ published = true;
1751
+ this.gpuScene = scene;
1752
+ this.gpuOwner = composition.token;
1753
+ this.gpuDevice = device;
1754
+ this.gpuStatus = 'resident';
1755
+ },
1756
+ discard: () => {
1757
+ if (published || released) return;
1758
+ released = true;
1759
+ scene.dispose();
1760
+ },
1761
+ release: () => {
1762
+ if (published || released) return;
1763
+ released = true;
1764
+ scene.dispose();
1765
+ },
1766
+ });
1767
+ }
1768
+
1769
+ createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown> {
1770
+ return {
1771
+ kind: 'scene-table',
1772
+ create: () => {
1773
+ const composition = this.composition;
1774
+ if (!scope.isAlive() || composition === undefined) {
1775
+ throw new Error('RenderScene CPU projection is not ready for recovery.');
1776
+ }
1777
+ return Object.freeze({
1778
+ generation: scope.generation,
1779
+ revision: composition.projection.inspect().revision,
1780
+ visibleSlots: composition.projection.slotsSnapshot().length,
1781
+ });
1782
+ },
1783
+ cleanup: () => undefined,
1784
+ };
1785
+ }
1786
+
1171
1787
  setPointsLinesInspections(inspections: readonly PointsLinesInspection[]): void {
1172
1788
  this.pointsLinesInspections = inspections.map((inspection) => ({
1173
1789
  ...inspection,
@@ -1184,6 +1800,11 @@ export class PersistentRenderScene {
1184
1800
  this.gpuStatus = 'inactive';
1185
1801
  this.composition = undefined;
1186
1802
  this.temporalCapture = undefined;
1803
+ this.visibilityFacets.clear();
1804
+ this.invalidateActiveEntityRevision();
1805
+ this.visibilityFacetFrame = undefined;
1806
+ this.visibilityProjectionCache = undefined;
1807
+ this.stableSlotByEntity.clear();
1187
1808
  this.pointsLinesInspections = [];
1188
1809
  }
1189
1810
 
@@ -1198,6 +1819,7 @@ export class PersistentRenderScene {
1198
1819
  transformUpdates: this.transformUpdates,
1199
1820
  lastResyncReason: this.lastResyncReason,
1200
1821
  projectionRecords: entry?.projection.inspect().records.length ?? 0,
1822
+ ...(entry === undefined ? {} : { probeBlend: entry.probeProjection.inspect() }),
1201
1823
  topology:
1202
1824
  entry?.topology.inspect() ??
1203
1825
  ({
@@ -1222,12 +1844,15 @@ export class PersistentRenderScene {
1222
1844
  scene: this.gpuScene,
1223
1845
  plan: entry.topology.plan(),
1224
1846
  slots: entry.projection.slotsSnapshot(),
1847
+ worldKeys: entry.worldKeys,
1225
1848
  };
1226
1849
  }
1227
1850
 
1228
1851
  /** Publish renderer-owned previous transforms after queue submission. */
1229
1852
  commitTemporalFrame(): Result<void, RhiError> {
1230
- const gpuCommit = this.gpuScene?.commitTemporalFrame();
1853
+ // Previous transforms are consumed only by TAA and motion blur. Ordinary
1854
+ // frames must not copy and upload the complete GPU transform table.
1855
+ const gpuCommit = this.gpuScene?.commitTemporalFrame(this.temporalCapture !== undefined);
1231
1856
  if (gpuCommit !== undefined && !gpuCommit.ok) return gpuCommit;
1232
1857
  const temporalCapture = this.temporalCapture;
1233
1858
  if (temporalCapture !== undefined) {
@@ -1279,19 +1904,76 @@ export class PersistentRenderScene {
1279
1904
  worlds: readonly World[],
1280
1905
  owner: { readonly cameraOwner: number; readonly resourceOwner: number },
1281
1906
  catalogEpoch: number,
1282
- buildCandidateFrame: () => ExtractedFrame,
1907
+ buildCandidateFrame: (renderables: 'full' | 'none') => ExtractedFrame,
1283
1908
  leases?: readonly RenderReadLease[],
1909
+ preserveVisibilityOnReorder = false,
1284
1910
  ): ExtractedFrame {
1285
- const candidateFrame = buildCandidateFrame();
1911
+ const previousEntry = this.composition;
1912
+ const candidateFrame = buildCandidateFrame('full');
1286
1913
  this.temporalCapture = undefined;
1287
1914
  this.worldEntitiesScanned = candidateFrame.renderables.length;
1915
+ const worldKeys = this.visibilityWorldKeysFor(worlds);
1916
+ if (!preserveVisibilityOnReorder) {
1917
+ this.visibilityFacets.clear();
1918
+ this.invalidateActiveEntityRevision();
1919
+ } else if (previousEntry !== undefined) {
1920
+ const previousKeys = new Set(
1921
+ previousEntry.projection
1922
+ .slotsSnapshot()
1923
+ .map((slot) =>
1924
+ worldEntityKey(previousEntry.worldKeys[slot.worldId] ?? slot.worldId, slot.entityKey),
1925
+ ),
1926
+ );
1927
+ const currentKeys = new Set(
1928
+ candidateFrame.renderables.map((renderable) =>
1929
+ worldEntityKey(worldKeys[renderable.worldId] ?? renderable.worldId, renderable.entityKey),
1930
+ ),
1931
+ );
1932
+ if (
1933
+ previousKeys.size !== currentKeys.size ||
1934
+ [...previousKeys].some((key) => !currentKeys.has(key))
1935
+ ) {
1936
+ this.visibilityFacets.clear();
1937
+ this.invalidateActiveEntityRevision();
1938
+ }
1939
+ }
1940
+ // Slot allocation is renderer-owned identity. Sort by stable World identity
1941
+ // before allocating so a worlds[] reorder does not silently change a
1942
+ // primitive's slot/generation. Dispatch is remapped below to the sorted
1943
+ // renderable projection and remains frame-order independent.
1944
+ const sorted = candidateFrame.renderables
1945
+ .map((snapshot, sourceIndex) => ({ snapshot, sourceIndex }))
1946
+ .sort((left, right) => {
1947
+ const leftWorld = worldKeys[left.snapshot.worldId] ?? left.snapshot.worldId;
1948
+ const rightWorld = worldKeys[right.snapshot.worldId] ?? right.snapshot.worldId;
1949
+ return leftWorld - rightWorld || left.snapshot.entityKey - right.snapshot.entityKey;
1950
+ });
1951
+ const sortedRenderables = sorted.map(({ snapshot }) => snapshot);
1952
+ const renderableIndex = new Int32Array(candidateFrame.renderables.length);
1953
+ for (let index = 0; index < sorted.length; index += 1) {
1954
+ const sourceIndex = sorted[index]?.sourceIndex;
1955
+ if (sourceIndex !== undefined) renderableIndex[sourceIndex] = index;
1956
+ }
1957
+ const sortedDispatch = candidateFrame.dispatch.flatMap((entry) => {
1958
+ const index = renderableIndex[entry.renderableIndex];
1959
+ return index === undefined ? [] : [{ ...entry, renderableIndex: index }];
1960
+ });
1961
+ const stableCandidateFrame = {
1962
+ ...candidateFrame,
1963
+ renderables: sortedRenderables,
1964
+ dispatch: sortedDispatch,
1965
+ };
1288
1966
  const projection = new RenderScene();
1289
- projection.reset(candidateFrame.renderables);
1967
+ projection.reset(sortedRenderables);
1968
+ this.stableSlotByEntity.clear();
1969
+ for (const slot of projection.slotsSnapshot()) {
1970
+ const stableWorld = worldKeys[slot.worldId] ?? slot.worldId;
1971
+ this.stableSlotByEntity.set(worldEntityKey(stableWorld, slot.entityKey), slot);
1972
+ }
1290
1973
  const transmissionDemand = new PersistentTransmissionDemandProjection();
1291
1974
  transmissionDemand.apply(transmissionDemandOperations(candidateFrame.renderables));
1292
1975
  const topology = new BatchTopology();
1293
1976
  topology.rebuild(projection.slotsSnapshot());
1294
- this.projectReflectionProbes(candidateFrame.reflectionProbes ?? []);
1295
1977
  const token = {};
1296
1978
  const entry: PersistentCompositionEntry = {
1297
1979
  token,
@@ -1301,31 +1983,69 @@ export class PersistentRenderScene {
1301
1983
  projection,
1302
1984
  topology,
1303
1985
  transmissionDemand,
1304
- readCursors:
1986
+ structure: {
1987
+ dispatch: stableCandidateFrame.dispatch,
1988
+ visibilityStats: stableCandidateFrame.visibilityStats,
1989
+ visibilitySnapshots: stableCandidateFrame.visibilitySnapshots,
1990
+ featureVisibilitySnapshots: stableCandidateFrame.featureVisibilitySnapshots,
1991
+ hiddenEntityReports: stableCandidateFrame.hiddenEntityReports,
1992
+ },
1993
+ worldKeys,
1994
+ readVersions:
1305
1995
  leases === undefined || leases.length !== worlds.length
1306
1996
  ? []
1307
- : leases.map((lease) => lease.inspectCursor()),
1997
+ : leases.map((lease) => lease.captureVersion()),
1308
1998
  leaseIdentities:
1309
1999
  leases === undefined || leases.length !== worlds.length
1310
2000
  ? []
1311
2001
  : leases.map((lease) => lease.worldIdentity),
2002
+ transformQueries:
2003
+ leases === undefined || leases.length !== worlds.length
2004
+ ? []
2005
+ : worlds.map(createGlobalTransformChangeQuery),
1312
2006
  catalogEpoch,
1313
2007
  rigidOnly: candidateFrame.renderables.every((renderable) => renderable.skin === undefined),
1314
- gpuDrivenCoverageKey: undefined,
1315
- gpuDrivenOwnsAll: false,
2008
+ probeProjection: new ProbeBlendSceneProjection(),
2009
+ temporalDemanded: frameDemandsTemporal(candidateFrame),
2010
+ gpuDrivenSceneRequired: true,
1316
2011
  };
1317
2012
  this.composition = entry;
2013
+ projection.setTemporalTracking(entry.temporalDemanded);
2014
+ // The projection must be installed before deriving probe primitives. On
2015
+ // the first composition rebuild `projectReflectionProbes` otherwise sees
2016
+ // no composition and silently drops every per-renderable selection,
2017
+ // leaving the producer on its neutral/no-demand path until an unrelated
2018
+ // later rebuild.
2019
+ this.projectReflectionProbes(candidateFrame.reflectionProbes ?? []);
2020
+ this.projectProbes(entry, candidateFrame);
1318
2021
  this.syncGpuOwner(token, projection, undefined, true);
1319
2022
  this.fullRebuilds += 1;
1320
2023
  this.lastResyncReason = 'attach';
1321
- const candidates = projection
1322
- .materialize()
1323
- .map((snapshot) => projection.withTemporal(snapshot));
1324
- return cullPersistentFrame(
1325
- { ...candidateFrame, renderables: [...candidates] },
2024
+ const materialized = projection.materialize();
2025
+ const candidates = entry.temporalDemanded
2026
+ ? materialized.map((snapshot) => projection.withTemporal(snapshot))
2027
+ : materialized;
2028
+ const frameWithProbes = attachProbeRecords(
2029
+ { ...stableCandidateFrame, renderables: candidates },
1326
2030
  candidates,
1327
- (snapshot) => projection.cullingWorldBounds(snapshot),
2031
+ projection.slotsSnapshot(),
2032
+ entry.probeProjection,
2033
+ );
2034
+ const slots = projection.slotsSnapshot();
2035
+ const result = cullPersistentFrame(
2036
+ frameWithProbes,
2037
+ frameWithProbes.renderables,
2038
+ (snapshot, index) => {
2039
+ const slot = slots[index];
2040
+ return slot === undefined
2041
+ ? projection.cullingWorldBounds(snapshot)
2042
+ : projection.cullingWorldBoundsAt(slot);
2043
+ },
1328
2044
  );
2045
+ this.frameEpoch += 1;
2046
+ this.visibilityFacetFrame = undefined;
2047
+ this.visibilityProjectionCache = undefined;
2048
+ return result;
1329
2049
  }
1330
2050
 
1331
2051
  /** Internal topology fact; the renderer does not expose demand as public API. */
@@ -1340,22 +2060,89 @@ export class PersistentRenderScene {
1340
2060
 
1341
2061
  /**
1342
2062
  * Derive an ephemeral frame plan from persistent scene facts. The scene
1343
- * retains identity/topology/cursors; no prior frame object is an authority.
2063
+ * retains identity/topology/read versions; no prior frame object is an authority.
1344
2064
  */
1345
2065
  private deriveFramePlan(
1346
2066
  entry: PersistentCompositionEntry,
1347
- buildCandidateFrame: () => ExtractedFrame,
2067
+ buildCandidateFrame: (renderables: 'full' | 'none') => ExtractedFrame,
1348
2068
  ): ExtractedFrame {
1349
- const candidateFrame = buildCandidateFrame();
2069
+ const resourceFrame = buildCandidateFrame('none');
2070
+ const candidateFrame: ExtractedFrame = { ...resourceFrame, ...entry.structure };
2071
+ entry.temporalDemanded = frameDemandsTemporal(candidateFrame);
2072
+ entry.projection.setTemporalTracking(entry.temporalDemanded);
2073
+ this.projectProbes(entry, candidateFrame);
1350
2074
  this.projectReflectionProbes(candidateFrame.reflectionProbes ?? []);
1351
- const candidates = entry.projection
1352
- .materialize()
1353
- .map((snapshot) => entry.projection.withTemporal(snapshot));
1354
- return cullPersistentFrame(
1355
- { ...candidateFrame, renderables: [...candidates] },
2075
+ const materialized = entry.projection.materialize();
2076
+ const candidates = entry.temporalDemanded
2077
+ ? materialized.map((snapshot) => entry.projection.withTemporal(snapshot))
2078
+ : materialized;
2079
+ const frameWithProbes = attachProbeRecords(
2080
+ { ...candidateFrame, renderables: candidates },
1356
2081
  candidates,
1357
- (snapshot) => entry.projection.cullingWorldBounds(snapshot),
2082
+ entry.projection.slotsSnapshot(),
2083
+ entry.probeProjection,
1358
2084
  );
2085
+ const slots = entry.projection.slotsSnapshot();
2086
+ const result = cullPersistentFrame(
2087
+ frameWithProbes,
2088
+ frameWithProbes.renderables,
2089
+ (snapshot, index) => {
2090
+ const slot = slots[index];
2091
+ return slot === undefined
2092
+ ? entry.projection.cullingWorldBounds(snapshot)
2093
+ : entry.projection.cullingWorldBoundsAt(slot);
2094
+ },
2095
+ );
2096
+ this.frameEpoch += 1;
2097
+ this.visibilityFacetFrame = undefined;
2098
+ this.visibilityProjectionCache = undefined;
2099
+ return result;
2100
+ }
2101
+
2102
+ private projectProbes(entry: PersistentCompositionEntry, frame: ExtractedFrame): void {
2103
+ const sky =
2104
+ frame.skylight === undefined
2105
+ ? {
2106
+ available: false,
2107
+ irradiance: [0, 0, 0] as const,
2108
+ fallbackReason: 'no-skylight',
2109
+ }
2110
+ : {
2111
+ available: true,
2112
+ identity: `skylight:${frame.skylight.entityHandle}`,
2113
+ sourceKey:
2114
+ frame.skylight.equirectHandle > 0
2115
+ ? `equirect:${frame.skylight.equirectHandle}`
2116
+ : `skylight:${frame.skylight.entityHandle}`,
2117
+ irradiance: [
2118
+ frame.skylight.color[0] * frame.skylight.intensity,
2119
+ frame.skylight.color[1] * frame.skylight.intensity,
2120
+ frame.skylight.color[2] * frame.skylight.intensity,
2121
+ ] as [number, number, number],
2122
+ };
2123
+ const probes = frame.lightProbes ?? [];
2124
+ // Sky is evaluated by the global environment path. With no local light
2125
+ // probes there can be no per-object ProbeBlendRecord, regardless of how
2126
+ // many dynamic objects moved this frame. Preserve the projection call so
2127
+ // a transition from probes to no probes still clears retained records.
2128
+ if (probes.length === 0) {
2129
+ entry.probeProjection.apply({ objects: [], probes, sky });
2130
+ return;
2131
+ }
2132
+ const objects: ProbeSceneObjectInput[] = entry.projection.slotsSnapshot().map((slot) => ({
2133
+ objectKey: slot.slot,
2134
+ generation: slot.generation,
2135
+ position: [
2136
+ slot.snapshot.transform.world[12] ?? 0,
2137
+ slot.snapshot.transform.world[13] ?? 0,
2138
+ slot.snapshot.transform.world[14] ?? 0,
2139
+ ],
2140
+ }));
2141
+ entry.probeProjection.apply({
2142
+ objects,
2143
+ probes,
2144
+ sky,
2145
+ });
1359
2146
  }
1360
2147
 
1361
2148
  private syncGpuOwner(
@@ -1364,6 +2151,7 @@ export class PersistentRenderScene {
1364
2151
  delta?: RenderSceneApplyResult,
1365
2152
  full = false,
1366
2153
  ): void {
2154
+ if (this.composition?.token === owner && !this.composition.gpuDrivenSceneRequired) return;
1367
2155
  const acquired = this.acquireGpuScene(owner, projection);
1368
2156
  if (acquired === undefined || acquired.rebuilt) return;
1369
2157
  const result = full