@forgeax/engine-render 0.1.20 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (645) hide show
  1. package/README.md +365 -29
  2. package/dist/assembly/backend-contract.d.ts +13 -0
  3. package/dist/assembly/backend-contract.d.ts.map +1 -1
  4. package/dist/assembly/device-feature-admission.d.ts +18 -0
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -0
  6. package/dist/assembly/directional-shadow-inspection.d.ts +38 -0
  7. package/dist/assembly/directional-shadow-inspection.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +30 -19
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/host-contract.d.ts +19 -0
  11. package/dist/assembly/host-contract.d.ts.map +1 -1
  12. package/dist/assembly/render-target-host.d.ts +46 -0
  13. package/dist/assembly/render-target-host.d.ts.map +1 -0
  14. package/dist/assets/render-asset-port.d.ts.map +1 -1
  15. package/dist/authoring.mjs +4 -1
  16. package/dist/authoring.mjs.map +1 -1
  17. package/dist/capture/cube-views.d.ts +21 -0
  18. package/dist/capture/cube-views.d.ts.map +1 -0
  19. package/dist/capture/scheduler.d.ts +54 -0
  20. package/dist/capture/scheduler.d.ts.map +1 -0
  21. package/dist/{chunk-PCHBPYML.mjs → chunk-4L3P6DT5.mjs} +2 -2
  22. package/dist/chunk-4L3P6DT5.mjs.map +1 -0
  23. package/dist/{chunk-VFPLTKCU.mjs → chunk-4NPUASZR.mjs} +15 -4
  24. package/dist/chunk-4NPUASZR.mjs.map +1 -0
  25. package/dist/chunk-DIYW3M5D.mjs +5085 -0
  26. package/dist/chunk-DIYW3M5D.mjs.map +1 -0
  27. package/dist/chunk-FBC4R44J.mjs +1003 -0
  28. package/dist/chunk-FBC4R44J.mjs.map +1 -0
  29. package/dist/{chunk-CDTWTHZR.mjs → chunk-FF7K3FSO.mjs} +474 -168
  30. package/dist/chunk-FF7K3FSO.mjs.map +1 -0
  31. package/dist/chunk-HZUPSYW7.mjs +1104 -0
  32. package/dist/chunk-HZUPSYW7.mjs.map +1 -0
  33. package/dist/{chunk-TBQJOK2L.mjs → chunk-QODA5UKU.mjs} +321 -269
  34. package/dist/chunk-QODA5UKU.mjs.map +1 -0
  35. package/dist/chunk-YALPS44A.mjs +296 -0
  36. package/dist/chunk-YALPS44A.mjs.map +1 -0
  37. package/dist/{chunk-4XINDLAY.mjs → chunk-ZUWKAZ2I.mjs} +1963 -2178
  38. package/dist/chunk-ZUWKAZ2I.mjs.map +1 -0
  39. package/dist/cluster-binner.d.ts +5 -0
  40. package/dist/cluster-binner.d.ts.map +1 -1
  41. package/dist/components/camera.d.ts +1 -0
  42. package/dist/components/camera.d.ts.map +1 -1
  43. package/dist/components/cube-camera.d.ts +20 -0
  44. package/dist/components/cube-camera.d.ts.map +1 -0
  45. package/dist/components/directional-light.d.ts +11 -2
  46. package/dist/components/directional-light.d.ts.map +1 -1
  47. package/dist/components/directional-shadow-filter.d.ts +25 -0
  48. package/dist/components/directional-shadow-filter.d.ts.map +1 -0
  49. package/dist/components/index.d.ts +5 -0
  50. package/dist/components/index.d.ts.map +1 -1
  51. package/dist/components/light-helpers.d.ts +4 -0
  52. package/dist/components/light-helpers.d.ts.map +1 -1
  53. package/dist/components/light-probe.d.ts +9 -0
  54. package/dist/components/light-probe.d.ts.map +1 -0
  55. package/dist/components/rect-area-light.d.ts +12 -0
  56. package/dist/components/rect-area-light.d.ts.map +1 -0
  57. package/dist/components/reflection-probe.d.ts +17 -0
  58. package/dist/components/reflection-probe.d.ts.map +1 -0
  59. package/dist/components/scene-instance.d.ts +4 -0
  60. package/dist/components/scene-instance.d.ts.map +1 -1
  61. package/dist/components/spot-light.d.ts +3 -0
  62. package/dist/components/spot-light.d.ts.map +1 -1
  63. package/dist/construct-renderer.mjs +14836 -9620
  64. package/dist/construct-renderer.mjs.map +1 -1
  65. package/dist/device/gpu-residency.d.ts +8 -1
  66. package/dist/device/gpu-residency.d.ts.map +1 -1
  67. package/dist/device/lifecycle-transaction.d.ts +2 -0
  68. package/dist/device/lifecycle-transaction.d.ts.map +1 -0
  69. package/dist/environment/frame.d.ts.map +1 -1
  70. package/dist/environment/lifecycle.d.ts +5 -0
  71. package/dist/environment/lifecycle.d.ts.map +1 -1
  72. package/dist/errors/gpu-driven.d.ts +26 -0
  73. package/dist/errors/gpu-driven.d.ts.map +1 -0
  74. package/dist/errors/index.d.ts +1 -0
  75. package/dist/errors/index.d.ts.map +1 -1
  76. package/dist/errors/recover.d.ts +9 -1
  77. package/dist/errors/recover.d.ts.map +1 -1
  78. package/dist/errors/render.d.ts +166 -93
  79. package/dist/errors/render.d.ts.map +1 -1
  80. package/dist/extract/directional-shadow-projection.d.ts +23 -0
  81. package/dist/extract/directional-shadow-projection.d.ts.map +1 -0
  82. package/dist/extract/environment.d.ts +14 -0
  83. package/dist/extract/environment.d.ts.map +1 -1
  84. package/dist/extract/gpu-driven.d.ts +50 -0
  85. package/dist/extract/gpu-driven.d.ts.map +1 -0
  86. package/dist/extract/visibility.d.ts.map +1 -1
  87. package/dist/features/fullscreen.d.ts.map +1 -1
  88. package/dist/features/targets.d.ts.map +1 -1
  89. package/dist/features/types.d.ts +10 -0
  90. package/dist/features/types.d.ts.map +1 -1
  91. package/dist/fullscreen-post-process-pass.d.ts +13 -2
  92. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  93. package/dist/gpu-driven/batch-topology.d.ts +34 -0
  94. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  95. package/dist/gpu-driven/prepared-draw.d.ts +63 -0
  96. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -0
  97. package/dist/gpu-driven/production-raster.d.ts +97 -2
  98. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  99. package/dist/gpu-driven/view-gpu.d.ts +64 -1
  100. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  101. package/dist/gpu-scene-schema.d.ts +14 -0
  102. package/dist/gpu-scene-schema.d.ts.map +1 -1
  103. package/dist/gpu-scene.d.ts +8 -1
  104. package/dist/gpu-scene.d.ts.map +1 -1
  105. package/dist/gpu-usage.d.ts +0 -1
  106. package/dist/gpu-usage.d.ts.map +1 -1
  107. package/dist/hdrp-buffers.d.ts +21 -19
  108. package/dist/hdrp-buffers.d.ts.map +1 -1
  109. package/dist/ibl/IblPipelineCache.d.ts +4 -0
  110. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  111. package/dist/ibl/kernel-cache.d.ts +36 -0
  112. package/dist/ibl/kernel-cache.d.ts.map +1 -0
  113. package/dist/index.d.ts +39 -3
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.mjs +91 -6
  116. package/dist/index.mjs.map +1 -1
  117. package/dist/inspection-types.d.ts +214 -1
  118. package/dist/inspection-types.d.ts.map +1 -1
  119. package/dist/instances-derived-bounds.d.ts +50 -0
  120. package/dist/instances-derived-bounds.d.ts.map +1 -0
  121. package/dist/internal.d.ts +9 -0
  122. package/dist/internal.d.ts.map +1 -1
  123. package/dist/internal.mjs +23 -2
  124. package/dist/internal.mjs.map +1 -1
  125. package/dist/light-buffer-layout.d.ts +58 -140
  126. package/dist/light-buffer-layout.d.ts.map +1 -1
  127. package/dist/materials.d.ts.map +1 -1
  128. package/dist/mesh-material-bindings.d.ts +54 -0
  129. package/dist/mesh-material-bindings.d.ts.map +1 -1
  130. package/dist/pbr-pipeline.d.ts +38 -14
  131. package/dist/pbr-pipeline.d.ts.map +1 -1
  132. package/dist/pipeline/standard-forward-lane.d.ts +2 -1
  133. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  134. package/dist/pipeline/standard-lighting/errors.d.ts +4 -0
  135. package/dist/pipeline/standard-lighting/errors.d.ts.map +1 -0
  136. package/dist/pipeline/standard-lighting/graph.d.ts +24 -0
  137. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -0
  138. package/dist/pipeline/standard-lighting/inspection.d.ts +38 -0
  139. package/dist/pipeline/standard-lighting/inspection.d.ts.map +1 -0
  140. package/dist/pipeline/standard-lighting/layout.d.ts +28 -0
  141. package/dist/pipeline/standard-lighting/layout.d.ts.map +1 -0
  142. package/dist/pipeline/standard-lighting/light-frame.d.ts +33 -0
  143. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -0
  144. package/dist/pipeline/standard-lighting/membership.d.ts +14 -0
  145. package/dist/pipeline/standard-lighting/membership.d.ts.map +1 -0
  146. package/dist/pipeline/standard-lighting/prepare.d.ts +16 -0
  147. package/dist/pipeline/standard-lighting/prepare.d.ts.map +1 -0
  148. package/dist/pipeline/standard-lighting/topology.d.ts +45 -0
  149. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -0
  150. package/dist/pipeline/standard-lighting/transport.d.ts +36 -0
  151. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -0
  152. package/dist/pipeline/standard-pipeline.d.ts +4 -13
  153. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  154. package/dist/pipeline/standard-post.d.ts.map +1 -1
  155. package/dist/pipeline/standard-profile.d.ts +5 -26
  156. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  157. package/dist/pipeline-builder.d.ts +2 -0
  158. package/dist/pipeline-builder.d.ts.map +1 -1
  159. package/dist/pipeline-spec.d.ts +40 -7
  160. package/dist/pipeline-spec.d.ts.map +1 -1
  161. package/dist/plugin.d.ts.map +1 -1
  162. package/dist/point-shadow-inspection.d.ts +21 -0
  163. package/dist/point-shadow-inspection.d.ts.map +1 -0
  164. package/dist/prepare/extended-lighting/ltc-resources.d.ts +31 -0
  165. package/dist/prepare/extended-lighting/ltc-resources.d.ts.map +1 -0
  166. package/dist/prepare/extended-lighting/resources.d.ts +72 -0
  167. package/dist/prepare/extended-lighting/resources.d.ts.map +1 -0
  168. package/dist/prepare/extended-lighting/spot-modifiers.d.ts +42 -0
  169. package/dist/prepare/extended-lighting/spot-modifiers.d.ts.map +1 -0
  170. package/dist/prepare/extended-lighting/state.d.ts +24 -0
  171. package/dist/prepare/extended-lighting/state.d.ts.map +1 -0
  172. package/dist/record/frame-lighting.d.ts +33 -36
  173. package/dist/record/frame-lighting.d.ts.map +1 -1
  174. package/dist/record/frame-snapshot.d.ts +78 -24
  175. package/dist/record/frame-snapshot.d.ts.map +1 -1
  176. package/dist/record/frame-targets.d.ts +5 -0
  177. package/dist/record/frame-targets.d.ts.map +1 -1
  178. package/dist/record/frame.d.ts +16 -6
  179. package/dist/record/frame.d.ts.map +1 -1
  180. package/dist/record/gpu-pass-timing/contract.d.ts +96 -0
  181. package/dist/record/gpu-pass-timing/contract.d.ts.map +1 -0
  182. package/dist/record/gpu-pass-timing/errors.d.ts +18 -0
  183. package/dist/record/gpu-pass-timing/errors.d.ts.map +1 -0
  184. package/dist/record/gpu-pass-timing/index.d.ts +8 -0
  185. package/dist/record/gpu-pass-timing/index.d.ts.map +1 -0
  186. package/dist/record/gpu-pass-timing/parser.d.ts +11 -0
  187. package/dist/record/gpu-pass-timing/parser.d.ts.map +1 -0
  188. package/dist/record/gpu-pass-timing/session.d.ts +43 -0
  189. package/dist/record/gpu-pass-timing/session.d.ts.map +1 -0
  190. package/dist/record/gpu-timing.d.ts +20 -3
  191. package/dist/record/gpu-timing.d.ts.map +1 -1
  192. package/dist/record/helpers.d.ts +0 -20
  193. package/dist/record/helpers.d.ts.map +1 -1
  194. package/dist/record/main-pass-geometry.d.ts +6 -1
  195. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  196. package/dist/record/main-pass-material.d.ts +25 -2
  197. package/dist/record/main-pass-material.d.ts.map +1 -1
  198. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  199. package/dist/record/main-pass.d.ts +2 -0
  200. package/dist/record/main-pass.d.ts.map +1 -1
  201. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  202. package/dist/record/render-context.d.ts +98 -6
  203. package/dist/record/render-context.d.ts.map +1 -1
  204. package/dist/record/shadow-pass.d.ts.map +1 -1
  205. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  206. package/dist/record/typed-frame-graph.d.ts +119 -4
  207. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  208. package/dist/record/view-ubo.d.ts +7 -5
  209. package/dist/record/view-ubo.d.ts.map +1 -1
  210. package/dist/reflection/filter.d.ts +22 -0
  211. package/dist/reflection/filter.d.ts.map +1 -0
  212. package/dist/reflection/gpu-table.d.ts +25 -0
  213. package/dist/reflection/gpu-table.d.ts.map +1 -0
  214. package/dist/reflection/inspection.d.ts +10 -0
  215. package/dist/reflection/inspection.d.ts.map +1 -0
  216. package/dist/reflection/projection.d.ts +140 -0
  217. package/dist/reflection/projection.d.ts.map +1 -0
  218. package/dist/reflection/record-owner.d.ts +60 -0
  219. package/dist/reflection/record-owner.d.ts.map +1 -0
  220. package/dist/render-contract.d.ts +62 -14
  221. package/dist/render-contract.d.ts.map +1 -1
  222. package/dist/render-graph-primitives.d.ts +1 -0
  223. package/dist/render-graph-primitives.d.ts.map +1 -1
  224. package/dist/render-pipeline.d.ts +49 -2
  225. package/dist/render-pipeline.d.ts.map +1 -1
  226. package/dist/render-system-extract.d.ts +160 -31
  227. package/dist/render-system-extract.d.ts.map +1 -1
  228. package/dist/render-system.d.ts +28 -2
  229. package/dist/render-system.d.ts.map +1 -1
  230. package/dist/renderer.d.ts +43 -0
  231. package/dist/renderer.d.ts.map +1 -0
  232. package/dist/scene/probe-blend-record.d.ts +22 -0
  233. package/dist/scene/probe-blend-record.d.ts.map +1 -0
  234. package/dist/scene/probe-blend.d.ts +142 -0
  235. package/dist/scene/probe-blend.d.ts.map +1 -0
  236. package/dist/scene/render-scene-types.d.ts +0 -6
  237. package/dist/scene/render-scene-types.d.ts.map +1 -1
  238. package/dist/scene/render-scene.d.ts +117 -9
  239. package/dist/scene/render-scene.d.ts.map +1 -1
  240. package/dist/scene/visibility/bounds.d.ts +20 -0
  241. package/dist/scene/visibility/bounds.d.ts.map +1 -0
  242. package/dist/scene/visibility/budget.d.ts +16 -0
  243. package/dist/scene/visibility/budget.d.ts.map +1 -0
  244. package/dist/scene/visibility/facet.d.ts +73 -0
  245. package/dist/scene/visibility/facet.d.ts.map +1 -0
  246. package/dist/scene/visibility/gpu-frame-timing.d.ts +41 -0
  247. package/dist/scene/visibility/gpu-frame-timing.d.ts.map +1 -0
  248. package/dist/scene/visibility/gpu-lod.d.ts +46 -0
  249. package/dist/scene/visibility/gpu-lod.d.ts.map +1 -0
  250. package/dist/scene/visibility/inspection.d.ts +132 -0
  251. package/dist/scene/visibility/inspection.d.ts.map +1 -0
  252. package/dist/scene/visibility/lod-selector.d.ts +25 -0
  253. package/dist/scene/visibility/lod-selector.d.ts.map +1 -0
  254. package/dist/scene/visibility/occlusion-confidence.d.ts +81 -0
  255. package/dist/scene/visibility/occlusion-confidence.d.ts.map +1 -0
  256. package/dist/scene/visibility/occlusion-pass.d.ts +57 -0
  257. package/dist/scene/visibility/occlusion-pass.d.ts.map +1 -0
  258. package/dist/scene/visibility/occlusion-query-pool.d.ts +57 -0
  259. package/dist/scene/visibility/occlusion-query-pool.d.ts.map +1 -0
  260. package/dist/scene/visibility/occlusion-runtime.d.ts +96 -0
  261. package/dist/scene/visibility/occlusion-runtime.d.ts.map +1 -0
  262. package/dist/scene/visibility/types.d.ts +34 -0
  263. package/dist/scene/visibility/types.d.ts.map +1 -0
  264. package/dist/shadow-atlas.d.ts +3 -3
  265. package/dist/shadow-atlas.d.ts.map +1 -1
  266. package/dist/ssao-buffers.d.ts +3 -0
  267. package/dist/ssao-buffers.d.ts.map +1 -1
  268. package/dist/ssr/admission.d.ts +122 -0
  269. package/dist/ssr/admission.d.ts.map +1 -0
  270. package/dist/ssr/composition.d.ts +32 -0
  271. package/dist/ssr/composition.d.ts.map +1 -0
  272. package/dist/ssr/errors.d.ts +39 -0
  273. package/dist/ssr/errors.d.ts.map +1 -0
  274. package/dist/ssr/identity.d.ts +13 -0
  275. package/dist/ssr/identity.d.ts.map +1 -0
  276. package/dist/systems/skin-palette-allocator.d.ts +1 -1
  277. package/dist/targets/contracts.d.ts +66 -0
  278. package/dist/targets/contracts.d.ts.map +1 -0
  279. package/dist/targets/graph-projection.d.ts +29 -0
  280. package/dist/targets/graph-projection.d.ts.map +1 -0
  281. package/dist/targets/material-source.d.ts +34 -0
  282. package/dist/targets/material-source.d.ts.map +1 -0
  283. package/dist/targets/owner.d.ts +32 -0
  284. package/dist/targets/owner.d.ts.map +1 -0
  285. package/dist/targets/physical.d.ts +18 -0
  286. package/dist/targets/physical.d.ts.map +1 -0
  287. package/dist/targets/readback.d.ts +39 -0
  288. package/dist/targets/readback.d.ts.map +1 -0
  289. package/dist/temporal/frame-coordinator.d.ts +13 -0
  290. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  291. package/dist/temporal/frame.d.ts +2 -0
  292. package/dist/temporal/frame.d.ts.map +1 -1
  293. package/dist/temporal/index.mjs +6 -5
  294. package/dist/temporal/view.d.ts +7 -0
  295. package/dist/temporal/view.d.ts.map +1 -1
  296. package/dist/tilemap-chunk-extract-system.d.ts +1 -1
  297. package/dist/typed-render-graph-primitives.d.ts +5 -1
  298. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  299. package/dist/typed-shadow-passes.d.ts +1 -1
  300. package/dist/typed-shadow-passes.d.ts.map +1 -1
  301. package/dist/volume/passes.d.ts +9 -5
  302. package/dist/volume/passes.d.ts.map +1 -1
  303. package/package.json +20 -20
  304. package/src/__tests__/bloom-exact-zero.unit.test.ts +48 -19
  305. package/src/__tests__/bloom-lifecycle.integration.test.ts +41 -13
  306. package/src/__tests__/camera-target-consumption.unit.test.ts +53 -0
  307. package/src/__tests__/cluster-binner.unit.test.ts +35 -0
  308. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +12 -2
  309. package/src/__tests__/cube-camera-scheduler.unit.test.ts +70 -0
  310. package/src/__tests__/cube-camera-single-submit.perf.test.ts +75 -0
  311. package/src/__tests__/cube-camera-view-matrices.unit.test.ts +41 -0
  312. package/src/__tests__/cube-camera-vocabulary.unit.test.ts +47 -0
  313. package/src/__tests__/cube-camera.integration.test.ts +42 -0
  314. package/src/__tests__/detached-inspection.integration.test.ts +6 -0
  315. package/src/__tests__/device-feature-admission.unit.test.ts +85 -0
  316. package/src/__tests__/direct-light-snapshot.unit.test.ts +62 -6
  317. package/src/__tests__/docs-gate-green.unit.test.ts +38 -0
  318. package/src/__tests__/environment-generation.integration.test.ts +16 -0
  319. package/src/__tests__/evidence-identity.ts +13 -0
  320. package/src/__tests__/evidence-identity.unit.test.ts +24 -0
  321. package/src/__tests__/extended-lighting-recovery.browser.test.ts +14 -0
  322. package/src/__tests__/extended-lighting-recovery.dawn.test.ts +14 -0
  323. package/src/__tests__/extended-lighting-recovery.integration.test.ts +81 -0
  324. package/src/__tests__/extended-lighting-resources.unit.test.ts +51 -0
  325. package/src/__tests__/extended-lighting-zero-feature.integration.test.ts +52 -0
  326. package/src/__tests__/factory-contract.integration.test.ts +288 -29
  327. package/src/__tests__/fog-component.unit.test.ts +67 -0
  328. package/src/__tests__/fog.integration.test.ts +86 -0
  329. package/src/__tests__/frame-observation.contract.test.ts +15 -0
  330. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +37 -0
  331. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +146 -0
  332. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +10 -8
  333. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +18 -1
  334. package/src/__tests__/gpu-driven-production.integration.test.ts +579 -17
  335. package/src/__tests__/gpu-driven-scaling.unit.test.ts +66 -2
  336. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +26 -1
  337. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +101 -6
  338. package/src/__tests__/gpu-driven-view.browser.test.ts +3 -3
  339. package/src/__tests__/gpu-driven-view.dawn.test.ts +3 -3
  340. package/src/__tests__/gpu-driven-view.unit.test.ts +50 -0
  341. package/src/__tests__/gpu-lod-evidence.ts +188 -0
  342. package/src/__tests__/gpu-lod-fallback.integration.test.ts +10 -0
  343. package/src/__tests__/gpu-lod-indirect.integration.test.ts +40 -0
  344. package/src/__tests__/gpu-lod-parity.dawn.test.ts +29 -0
  345. package/src/__tests__/gpu-lod-readiness.integration.test.ts +52 -0
  346. package/src/__tests__/gpu-lod-schema.unit.test.ts +66 -0
  347. package/src/__tests__/gpu-lod-selector.unit.test.ts +28 -0
  348. package/src/__tests__/gpu-lod-silhouette.browser.test.ts +13 -0
  349. package/src/__tests__/gpu-pass-timing-browser-runner.ts +281 -0
  350. package/src/__tests__/gpu-pass-timing-coexistence.integration.test.ts +80 -0
  351. package/src/__tests__/gpu-pass-timing-contract.unit.test.ts +243 -0
  352. package/src/__tests__/gpu-pass-timing-exact-zero.unit.test.ts +131 -0
  353. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +230 -0
  354. package/src/__tests__/gpu-pass-timing-observation.unit.test.ts +233 -0
  355. package/src/__tests__/gpu-pass-timing-session.unit.test.ts +438 -0
  356. package/src/__tests__/gpu-pass-timing.browser.test.ts +26 -0
  357. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +125 -0
  358. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +66 -1
  359. package/src/__tests__/gpu-scene.dawn.test.ts +8 -1
  360. package/src/__tests__/gpu-scene.unit.test.ts +49 -3
  361. package/src/__tests__/hdrp-group2-visibility.unit.test.ts +24 -0
  362. package/src/__tests__/instances-culling.integration.test.ts +94 -0
  363. package/src/__tests__/instances-derived-bounds.unit.test.ts +118 -0
  364. package/src/__tests__/light-buffer-layout.unit.test.ts +113 -3
  365. package/src/__tests__/light-inspection.unit.test.ts +55 -0
  366. package/src/__tests__/light-resource-error.unit.test.ts +43 -0
  367. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +109 -0
  368. package/src/__tests__/lod-bounds.unit.test.ts +42 -0
  369. package/src/__tests__/lod-cpu-lane.integration.test.ts +55 -0
  370. package/src/__tests__/lod-cpu-no-change.perf.test.ts +19 -0
  371. package/src/__tests__/lod-gpu-frame-timing.unit.test.ts +49 -0
  372. package/src/__tests__/lod-multi-view.integration.test.ts +63 -0
  373. package/src/__tests__/lod-occlusion-device-loss.integration.test.ts +27 -0
  374. package/src/__tests__/lod-occlusion-final-decision.integration.test.ts +47 -0
  375. package/src/__tests__/lod-occlusion-inspection.integration.test.ts +217 -0
  376. package/src/__tests__/lod-occlusion-inspection.schema.test-d.ts +14 -0
  377. package/src/__tests__/lod-occlusion-inspection.unit.test.ts +72 -0
  378. package/src/__tests__/lod-occlusion-visual-evidence.ts +91 -0
  379. package/src/__tests__/lod-occlusion.browser.test.ts +15 -0
  380. package/src/__tests__/lod-occlusion.dawn.test.ts +20 -0
  381. package/src/__tests__/lod-selector.profile.test.ts +35 -0
  382. package/src/__tests__/lod-selector.unit.test.ts +68 -0
  383. package/src/__tests__/lod-visibility-identity.unit.test.ts +198 -0
  384. package/src/__tests__/lod-visibility-lifecycle.integration.test.ts +132 -0
  385. package/src/__tests__/ltc-resources.unit.test.ts +32 -0
  386. package/src/__tests__/material-contract-inventory.unit.test.ts +17 -0
  387. package/src/__tests__/material-observation.integration.test.ts +101 -0
  388. package/src/__tests__/occlusion-confidence.unit.test.ts +168 -0
  389. package/src/__tests__/occlusion-invalidation.integration.test.ts +37 -0
  390. package/src/__tests__/occlusion-pass-routing.unit.test.ts +16 -0
  391. package/src/__tests__/occlusion-pass.dawn.test.ts +20 -0
  392. package/src/__tests__/occlusion-pass.integration.test.ts +43 -0
  393. package/src/__tests__/occlusion-pass.rhi-null.test.ts +22 -0
  394. package/src/__tests__/occlusion-query-pool.rhi-null.test.ts +21 -0
  395. package/src/__tests__/occlusion-query-pool.unit.test.ts +95 -0
  396. package/src/__tests__/occlusion-query-ticket.integration.test.ts +47 -0
  397. package/src/__tests__/occlusion-query.dawn.test.ts +16 -0
  398. package/src/__tests__/occlusion-query.evidence.ts +178 -0
  399. package/src/__tests__/occlusion-query.rhi-null.test.ts +31 -0
  400. package/src/__tests__/occlusion-runtime.unit.test.ts +120 -0
  401. package/src/__tests__/pbr-view-layout.unit.test.ts +16 -0
  402. package/src/__tests__/point-shadow-inspection.unit.test.ts +60 -0
  403. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +196 -0
  404. package/src/__tests__/probe-bindings.unit.test.ts +200 -0
  405. package/src/__tests__/probe-blend-numeric.unit.test.ts +67 -0
  406. package/src/__tests__/probe-blend-oracle.unit.test.ts +136 -0
  407. package/src/__tests__/probe-capacity.integration.test.ts +97 -0
  408. package/src/__tests__/probe-recovery.integration.test.ts +104 -0
  409. package/src/__tests__/probe-scene-dirty.integration.test.ts +211 -0
  410. package/src/__tests__/profiler-phase-catalog.test.ts +4 -1
  411. package/src/__tests__/profiler-phase-token.test.ts +1 -0
  412. package/src/__tests__/public-surface.test-d.ts +10 -0
  413. package/src/__tests__/rect-area-boundary.unit.test.ts +24 -0
  414. package/src/__tests__/reflection-fallback-graph.unit.test.ts +67 -0
  415. package/src/__tests__/reflection-fallback-inspection.unit.test.ts +77 -0
  416. package/src/__tests__/reflection-fallback-lifecycle.integration.test.ts +21 -0
  417. package/src/__tests__/reflection-fallback-projection.unit.test.ts +90 -0
  418. package/src/__tests__/reflection-fallback-source.unit.test.ts +32 -0
  419. package/src/__tests__/reflection-probe-authoring.unit.test.ts +71 -0
  420. package/src/__tests__/reflection-probe-filter.unit.test.ts +54 -0
  421. package/src/__tests__/reflection-probe-ibl-topology.unit.test.ts +45 -0
  422. package/src/__tests__/reflection-probe-owner-placement.unit.test.ts +39 -0
  423. package/src/__tests__/reflection-probe-pass-encode.integration.test.ts +101 -0
  424. package/src/__tests__/reflection-probe-performance.perf.test.ts +60 -0
  425. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +120 -0
  426. package/src/__tests__/reflection-probe-recovery.integration.test.ts +55 -0
  427. package/src/__tests__/reflection-probe-selection.unit.test.ts +46 -0
  428. package/src/__tests__/render-error-code-owner.test-d.ts +19 -3
  429. package/src/__tests__/render-feature-docs.unit.test.ts +12 -7
  430. package/src/__tests__/render-graph-fallback.integration.test.ts +24 -0
  431. package/src/__tests__/render-perf/extended-lighting.bench.test.ts +66 -0
  432. package/src/__tests__/render-scene-projection.unit.test.ts +447 -18
  433. package/src/__tests__/render-target-admission.unit.test.ts +80 -0
  434. package/src/__tests__/render-target-graph-projection.unit.test.ts +102 -0
  435. package/src/__tests__/render-target-inspection.unit.test.ts +52 -0
  436. package/src/__tests__/render-target-material-source.test-d.ts +30 -0
  437. package/src/__tests__/render-target-material-source.unit.test.ts +97 -0
  438. package/src/__tests__/render-target-owner-placement.unit.test.ts +203 -0
  439. package/src/__tests__/render-target-owner.unit.test.ts +122 -0
  440. package/src/__tests__/render-target-probe-soak.perf.test.ts +94 -0
  441. package/src/__tests__/render-target-public-schema.json +107 -0
  442. package/src/__tests__/render-target-public-schema.unit.test.ts +91 -0
  443. package/src/__tests__/render-target-public-surface.test-d.ts +83 -0
  444. package/src/__tests__/render-target-readback.integration.test.ts +90 -0
  445. package/src/__tests__/render-target-recovery.integration.test.ts +85 -0
  446. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +124 -0
  447. package/src/__tests__/renderer-lifecycle.integration.test.ts +14 -0
  448. package/src/__tests__/shader-stage-surface.unit.test.ts +19 -4
  449. package/src/__tests__/skin-motion-regression.unit.test.ts +154 -0
  450. package/src/__tests__/skinned-shadow-caster.test.ts +13 -4
  451. package/src/__tests__/spot-modifier-projection.unit.test.ts +70 -0
  452. package/src/__tests__/ssr-admission-matrix.integration.test.ts +206 -0
  453. package/src/__tests__/ssr-composition.unit.test.ts +46 -0
  454. package/src/__tests__/ssr-fallback-zero-work.bench.test.ts +94 -0
  455. package/src/__tests__/ssr-recovery-errors.unit.test.ts +58 -0
  456. package/src/__tests__/standard-cluster-capability.integration.test.ts +63 -0
  457. package/src/__tests__/standard-cluster-transport.unit.test.ts +121 -0
  458. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +299 -0
  459. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +15 -5
  460. package/src/__tests__/standard-lighting-inspection.integration.test.ts +138 -0
  461. package/src/__tests__/standard-lighting-ssot.unit.test.ts +18 -0
  462. package/src/__tests__/standard-lighting-write.unit.test.ts +441 -0
  463. package/src/__tests__/standard-lighting.integration.test.ts +67 -0
  464. package/src/__tests__/standard-pipeline.dawn.test.ts +206 -5
  465. package/src/__tests__/standard-pipeline.integration.test.ts +181 -150
  466. package/src/__tests__/standard-probe-binding.unit.test.ts +25 -0
  467. package/src/__tests__/standard-profile-oracle.unit.test.ts +23 -38
  468. package/src/__tests__/temporal-fallback-generation.integration.test.ts +16 -0
  469. package/src/__tests__/temporal-view.unit.test.ts +3 -3
  470. package/src/__tests__/transmission-prewarm.unit.test.ts +69 -0
  471. package/src/__tests__/typed-pipeline-topology.unit.test.ts +395 -14
  472. package/src/__tests__/urp-pipeline-targets.unit.test.ts +2 -2
  473. package/src/__tests__/visibility-budget.integration.test.ts +140 -0
  474. package/src/__tests__/visibility-budget.unit.test.ts +70 -0
  475. package/src/__tests__/visibility-extract.integration.test.ts +11 -6
  476. package/src/__tests__/visibility-resolver.unit.test.ts +15 -0
  477. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +6 -3
  478. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +31 -28
  479. package/src/assembly/backend-contract.ts +13 -0
  480. package/src/assembly/device-feature-admission.ts +80 -0
  481. package/src/assembly/directional-shadow-inspection.ts +122 -0
  482. package/src/assembly/factory.ts +1411 -366
  483. package/src/assembly/host-contract.ts +35 -0
  484. package/src/assembly/render-target-host.ts +847 -0
  485. package/src/assets/asset-decoders.ts +1 -1
  486. package/src/assets/render-asset-port.ts +7 -1
  487. package/src/capture/cube-views.ts +69 -0
  488. package/src/capture/scheduler.ts +265 -0
  489. package/src/cluster-binner.ts +31 -0
  490. package/src/components/__tests__/lighting-authoring.unit.test.ts +39 -0
  491. package/src/components/__tests__/rect-area-light.unit.test.ts +33 -0
  492. package/src/components/__tests__/schema-contract.unit.test.ts +12 -0
  493. package/src/components/__tests__/schema-parity.unit.test.ts +9 -0
  494. package/src/components/camera.ts +4 -0
  495. package/src/components/cube-camera.ts +52 -0
  496. package/src/components/directional-light.ts +22 -2
  497. package/src/components/directional-shadow-filter.ts +57 -0
  498. package/src/components/index.ts +5 -0
  499. package/src/components/light-helpers.ts +133 -8
  500. package/src/components/light-probe.ts +13 -0
  501. package/src/components/rect-area-light.ts +15 -0
  502. package/src/components/reflection-probe.ts +49 -0
  503. package/src/components/scene-instance.ts +6 -0
  504. package/src/components/spot-light.ts +3 -0
  505. package/src/device/__tests__/extended-lighting-recovery.unit.test.ts +24 -0
  506. package/src/device/gpu-residency.ts +140 -9
  507. package/src/device/lifecycle-transaction.ts +12 -0
  508. package/src/environment/frame.ts +6 -24
  509. package/src/environment/lifecycle.ts +20 -1
  510. package/src/errors/gpu-driven.ts +92 -0
  511. package/src/errors/index.ts +1 -0
  512. package/src/errors/recover.ts +32 -0
  513. package/src/errors/render.ts +341 -143
  514. package/src/extract/camera.ts +3 -3
  515. package/src/extract/directional-shadow-projection.ts +61 -0
  516. package/src/extract/environment.ts +57 -0
  517. package/src/extract/gpu-driven.ts +108 -0
  518. package/src/extract/visibility.ts +2 -1
  519. package/src/features/fullscreen.ts +3 -0
  520. package/src/features/targets.ts +4 -1
  521. package/src/features/types.ts +10 -0
  522. package/src/fullscreen-post-process-pass.ts +43 -2
  523. package/src/gpu-driven/batch-topology.ts +207 -28
  524. package/src/gpu-driven/prepared-draw.ts +208 -0
  525. package/src/gpu-driven/production-raster.ts +953 -43
  526. package/src/gpu-driven/view-gpu.ts +411 -29
  527. package/src/gpu-scene-schema.ts +13 -0
  528. package/src/gpu-scene.ts +140 -64
  529. package/src/gpu-usage.ts +0 -1
  530. package/src/hdrp-buffers.ts +169 -63
  531. package/src/ibl/IblPipelineCache.ts +11 -0
  532. package/src/ibl/kernel-cache.ts +88 -0
  533. package/src/index.ts +222 -0
  534. package/src/inspection-types.ts +276 -1
  535. package/src/instances-derived-bounds.ts +171 -0
  536. package/src/internal.ts +38 -0
  537. package/src/light-buffer-layout.ts +155 -310
  538. package/src/materials.ts +16 -2
  539. package/src/mesh-material-bindings.ts +132 -0
  540. package/src/pbr-pipeline.ts +255 -60
  541. package/src/pipeline/standard-forward-lane.ts +93 -16
  542. package/src/pipeline/standard-lighting/errors.ts +6 -0
  543. package/src/pipeline/standard-lighting/graph.ts +158 -0
  544. package/src/pipeline/standard-lighting/inspection.ts +88 -0
  545. package/src/pipeline/standard-lighting/layout.ts +31 -0
  546. package/src/pipeline/standard-lighting/light-frame.ts +35 -0
  547. package/src/pipeline/standard-lighting/membership.ts +43 -0
  548. package/src/pipeline/standard-lighting/prepare.ts +37 -0
  549. package/src/pipeline/standard-lighting/topology.ts +120 -0
  550. package/src/pipeline/standard-lighting/transport.ts +78 -0
  551. package/src/pipeline/standard-pipeline.ts +124 -193
  552. package/src/pipeline/standard-post.ts +26 -7
  553. package/src/pipeline/standard-profile.ts +17 -47
  554. package/src/pipeline-builder.ts +5 -2
  555. package/src/pipeline-spec.ts +95 -21
  556. package/src/plugin.ts +4 -0
  557. package/src/point-shadow-inspection.ts +55 -0
  558. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +56 -0
  559. package/src/prepare/extended-lighting/ltc-resources.ts +136 -0
  560. package/src/prepare/extended-lighting/resources.ts +389 -0
  561. package/src/prepare/extended-lighting/spot-modifiers.ts +194 -0
  562. package/src/prepare/extended-lighting/state.ts +92 -0
  563. package/src/record/__tests__/gpu-timing.unit.test.ts +177 -0
  564. package/src/record/__tests__/temporal-payload.unit.test.ts +64 -1
  565. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +73 -0
  566. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +20 -1
  567. package/src/record/__tests__/view-ubo-layout.unit.test.ts +40 -0
  568. package/src/record/frame-lighting.ts +591 -452
  569. package/src/record/frame-snapshot.ts +92 -39
  570. package/src/record/frame-targets.ts +17 -0
  571. package/src/record/frame.ts +662 -158
  572. package/src/record/gpu-pass-timing/contract.ts +180 -0
  573. package/src/record/gpu-pass-timing/errors.ts +34 -0
  574. package/src/record/gpu-pass-timing/index.ts +37 -0
  575. package/src/record/gpu-pass-timing/parser.ts +97 -0
  576. package/src/record/gpu-pass-timing/session.ts +938 -0
  577. package/src/record/gpu-timing.ts +112 -39
  578. package/src/record/helpers.ts +0 -58
  579. package/src/record/main-pass-geometry.ts +324 -119
  580. package/src/record/main-pass-material.ts +103 -10
  581. package/src/record/main-pass-sprite-draws.ts +99 -103
  582. package/src/record/main-pass.ts +71 -15
  583. package/src/record/mesh-ssbo.ts +47 -36
  584. package/src/record/render-context.ts +136 -3
  585. package/src/record/shadow-pass.ts +83 -68
  586. package/src/record/skybox-post-pass.ts +20 -4
  587. package/src/record/typed-frame-graph.ts +1034 -43
  588. package/src/record/view-ubo.ts +72 -47
  589. package/src/reflection/filter.ts +81 -0
  590. package/src/reflection/gpu-table.ts +44 -0
  591. package/src/reflection/inspection.ts +67 -0
  592. package/src/reflection/projection.ts +476 -0
  593. package/src/reflection/record-owner.ts +1283 -0
  594. package/src/render-contract.ts +93 -10
  595. package/src/render-graph-primitives.ts +9 -4
  596. package/src/render-pipeline.ts +122 -2
  597. package/src/render-system-extract.ts +1041 -225
  598. package/src/render-system.ts +1310 -85
  599. package/src/renderer.ts +222 -0
  600. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +36 -0
  601. package/src/scene/probe-blend-record.ts +54 -0
  602. package/src/scene/probe-blend.ts +813 -0
  603. package/src/scene/render-scene-types.ts +0 -7
  604. package/src/scene/render-scene.ts +942 -171
  605. package/src/scene/visibility/bounds.ts +57 -0
  606. package/src/scene/visibility/budget.ts +40 -0
  607. package/src/scene/visibility/facet.ts +400 -0
  608. package/src/scene/visibility/gpu-frame-timing.ts +101 -0
  609. package/src/scene/visibility/gpu-lod.ts +119 -0
  610. package/src/scene/visibility/inspection.ts +313 -0
  611. package/src/scene/visibility/lod-selector.ts +84 -0
  612. package/src/scene/visibility/occlusion-confidence.ts +478 -0
  613. package/src/scene/visibility/occlusion-pass.ts +165 -0
  614. package/src/scene/visibility/occlusion-query-pool.ts +178 -0
  615. package/src/scene/visibility/occlusion-runtime.ts +672 -0
  616. package/src/scene/visibility/types.ts +50 -0
  617. package/src/shadow-atlas.ts +5 -5
  618. package/src/ssao-buffers.ts +9 -2
  619. package/src/ssr/admission.ts +451 -0
  620. package/src/ssr/composition.ts +73 -0
  621. package/src/ssr/errors.ts +76 -0
  622. package/src/ssr/identity.ts +12 -0
  623. package/src/systems/skin-palette-allocator.ts +1 -1
  624. package/src/targets/contracts.ts +182 -0
  625. package/src/targets/graph-projection.ts +169 -0
  626. package/src/targets/material-source.ts +143 -0
  627. package/src/targets/owner.ts +214 -0
  628. package/src/targets/physical.ts +18 -0
  629. package/src/targets/readback.ts +153 -0
  630. package/src/temporal/frame-coordinator.ts +26 -0
  631. package/src/temporal/frame.ts +14 -1
  632. package/src/temporal/view.ts +11 -0
  633. package/src/tilemap-chunk-extract-system.ts +4 -4
  634. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +59 -20
  635. package/src/typed-render-graph-primitives.ts +53 -3
  636. package/src/typed-shadow-passes.ts +46 -40
  637. package/src/volume/passes.ts +61 -58
  638. package/dist/.tsbuildinfo +0 -1
  639. package/dist/chunk-4XINDLAY.mjs.map +0 -1
  640. package/dist/chunk-CDTWTHZR.mjs.map +0 -1
  641. package/dist/chunk-M2MP4ZOU.mjs +0 -73
  642. package/dist/chunk-M2MP4ZOU.mjs.map +0 -1
  643. package/dist/chunk-PCHBPYML.mjs.map +0 -1
  644. package/dist/chunk-TBQJOK2L.mjs.map +0 -1
  645. package/dist/chunk-VFPLTKCU.mjs.map +0 -1
@@ -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,11 +11,14 @@ 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,
21
+ ReflectionProbe,
19
22
  SkyboxBackground,
20
23
  Skylight,
21
24
  SpotLight,
@@ -30,8 +33,15 @@ import {
30
33
  } from '../gpu-driven/batch-topology';
31
34
  import { GpuScene } from '../gpu-scene';
32
35
  import type { PersistentRenderSceneInspection } from '../inspection-types';
36
+ import { fingerprintNumericArray, InstanceBoundsCache } from '../instances-derived-bounds';
33
37
  import type { PointsLinesInspection } from '../points-lines/inspection';
34
38
  import type { PointsLinesRetainedSnapshot } from '../points-lines/snapshot';
39
+ import { worldEntityKey } from '../record/frame-snapshot';
40
+ import {
41
+ type ReflectionProbeFact,
42
+ ReflectionProbeProjection,
43
+ type ReflectionProbeSelectionResult,
44
+ } from '../reflection/projection';
35
45
  import type { CameraSnapshot } from '../render-contract';
36
46
  import type {
37
47
  DispatchEntry,
@@ -48,21 +58,30 @@ import {
48
58
  type TransmissionDemandOperation,
49
59
  } from '../transmission/projection';
50
60
  import { VolumetricFog } from '../volume/component';
61
+ import { ProbeBlendSceneProjection, type ProbeSceneObjectInput } from './probe-blend';
51
62
  import type {
52
63
  RenderSceneApplyResult,
53
64
  RenderSceneBounds,
54
- RenderSceneDelta,
55
65
  RenderSceneInspection,
56
66
  RenderSceneOperation,
57
67
  RenderSceneRecord,
58
68
  RenderSceneResyncReason,
59
69
  RenderSceneSlot,
60
70
  } from './render-scene-types';
71
+ import { createVisibilityBudget, type VisibilityBudget } from './visibility/budget';
72
+ import {
73
+ primitiveKey,
74
+ primitiveKeyId,
75
+ VisibilityFacetStore,
76
+ viewKey,
77
+ viewKeyId,
78
+ } from './visibility/facet';
79
+ import { decideVisibility } from './visibility/occlusion-confidence';
80
+ import type { PrimitiveKey, ViewKey, VisibilityCandidate } from './visibility/types';
61
81
 
62
82
  export type {
63
83
  RenderSceneApplyResult,
64
84
  RenderSceneBounds,
65
- RenderSceneDelta,
66
85
  RenderSceneIdentity,
67
86
  RenderSceneInspection,
68
87
  RenderSceneOperation,
@@ -79,6 +98,14 @@ interface PendingIdentity {
79
98
  removed: boolean;
80
99
  }
81
100
 
101
+ interface WorldTransformSpan {
102
+ readonly worldId: number;
103
+ readonly entities: Readonly<Uint32Array>;
104
+ readonly worlds: Readonly<Float32Array>;
105
+ }
106
+
107
+ type GlobalTransformChangeQuery = Query<readonly [typeof GlobalTransform]>;
108
+
82
109
  export interface RenderSceneSubmissionCapture {
83
110
  readonly revision: number;
84
111
  readonly slots: readonly {
@@ -193,29 +220,69 @@ function intersects(left: RenderSceneBounds, right: RenderSceneBounds): boolean
193
220
  );
194
221
  }
195
222
 
196
- function worldBounds(snapshot: RenderableSnapshot): RenderSceneBounds | undefined {
223
+ function worldBounds(
224
+ snapshot: RenderableSnapshot,
225
+ target?: RenderSceneBounds,
226
+ ): RenderSceneBounds | undefined {
197
227
  const local = snapshot.localAabb;
198
228
  if (local === undefined || local.length < 6) return undefined;
199
229
  const world = snapshot.transform.world;
200
- const min: [number, number, number] = [Infinity, Infinity, Infinity];
201
- const max: [number, number, number] = [-Infinity, -Infinity, -Infinity];
202
- for (const x of [local[0] ?? 0, local[3] ?? 0]) {
203
- for (const y of [local[1] ?? 0, local[4] ?? 0]) {
204
- for (const z of [local[2] ?? 0, local[5] ?? 0]) {
205
- const tx =
206
- (world[0] ?? 0) * x + (world[4] ?? 0) * y + (world[8] ?? 0) * z + (world[12] ?? 0);
207
- const ty =
208
- (world[1] ?? 0) * x + (world[5] ?? 0) * y + (world[9] ?? 0) * z + (world[13] ?? 0);
209
- const tz =
210
- (world[2] ?? 0) * x + (world[6] ?? 0) * y + (world[10] ?? 0) * z + (world[14] ?? 0);
211
- min[0] = Math.min(min[0], tx);
212
- min[1] = Math.min(min[1], ty);
213
- min[2] = Math.min(min[2], tz);
214
- max[0] = Math.max(max[0], tx);
215
- max[1] = Math.max(max[1], ty);
216
- max[2] = Math.max(max[2], tz);
217
- }
218
- }
230
+ const min = (target?.min ?? [0, 0, 0]) as [number, number, number];
231
+ const max = (target?.max ?? [0, 0, 0]) as [number, number, number];
232
+ const centerX = ((local[0] ?? 0) + (local[3] ?? 0)) * 0.5;
233
+ const centerY = ((local[1] ?? 0) + (local[4] ?? 0)) * 0.5;
234
+ const centerZ = ((local[2] ?? 0) + (local[5] ?? 0)) * 0.5;
235
+ const extentX = ((local[3] ?? 0) - (local[0] ?? 0)) * 0.5;
236
+ const extentY = ((local[4] ?? 0) - (local[1] ?? 0)) * 0.5;
237
+ const extentZ = ((local[5] ?? 0) - (local[2] ?? 0)) * 0.5;
238
+ const worldCenterX =
239
+ (world[0] ?? 0) * centerX +
240
+ (world[4] ?? 0) * centerY +
241
+ (world[8] ?? 0) * centerZ +
242
+ (world[12] ?? 0);
243
+ const worldCenterY =
244
+ (world[1] ?? 0) * centerX +
245
+ (world[5] ?? 0) * centerY +
246
+ (world[9] ?? 0) * centerZ +
247
+ (world[13] ?? 0);
248
+ const worldCenterZ =
249
+ (world[2] ?? 0) * centerX +
250
+ (world[6] ?? 0) * centerY +
251
+ (world[10] ?? 0) * centerZ +
252
+ (world[14] ?? 0);
253
+ const worldExtentX =
254
+ Math.abs(world[0] ?? 0) * extentX +
255
+ Math.abs(world[4] ?? 0) * extentY +
256
+ Math.abs(world[8] ?? 0) * extentZ;
257
+ const worldExtentY =
258
+ Math.abs(world[1] ?? 0) * extentX +
259
+ Math.abs(world[5] ?? 0) * extentY +
260
+ Math.abs(world[9] ?? 0) * extentZ;
261
+ const worldExtentZ =
262
+ Math.abs(world[2] ?? 0) * extentX +
263
+ Math.abs(world[6] ?? 0) * extentY +
264
+ Math.abs(world[10] ?? 0) * extentZ;
265
+ min[0] = worldCenterX - worldExtentX;
266
+ min[1] = worldCenterY - worldExtentY;
267
+ min[2] = worldCenterZ - worldExtentZ;
268
+ max[0] = worldCenterX + worldExtentX;
269
+ max[1] = worldCenterY + worldExtentY;
270
+ max[2] = worldCenterZ + worldExtentZ;
271
+ return target ?? { min, max };
272
+ }
273
+
274
+ function boundsFromArray(bounds: ArrayLike<number>): RenderSceneBounds | undefined {
275
+ if (bounds.length < 6) return undefined;
276
+ const min: [number, number, number] = [Number(bounds[0]), Number(bounds[1]), Number(bounds[2])];
277
+ const max: [number, number, number] = [Number(bounds[3]), Number(bounds[4]), Number(bounds[5])];
278
+ if (
279
+ !min.every(Number.isFinite) ||
280
+ !max.every(Number.isFinite) ||
281
+ min[0] > max[0] ||
282
+ min[1] > max[1] ||
283
+ min[2] > max[2]
284
+ ) {
285
+ return undefined;
219
286
  }
220
287
  return { min, max };
221
288
  }
@@ -230,19 +297,23 @@ function worldBounds(snapshot: RenderableSnapshot): RenderSceneBounds | undefine
230
297
  export class RenderScene {
231
298
  private readonly slots: Array<RenderSceneSlot | undefined> = [];
232
299
  private readonly lastSubmittedSlots: Array<RenderSceneSlot | undefined> = [];
300
+ private readonly previousWorldBySlot: Array<Float32Array | undefined> = [];
233
301
  private readonly generations: number[] = [];
234
302
  private readonly freeSlots: number[] = [];
235
303
  private readonly slotsByWorld = new Map<number, Map<number, number>>();
236
304
  private readonly slotsByMaterial = new Map<number, Set<number>>();
237
305
  private readonly dirtySinceSubmitted = new Set<number>();
306
+ private readonly transformUpdatedSlots: RenderSceneSlot[] = [];
307
+ private temporalTracking = false;
238
308
  private lastSubmittedVisible = new Set<string>();
239
309
  private submittedEpoch = 0;
240
- // World bounds are a pure projection of each immutable snapshot. Cache the
241
- // derived value by snapshot identity so repeated view queries do not redo
242
- // the eight-corner transform for every view in the same frame.
243
- private readonly worldBoundsCache = new WeakMap<RenderableSnapshot, RenderSceneBounds | null>();
310
+ // Bounds are a derived column of the stable render slot, not metadata of a
311
+ // transient snapshot wrapper. Undefined means dirty; null means that the
312
+ // retained row has no usable bounds and must be conservatively visible.
313
+ private readonly worldBoundsBySlot: Array<RenderSceneBounds | null | undefined> = [];
314
+ private readonly instanceBoundsCache = new InstanceBoundsCache();
244
315
  private orderedSlots: number[] = [];
245
- private materialized: readonly RenderableSnapshot[] | undefined;
316
+ private materialized: RenderableSnapshot[] | undefined;
246
317
  private slotSnapshot: readonly RenderSceneSlot[] | undefined;
247
318
  private revision = 0;
248
319
  private noChangeFrames = 0;
@@ -350,6 +421,7 @@ export class RenderScene {
350
421
  };
351
422
  this.unindexMaterials(pending.initial);
352
423
  this.slots[pending.initial.slot] = updatedSlot;
424
+ this.worldBoundsBySlot[updatedSlot.slot] = undefined;
353
425
  this.indexMaterials(updatedSlot);
354
426
  this.dirtySinceSubmitted.add(updatedSlot.slot);
355
427
  updatedSlots.push(updatedSlot);
@@ -375,19 +447,55 @@ export class RenderScene {
375
447
  };
376
448
  }
377
449
 
378
- applyDelta(delta: RenderSceneDelta): RenderSceneApplyResult {
379
- if (delta.overflowed || delta.resync !== undefined) {
380
- const snapshots = delta.resync ?? [];
381
- this.reset(snapshots);
382
- this.resyncs += 1;
383
- this.lastResyncReason = delta.reason ?? 'journal-overflow';
384
- return {
385
- ...this.emptyResult(),
386
- created: snapshots.length,
387
- resynced: 1,
388
- };
450
+ /** Apply version-selected GlobalTransform columns without allocating per-entity operations. */
451
+ applyTransformSpans(spans: readonly WorldTransformSpan[]): RenderSceneApplyResult {
452
+ this.deltaFrames += 1;
453
+ let scanned = 0;
454
+ let updated = 0;
455
+ let ignoredLateUpdates = 0;
456
+ const updatedSlots = this.transformUpdatedSlots;
457
+ updatedSlots.length = 0;
458
+ for (const span of spans) {
459
+ scanned += span.entities.length;
460
+ for (let row = 0; row < span.entities.length; row += 1) {
461
+ const entity = span.entities[row];
462
+ if (entity === undefined) continue;
463
+ const slot = this.lookup(span.worldId, entity);
464
+ if (slot === undefined) {
465
+ ignoredLateUpdates += 1;
466
+ continue;
467
+ }
468
+ const target = slot.snapshot.transform.world;
469
+ const sourceStart = row * 16;
470
+ for (let index = 0; index < 16; index += 1) {
471
+ target[index] = span.worlds[sourceStart + index] ?? target[index] ?? 0;
472
+ }
473
+ if (slot.snapshot.instances === undefined) {
474
+ const cached = this.worldBoundsBySlot[slot.slot];
475
+ const bounds = worldBounds(slot.snapshot, cached ?? undefined);
476
+ this.worldBoundsBySlot[slot.slot] = bounds ?? null;
477
+ } else {
478
+ this.worldBoundsBySlot[slot.slot] = undefined;
479
+ }
480
+ if (this.temporalTracking) this.dirtySinceSubmitted.add(slot.slot);
481
+ updatedSlots.push(slot);
482
+ updated += 1;
483
+ }
389
484
  }
390
- return this.apply(delta.operations);
485
+ this.renderableScans = scanned;
486
+ if (updated > 0) this.revision += 1;
487
+ return {
488
+ created: 0,
489
+ updated,
490
+ removed: 0,
491
+ recreated: 0,
492
+ ignoredLateUpdates,
493
+ createdSlots: [],
494
+ updatedSlots,
495
+ removedSlots: [],
496
+ recreatedSlots: [],
497
+ resynced: 0,
498
+ };
391
499
  }
392
500
 
393
501
  /** Reconcile the current identity set while retaining slots for survivors. */
@@ -422,6 +530,7 @@ export class RenderScene {
422
530
  };
423
531
  this.unindexMaterials(existing);
424
532
  this.slots[existing.slot] = updated;
533
+ this.worldBoundsBySlot[updated.slot] = undefined;
425
534
  this.indexMaterials(updated);
426
535
  this.dirtySinceSubmitted.add(updated.slot);
427
536
  this.orderedSlots.push(existing.slot);
@@ -436,14 +545,25 @@ export class RenderScene {
436
545
  this.reset(snapshots);
437
546
  }
438
547
 
439
- materialize(): readonly RenderableSnapshot[] {
548
+ /** Enable previous-frame publication only while a temporal feature consumes it. */
549
+ setTemporalTracking(enabled: boolean): void {
550
+ if (enabled === this.temporalTracking) return;
551
+ this.temporalTracking = enabled;
552
+ if (!enabled) {
553
+ this.dirtySinceSubmitted.clear();
554
+ return;
555
+ }
556
+ for (const slot of this.orderedSlots) this.dirtySinceSubmitted.add(slot);
557
+ }
558
+
559
+ materialize(): RenderableSnapshot[] {
440
560
  if (this.materialized !== undefined) return this.materialized;
441
561
  const snapshots: RenderableSnapshot[] = [];
442
562
  for (const slot of this.orderedSlots) {
443
563
  const record = this.slots[slot];
444
564
  if (record !== undefined) snapshots.push(record.snapshot);
445
565
  }
446
- this.materialized = Object.freeze(snapshots);
566
+ this.materialized = snapshots;
447
567
  return this.materialized;
448
568
  }
449
569
 
@@ -489,6 +609,15 @@ export class RenderScene {
489
609
  }
490
610
  for (const update of capture.slots) {
491
611
  this.lastSubmittedSlots[update.slot] = update.current;
612
+ const currentWorld = update.current?.snapshot.transform.world;
613
+ if (currentWorld !== undefined) {
614
+ let previousWorld = this.previousWorldBySlot[update.slot];
615
+ if (previousWorld === undefined) {
616
+ previousWorld = new Float32Array(16);
617
+ this.previousWorldBySlot[update.slot] = previousWorld;
618
+ }
619
+ previousWorld.set(currentWorld);
620
+ }
492
621
  this.dirtySinceSubmitted.delete(update.slot);
493
622
  }
494
623
  this.lastSubmittedVisible = new Set(capture.visible);
@@ -551,12 +680,7 @@ export class RenderScene {
551
680
  for (const slot of this.orderedSlots) {
552
681
  const record = this.slots[slot];
553
682
  if (record === undefined) continue;
554
- const snapshot = record.snapshot;
555
- let candidate = this.worldBoundsCache.get(snapshot);
556
- if (candidate === undefined && !this.worldBoundsCache.has(snapshot)) {
557
- candidate = worldBounds(snapshot) ?? null;
558
- this.worldBoundsCache.set(snapshot, candidate);
559
- }
683
+ const candidate = this.cullingWorldBoundsAt(record);
560
684
  if (candidate === null || candidate === undefined) continue;
561
685
  if (intersects(candidate, bounds)) records.push(record);
562
686
  }
@@ -614,8 +738,15 @@ export class RenderScene {
614
738
  entityKey: snapshot.entityKey,
615
739
  snapshot: ownSnapshot(snapshot),
616
740
  };
741
+ let previousWorld = this.previousWorldBySlot[slot];
742
+ if (previousWorld === undefined) {
743
+ previousWorld = new Float32Array(16);
744
+ this.previousWorldBySlot[slot] = previousWorld;
745
+ }
746
+ previousWorld.set(record.snapshot.transform.world);
617
747
  this.generations[slot] = generation;
618
748
  this.slots[slot] = record;
749
+ this.worldBoundsBySlot[slot] = undefined;
619
750
  let entities = this.slotsByWorld.get(snapshot.worldId);
620
751
  if (entities === undefined) {
621
752
  entities = new Map<number, number>();
@@ -629,6 +760,8 @@ export class RenderScene {
629
760
 
630
761
  private release(record: RenderSceneSlot): void {
631
762
  this.slots[record.slot] = undefined;
763
+ this.worldBoundsBySlot[record.slot] = undefined;
764
+ this.instanceBoundsCache.invalidate(record.entityKey, record.worldId);
632
765
  this.unindexMaterials(record);
633
766
  const entities = this.slotsByWorld.get(record.worldId);
634
767
  entities?.delete(record.entityKey);
@@ -690,12 +823,16 @@ export class RenderScene {
690
823
  reasons.includes('geometry-revision') ||
691
824
  reasons.includes('material-revision');
692
825
  const previous = seedCurrent || submitted === undefined ? slot.snapshot : submitted.snapshot;
826
+ const previousWorld = this.previousWorldBySlot[slot.slot];
693
827
  return {
694
828
  previousEpoch: seedCurrent ? undefined : this.submittedEpoch,
695
829
  previousSource: seedCurrent ? 'current-seed' : 'last-submitted',
696
830
  reactive: reasons.length > 0,
697
831
  reactiveReasons: reasons,
698
- previousTransform: previous.transform,
832
+ previousTransform:
833
+ seedCurrent || previousWorld === undefined
834
+ ? previous.transform
835
+ : { ...previous.transform, world: previousWorld },
699
836
  previousInstances: previous.instances,
700
837
  previousSkin: previous.skin,
701
838
  previousMorphWeights: previous.morph?.weights,
@@ -706,23 +843,104 @@ export class RenderScene {
706
843
  this.materialized = undefined;
707
844
  this.slotSnapshot = undefined;
708
845
  }
709
- }
710
846
 
711
- function readTransformWorld(world: World, entity: EntityHandle): Float32Array | undefined {
712
- const transform = world.get(entity, Transform);
713
- return transform.ok ? new Float32Array(transform.value.world) : undefined;
847
+ /**
848
+ * Return the CPU culling projection for one retained snapshot. Instances
849
+ * use a renderer-derived union, while the GPU scene keeps the mesh-local
850
+ * bounds for its independent per-instance visibility pass. Unknown or
851
+ * empty instance facts deliberately return undefined (conservative no-cull).
852
+ */
853
+ cullingWorldBoundsAt(slot: RenderSceneSlot): RenderSceneBounds | undefined {
854
+ let candidate = this.worldBoundsBySlot[slot.slot];
855
+ if (candidate !== undefined) return candidate ?? undefined;
856
+ const snapshot = slot.snapshot;
857
+ if (snapshot.instances !== undefined) {
858
+ const instanceCount = snapshot.instances.instanceCount;
859
+ if (instanceCount === 0 || snapshot.localAabb === undefined) {
860
+ this.worldBoundsBySlot[slot.slot] = null;
861
+ return undefined;
862
+ }
863
+ const derived = this.instanceBoundsCache.get({
864
+ worldId: snapshot.worldId,
865
+ entityKey: snapshot.entityKey,
866
+ meshGeneration: fingerprintNumericArray(snapshot.localAabb) ^ (snapshot.assetHandle >>> 0),
867
+ transformGeneration: fingerprintNumericArray(snapshot.transform.world),
868
+ matrixGeneration: snapshot.instances.archVersion,
869
+ meshAabb: snapshot.localAabb,
870
+ entityWorld: snapshot.transform.world,
871
+ transforms: snapshot.instances.transforms,
872
+ });
873
+ candidate = boundsFromArray(derived ?? []);
874
+ } else {
875
+ candidate = worldBounds(snapshot, candidate ?? undefined);
876
+ }
877
+ this.worldBoundsBySlot[slot.slot] = candidate ?? null;
878
+ return candidate;
879
+ }
880
+
881
+ cullingWorldBounds(snapshot: RenderableSnapshot): RenderSceneBounds | undefined {
882
+ const slot = this.lookup(snapshot.worldId, snapshot.entityKey);
883
+ return slot === undefined ? worldBounds(snapshot) : this.cullingWorldBoundsAt(slot);
884
+ }
714
885
  }
715
886
 
716
887
  export interface PersistentRenderSceneOptions {
717
888
  readonly getDevice?: (() => RhiDevice) | undefined;
718
889
  readonly onGpuError?: ((error: RhiError) => void) | undefined;
719
890
  readonly onSharedRefMutation?: ((worldId: number, handle: number) => void) | undefined;
891
+ readonly configuredQueryBudget?: number | undefined;
720
892
  }
721
893
 
722
894
  export interface PersistentGpuDrivenState {
723
895
  readonly scene: GpuScene;
724
896
  readonly plan: SubmissionPlan;
725
897
  readonly slots: readonly RenderSceneSlot[];
898
+ /** Stable renderer-local World keys aligned with RenderSceneSlot.worldId. */
899
+ readonly worldKeys?: readonly number[];
900
+ }
901
+
902
+ export interface CpuDirectViewInput {
903
+ readonly view: ViewKey;
904
+ readonly primitives: readonly PrimitiveKey[];
905
+ }
906
+
907
+ export interface CpuDirectCandidate {
908
+ readonly view: ViewKey;
909
+ readonly primitive: PrimitiveKey;
910
+ readonly candidate: VisibilityCandidate;
911
+ }
912
+
913
+ /**
914
+ * The one renderer-owned submission projection for a display view.
915
+ *
916
+ * Occlusion completion updates the facet asynchronously, so extraction must
917
+ * retain every query candidate while record/prepare consume this projection.
918
+ * Keeping the dispatch reindex here prevents CPU direct and GPU indirect
919
+ * lanes from rebuilding a second visibility map with different identity
920
+ * semantics.
921
+ */
922
+ export interface PersistentVisibilityProjection {
923
+ readonly renderables: readonly RenderableSnapshot[];
924
+ readonly dispatch: readonly DispatchEntry[];
925
+ readonly activeEntityKeys: ReadonlySet<number>;
926
+ /** Monotonic owner revision for exactly this active-key projection. */
927
+ readonly activeEntityRevision: number;
928
+ readonly suppressed: number;
929
+ }
930
+
931
+ /** Project one selected candidate per view and primitive from the facet owner. */
932
+ export function projectCpuDirectCandidates(
933
+ facets: VisibilityFacetStore,
934
+ views: readonly CpuDirectViewInput[],
935
+ ): readonly CpuDirectCandidate[] {
936
+ const candidates: CpuDirectCandidate[] = [];
937
+ for (const { view, primitives } of views) {
938
+ for (const primitive of primitives) {
939
+ const candidate = facets.getCandidate(view, primitive);
940
+ if (candidate !== undefined) candidates.push({ view, primitive, candidate });
941
+ }
942
+ }
943
+ return candidates;
726
944
  }
727
945
 
728
946
  type PersistentGpuSceneInspection = PersistentRenderSceneInspection['gpu'];
@@ -735,16 +953,29 @@ interface PersistentCompositionEntry {
735
953
  readonly projection: RenderScene;
736
954
  readonly topology: BatchTopology;
737
955
  readonly transmissionDemand: PersistentTransmissionDemandProjection;
738
- readonly readCursors: number[];
956
+ readonly structure: Pick<
957
+ ExtractedFrame,
958
+ | 'dispatch'
959
+ | 'visibilityStats'
960
+ | 'visibilitySnapshots'
961
+ | 'featureVisibilitySnapshots'
962
+ | 'hiddenEntityReports'
963
+ >;
964
+ /** Stable renderer-local World keys; unlike RenderableSnapshot.worldId they survive reorder. */
965
+ readonly worldKeys: readonly number[];
966
+ readonly readVersions: RenderReadVersion[];
739
967
  readonly leaseIdentities: readonly string[];
968
+ readonly transformQueries: readonly GlobalTransformChangeQuery[];
740
969
  catalogEpoch: number;
741
970
  readonly rigidOnly: boolean;
742
- gpuDrivenCoverageKey: string | undefined;
743
- gpuDrivenOwnsAll: boolean;
971
+ readonly probeProjection: ProbeBlendSceneProjection;
972
+ temporalDemanded: boolean;
973
+ gpuDrivenSceneRequired: boolean;
744
974
  }
745
975
 
746
976
  const RENDER_RELEVANT_COMPONENT_IDS = new Set([
747
977
  componentId(Transform),
978
+ componentId(GlobalTransform),
748
979
  componentId(ChildOf),
749
980
  componentId(MeshFilter),
750
981
  componentId(MeshRenderer),
@@ -755,22 +986,33 @@ const RENDER_RELEVANT_COMPONENT_IDS = new Set([
755
986
  componentId(Layer),
756
987
  componentId(Visibility),
757
988
  componentId(Camera),
989
+ componentId(MotionBlur),
758
990
  componentId(DirectionalLight),
991
+ componentId(LightProbe),
759
992
  componentId(PointLight),
760
993
  componentId(PointLightShadow),
761
994
  componentId(SpotLight),
762
995
  componentId(Skylight),
763
996
  componentId(SkyboxBackground),
764
997
  componentId(PostProcessParams),
998
+ componentId(ReflectionProbe),
765
999
  componentId(VolumetricFog),
766
1000
  ]);
767
1001
 
768
- const NON_RENDERABLE_TRANSFORM_CONSUMERS = [
769
- Camera,
770
- DirectionalLight,
771
- PointLight,
772
- SpotLight,
773
- ] as const;
1002
+ function createGlobalTransformChangeQuery(world: World): GlobalTransformChangeQuery {
1003
+ const result = world.query({ read: [GlobalTransform], changed: [GlobalTransform] });
1004
+ if (!result.ok) throw result.error;
1005
+ const query = result.value as GlobalTransformChangeQuery;
1006
+ for (const _span of query.spans().unwrap()) {
1007
+ // The full rebuild consumed these values; establish the observation baseline.
1008
+ }
1009
+ return query;
1010
+ }
1011
+
1012
+ function frameDemandsTemporal(frame: ExtractedFrame): boolean {
1013
+ const camera = frame.cameras[0];
1014
+ return camera?.antialias === 'taa' || (camera?.motionBlur?.shutterAngle ?? 0) > 0;
1015
+ }
774
1016
 
775
1017
  function cameraFrusta(cameras: readonly CameraSnapshot[]): readonly Float32Array[] {
776
1018
  const planes: Float32Array[] = [];
@@ -807,14 +1049,23 @@ function cameraFrusta(cameras: readonly CameraSnapshot[]): readonly Float32Array
807
1049
  return planes;
808
1050
  }
809
1051
 
1052
+ let cullProjectedIndexScratch = new Int32Array(0);
1053
+
810
1054
  function cullPersistentFrame(
811
1055
  frame: ExtractedFrame,
812
1056
  candidates: readonly RenderableSnapshot[],
1057
+ boundsOf: (snapshot: RenderableSnapshot, index: number) => RenderSceneBounds | undefined = (
1058
+ snapshot,
1059
+ ) => worldBounds(snapshot),
813
1060
  ): ExtractedFrame {
814
1061
  const planes = cameraFrusta(frame.cameras);
815
1062
  const visible: RenderableSnapshot[] = [];
816
- const projectedIndex = new Int32Array(candidates.length);
817
- projectedIndex.fill(-1);
1063
+ if (cullProjectedIndexScratch.length < candidates.length) {
1064
+ cullProjectedIndexScratch = new Int32Array(candidates.length);
1065
+ }
1066
+ const projectedIndex = cullProjectedIndexScratch;
1067
+ projectedIndex.fill(-1, 0, candidates.length);
1068
+ const worldAabb = box3.create();
818
1069
  let total = 0;
819
1070
  let culled = 0;
820
1071
 
@@ -822,10 +1073,15 @@ function cullPersistentFrame(
822
1073
  const candidate = candidates[index];
823
1074
  if (candidate === undefined) continue;
824
1075
  let isVisible = true;
825
- if (candidate.localAabb !== undefined) {
1076
+ const candidateBounds = boundsOf(candidate, index);
1077
+ if (candidateBounds !== undefined) {
826
1078
  total += 1;
827
- const worldAabb = box3.create();
828
- box3.transformBox3(worldAabb, candidate.localAabb, candidate.transform.world);
1079
+ worldAabb[0] = candidateBounds.min[0] ?? 0;
1080
+ worldAabb[1] = candidateBounds.min[1] ?? 0;
1081
+ worldAabb[2] = candidateBounds.min[2] ?? 0;
1082
+ worldAabb[3] = candidateBounds.max[0] ?? 0;
1083
+ worldAabb[4] = candidateBounds.max[1] ?? 0;
1084
+ worldAabb[5] = candidateBounds.max[2] ?? 0;
829
1085
  isVisible = planes.length === 0;
830
1086
  for (const cameraPlanes of planes) {
831
1087
  if (
@@ -842,7 +1098,12 @@ function cullPersistentFrame(
842
1098
  continue;
843
1099
  }
844
1100
  projectedIndex[index] = visible.length;
845
- visible.push(candidate);
1101
+ // The culling candidate is the persistent projection snapshot used for
1102
+ // bounds, while `frame.renderables[index]` may carry frame-owned
1103
+ // attachments (for example the probe blend record). Preserve that
1104
+ // attached renderable in the output; pushing the bare candidate here
1105
+ // silently drops the object-level Probe ABI before record selection.
1106
+ visible.push(frame.renderables[index] ?? candidate);
846
1107
  }
847
1108
 
848
1109
  const dispatch: DispatchEntry[] = [];
@@ -860,8 +1121,33 @@ function cullPersistentFrame(
860
1121
  };
861
1122
  }
862
1123
 
1124
+ function attachProbeRecords(
1125
+ frame: ExtractedFrame,
1126
+ candidates: RenderableSnapshot[],
1127
+ slots: readonly RenderSceneSlot[],
1128
+ projection: ProbeBlendSceneProjection,
1129
+ ): ExtractedFrame {
1130
+ if (!projection.hasRecords()) {
1131
+ return frame.renderables === candidates ? frame : { ...frame, renderables: candidates };
1132
+ }
1133
+ const objectKeys = new Map<string, number>();
1134
+ for (const slot of slots)
1135
+ objectKeys.set(identityKey(slot.worldId, slot.snapshot.entityKey), slot.slot);
1136
+ const renderables = candidates.map((candidate) => {
1137
+ const slot = objectKeys.get(identityKey(candidate.worldId, candidate.entityKey));
1138
+ const record = slot === undefined ? undefined : projection.getRecord(slot);
1139
+ return record === undefined ? candidate : { ...candidate, probeBlendRecord: record };
1140
+ });
1141
+ return { ...frame, renderables };
1142
+ }
1143
+
863
1144
  /** Persistent scene projection used by the renderer's multi-World composition path. */
864
1145
  export class PersistentRenderScene {
1146
+ private readonly visibilityBudget: VisibilityBudget;
1147
+ private readonly visibilityFacets: VisibilityFacetStore;
1148
+ /** World identity is the author/runtime identity; array position is only a frame routing detail. */
1149
+ private readonly visibilityWorldKeys = new Map<string, number>();
1150
+ private nextVisibilityWorldKey = 0;
865
1151
  private readonly materialSnapshotCaches: MaterialSnapshotCachesByWorld = new WeakMap();
866
1152
  private fullRebuilds = 0;
867
1153
  private worldEntitiesScanned = 0;
@@ -878,24 +1164,349 @@ export class PersistentRenderScene {
878
1164
  private temporalCapture:
879
1165
  | { readonly projection: RenderScene; readonly capture: RenderSceneSubmissionCapture }
880
1166
  | undefined;
1167
+ /** Monotonic token for camera/dynamic frame facts consumed by visibility caches. */
1168
+ private frameEpoch = 0;
1169
+ private visibilityFacetFrame:
1170
+ | {
1171
+ readonly frameEpoch: number;
1172
+ readonly viewId: string;
1173
+ readonly renderables: readonly RenderableSnapshot[];
1174
+ }
1175
+ | undefined;
1176
+ private visibilityProjectionCache:
1177
+ | {
1178
+ readonly frameEpoch: number;
1179
+ readonly drawRevision: number;
1180
+ readonly viewId: string;
1181
+ readonly renderables: readonly RenderableSnapshot[];
1182
+ readonly dispatch: readonly DispatchEntry[];
1183
+ readonly projection: PersistentVisibilityProjection;
1184
+ }
1185
+ | undefined;
1186
+ private visibilityProjectionRevision = 0;
1187
+ private lastActiveEntityKeys: ReadonlySet<number> | undefined;
1188
+ /** Stable renderer-world/entity lookup; rebuilt only with composition topology. */
1189
+ private readonly stableSlotByEntity = new Map<number, RenderSceneSlot>();
881
1190
  private pointsLinesInspections: readonly PointsLinesInspection[] = [];
1191
+ private readonly reflectionProbes = new ReflectionProbeProjection();
882
1192
 
883
- constructor(private readonly options: PersistentRenderSceneOptions = {}) {}
1193
+ constructor(private readonly options: PersistentRenderSceneOptions = {}) {
1194
+ this.visibilityBudget = createVisibilityBudget(options.configuredQueryBudget);
1195
+ this.visibilityFacets = new VisibilityFacetStore(this.visibilityBudget);
1196
+ }
884
1197
 
885
1198
  /** RenderScene owns the per-World material fact cache used during extraction. */
886
1199
  materialSnapshotCacheStore(): MaterialSnapshotCachesByWorld {
887
1200
  return this.materialSnapshotCaches;
888
1201
  }
889
1202
 
1203
+ /** The sole renderer-owned ViewKey x PrimitiveKey visibility state. */
1204
+ visibilityFacetStore(): VisibilityFacetStore {
1205
+ return this.visibilityFacets;
1206
+ }
1207
+
1208
+ visibilityBudgetValue(): VisibilityBudget {
1209
+ return this.visibilityBudget;
1210
+ }
1211
+
1212
+ private visibilityWorldKey(world: World | undefined, fallback: number): number {
1213
+ if (world === undefined) return fallback;
1214
+ const existing = this.visibilityWorldKeys.get(world.identity);
1215
+ if (existing !== undefined) return existing;
1216
+ const key = this.nextVisibilityWorldKey;
1217
+ this.nextVisibilityWorldKey += 1;
1218
+ this.visibilityWorldKeys.set(world.identity, key);
1219
+ return key;
1220
+ }
1221
+
1222
+ private worldKeyAt(worlds: readonly World[], worldId: number): number {
1223
+ return this.visibilityWorldKey(worlds[worldId], worldId);
1224
+ }
1225
+
1226
+ /** Stable World routing keys for the current worlds[] frame projection. */
1227
+ visibilityWorldKeysFor(worlds: readonly World[]): readonly number[] {
1228
+ return worlds.map((world, worldId) => this.visibilityWorldKey(world, worldId));
1229
+ }
1230
+
1231
+ /** Current retained slots, available to CPU query transport as well as GPU. */
1232
+ compositionSlots(): readonly RenderSceneSlot[] {
1233
+ return this.composition?.projection.slotsSnapshot() ?? [];
1234
+ }
1235
+
1236
+ /** Stable renderer-world/entity lookup shared by query and final projection paths. */
1237
+ compositionSlotByStableEntity(): ReadonlyMap<number, RenderSceneSlot> {
1238
+ return this.stableSlotByEntity;
1239
+ }
1240
+
1241
+ private stableEntityKey(
1242
+ worlds: readonly World[],
1243
+ renderable: Pick<RenderableSnapshot, 'worldId' | 'entityKey'>,
1244
+ ): number {
1245
+ return worldEntityKey(this.worldKeyAt(worlds, renderable.worldId), renderable.entityKey);
1246
+ }
1247
+
1248
+ /** Feed the facet from extracted renderables using persistent slot identity. */
1249
+ updateVisibilityFacet(
1250
+ worlds: readonly World[],
1251
+ camera: CameraSnapshot | undefined,
1252
+ renderables: readonly RenderableSnapshot[],
1253
+ ): void {
1254
+ const world = worlds[camera?.worldId ?? 0];
1255
+ if (world === undefined || camera === undefined) return;
1256
+ const view = viewKey({
1257
+ attachmentId: world.identity,
1258
+ cameraEntity: camera.entityKey ?? 0,
1259
+ viewRole: 'main',
1260
+ viewGeneration: camera.historyVersion ?? 0,
1261
+ });
1262
+ const viewId = viewKeyId(view);
1263
+ const previous = this.visibilityFacetFrame;
1264
+ if (
1265
+ previous?.frameEpoch === this.frameEpoch &&
1266
+ previous.viewId === viewId &&
1267
+ previous.renderables === renderables
1268
+ ) {
1269
+ return;
1270
+ }
1271
+ this.visibilityFacets.activateView(view);
1272
+ const activePrimitiveIds = new Set<string>();
1273
+ for (const renderable of renderables) {
1274
+ const bounds = renderable.localAabb;
1275
+ if (
1276
+ renderable.lods === undefined ||
1277
+ renderable.lods.length === 0 ||
1278
+ bounds === undefined ||
1279
+ bounds.length < 6 ||
1280
+ [...bounds].some((value) => !Number.isFinite(value))
1281
+ ) {
1282
+ continue;
1283
+ }
1284
+ const slot = this.stableSlotByEntity.get(this.stableEntityKey(worlds, renderable));
1285
+ if (slot === undefined) continue;
1286
+ const primitive = primitiveKey({
1287
+ attachmentId: world.identity,
1288
+ worldGeneration: this.worldKeyAt(worlds, renderable.worldId),
1289
+ primitiveSlot: slot.slot,
1290
+ slotGeneration: slot.generation,
1291
+ });
1292
+ activePrimitiveIds.add(primitiveKeyId(primitive));
1293
+ this.visibilityFacets.setCandidate(view, primitive, { level: 0, confidence: 1 });
1294
+ }
1295
+ this.visibilityFacets.pruneCandidates(view, activePrimitiveIds);
1296
+ this.visibilityFacetFrame = { frameEpoch: this.frameEpoch, viewId, renderables };
1297
+ }
1298
+
1299
+ /**
1300
+ * Project the current facet into the final primary-raster submission.
1301
+ *
1302
+ * Only renderables carrying authored lower-detail levels are queryable. A
1303
+ * plain mesh therefore remains on the ordinary visibility path, while an
1304
+ * LOD candidate with two accepted zero-sample results is suppressed until
1305
+ * the confidence scheduler requests its bounded re-test. Unknown identity,
1306
+ * invalid bounds, and non-queryable confidence all remain conservatively
1307
+ * visible through `decideVisibility`.
1308
+ */
1309
+ projectVisibility(
1310
+ worlds: readonly World[],
1311
+ camera: CameraSnapshot | undefined,
1312
+ renderables: readonly RenderableSnapshot[],
1313
+ dispatch: readonly DispatchEntry[],
1314
+ ): PersistentVisibilityProjection {
1315
+ if (camera === undefined) {
1316
+ const activeEntityKeys = new Set(
1317
+ renderables.map((renderable) => this.stableEntityKey(worlds, renderable)),
1318
+ );
1319
+ return {
1320
+ renderables,
1321
+ dispatch,
1322
+ activeEntityKeys,
1323
+ activeEntityRevision: this.activeEntityRevision(activeEntityKeys),
1324
+ suppressed: 0,
1325
+ };
1326
+ }
1327
+ const cameraWorld = worlds[camera.worldId ?? 0];
1328
+ if (cameraWorld === undefined) {
1329
+ const activeEntityKeys = new Set(
1330
+ renderables.map((renderable) => this.stableEntityKey(worlds, renderable)),
1331
+ );
1332
+ return {
1333
+ renderables,
1334
+ dispatch,
1335
+ activeEntityKeys,
1336
+ activeEntityRevision: this.activeEntityRevision(activeEntityKeys),
1337
+ suppressed: 0,
1338
+ };
1339
+ }
1340
+ const view = viewKey({
1341
+ attachmentId: cameraWorld.identity,
1342
+ cameraEntity: camera.entityKey ?? 0,
1343
+ viewRole: 'main',
1344
+ viewGeneration: camera.historyVersion ?? 0,
1345
+ });
1346
+ const viewId = viewKeyId(view);
1347
+ const cached = this.visibilityProjectionCache;
1348
+ if (
1349
+ cached?.frameEpoch === this.frameEpoch &&
1350
+ cached.drawRevision === this.visibilityFacets.drawRevisionValue &&
1351
+ cached.viewId === viewId &&
1352
+ cached.renderables === renderables &&
1353
+ cached.dispatch === dispatch
1354
+ ) {
1355
+ return cached.projection;
1356
+ }
1357
+ const projectedIndex = new Int32Array(renderables.length);
1358
+ projectedIndex.fill(-1);
1359
+ const projected: RenderableSnapshot[] = [];
1360
+ const activeEntityKeys = new Set<number>();
1361
+ let suppressed = 0;
1362
+ for (let index = 0; index < renderables.length; index += 1) {
1363
+ const renderable = renderables[index];
1364
+ if (renderable === undefined) continue;
1365
+ const slot = this.stableSlotByEntity.get(this.stableEntityKey(worlds, renderable));
1366
+ let draw = true;
1367
+ if (slot !== undefined && (renderable.lods?.length ?? 0) > 0) {
1368
+ const primitive = primitiveKey({
1369
+ attachmentId: cameraWorld.identity,
1370
+ worldGeneration: this.worldKeyAt(worlds, renderable.worldId),
1371
+ primitiveSlot: slot.slot,
1372
+ slotGeneration: slot.generation,
1373
+ });
1374
+ const bounds = renderable.localAabb;
1375
+ const validBounds =
1376
+ bounds !== undefined &&
1377
+ bounds.length >= 6 &&
1378
+ [...bounds].every((value) => Number.isFinite(value));
1379
+ draw = decideVisibility({
1380
+ authorVisible: true,
1381
+ validBounds,
1382
+ frustumVisible: true,
1383
+ lodReady: true,
1384
+ occlusion: this.visibilityFacets.getConfidence(view, primitive),
1385
+ lane: 'gpu',
1386
+ }).draw;
1387
+ }
1388
+ if (!draw) {
1389
+ suppressed += 1;
1390
+ continue;
1391
+ }
1392
+ projectedIndex[index] = projected.length;
1393
+ projected.push(renderable);
1394
+ activeEntityKeys.add(this.stableEntityKey(worlds, renderable));
1395
+ }
1396
+ const projectedDispatch: DispatchEntry[] = [];
1397
+ for (const entry of dispatch) {
1398
+ const renderableIndex = projectedIndex[entry.renderableIndex];
1399
+ if (renderableIndex === undefined || renderableIndex < 0) continue;
1400
+ projectedDispatch.push({ ...entry, renderableIndex });
1401
+ }
1402
+ const projection = {
1403
+ renderables: Object.freeze(projected),
1404
+ dispatch: Object.freeze(projectedDispatch),
1405
+ activeEntityKeys,
1406
+ activeEntityRevision: this.activeEntityRevision(activeEntityKeys),
1407
+ suppressed,
1408
+ };
1409
+ this.visibilityProjectionCache = {
1410
+ frameEpoch: this.frameEpoch,
1411
+ drawRevision: this.visibilityFacets.drawRevisionValue,
1412
+ viewId,
1413
+ renderables,
1414
+ dispatch,
1415
+ projection,
1416
+ };
1417
+ return projection;
1418
+ }
1419
+
1420
+ /**
1421
+ * Advance the projection revision only when the active stable-entity set
1422
+ * changes. Camera fingerprints and scene-plan identity cover view/topology
1423
+ * changes; keeping this token set-stable preserves the GPU filtered-plan
1424
+ * cache on ordinary no-change frames.
1425
+ */
1426
+ private activeEntityRevision(activeEntityKeys: ReadonlySet<number>): number {
1427
+ const previous = this.lastActiveEntityKeys;
1428
+ if (
1429
+ previous === undefined ||
1430
+ previous.size !== activeEntityKeys.size ||
1431
+ [...previous].some((key) => !activeEntityKeys.has(key))
1432
+ ) {
1433
+ this.lastActiveEntityKeys = new Set(activeEntityKeys);
1434
+ this.visibilityProjectionRevision += 1;
1435
+ }
1436
+ return this.visibilityProjectionRevision;
1437
+ }
1438
+
1439
+ private invalidateActiveEntityRevision(): void {
1440
+ this.lastActiveEntityKeys = undefined;
1441
+ this.visibilityProjectionRevision += 1;
1442
+ }
1443
+
1444
+ /** Project probe facts and per-primitive selection into the persistent scene owner. */
1445
+ projectReflectionProbes(probes: readonly ReflectionProbeFact[]): void {
1446
+ const projection = this.composition?.projection;
1447
+ if (projection === undefined) return;
1448
+ if (probes.length === 0) {
1449
+ this.reflectionProbes.update([], []);
1450
+ return;
1451
+ }
1452
+ const primitives = projection
1453
+ .slotsSnapshot()
1454
+ .map((slot) => {
1455
+ const resolved = projection.cullingWorldBoundsAt(slot);
1456
+ if (resolved === undefined || resolved === null) return undefined;
1457
+ return {
1458
+ worldId: slot.worldId,
1459
+ entityKey: slot.entityKey,
1460
+ renderableKey: `${slot.worldId}:${slot.entityKey}`,
1461
+ center: [
1462
+ (resolved.min[0] + resolved.max[0]) / 2,
1463
+ (resolved.min[1] + resolved.max[1]) / 2,
1464
+ (resolved.min[2] + resolved.max[2]) / 2,
1465
+ ] as [number, number, number],
1466
+ };
1467
+ })
1468
+ .filter((primitive): primitive is NonNullable<typeof primitive> => primitive !== undefined);
1469
+ this.reflectionProbes.update(probes, primitives);
1470
+ }
1471
+
1472
+ reflectionProbeSelection(worldId: number, entityKey: number): ReflectionProbeSelectionResult {
1473
+ return this.reflectionProbes.selection(worldId, entityKey);
1474
+ }
1475
+
1476
+ reflectionProbeProjection(): ReturnType<ReflectionProbeProjection['snapshot']> {
1477
+ return this.reflectionProbes.snapshot();
1478
+ }
1479
+
890
1480
  extractComposition(
891
1481
  worlds: readonly World[],
892
1482
  owner: { readonly cameraOwner: number; readonly resourceOwner: number },
893
1483
  catalogEpoch: number,
894
- buildCandidateFrame: () => ExtractedFrame,
1484
+ buildCandidateFrame: (renderables: 'full' | 'none') => ExtractedFrame,
895
1485
  leases?: readonly RenderReadLease[],
896
1486
  ): ExtractedFrame {
897
1487
  this.worldEntitiesScanned = 0;
898
1488
  const entry = this.composition;
1489
+ const sameWorldSet =
1490
+ entry !== undefined &&
1491
+ entry.worlds.length === worlds.length &&
1492
+ entry.worlds.every((world) => worlds.includes(world));
1493
+ const worldOrderChanged =
1494
+ entry !== undefined &&
1495
+ sameWorldSet &&
1496
+ entry.worlds.some((world, index) => world !== worlds[index]);
1497
+ const leaseSetPreserved =
1498
+ entry !== undefined &&
1499
+ leases !== undefined &&
1500
+ leases.length === worlds.length &&
1501
+ entry.leaseIdentities.length === leases.length &&
1502
+ entry.leaseIdentities.every((identity) =>
1503
+ leases.some((lease) => lease.worldIdentity === identity),
1504
+ );
1505
+ const preserveVisibilityOnReorder =
1506
+ entry !== undefined &&
1507
+ worldOrderChanged &&
1508
+ entry.catalogEpoch === catalogEpoch &&
1509
+ leaseSetPreserved;
899
1510
  if (
900
1511
  entry === undefined ||
901
1512
  entry.catalogEpoch !== catalogEpoch ||
@@ -906,13 +1517,19 @@ export class PersistentRenderScene {
906
1517
  leases === undefined ||
907
1518
  leases.length !== worlds.length
908
1519
  ) {
909
- return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
1520
+ return this.rebuildComposition(
1521
+ worlds,
1522
+ owner,
1523
+ catalogEpoch,
1524
+ buildCandidateFrame,
1525
+ leases,
1526
+ preserveVisibilityOnReorder,
1527
+ );
910
1528
  }
911
1529
  if (!entry.rigidOnly) {
912
1530
  return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
913
1531
  }
914
1532
 
915
- const operations: RenderSceneOperation[] = [];
916
1533
  let requiresRebuild = false;
917
1534
  for (let worldId = 0; worldId < worlds.length; worldId += 1) {
918
1535
  const world = worlds[worldId];
@@ -922,14 +1539,19 @@ export class PersistentRenderScene {
922
1539
  requiresRebuild = true;
923
1540
  break;
924
1541
  }
1542
+ const version = entry.readVersions[worldId];
1543
+ if (version === undefined) {
1544
+ requiresRebuild = true;
1545
+ break;
1546
+ }
925
1547
  let read: ReturnType<RenderReadLease['readChanges']>;
926
1548
  try {
927
- read = lease.readChanges(entry.readCursors[worldId] ?? 0);
1549
+ read = lease.readChanges(version);
928
1550
  } catch {
929
1551
  requiresRebuild = true;
930
1552
  break;
931
1553
  }
932
- if (read.status === 'overflow') {
1554
+ if (read.status !== 'ok') {
933
1555
  requiresRebuild = true;
934
1556
  break;
935
1557
  }
@@ -942,119 +1564,86 @@ export class PersistentRenderScene {
942
1564
  requiresRebuild = true;
943
1565
  break;
944
1566
  }
945
- const derivedTransforms = new Set<EntityHandle>();
946
- for (const record of changes.records) {
1567
+ for (const changedComponentId of changes.changedComponentIds) {
1568
+ if (!RENDER_RELEVANT_COMPONENT_IDS.has(changedComponentId)) continue;
947
1569
  if (
948
- record.kind === 'derived-component-changed' &&
949
- record.componentId === componentId(Transform)
1570
+ changedComponentId !== componentId(Transform) &&
1571
+ changedComponentId !== componentId(GlobalTransform)
950
1572
  ) {
951
- derivedTransforms.add(record.entity as EntityHandle);
952
- }
953
- }
954
- for (const record of changes.records) {
955
- if (
956
- record.kind !== 'entity-removed' &&
957
- record.componentId !== undefined &&
958
- !RENDER_RELEVANT_COMPONENT_IDS.has(record.componentId)
959
- ) {
960
- continue;
961
- }
962
- const authoredTransform =
963
- record.kind === 'component-changed' && record.componentId === componentId(Transform);
964
- const derivedTransform =
965
- record.kind === 'derived-component-changed' &&
966
- record.componentId === componentId(Transform);
967
- if (derivedTransform) continue;
968
- if (authoredTransform && derivedTransforms.has(record.entity as EntityHandle)) continue;
969
- if (
970
- (authoredTransform || record.componentId === componentId(Transform)) &&
971
- entry.projection.has(worldId, record.entity)
972
- ) {
973
- const worldView = readTransformWorld(world, record.entity as EntityHandle);
974
- if (worldView === undefined) {
975
- requiresRebuild = true;
976
- break;
977
- }
978
- operations.push({
979
- kind: 'update-transform',
980
- worldId,
981
- entityKey: record.entity,
982
- world: new Float32Array(worldView),
983
- });
984
- continue;
1573
+ requiresRebuild = true;
1574
+ break;
985
1575
  }
986
- requiresRebuild = true;
987
- break;
988
1576
  }
989
- if (!requiresRebuild) {
990
- for (const entity of derivedTransforms) {
991
- if (entry.projection.has(worldId, entity)) {
992
- const worldView = readTransformWorld(world, entity);
993
- if (worldView === undefined) {
994
- requiresRebuild = true;
995
- break;
996
- }
997
- operations.push({
998
- kind: 'update-transform',
999
- worldId,
1000
- entityKey: entity,
1001
- world: new Float32Array(worldView),
1002
- });
1003
- continue;
1004
- }
1005
- // Camera snapshots are rebuilt from the current candidate frame and
1006
- // drive culling there; moving a camera must not invalidate the
1007
- // retained scene projection or recreate its GPU tables.
1008
- if (
1009
- !world.hasComponent(entity, Camera) &&
1010
- NON_RENDERABLE_TRANSFORM_CONSUMERS.some((component) =>
1011
- world.hasComponent(entity, component),
1012
- )
1013
- ) {
1014
- requiresRebuild = true;
1015
- break;
1016
- }
1577
+ if (requiresRebuild) break;
1578
+ entry.readVersions[worldId] = read.version;
1579
+ }
1580
+ if (requiresRebuild) {
1581
+ return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
1582
+ }
1583
+ const transformSpans: WorldTransformSpan[] = [];
1584
+ for (let worldId = 0; worldId < entry.transformQueries.length; worldId += 1) {
1585
+ const query = entry.transformQueries[worldId];
1586
+ if (query === undefined) continue;
1587
+ for (const span of query.spans().unwrap()) {
1588
+ const changedWorlds = span.get(GlobalTransform).world;
1589
+ if (!(changedWorlds instanceof Float32Array)) {
1590
+ requiresRebuild = true;
1591
+ break;
1017
1592
  }
1593
+ transformSpans.push({ worldId, entities: span.entities, worlds: changedWorlds });
1018
1594
  }
1019
1595
  if (requiresRebuild) break;
1020
- entry.readCursors[worldId] = read.cursor;
1021
1596
  }
1022
1597
  if (requiresRebuild) {
1023
1598
  return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
1024
1599
  }
1025
- if (operations.length === 0) {
1600
+ if (transformSpans.length === 0) {
1026
1601
  this.noChangeFrames += 1;
1027
1602
  this.syncGpuOwner(entry.token, entry.projection);
1028
1603
  return this.deriveFramePlan(entry, buildCandidateFrame);
1029
1604
  }
1030
- const delta = entry.projection.apply(operations);
1605
+ const delta = entry.projection.applyTransformSpans(transformSpans);
1031
1606
  entry.topology.apply(delta);
1032
1607
  this.syncGpuOwner(entry.token, entry.projection, delta);
1033
1608
  this.deltaFrames += 1;
1034
- this.transformUpdates += operations.length;
1609
+ this.transformUpdates += delta.updated;
1035
1610
  return this.deriveFramePlan(entry, buildCandidateFrame);
1036
1611
  }
1037
1612
 
1038
1613
  invalidate(): void {
1039
1614
  this.composition = undefined;
1040
1615
  this.temporalCapture = undefined;
1616
+ this.visibilityFacets.clear();
1617
+ this.invalidateActiveEntityRevision();
1618
+ this.visibilityFacetFrame = undefined;
1619
+ this.visibilityProjectionCache = undefined;
1620
+ this.stableSlotByEntity.clear();
1041
1621
  this.lastResyncReason = 'explicit-invalidate';
1042
1622
  }
1043
1623
 
1044
- setCompositionGpuDrivenCoverage(key: string, ownsAll: boolean): void {
1624
+ setCompositionGpuSceneDemand(sceneRowsRequired: boolean): void {
1045
1625
  const entry = this.composition;
1046
1626
  if (entry === undefined) return;
1047
- entry.gpuDrivenCoverageKey = key;
1048
- entry.gpuDrivenOwnsAll = ownsAll;
1627
+ entry.gpuDrivenSceneRequired = sceneRowsRequired;
1049
1628
  }
1050
1629
 
1051
1630
  detach(world: World): void {
1631
+ this.visibilityFacets.detachAttachment(world.identity);
1052
1632
  this.materialSnapshotCaches.delete(world);
1053
1633
  const detachedComposition = this.composition?.worlds.includes(world)
1054
1634
  ? this.composition
1055
1635
  : undefined;
1056
1636
  if (detachedComposition !== undefined) this.composition = undefined;
1057
1637
  if (detachedComposition !== undefined) this.temporalCapture = undefined;
1638
+ if (detachedComposition !== undefined) {
1639
+ this.visibilityFacets.clear();
1640
+ this.invalidateActiveEntityRevision();
1641
+ }
1642
+ if (detachedComposition !== undefined) {
1643
+ this.visibilityFacetFrame = undefined;
1644
+ this.visibilityProjectionCache = undefined;
1645
+ this.stableSlotByEntity.clear();
1646
+ }
1058
1647
  if (this.gpuOwner !== world && this.gpuOwner !== detachedComposition?.token) return;
1059
1648
  this.gpuScene?.dispose();
1060
1649
  this.gpuScene = undefined;
@@ -1065,7 +1654,7 @@ export class PersistentRenderScene {
1065
1654
 
1066
1655
  /** Drop only device-owned tables; the CPU projection remains the recovery authority. */
1067
1656
  resetGpuForRecover(): void {
1068
- if (this.composition !== undefined) this.composition.gpuDrivenOwnsAll = false;
1657
+ if (this.composition !== undefined) this.composition.gpuDrivenSceneRequired = true;
1069
1658
  this.gpuScene = undefined;
1070
1659
  this.gpuOwner = undefined;
1071
1660
  this.gpuDevice = undefined;
@@ -1088,6 +1677,11 @@ export class PersistentRenderScene {
1088
1677
  this.gpuStatus = 'inactive';
1089
1678
  this.composition = undefined;
1090
1679
  this.temporalCapture = undefined;
1680
+ this.visibilityFacets.clear();
1681
+ this.invalidateActiveEntityRevision();
1682
+ this.visibilityFacetFrame = undefined;
1683
+ this.visibilityProjectionCache = undefined;
1684
+ this.stableSlotByEntity.clear();
1091
1685
  this.pointsLinesInspections = [];
1092
1686
  }
1093
1687
 
@@ -1102,6 +1696,7 @@ export class PersistentRenderScene {
1102
1696
  transformUpdates: this.transformUpdates,
1103
1697
  lastResyncReason: this.lastResyncReason,
1104
1698
  projectionRecords: entry?.projection.inspect().records.length ?? 0,
1699
+ ...(entry === undefined ? {} : { probeBlend: entry.probeProjection.inspect() }),
1105
1700
  topology:
1106
1701
  entry?.topology.inspect() ??
1107
1702
  ({
@@ -1126,12 +1721,15 @@ export class PersistentRenderScene {
1126
1721
  scene: this.gpuScene,
1127
1722
  plan: entry.topology.plan(),
1128
1723
  slots: entry.projection.slotsSnapshot(),
1724
+ worldKeys: entry.worldKeys,
1129
1725
  };
1130
1726
  }
1131
1727
 
1132
1728
  /** Publish renderer-owned previous transforms after queue submission. */
1133
1729
  commitTemporalFrame(): Result<void, RhiError> {
1134
- const gpuCommit = this.gpuScene?.commitTemporalFrame();
1730
+ // Previous transforms are consumed only by TAA and motion blur. Ordinary
1731
+ // frames must not copy and upload the complete GPU transform table.
1732
+ const gpuCommit = this.gpuScene?.commitTemporalFrame(this.temporalCapture !== undefined);
1135
1733
  if (gpuCommit !== undefined && !gpuCommit.ok) return gpuCommit;
1136
1734
  const temporalCapture = this.temporalCapture;
1137
1735
  if (temporalCapture !== undefined) {
@@ -1183,14 +1781,72 @@ export class PersistentRenderScene {
1183
1781
  worlds: readonly World[],
1184
1782
  owner: { readonly cameraOwner: number; readonly resourceOwner: number },
1185
1783
  catalogEpoch: number,
1186
- buildCandidateFrame: () => ExtractedFrame,
1784
+ buildCandidateFrame: (renderables: 'full' | 'none') => ExtractedFrame,
1187
1785
  leases?: readonly RenderReadLease[],
1786
+ preserveVisibilityOnReorder = false,
1188
1787
  ): ExtractedFrame {
1189
- const candidateFrame = buildCandidateFrame();
1788
+ const previousEntry = this.composition;
1789
+ const candidateFrame = buildCandidateFrame('full');
1190
1790
  this.temporalCapture = undefined;
1191
1791
  this.worldEntitiesScanned = candidateFrame.renderables.length;
1792
+ const worldKeys = this.visibilityWorldKeysFor(worlds);
1793
+ if (!preserveVisibilityOnReorder) {
1794
+ this.visibilityFacets.clear();
1795
+ this.invalidateActiveEntityRevision();
1796
+ } else if (previousEntry !== undefined) {
1797
+ const previousKeys = new Set(
1798
+ previousEntry.projection
1799
+ .slotsSnapshot()
1800
+ .map((slot) =>
1801
+ worldEntityKey(previousEntry.worldKeys[slot.worldId] ?? slot.worldId, slot.entityKey),
1802
+ ),
1803
+ );
1804
+ const currentKeys = new Set(
1805
+ candidateFrame.renderables.map((renderable) =>
1806
+ worldEntityKey(worldKeys[renderable.worldId] ?? renderable.worldId, renderable.entityKey),
1807
+ ),
1808
+ );
1809
+ if (
1810
+ previousKeys.size !== currentKeys.size ||
1811
+ [...previousKeys].some((key) => !currentKeys.has(key))
1812
+ ) {
1813
+ this.visibilityFacets.clear();
1814
+ this.invalidateActiveEntityRevision();
1815
+ }
1816
+ }
1817
+ // Slot allocation is renderer-owned identity. Sort by stable World identity
1818
+ // before allocating so a worlds[] reorder does not silently change a
1819
+ // primitive's slot/generation. Dispatch is remapped below to the sorted
1820
+ // renderable projection and remains frame-order independent.
1821
+ const sorted = candidateFrame.renderables
1822
+ .map((snapshot, sourceIndex) => ({ snapshot, sourceIndex }))
1823
+ .sort((left, right) => {
1824
+ const leftWorld = worldKeys[left.snapshot.worldId] ?? left.snapshot.worldId;
1825
+ const rightWorld = worldKeys[right.snapshot.worldId] ?? right.snapshot.worldId;
1826
+ return leftWorld - rightWorld || left.snapshot.entityKey - right.snapshot.entityKey;
1827
+ });
1828
+ const sortedRenderables = sorted.map(({ snapshot }) => snapshot);
1829
+ const renderableIndex = new Int32Array(candidateFrame.renderables.length);
1830
+ for (let index = 0; index < sorted.length; index += 1) {
1831
+ const sourceIndex = sorted[index]?.sourceIndex;
1832
+ if (sourceIndex !== undefined) renderableIndex[sourceIndex] = index;
1833
+ }
1834
+ const sortedDispatch = candidateFrame.dispatch.flatMap((entry) => {
1835
+ const index = renderableIndex[entry.renderableIndex];
1836
+ return index === undefined ? [] : [{ ...entry, renderableIndex: index }];
1837
+ });
1838
+ const stableCandidateFrame = {
1839
+ ...candidateFrame,
1840
+ renderables: sortedRenderables,
1841
+ dispatch: sortedDispatch,
1842
+ };
1192
1843
  const projection = new RenderScene();
1193
- projection.reset(candidateFrame.renderables);
1844
+ projection.reset(sortedRenderables);
1845
+ this.stableSlotByEntity.clear();
1846
+ for (const slot of projection.slotsSnapshot()) {
1847
+ const stableWorld = worldKeys[slot.worldId] ?? slot.worldId;
1848
+ this.stableSlotByEntity.set(worldEntityKey(stableWorld, slot.entityKey), slot);
1849
+ }
1194
1850
  const transmissionDemand = new PersistentTransmissionDemandProjection();
1195
1851
  transmissionDemand.apply(transmissionDemandOperations(candidateFrame.renderables));
1196
1852
  const topology = new BatchTopology();
@@ -1204,27 +1860,69 @@ export class PersistentRenderScene {
1204
1860
  projection,
1205
1861
  topology,
1206
1862
  transmissionDemand,
1207
- readCursors:
1863
+ structure: {
1864
+ dispatch: stableCandidateFrame.dispatch,
1865
+ visibilityStats: stableCandidateFrame.visibilityStats,
1866
+ visibilitySnapshots: stableCandidateFrame.visibilitySnapshots,
1867
+ featureVisibilitySnapshots: stableCandidateFrame.featureVisibilitySnapshots,
1868
+ hiddenEntityReports: stableCandidateFrame.hiddenEntityReports,
1869
+ },
1870
+ worldKeys,
1871
+ readVersions:
1208
1872
  leases === undefined || leases.length !== worlds.length
1209
1873
  ? []
1210
- : leases.map((lease) => lease.inspectCursor()),
1874
+ : leases.map((lease) => lease.captureVersion()),
1211
1875
  leaseIdentities:
1212
1876
  leases === undefined || leases.length !== worlds.length
1213
1877
  ? []
1214
1878
  : leases.map((lease) => lease.worldIdentity),
1879
+ transformQueries:
1880
+ leases === undefined || leases.length !== worlds.length
1881
+ ? []
1882
+ : worlds.map(createGlobalTransformChangeQuery),
1215
1883
  catalogEpoch,
1216
1884
  rigidOnly: candidateFrame.renderables.every((renderable) => renderable.skin === undefined),
1217
- gpuDrivenCoverageKey: undefined,
1218
- gpuDrivenOwnsAll: false,
1885
+ probeProjection: new ProbeBlendSceneProjection(),
1886
+ temporalDemanded: frameDemandsTemporal(candidateFrame),
1887
+ gpuDrivenSceneRequired: true,
1219
1888
  };
1220
1889
  this.composition = entry;
1890
+ projection.setTemporalTracking(entry.temporalDemanded);
1891
+ // The projection must be installed before deriving probe primitives. On
1892
+ // the first composition rebuild `projectReflectionProbes` otherwise sees
1893
+ // no composition and silently drops every per-renderable selection,
1894
+ // leaving the producer on its neutral/no-demand path until an unrelated
1895
+ // later rebuild.
1896
+ this.projectReflectionProbes(candidateFrame.reflectionProbes ?? []);
1897
+ this.projectProbes(entry, candidateFrame);
1221
1898
  this.syncGpuOwner(token, projection, undefined, true);
1222
1899
  this.fullRebuilds += 1;
1223
1900
  this.lastResyncReason = 'attach';
1224
- const candidates = projection
1225
- .materialize()
1226
- .map((snapshot) => projection.withTemporal(snapshot));
1227
- return cullPersistentFrame({ ...candidateFrame, renderables: [...candidates] }, candidates);
1901
+ const materialized = projection.materialize();
1902
+ const candidates = entry.temporalDemanded
1903
+ ? materialized.map((snapshot) => projection.withTemporal(snapshot))
1904
+ : materialized;
1905
+ const frameWithProbes = attachProbeRecords(
1906
+ { ...stableCandidateFrame, renderables: candidates },
1907
+ candidates,
1908
+ projection.slotsSnapshot(),
1909
+ entry.probeProjection,
1910
+ );
1911
+ const slots = projection.slotsSnapshot();
1912
+ const result = cullPersistentFrame(
1913
+ frameWithProbes,
1914
+ frameWithProbes.renderables,
1915
+ (snapshot, index) => {
1916
+ const slot = slots[index];
1917
+ return slot === undefined
1918
+ ? projection.cullingWorldBounds(snapshot)
1919
+ : projection.cullingWorldBoundsAt(slot);
1920
+ },
1921
+ );
1922
+ this.frameEpoch += 1;
1923
+ this.visibilityFacetFrame = undefined;
1924
+ this.visibilityProjectionCache = undefined;
1925
+ return result;
1228
1926
  }
1229
1927
 
1230
1928
  /** Internal topology fact; the renderer does not expose demand as public API. */
@@ -1239,17 +1937,89 @@ export class PersistentRenderScene {
1239
1937
 
1240
1938
  /**
1241
1939
  * Derive an ephemeral frame plan from persistent scene facts. The scene
1242
- * retains identity/topology/cursors; no prior frame object is an authority.
1940
+ * retains identity/topology/read versions; no prior frame object is an authority.
1243
1941
  */
1244
1942
  private deriveFramePlan(
1245
1943
  entry: PersistentCompositionEntry,
1246
- buildCandidateFrame: () => ExtractedFrame,
1944
+ buildCandidateFrame: (renderables: 'full' | 'none') => ExtractedFrame,
1247
1945
  ): ExtractedFrame {
1248
- const candidateFrame = buildCandidateFrame();
1249
- const candidates = entry.projection
1250
- .materialize()
1251
- .map((snapshot) => entry.projection.withTemporal(snapshot));
1252
- return cullPersistentFrame({ ...candidateFrame, renderables: [...candidates] }, candidates);
1946
+ const resourceFrame = buildCandidateFrame('none');
1947
+ const candidateFrame: ExtractedFrame = { ...resourceFrame, ...entry.structure };
1948
+ entry.temporalDemanded = frameDemandsTemporal(candidateFrame);
1949
+ entry.projection.setTemporalTracking(entry.temporalDemanded);
1950
+ this.projectProbes(entry, candidateFrame);
1951
+ this.projectReflectionProbes(candidateFrame.reflectionProbes ?? []);
1952
+ const materialized = entry.projection.materialize();
1953
+ const candidates = entry.temporalDemanded
1954
+ ? materialized.map((snapshot) => entry.projection.withTemporal(snapshot))
1955
+ : materialized;
1956
+ const frameWithProbes = attachProbeRecords(
1957
+ { ...candidateFrame, renderables: candidates },
1958
+ candidates,
1959
+ entry.projection.slotsSnapshot(),
1960
+ entry.probeProjection,
1961
+ );
1962
+ const slots = entry.projection.slotsSnapshot();
1963
+ const result = cullPersistentFrame(
1964
+ frameWithProbes,
1965
+ frameWithProbes.renderables,
1966
+ (snapshot, index) => {
1967
+ const slot = slots[index];
1968
+ return slot === undefined
1969
+ ? entry.projection.cullingWorldBounds(snapshot)
1970
+ : entry.projection.cullingWorldBoundsAt(slot);
1971
+ },
1972
+ );
1973
+ this.frameEpoch += 1;
1974
+ this.visibilityFacetFrame = undefined;
1975
+ this.visibilityProjectionCache = undefined;
1976
+ return result;
1977
+ }
1978
+
1979
+ private projectProbes(entry: PersistentCompositionEntry, frame: ExtractedFrame): void {
1980
+ const sky =
1981
+ frame.skylight === undefined
1982
+ ? {
1983
+ available: false,
1984
+ irradiance: [0, 0, 0] as const,
1985
+ fallbackReason: 'no-skylight',
1986
+ }
1987
+ : {
1988
+ available: true,
1989
+ identity: `skylight:${frame.skylight.entityHandle}`,
1990
+ sourceKey:
1991
+ frame.skylight.equirectHandle > 0
1992
+ ? `equirect:${frame.skylight.equirectHandle}`
1993
+ : `skylight:${frame.skylight.entityHandle}`,
1994
+ irradiance: [
1995
+ frame.skylight.color[0] * frame.skylight.intensity,
1996
+ frame.skylight.color[1] * frame.skylight.intensity,
1997
+ frame.skylight.color[2] * frame.skylight.intensity,
1998
+ ] as [number, number, number],
1999
+ };
2000
+ const probes = frame.lightProbes ?? [];
2001
+ // Sky is evaluated by the global environment path. With no local light
2002
+ // probes there can be no per-object ProbeBlendRecord, regardless of how
2003
+ // many dynamic objects moved this frame. Preserve the projection call so
2004
+ // a transition from probes to no probes still clears retained records.
2005
+ if (probes.length === 0) {
2006
+ entry.probeProjection.apply({ objects: [], probes, sky });
2007
+ return;
2008
+ }
2009
+ const objects: ProbeSceneObjectInput[] = entry.projection.slotsSnapshot().map((slot) => ({
2010
+ objectKey: slot.slot,
2011
+ generation: slot.generation,
2012
+ position: [
2013
+ slot.snapshot.transform.world[12] ?? 0,
2014
+ slot.snapshot.transform.world[13] ?? 0,
2015
+ slot.snapshot.transform.world[14] ?? 0,
2016
+ ],
2017
+ }));
2018
+ entry.probeProjection.apply({
2019
+ objects,
2020
+ probes,
2021
+ sky,
2022
+ });
1253
2023
  }
1254
2024
 
1255
2025
  private syncGpuOwner(
@@ -1258,6 +2028,7 @@ export class PersistentRenderScene {
1258
2028
  delta?: RenderSceneApplyResult,
1259
2029
  full = false,
1260
2030
  ): void {
2031
+ if (this.composition?.token === owner && !this.composition.gpuDrivenSceneRequired) return;
1261
2032
  const acquired = this.acquireGpuScene(owner, projection);
1262
2033
  if (acquired === undefined || acquired.rebuilt) return;
1263
2034
  const result = full