@forgeax/engine-render 0.1.27 → 0.1.29

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 (655) hide show
  1. package/README.md +842 -124
  2. package/dist/assembly/atmosphere-shader-prewarm.d.ts +6 -0
  3. package/dist/assembly/atmosphere-shader-prewarm.d.ts.map +1 -0
  4. package/dist/assembly/dynamic-geometry-host.d.ts +52 -0
  5. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -0
  6. package/dist/assembly/dynamic-geometry-runtime.d.ts +20 -0
  7. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +3 -1
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/gpu-driven-owner.d.ts +10 -0
  11. package/dist/assembly/gpu-driven-owner.d.ts.map +1 -0
  12. package/dist/assembly/host-contract.d.ts +19 -0
  13. package/dist/assembly/host-contract.d.ts.map +1 -1
  14. package/dist/assembly/material/assembly.d.ts +7 -0
  15. package/dist/assembly/material/assembly.d.ts.map +1 -1
  16. package/dist/assembly/material-shader-policy.d.ts +3 -1
  17. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  18. package/dist/assembly/recovery/failure-location.d.ts +12 -0
  19. package/dist/assembly/recovery/failure-location.d.ts.map +1 -0
  20. package/dist/assembly/recovery/renderer-recover.d.ts +6 -4
  21. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  22. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  23. package/dist/assembly/renderer-feature-inspection.d.ts +5 -0
  24. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -0
  25. package/dist/assembly/renderer-frame-transaction.d.ts +19 -0
  26. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  27. package/dist/assembly/renderer-helpers.d.ts +3 -0
  28. package/dist/assembly/renderer-helpers.d.ts.map +1 -1
  29. package/dist/assembly/renderer-inspection.d.ts +4 -0
  30. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  31. package/dist/assembly/shader-prewarm-policy.d.ts +6 -0
  32. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -1
  33. package/dist/assembly/skin-palette-owner.d.ts +8 -0
  34. package/dist/assembly/skin-palette-owner.d.ts.map +1 -0
  35. package/dist/assembly/ssr-shader-prewarm.d.ts +6 -0
  36. package/dist/assembly/ssr-shader-prewarm.d.ts.map +1 -0
  37. package/dist/assembly/webgpu-device-loss.d.ts +15 -0
  38. package/dist/assembly/webgpu-device-loss.d.ts.map +1 -0
  39. package/dist/assembly/webgpu-pbr-ready.d.ts +38 -0
  40. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -0
  41. package/dist/assembly/webgpu-ready-mipmap.d.ts +11 -0
  42. package/dist/assembly/webgpu-ready-mipmap.d.ts.map +1 -0
  43. package/dist/assembly/webgpu-ready.d.ts +7 -3
  44. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  45. package/dist/assembly/webgpu-renderer-bootstrap.d.ts +10 -0
  46. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -0
  47. package/dist/assembly/webgpu-renderer-contract.d.ts +62 -0
  48. package/dist/assembly/webgpu-renderer-contract.d.ts.map +1 -0
  49. package/dist/assembly/webgpu-renderer-guards.d.ts +9 -0
  50. package/dist/assembly/webgpu-renderer-guards.d.ts.map +1 -0
  51. package/dist/assembly/webgpu-renderer-observation.d.ts +6 -0
  52. package/dist/assembly/webgpu-renderer-observation.d.ts.map +1 -0
  53. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts +12 -0
  54. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts.map +1 -0
  55. package/dist/assembly/webgpu-renderer.d.ts +7 -141
  56. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  57. package/dist/assembly/webgpu-vertex-layouts.d.ts +28 -84
  58. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -1
  59. package/dist/authoring.mjs +3 -3
  60. package/dist/{chunk-ADHHVYLW.mjs → chunk-33XZFTQU.mjs} +245 -39
  61. package/dist/chunk-33XZFTQU.mjs.map +1 -0
  62. package/dist/{chunk-GF523LHF.mjs → chunk-BLXGONR2.mjs} +30 -12
  63. package/dist/chunk-BLXGONR2.mjs.map +1 -0
  64. package/dist/{chunk-KM2NOX2I.mjs → chunk-BQVQOQJH.mjs} +958 -254
  65. package/dist/chunk-BQVQOQJH.mjs.map +1 -0
  66. package/dist/{chunk-HKXTW355.mjs → chunk-BTWH7BP4.mjs} +386 -17
  67. package/dist/chunk-BTWH7BP4.mjs.map +1 -0
  68. package/dist/{chunk-TMIASV2N.mjs → chunk-FUODTTIA.mjs} +3934 -676
  69. package/dist/chunk-FUODTTIA.mjs.map +1 -0
  70. package/dist/{chunk-FJ6P52EE.mjs → chunk-ILQ5MEZF.mjs} +3 -17
  71. package/dist/chunk-ILQ5MEZF.mjs.map +1 -0
  72. package/dist/{chunk-E34VL5VI.mjs → chunk-JQTSEI3Z.mjs} +3 -3
  73. package/dist/{chunk-E34VL5VI.mjs.map → chunk-JQTSEI3Z.mjs.map} +1 -1
  74. package/dist/{chunk-GE3SDD72.mjs → chunk-NICXSHAT.mjs} +16 -38
  75. package/dist/chunk-NICXSHAT.mjs.map +1 -0
  76. package/dist/{chunk-ZZ4YQ474.mjs → chunk-NRNXBLIJ.mjs} +577 -7
  77. package/dist/chunk-NRNXBLIJ.mjs.map +1 -0
  78. package/dist/{chunk-JDWARUOI.mjs → chunk-PA5DRJNI.mjs} +17 -3
  79. package/dist/chunk-PA5DRJNI.mjs.map +1 -0
  80. package/dist/{chunk-XDP6XE5V.mjs → chunk-S4DNKQYQ.mjs} +1118 -446
  81. package/dist/chunk-S4DNKQYQ.mjs.map +1 -0
  82. package/dist/components/camera.d.ts +62 -2
  83. package/dist/components/camera.d.ts.map +1 -1
  84. package/dist/components/index.d.ts +1 -0
  85. package/dist/components/index.d.ts.map +1 -1
  86. package/dist/components/instances.d.ts +6 -20
  87. package/dist/components/instances.d.ts.map +1 -1
  88. package/dist/components/scene-instance.d.ts +2 -1
  89. package/dist/components/scene-instance.d.ts.map +1 -1
  90. package/dist/components/screen-space-reflection.d.ts +14 -0
  91. package/dist/components/screen-space-reflection.d.ts.map +1 -0
  92. package/dist/construct-renderer.mjs +13805 -7186
  93. package/dist/construct-renderer.mjs.map +1 -1
  94. package/dist/device/gpu-residency.d.ts +11 -17
  95. package/dist/device/gpu-residency.d.ts.map +1 -1
  96. package/dist/device/mesh-residency-lifetime.d.ts +21 -0
  97. package/dist/device/mesh-residency-lifetime.d.ts.map +1 -0
  98. package/dist/dynamic-geometry.d.ts +125 -0
  99. package/dist/dynamic-geometry.d.ts.map +1 -0
  100. package/dist/environment/background.d.ts +6 -0
  101. package/dist/environment/background.d.ts.map +1 -0
  102. package/dist/errors/gpu-driven.d.ts +6 -3
  103. package/dist/errors/gpu-driven.d.ts.map +1 -1
  104. package/dist/errors/render.d.ts +7 -4
  105. package/dist/errors/render.d.ts.map +1 -1
  106. package/dist/extract/camera.d.ts.map +1 -1
  107. package/dist/extract/gpu-driven.d.ts +33 -3
  108. package/dist/extract/gpu-driven.d.ts.map +1 -1
  109. package/dist/features/host.d.ts +23 -1
  110. package/dist/features/host.d.ts.map +1 -1
  111. package/dist/features/noise-texture.d.ts +17 -0
  112. package/dist/features/noise-texture.d.ts.map +1 -0
  113. package/dist/features/plan.d.ts +83 -2
  114. package/dist/features/plan.d.ts.map +1 -1
  115. package/dist/features/prepared-gpu-work.d.ts +37 -4
  116. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  117. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  118. package/dist/features/prepared-graphics.d.ts +5 -0
  119. package/dist/features/prepared-graphics.d.ts.map +1 -1
  120. package/dist/features/render-graph-compute.d.ts +5 -3
  121. package/dist/features/render-graph-compute.d.ts.map +1 -1
  122. package/dist/features/render-graph-contribution.d.ts +11 -3
  123. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  124. package/dist/features/render-graph-raster.d.ts +19 -3
  125. package/dist/features/render-graph-raster.d.ts.map +1 -1
  126. package/dist/features/types.d.ts +11 -0
  127. package/dist/features/types.d.ts.map +1 -1
  128. package/dist/fullscreen-post-process-pass.d.ts +2 -0
  129. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  130. package/dist/gpu-driven/batch-topology.d.ts +34 -1
  131. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  132. package/dist/gpu-driven/pbr-program.d.ts +10 -0
  133. package/dist/gpu-driven/pbr-program.d.ts.map +1 -0
  134. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  135. package/dist/gpu-driven/production-raster.d.ts +144 -23
  136. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  137. package/dist/gpu-driven/shadow-views.d.ts +126 -0
  138. package/dist/gpu-driven/shadow-views.d.ts.map +1 -0
  139. package/dist/gpu-driven/view-gpu.d.ts +18 -2
  140. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  141. package/dist/gpu-scene-schema.d.ts +2 -8
  142. package/dist/gpu-scene-schema.d.ts.map +1 -1
  143. package/dist/gpu-scene.d.ts +14 -0
  144. package/dist/gpu-scene.d.ts.map +1 -1
  145. package/dist/gpu-texture-usage.d.ts +1 -0
  146. package/dist/gpu-texture-usage.d.ts.map +1 -1
  147. package/dist/hdrp-buffers.d.ts +6 -0
  148. package/dist/hdrp-buffers.d.ts.map +1 -1
  149. package/dist/ibl/frame-resources.d.ts +8 -0
  150. package/dist/ibl/frame-resources.d.ts.map +1 -0
  151. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  152. package/dist/index.d.ts +21 -8
  153. package/dist/index.d.ts.map +1 -1
  154. package/dist/index.mjs +8 -7
  155. package/dist/index.mjs.map +1 -1
  156. package/dist/inspection-types.d.ts +164 -0
  157. package/dist/inspection-types.d.ts.map +1 -1
  158. package/dist/instances-derived-bounds.d.ts +11 -0
  159. package/dist/instances-derived-bounds.d.ts.map +1 -1
  160. package/dist/instances.d.ts +89 -44
  161. package/dist/instances.d.ts.map +1 -1
  162. package/dist/internal.d.ts +3 -0
  163. package/dist/internal.d.ts.map +1 -1
  164. package/dist/internal.mjs +118 -10
  165. package/dist/internal.mjs.map +1 -1
  166. package/dist/lifecycle.d.ts +2 -1
  167. package/dist/lifecycle.d.ts.map +1 -1
  168. package/dist/material-row.d.ts +5 -0
  169. package/dist/material-row.d.ts.map +1 -0
  170. package/dist/materials.d.ts +15 -0
  171. package/dist/materials.d.ts.map +1 -1
  172. package/dist/pbr-pipeline.d.ts +21 -5
  173. package/dist/pbr-pipeline.d.ts.map +1 -1
  174. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  175. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts +12 -0
  176. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts.map +1 -0
  177. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts +70 -0
  178. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts.map +1 -0
  179. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts +75 -0
  180. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts.map +1 -0
  181. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts +111 -0
  182. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts.map +1 -0
  183. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts +16 -0
  184. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts.map +1 -0
  185. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts +14 -0
  186. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts.map +1 -0
  187. package/dist/pipeline/standard-output/auto-exposure/state.d.ts +46 -0
  188. package/dist/pipeline/standard-output/auto-exposure/state.d.ts.map +1 -0
  189. package/dist/pipeline/standard-output/color-transform.d.ts +27 -0
  190. package/dist/pipeline/standard-output/color-transform.d.ts.map +1 -0
  191. package/dist/pipeline/standard-output/graph.d.ts +15 -0
  192. package/dist/pipeline/standard-output/graph.d.ts.map +1 -0
  193. package/dist/pipeline/standard-output/lut-admission.d.ts +29 -0
  194. package/dist/pipeline/standard-output/lut-admission.d.ts.map +1 -0
  195. package/dist/pipeline/standard-output/lut-gpu.d.ts +48 -0
  196. package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -0
  197. package/dist/pipeline/standard-output/lut-state.d.ts +68 -0
  198. package/dist/pipeline/standard-output/lut-state.d.ts.map +1 -0
  199. package/dist/pipeline/standard-output/resources.d.ts +10 -0
  200. package/dist/pipeline/standard-output/resources.d.ts.map +1 -0
  201. package/dist/pipeline/standard-output/types.d.ts +39 -0
  202. package/dist/pipeline/standard-output/types.d.ts.map +1 -0
  203. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  204. package/dist/pipeline/standard-post.d.ts +2 -2
  205. package/dist/pipeline/standard-post.d.ts.map +1 -1
  206. package/dist/pipeline-builder.d.ts +2 -1
  207. package/dist/pipeline-builder.d.ts.map +1 -1
  208. package/dist/pipeline-spec-types.d.ts +4 -0
  209. package/dist/pipeline-spec-types.d.ts.map +1 -1
  210. package/dist/pipeline-spec.d.ts +1 -9
  211. package/dist/pipeline-spec.d.ts.map +1 -1
  212. package/dist/points-lines/standard-owner.d.ts +1 -1
  213. package/dist/points-lines/standard-owner.d.ts.map +1 -1
  214. package/dist/prepare/prepared-graphics-resolver.d.ts +6 -1
  215. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  216. package/dist/record/dynamic-geometry-consumption.d.ts +15 -0
  217. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -0
  218. package/dist/record/frame-lighting.d.ts +3 -3
  219. package/dist/record/frame-lighting.d.ts.map +1 -1
  220. package/dist/record/frame-snapshot.d.ts +106 -5
  221. package/dist/record/frame-snapshot.d.ts.map +1 -1
  222. package/dist/record/frame-targets.d.ts.map +1 -1
  223. package/dist/record/frame.d.ts +39 -4
  224. package/dist/record/frame.d.ts.map +1 -1
  225. package/dist/record/main-pass-geometry.d.ts +7 -8
  226. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  227. package/dist/record/main-pass-material.d.ts +38 -1
  228. package/dist/record/main-pass-material.d.ts.map +1 -1
  229. package/dist/record/main-pass-sprite-draws.d.ts +2 -10
  230. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  231. package/dist/record/main-pass.d.ts +12 -2
  232. package/dist/record/main-pass.d.ts.map +1 -1
  233. package/dist/record/mesh-ssbo.d.ts +15 -6
  234. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  235. package/dist/record/prepared-material-bindings.d.ts +19 -0
  236. package/dist/record/prepared-material-bindings.d.ts.map +1 -0
  237. package/dist/record/recovery-pipeline.d.ts +2 -9
  238. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  239. package/dist/record/render-context.d.ts +76 -26
  240. package/dist/record/render-context.d.ts.map +1 -1
  241. package/dist/record/shadow-pass.d.ts +62 -7
  242. package/dist/record/shadow-pass.d.ts.map +1 -1
  243. package/dist/record/skybox-post-pass.d.ts +1 -1
  244. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  245. package/dist/record/typed-frame-graph.d.ts +35 -7
  246. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  247. package/dist/record/view-ubo.d.ts +1 -0
  248. package/dist/record/view-ubo.d.ts.map +1 -1
  249. package/dist/recovery/render-system-candidate.d.ts +10 -2
  250. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  251. package/dist/recovery/render-system-roots.d.ts +11 -0
  252. package/dist/recovery/render-system-roots.d.ts.map +1 -0
  253. package/dist/recovery/types.d.ts +76 -0
  254. package/dist/recovery/types.d.ts.map +1 -0
  255. package/dist/reflection/record-owner.d.ts +0 -2
  256. package/dist/reflection/record-owner.d.ts.map +1 -1
  257. package/dist/render-contract.d.ts +185 -30
  258. package/dist/render-contract.d.ts.map +1 -1
  259. package/dist/render-graph-primitives.d.ts +1 -0
  260. package/dist/render-graph-primitives.d.ts.map +1 -1
  261. package/dist/render-pipeline.d.ts +64 -5
  262. package/dist/render-pipeline.d.ts.map +1 -1
  263. package/dist/render-system-extract-tail.d.ts +7 -5
  264. package/dist/render-system-extract-tail.d.ts.map +1 -1
  265. package/dist/render-system-extract.d.ts +101 -41
  266. package/dist/render-system-extract.d.ts.map +1 -1
  267. package/dist/render-system-projections.d.ts +22 -0
  268. package/dist/render-system-projections.d.ts.map +1 -0
  269. package/dist/render-system.d.ts +34 -77
  270. package/dist/render-system.d.ts.map +1 -1
  271. package/dist/scene/render-scene-types.d.ts +14 -7
  272. package/dist/scene/render-scene-types.d.ts.map +1 -1
  273. package/dist/scene/render-scene.d.ts +29 -11
  274. package/dist/scene/render-scene.d.ts.map +1 -1
  275. package/dist/scene/shadow-visibility.d.ts +5 -0
  276. package/dist/scene/shadow-visibility.d.ts.map +1 -0
  277. package/dist/ssr/admission.d.ts +70 -1
  278. package/dist/ssr/admission.d.ts.map +1 -1
  279. package/dist/ssr/compose.d.ts +13 -0
  280. package/dist/ssr/compose.d.ts.map +1 -0
  281. package/dist/ssr/errors.d.ts +35 -0
  282. package/dist/ssr/errors.d.ts.map +1 -1
  283. package/dist/ssr/graph.d.ts +60 -0
  284. package/dist/ssr/graph.d.ts.map +1 -0
  285. package/dist/ssr/history.d.ts +97 -0
  286. package/dist/ssr/history.d.ts.map +1 -0
  287. package/dist/ssr/hiz.d.ts +37 -0
  288. package/dist/ssr/hiz.d.ts.map +1 -0
  289. package/dist/ssr/inspection.d.ts +42 -0
  290. package/dist/ssr/inspection.d.ts.map +1 -0
  291. package/dist/ssr/resources.d.ts +58 -0
  292. package/dist/ssr/resources.d.ts.map +1 -0
  293. package/dist/ssr/temporal.d.ts +38 -0
  294. package/dist/ssr/temporal.d.ts.map +1 -0
  295. package/dist/systems/skin-palette-allocator.d.ts +32 -10
  296. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  297. package/dist/systems/skin-palette-types.d.ts +15 -0
  298. package/dist/systems/skin-palette-types.d.ts.map +1 -1
  299. package/dist/systems/transparent-dispatch.d.ts +9 -0
  300. package/dist/systems/transparent-dispatch.d.ts.map +1 -0
  301. package/dist/targets/material-source.d.ts +7 -0
  302. package/dist/targets/material-source.d.ts.map +1 -1
  303. package/dist/temporal/frame-coordinator.d.ts +7 -0
  304. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  305. package/dist/temporal/gpu.d.ts +8 -0
  306. package/dist/temporal/gpu.d.ts.map +1 -1
  307. package/dist/temporal/history.d.ts.map +1 -1
  308. package/dist/temporal/index.mjs +6 -6
  309. package/dist/temporal/inspection.d.ts.map +1 -1
  310. package/dist/temporal/standard-scene-data.d.ts +2 -1
  311. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  312. package/dist/temporal/temporal-view.d.ts +2 -0
  313. package/dist/temporal/temporal-view.d.ts.map +1 -1
  314. package/dist/typed-render-graph-primitives.d.ts +35 -1
  315. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  316. package/dist/typed-shadow-passes.d.ts +3 -1
  317. package/dist/typed-shadow-passes.d.ts.map +1 -1
  318. package/dist/volume/temporal.d.ts +6 -0
  319. package/dist/volume/temporal.d.ts.map +1 -1
  320. package/package.json +21 -20
  321. package/src/__tests__/auto-exposure-gpu-evidence.ts +291 -0
  322. package/src/__tests__/auto-exposure-inspection.unit.test.ts +80 -0
  323. package/src/__tests__/auto-exposure-transaction.integration.test.ts +99 -0
  324. package/src/__tests__/auto-exposure.browser.test.ts +33 -0
  325. package/src/__tests__/auto-exposure.dawn.test.ts +33 -0
  326. package/src/__tests__/detached-inspection.integration.test.ts +5 -0
  327. package/src/__tests__/device-scope-lifecycle.unit.test.ts +18 -0
  328. package/src/__tests__/device-scope-stale-matrix.unit.test.ts +18 -0
  329. package/src/__tests__/docs-gate-green.unit.test.ts +3 -3
  330. package/src/__tests__/dynamic-geometry-host.unit.test.ts +1123 -0
  331. package/src/__tests__/dynamic-geometry.unit.test.ts +233 -0
  332. package/src/__tests__/factory-contract.integration.test.ts +73 -22
  333. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +19 -0
  334. package/src/__tests__/feature-depth-input.dawn.test.ts +121 -0
  335. package/src/__tests__/feature-noise.unit.test.ts +38 -0
  336. package/src/__tests__/fixtures/taa-resolve-pinned-baseline.wgsl +301 -0
  337. package/src/__tests__/frame-camera-selection.unit.test.ts +131 -0
  338. package/src/__tests__/frame-plan-contract.unit.test.ts +24 -0
  339. package/src/__tests__/frame-targets.unit.test.ts +24 -0
  340. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +3 -0
  341. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +79 -0
  342. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +287 -6
  343. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +122 -142
  344. package/src/__tests__/gpu-driven-indirect-raster.browser.test.ts +10 -1
  345. package/src/__tests__/gpu-driven-lod-cache.unit.test.ts +264 -0
  346. package/src/__tests__/gpu-driven-mesh-world-identity.unit.test.ts +349 -0
  347. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +1417 -0
  348. package/src/__tests__/gpu-driven-pbr.integration.test.ts +21 -0
  349. package/src/__tests__/gpu-driven-production-fixture.ts +96 -0
  350. package/src/__tests__/gpu-driven-production.integration.test.ts +1075 -93
  351. package/src/__tests__/gpu-driven-scaling.unit.test.ts +1 -3
  352. package/src/__tests__/gpu-driven-shadow-views.browser.test.ts +63 -0
  353. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +294 -0
  354. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +97 -0
  355. package/src/__tests__/gpu-driven-shadow-views.unit.test.ts +168 -0
  356. package/src/__tests__/gpu-driven-skin-contract.test-d.ts +11 -0
  357. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +72 -20
  358. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +30 -9
  359. package/src/__tests__/gpu-driven-view.unit.test.ts +159 -52
  360. package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -1
  361. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +42 -0
  362. package/src/__tests__/gpu-scene-render-graph-gpu.ts +19 -5
  363. package/src/__tests__/gpu-scene.dawn.test.ts +40 -13
  364. package/src/__tests__/gpu-scene.unit.test.ts +559 -33
  365. package/src/__tests__/instances-culling.integration.test.ts +36 -10
  366. package/src/__tests__/instances-derived-bounds.unit.test.ts +54 -0
  367. package/src/__tests__/instances-store.unit.test.ts +242 -39
  368. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +6 -0
  369. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +13 -16
  370. package/src/__tests__/lut-lkg-transaction.integration.test.ts +122 -0
  371. package/src/__tests__/material-contract-inventory.unit.test.ts +5 -6
  372. package/src/__tests__/material-dispatch-shader-id.unit.test.ts +51 -0
  373. package/src/__tests__/material-lane-selection.unit.test.ts +60 -0
  374. package/src/__tests__/material-numeric-upload.unit.test.ts +53 -0
  375. package/src/__tests__/material-texture-handles.unit.test.ts +18 -0
  376. package/src/__tests__/material-texture-source.unit.test.ts +87 -0
  377. package/src/__tests__/materials-authoring-validation.unit.test.ts +41 -0
  378. package/src/__tests__/materials-standard-contract.unit.test.ts +52 -2
  379. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +4 -0
  380. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +53 -0
  381. package/src/__tests__/normal-fallback.dawn.test.ts +122 -0
  382. package/src/__tests__/particle-material-input-layout.unit.test.ts +104 -0
  383. package/src/__tests__/physical-clearcoat.integration.test.ts +37 -3
  384. package/src/__tests__/pipeline-texture-specialization.unit.test.ts +138 -0
  385. package/src/__tests__/point-shadow-inspection.unit.test.ts +2 -0
  386. package/src/__tests__/prepared-gpu-driven-pbr-contract.test-d.ts +31 -0
  387. package/src/__tests__/prepared-gpu-driven-pbr-contract.unit.test.ts +39 -0
  388. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +35 -4
  389. package/src/__tests__/prepared-graphics-capability.unit.test.ts +21 -0
  390. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +6 -2
  391. package/src/__tests__/profiler-phase-catalog.test.ts +5 -0
  392. package/src/__tests__/public-surface.test-d.ts +3 -0
  393. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +30 -12
  394. package/src/__tests__/reflection-fallback-async-publication.unit.test.ts +104 -0
  395. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +2 -1
  396. package/src/__tests__/render-error-code-owner.test-d.ts +4 -0
  397. package/src/__tests__/render-error-detail-ssot.test-d.ts +22 -0
  398. package/src/__tests__/render-error-exhaustive.test-d.ts +15 -0
  399. package/src/__tests__/render-feature-gpu-work.integration.test.ts +285 -2
  400. package/src/__tests__/render-feature-plan.unit.test.ts +245 -0
  401. package/src/__tests__/render-feature-shadow-order.unit.test.ts +228 -0
  402. package/src/__tests__/render-feature-zero-work.unit.test.ts +35 -1
  403. package/src/__tests__/render-graph-fallback.integration.test.ts +2 -2
  404. package/src/__tests__/render-owner-cohesion.unit.test.ts +55 -0
  405. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +426 -0
  406. package/src/__tests__/render-scene-projection.unit.test.ts +914 -23
  407. package/src/__tests__/render-scene-resource-updates.integration.test.ts +214 -0
  408. package/src/__tests__/render-scene-skin-lifecycle.integration.test.ts +173 -0
  409. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +133 -0
  410. package/src/__tests__/render-target-material-source.test-d.ts +27 -3
  411. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +61 -1
  412. package/src/__tests__/renderer-frame-transaction.integration.test.ts +923 -5
  413. package/src/__tests__/shader-manifest-fixture.ts +39 -0
  414. package/src/__tests__/skin-motion-regression.unit.test.ts +0 -33
  415. package/src/__tests__/skin-palette-persistent.integration.test.ts +28 -0
  416. package/src/__tests__/skin-palette-persistent.unit.test.ts +247 -0
  417. package/src/__tests__/skinned-shadow-caster.test.ts +118 -14
  418. package/src/__tests__/specular-environment-scale.dawn.test.ts +156 -0
  419. package/src/__tests__/ssr-admission-matrix.integration.test.ts +7 -1
  420. package/src/__tests__/ssr-confidence-compose.unit.test.ts +84 -0
  421. package/src/__tests__/ssr-coordinate-oracle.unit.test.ts +28 -0
  422. package/src/__tests__/ssr-format-probe-gating.integration.test.ts +82 -0
  423. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +91 -0
  424. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +112 -0
  425. package/src/__tests__/ssr-gpu-dispatch.ts +946 -0
  426. package/src/__tests__/ssr-graph-topology.unit.test.ts +104 -0
  427. package/src/__tests__/ssr-history-filter.dawn.test.ts +198 -0
  428. package/src/__tests__/ssr-hit-color.dawn.test.ts +273 -0
  429. package/src/__tests__/ssr-hiz-odd-reduction.dawn.test.ts +569 -0
  430. package/src/__tests__/ssr-hiz-rescue.dawn.test.ts +279 -0
  431. package/src/__tests__/ssr-hiz.browser.test.ts +150 -0
  432. package/src/__tests__/ssr-hiz.dawn.test.ts +150 -0
  433. package/src/__tests__/ssr-hiz.unit.test.ts +56 -0
  434. package/src/__tests__/ssr-inspection.unit.test.ts +105 -0
  435. package/src/__tests__/ssr-memory-budget.unit.test.ts +45 -0
  436. package/src/__tests__/ssr-performance-sequence-aggregation.unit.test.ts +15 -0
  437. package/src/__tests__/ssr-public-surface.test-d.ts +37 -0
  438. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +387 -0
  439. package/src/__tests__/ssr-reflection-mip-reducer.dawn.test.ts +309 -0
  440. package/src/__tests__/ssr-reprojection-probe.ts +90 -0
  441. package/src/__tests__/ssr-roughness-mip.dawn.test.ts +163 -0
  442. package/src/__tests__/ssr-shader-prewarm.unit.test.ts +59 -0
  443. package/src/__tests__/ssr-spatial.integration.test.ts +141 -0
  444. package/src/__tests__/ssr-stable-lattice.dawn.test.ts +475 -0
  445. package/src/__tests__/ssr-temporal-history.unit.test.ts +202 -0
  446. package/src/__tests__/ssr-texel-crossing.dawn.test.ts +448 -0
  447. package/src/__tests__/ssr-zero-work.integration.test.ts +87 -0
  448. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +44 -12
  449. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +29 -0
  450. package/src/__tests__/standard-output-chain-contract.unit.test.ts +99 -0
  451. package/src/__tests__/standard-output-chain.integration.test.ts +73 -0
  452. package/src/__tests__/standard-output-gpu-evidence.ts +205 -0
  453. package/src/__tests__/standard-output.browser.test.ts +15 -0
  454. package/src/__tests__/standard-output.dawn.test.ts +15 -0
  455. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +38 -0
  456. package/src/__tests__/standard-pipeline.integration.test.ts +320 -3
  457. package/src/__tests__/standard-profile-oracle.unit.test.ts +22 -0
  458. package/src/__tests__/standard-surface-lighting.unit.test.ts +41 -0
  459. package/src/__tests__/standard-texture-specialization.dawn.test.ts +141 -0
  460. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +6 -3
  461. package/src/__tests__/taa-coverage-edge.dawn.test.ts +193 -0
  462. package/src/__tests__/taa-history-clipping.dawn.test.ts +328 -0
  463. package/src/__tests__/taa-neighborhood-equivalence.dawn.test.ts +778 -0
  464. package/src/__tests__/taa-stability-feedback.browser.test.ts +23 -0
  465. package/src/__tests__/taa-stability-feedback.dawn.test.ts +68 -0
  466. package/src/__tests__/taa-stability-feedback.ts +341 -0
  467. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +49 -7
  468. package/src/__tests__/temporal-reset-matrix.unit.test.ts +9 -0
  469. package/src/__tests__/temporal-transaction.integration.test.ts +28 -0
  470. package/src/__tests__/texture-usage-surface.unit.test.ts +4 -2
  471. package/src/__tests__/typed-pipeline-topology.unit.test.ts +109 -4
  472. package/src/__tests__/typed-scene-pass-material-cache.integration.test.ts +126 -0
  473. package/src/__tests__/vertex-layouts-ssot.unit.test.ts +47 -0
  474. package/src/__tests__/visibility-extract.perf.test.ts +20 -8
  475. package/src/__tests__/visibility-instances.integration.test.ts +19 -7
  476. package/src/__tests__/visibility-producer-matrix.integration.test.ts +20 -9
  477. package/src/assembly/atmosphere-shader-prewarm.ts +38 -0
  478. package/src/assembly/dynamic-geometry-host.ts +887 -0
  479. package/src/assembly/dynamic-geometry-runtime.ts +71 -0
  480. package/src/assembly/factory.ts +4 -0
  481. package/src/assembly/gpu-driven-owner.ts +29 -0
  482. package/src/assembly/host-contract.ts +36 -0
  483. package/src/assembly/material/assembly.ts +38 -0
  484. package/src/assembly/material-shader-policy.ts +71 -28
  485. package/src/assembly/recovery/failure-location.ts +62 -0
  486. package/src/assembly/recovery/renderer-recover.ts +19 -17
  487. package/src/assembly/renderer-facade.ts +29 -0
  488. package/src/assembly/renderer-feature-inspection.ts +13 -0
  489. package/src/assembly/renderer-frame-transaction.ts +63 -0
  490. package/src/assembly/renderer-helpers.ts +26 -0
  491. package/src/assembly/renderer-inspection.ts +6 -0
  492. package/src/assembly/shader-prewarm-policy.ts +32 -0
  493. package/src/assembly/skin-palette-owner.ts +21 -0
  494. package/src/assembly/ssr-shader-prewarm.ts +41 -0
  495. package/src/assembly/webgpu-device-loss.ts +75 -0
  496. package/src/assembly/webgpu-pbr-ready.ts +232 -0
  497. package/src/assembly/webgpu-ready-mipmap.ts +26 -0
  498. package/src/assembly/webgpu-ready.ts +142 -121
  499. package/src/assembly/webgpu-renderer-bootstrap.ts +85 -0
  500. package/src/assembly/webgpu-renderer-contract.ts +69 -0
  501. package/src/assembly/webgpu-renderer-guards.ts +32 -0
  502. package/src/assembly/webgpu-renderer-observation.ts +43 -0
  503. package/src/assembly/webgpu-renderer-recovery-failure.ts +62 -0
  504. package/src/assembly/webgpu-renderer.ts +675 -614
  505. package/src/assembly/webgpu-vertex-layouts.ts +233 -87
  506. package/src/components/__tests__/camera-exposure.contract.unit.test.ts +153 -0
  507. package/src/components/camera.ts +287 -7
  508. package/src/components/index.ts +1 -0
  509. package/src/components/instances.ts +45 -18
  510. package/src/components/scene-instance.ts +2 -1
  511. package/src/components/screen-space-reflection.ts +21 -0
  512. package/src/components/sprite-animation.ts +3 -3
  513. package/src/components/sprite-instances.ts +1 -1
  514. package/src/device/gpu-residency.ts +96 -93
  515. package/src/device/mesh-residency-lifetime.ts +73 -0
  516. package/src/dynamic-geometry.ts +736 -0
  517. package/src/environment/background.ts +263 -0
  518. package/src/errors/gpu-driven.ts +39 -5
  519. package/src/errors/render.ts +27 -3
  520. package/src/extract/camera.ts +26 -1
  521. package/src/extract/gpu-driven.ts +185 -10
  522. package/src/features/host.ts +237 -19
  523. package/src/features/noise-texture.ts +91 -0
  524. package/src/features/plan.ts +553 -24
  525. package/src/features/prepared-gpu-work.ts +631 -49
  526. package/src/features/prepared-graphics-store.ts +31 -0
  527. package/src/features/prepared-graphics.ts +5 -0
  528. package/src/features/render-graph-compute.ts +41 -8
  529. package/src/features/render-graph-contribution.ts +141 -3
  530. package/src/features/render-graph-raster.ts +143 -30
  531. package/src/features/types.ts +11 -0
  532. package/src/fullscreen-post-process-pass.ts +3 -0
  533. package/src/gpu-driven/batch-topology.ts +424 -102
  534. package/src/gpu-driven/pbr-program.ts +31 -0
  535. package/src/gpu-driven/prepared-draw.ts +29 -4
  536. package/src/gpu-driven/production-raster.ts +2267 -694
  537. package/src/gpu-driven/shadow-views.ts +539 -0
  538. package/src/gpu-driven/view-gpu.ts +188 -41
  539. package/src/gpu-scene-schema.ts +52 -11
  540. package/src/gpu-scene.ts +186 -99
  541. package/src/gpu-texture-usage.ts +1 -0
  542. package/src/hdrp-buffers.ts +72 -36
  543. package/src/ibl/frame-resources.ts +38 -0
  544. package/src/ibl/skylight-bind-group.ts +5 -4
  545. package/src/index.ts +125 -1
  546. package/src/inspection-types.ts +191 -0
  547. package/src/instances-derived-bounds.ts +258 -34
  548. package/src/instances.ts +459 -150
  549. package/src/internal.ts +22 -0
  550. package/src/lifecycle.ts +2 -0
  551. package/src/material-row.ts +145 -0
  552. package/src/materials.ts +44 -8
  553. package/src/pbr-pipeline.ts +159 -17
  554. package/src/pipeline/standard-forward-lane.ts +52 -24
  555. package/src/pipeline/standard-output/__tests__/lut-admission.unit.test.ts +103 -0
  556. package/src/pipeline/standard-output/auto-exposure/__tests__/capability.unit.test.ts +40 -0
  557. package/src/pipeline/standard-output/auto-exposure/__tests__/gpu.contract.unit.test.ts +125 -0
  558. package/src/pipeline/standard-output/auto-exposure/__tests__/graph.contract.unit.test.ts +94 -0
  559. package/src/pipeline/standard-output/auto-exposure/__tests__/oracle.test.ts +108 -0
  560. package/src/pipeline/standard-output/auto-exposure/__tests__/preset.test.ts +26 -0
  561. package/src/pipeline/standard-output/auto-exposure/capability.ts +38 -0
  562. package/src/pipeline/standard-output/auto-exposure/gpu.ts +567 -0
  563. package/src/pipeline/standard-output/auto-exposure/graph.ts +221 -0
  564. package/src/pipeline/standard-output/auto-exposure/inspection.ts +231 -0
  565. package/src/pipeline/standard-output/auto-exposure/oracle.ts +291 -0
  566. package/src/pipeline/standard-output/auto-exposure/preset.ts +28 -0
  567. package/src/pipeline/standard-output/auto-exposure/state.ts +156 -0
  568. package/src/pipeline/standard-output/color-transform.ts +124 -0
  569. package/src/pipeline/standard-output/graph.ts +139 -0
  570. package/src/pipeline/standard-output/lut-admission.ts +121 -0
  571. package/src/pipeline/standard-output/lut-gpu.ts +399 -0
  572. package/src/pipeline/standard-output/lut-state.ts +176 -0
  573. package/src/pipeline/standard-output/resources.ts +121 -0
  574. package/src/pipeline/standard-output/types.ts +58 -0
  575. package/src/pipeline/standard-pipeline.ts +192 -59
  576. package/src/pipeline/standard-post.ts +186 -27
  577. package/src/pipeline-builder.ts +16 -6
  578. package/src/pipeline-spec-types.ts +4 -0
  579. package/src/pipeline-spec.ts +37 -29
  580. package/src/points-lines/standard-owner.ts +6 -14
  581. package/src/prepare/prepared-graphics-resolver.ts +9 -2
  582. package/src/record/__tests__/material-slot-plan.unit.test.ts +48 -0
  583. package/src/record/__tests__/mesh-ssbo.unit.test.ts +40 -1
  584. package/src/record/__tests__/recovery-pipeline.unit.test.ts +2 -29
  585. package/src/record/__tests__/sprite-material-abi.unit.test.ts +36 -5
  586. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +45 -0
  587. package/src/record/__tests__/view-ubo-layout.unit.test.ts +5 -0
  588. package/src/record/dynamic-geometry-consumption.ts +59 -0
  589. package/src/record/frame-lighting.ts +12 -11
  590. package/src/record/frame-snapshot.ts +133 -23
  591. package/src/record/frame-targets.ts +9 -0
  592. package/src/record/frame.ts +727 -133
  593. package/src/record/main-pass-geometry.ts +162 -98
  594. package/src/record/main-pass-material.ts +115 -26
  595. package/src/record/main-pass-sprite-draws.ts +18 -107
  596. package/src/record/main-pass.ts +78 -8
  597. package/src/record/material-uniforms.ts +1 -1
  598. package/src/record/mesh-ssbo.ts +59 -15
  599. package/src/record/prepared-material-bindings.ts +234 -0
  600. package/src/record/recovery-pipeline.ts +59 -136
  601. package/src/record/render-context.ts +92 -22
  602. package/src/record/shadow-pass.ts +1231 -209
  603. package/src/record/skybox-post-pass.ts +14 -3
  604. package/src/record/typed-frame-graph.ts +1006 -53
  605. package/src/record/view-ubo.ts +11 -0
  606. package/src/recovery/render-system-candidate.ts +90 -66
  607. package/src/recovery/render-system-roots.ts +113 -0
  608. package/src/recovery/types.ts +77 -0
  609. package/src/reflection/record-owner.ts +26 -54
  610. package/src/render-contract.ts +219 -25
  611. package/src/render-graph-primitives.ts +6 -2
  612. package/src/render-pipeline.ts +70 -1
  613. package/src/render-system-extract-tail.ts +409 -160
  614. package/src/render-system-extract.ts +376 -102
  615. package/src/render-system-projections.ts +241 -0
  616. package/src/render-system.ts +692 -791
  617. package/src/scene/render-scene-types.ts +13 -4
  618. package/src/scene/render-scene.ts +569 -355
  619. package/src/scene/shadow-visibility.ts +21 -0
  620. package/src/ssr/admission.ts +380 -14
  621. package/src/ssr/compose.ts +144 -0
  622. package/src/ssr/errors.ts +48 -0
  623. package/src/ssr/graph.ts +777 -0
  624. package/src/ssr/history.ts +436 -0
  625. package/src/ssr/hiz.ts +124 -0
  626. package/src/ssr/inspection.ts +124 -0
  627. package/src/ssr/resources.ts +202 -0
  628. package/src/ssr/temporal.ts +164 -0
  629. package/src/systems/skin-palette-allocator.ts +495 -148
  630. package/src/systems/skin-palette-types.ts +17 -0
  631. package/src/systems/transparent-dispatch.ts +90 -0
  632. package/src/targets/material-source.ts +34 -0
  633. package/src/temporal/coverage.ts +1 -1
  634. package/src/temporal/frame-coordinator.ts +23 -2
  635. package/src/temporal/gpu.ts +74 -12
  636. package/src/temporal/history.ts +3 -1
  637. package/src/temporal/inspection.ts +2 -1
  638. package/src/temporal/standard-scene-data.ts +3 -1
  639. package/src/temporal/temporal-view.ts +3 -0
  640. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +204 -139
  641. package/src/typed-render-graph-primitives.ts +266 -74
  642. package/src/typed-shadow-passes.ts +122 -12
  643. package/src/volume/temporal.ts +10 -0
  644. package/dist/chunk-ADHHVYLW.mjs.map +0 -1
  645. package/dist/chunk-FJ6P52EE.mjs.map +0 -1
  646. package/dist/chunk-GE3SDD72.mjs.map +0 -1
  647. package/dist/chunk-GF523LHF.mjs.map +0 -1
  648. package/dist/chunk-HKXTW355.mjs.map +0 -1
  649. package/dist/chunk-JDWARUOI.mjs.map +0 -1
  650. package/dist/chunk-KM2NOX2I.mjs.map +0 -1
  651. package/dist/chunk-TMIASV2N.mjs.map +0 -1
  652. package/dist/chunk-XDP6XE5V.mjs.map +0 -1
  653. package/dist/chunk-ZZ4YQ474.mjs.map +0 -1
  654. package/src/__tests__/instances-world-ownership.integration.test.ts +0 -62
  655. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +0 -36
@@ -3,41 +3,40 @@ import { componentId } from '@forgeax/engine-ecs/internal';
3
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, GlobalTransform, MorphWeights, Transform } from '@forgeax/engine-scene';
6
+ import {
7
+ ChildOf,
8
+ collectSubtree,
9
+ GlobalTransform,
10
+ MorphWeights,
11
+ Transform,
12
+ } from '@forgeax/engine-scene';
7
13
  import { Skin } from '@forgeax/engine-skinning';
8
14
  import { err, ok, type Result } from '@forgeax/engine-types';
9
15
  import {
10
- Camera,
11
- DirectionalLight,
12
16
  Instances,
13
17
  Layer,
14
- LightProbe,
15
18
  Lines,
16
19
  MeshFilter,
17
20
  MeshRenderer,
18
- MotionBlur,
19
- PointLight,
20
- PointLightShadow,
21
21
  Points,
22
- PostProcessParams,
23
- ReflectionProbe,
24
- SkyboxBackground,
25
- Skylight,
26
22
  SortKey,
27
- SpotLight,
28
23
  SpriteInstances,
29
24
  SpriteRegionOverride,
30
25
  Visibility,
31
26
  } from '../components';
32
27
  import type { DeviceScope, LifecycleResourceSpec } from '../device/device-scope';
28
+ import { gpuDrivenShadowDrawKey } from '../extract/gpu-driven';
33
29
  import {
34
30
  BatchTopology,
35
31
  type BatchTopologyInspection,
36
32
  type SubmissionPlan,
37
33
  } from '../gpu-driven/batch-topology';
38
34
  import { GpuScene } from '../gpu-scene';
39
- import type { PersistentRenderSceneInspection } from '../inspection-types';
40
- import type { InstanceProjectionStore } from '../instances';
35
+ import type {
36
+ GpuDrivenStructureMetrics,
37
+ PersistentRenderSceneInspection,
38
+ } from '../inspection-types';
39
+ import type { InstanceCollectionStore } from '../instances';
41
40
  import { fingerprintNumericArray, InstanceBoundsCache } from '../instances-derived-bounds';
42
41
  import type { PointsLinesInspection } from '../points-lines/inspection';
43
42
  import type { PointsLinesRetainedSnapshot } from '../points-lines/snapshot';
@@ -57,13 +56,11 @@ import type {
57
56
  RenderableSnapshot,
58
57
  RenderableTemporalSnapshot,
59
58
  } from '../render-system-extract';
60
- import type { SkinPaletteAllocator } from '../systems/skin-palette-allocator';
61
59
  import {
62
60
  PersistentTransmissionDemandProjection,
63
61
  type TransmissionDemand,
64
62
  type TransmissionDemandOperation,
65
63
  } from '../transmission/projection';
66
- import { VolumetricFog } from '../volume/component';
67
64
  import { ProbeBlendSceneProjection, type ProbeSceneObjectInput } from './probe-blend';
68
65
  import type {
69
66
  RenderSceneApplyResult,
@@ -74,6 +71,7 @@ import type {
74
71
  RenderSceneResyncReason,
75
72
  RenderSceneSlot,
76
73
  } from './render-scene-types';
74
+ import { buildShadowFrusta } from './shadow-visibility';
77
75
  import { createVisibilityBudget, type VisibilityBudget } from './visibility/budget';
78
76
  import {
79
77
  primitiveKey,
@@ -103,6 +101,10 @@ interface PendingIdentity {
103
101
  current: RenderableSnapshot | undefined;
104
102
  removed: boolean;
105
103
  contentChanged: boolean;
104
+ /** An Instances row changed during this apply batch. */
105
+ instanceChanged: boolean;
106
+ /** A retained world transform changed during this apply batch. */
107
+ transformChanged: boolean;
106
108
  }
107
109
 
108
110
  interface WorldTransformSpan {
@@ -137,6 +139,16 @@ export interface RenderSceneSubmissionDelta {
137
139
  readonly slots: readonly number[];
138
140
  }
139
141
 
142
+ /**
143
+ * Complete ShadowCaster producer projection retained beside the camera-cull
144
+ * submission frame. The persistent scene owns this sequence; record consumes
145
+ * it for the CPU residual lane without rebuilding a second scene projection.
146
+ */
147
+ export interface PersistentShadowCasterProjection {
148
+ readonly renderables: readonly RenderableSnapshot[];
149
+ readonly dispatch: readonly DispatchEntry[];
150
+ }
151
+
140
152
  function identityKey(worldId: number, entityKey: number): string {
141
153
  return `${worldId}:${entityKey}`;
142
154
  }
@@ -222,13 +234,6 @@ function ownPointsLinesSnapshot(
222
234
  };
223
235
  }
224
236
 
225
- function withWorld(snapshot: RenderableSnapshot, world: Float32Array): RenderableSnapshot {
226
- return {
227
- ...snapshot,
228
- transform: { ...snapshot.transform, world: new Float32Array(world) },
229
- };
230
- }
231
-
232
237
  function intersects(left: RenderSceneBounds, right: RenderSceneBounds): boolean {
233
238
  return (
234
239
  left.min[0] <= right.max[0] &&
@@ -322,8 +327,8 @@ export class RenderScene {
322
327
  private readonly freeSlots: number[] = [];
323
328
  private readonly slotsByWorld = new Map<number, Map<number, number>>();
324
329
  private readonly slotsByMaterial = new Map<number, Set<number>>();
330
+ private readonly slotsByAsset = new Map<number, Set<number>>();
325
331
  private readonly dirtySinceSubmitted = new Set<number>();
326
- private readonly transformUpdatedSlots: RenderSceneSlot[] = [];
327
332
  private temporalTracking = false;
328
333
  private lastSubmittedVisible = new Set<string>();
329
334
  private submittedEpoch = 0;
@@ -348,13 +353,26 @@ export class RenderScene {
348
353
  return this.orderedSlots.length;
349
354
  }
350
355
 
351
- apply(operations: readonly RenderSceneOperation[]): RenderSceneApplyResult {
352
- if (operations.length === 0) {
356
+ apply(
357
+ operations: readonly RenderSceneOperation[],
358
+ spans: readonly WorldTransformSpan[] = [],
359
+ ): RenderSceneApplyResult {
360
+ if (operations.length === 0 && spans.length === 0) {
353
361
  this.noChangeFrames += 1;
354
362
  this.renderableScans = 0;
355
363
  return this.emptyResult();
356
364
  }
357
365
 
366
+ for (const operation of operations) {
367
+ if (
368
+ operation.kind === 'update' &&
369
+ operation.snapshot !== undefined &&
370
+ (operation.worldId !== operation.snapshot.worldId ||
371
+ operation.entityKey !== operation.snapshot.entityKey)
372
+ ) {
373
+ throw new RangeError('RenderScene update identity must match its source snapshot');
374
+ }
375
+ }
358
376
  this.deltaFrames += 1;
359
377
  this.renderableScans = operations.length;
360
378
  const pendingByWorld = new Map<number, Map<number, PendingIdentity>>();
@@ -377,31 +395,75 @@ export class RenderScene {
377
395
  current: initial?.snapshot,
378
396
  removed: false,
379
397
  contentChanged: false,
398
+ instanceChanged: false,
399
+ transformChanged: false,
380
400
  };
381
401
  entities.set(entityKey, pending);
382
402
  pendingOrder.push(pending);
383
403
  return pending;
384
404
  };
385
405
 
386
- for (const operation of operations) {
387
- if (operation.kind === 'create') {
388
- const { worldId, entityKey } = operation.snapshot;
389
- const pending = pendingFor(worldId, entityKey);
390
- pending.current = ownSnapshot(operation.snapshot);
391
- pending.contentChanged = true;
392
- continue;
406
+ const updateWorld = (pending: PendingIdentity, source: Float32Array, offset: number): void => {
407
+ const target = pending.current?.transform.world;
408
+ if (target === undefined) return;
409
+ for (let lane = 0; lane < 16; lane += 1) {
410
+ const value = source[offset + lane] ?? 0;
411
+ if (target[lane] !== value) {
412
+ target[lane] = value;
413
+ pending.transformChanged = true;
414
+ }
393
415
  }
416
+ };
417
+ for (const operation of operations) {
394
418
  const pending = pendingFor(operation.worldId, operation.entityKey);
395
419
  if (operation.kind === 'remove') {
396
420
  pending.current = undefined;
397
421
  pending.removed = true;
398
422
  continue;
399
423
  }
424
+ if (operation.snapshot !== undefined) {
425
+ const previousMaterials = pending.current?.materials;
426
+ const next = ownSnapshot(operation.snapshot);
427
+ pending.current =
428
+ previousMaterials !== undefined &&
429
+ previousMaterials.length === next.materials.length &&
430
+ previousMaterials.every((material, index) => material === next.materials[index])
431
+ ? { ...next, materials: previousMaterials }
432
+ : next;
433
+ pending.contentChanged = true;
434
+ }
400
435
  if (pending.current === undefined) {
401
436
  ignoredLateUpdates += 1;
402
437
  continue;
403
438
  }
404
- pending.current = withWorld(pending.current, operation.world);
439
+ if (operation.instances !== undefined) {
440
+ pending.current = {
441
+ ...pending.current,
442
+ instances: {
443
+ ...operation.instances,
444
+ transforms: new Float32Array(operation.instances.transforms),
445
+ },
446
+ };
447
+ pending.instanceChanged = true;
448
+ }
449
+ if (operation.world !== undefined) {
450
+ updateWorld(pending, operation.world, 0);
451
+ }
452
+ }
453
+ // The same pending identity merges column updates with all other source
454
+ // edits. There is one publication and one consumer delta per entity.
455
+ for (const span of spans) {
456
+ this.renderableScans += span.entities.length;
457
+ for (let row = 0; row < span.entities.length; row += 1) {
458
+ const entity = span.entities[row];
459
+ if (entity === undefined) continue;
460
+ const pending = pendingFor(span.worldId, entity);
461
+ if (pending.current === undefined) {
462
+ ignoredLateUpdates += 1;
463
+ continue;
464
+ }
465
+ updateWorld(pending, span.worlds, row * 16);
466
+ }
405
467
  }
406
468
 
407
469
  let created = 0;
@@ -411,6 +473,7 @@ export class RenderScene {
411
473
  const createdSlots: RenderSceneSlot[] = [];
412
474
  const updatedSlots: RenderSceneSlot[] = [];
413
475
  const contentUpdatedSlots: RenderSceneSlot[] = [];
476
+ const instanceUpdatedSlots: RenderSceneSlot[] = [];
414
477
  const removedSlots: RenderSceneRecord[] = [];
415
478
  const recreatedSlots: RenderSceneSlot[] = [];
416
479
  for (const pending of pendingOrder) {
@@ -438,24 +501,38 @@ export class RenderScene {
438
501
  recreated += 1;
439
502
  continue;
440
503
  }
441
- const updatedSlot: RenderSceneSlot = {
442
- ...pending.initial,
443
- snapshot: pending.current,
444
- };
445
- this.unindexMaterials(pending.initial);
504
+ if (!pending.contentChanged && !pending.instanceChanged && !pending.transformChanged)
505
+ continue;
506
+ const updatedSlot: RenderSceneSlot =
507
+ pending.current === pending.initial.snapshot
508
+ ? pending.initial
509
+ : { ...pending.initial, snapshot: pending.current };
510
+ if (pending.contentChanged) this.unindexMaterials(pending.initial);
446
511
  this.slots[pending.initial.slot] = updatedSlot;
447
- this.worldBoundsBySlot[updatedSlot.slot] = undefined;
448
- this.indexMaterials(updatedSlot);
449
- this.dirtySinceSubmitted.add(updatedSlot.slot);
512
+ if (pending.current.instances === undefined && !pending.contentChanged) {
513
+ const cached = this.worldBoundsBySlot[updatedSlot.slot];
514
+ this.worldBoundsBySlot[updatedSlot.slot] =
515
+ worldBounds(pending.current, cached ?? undefined) ?? null;
516
+ } else {
517
+ this.worldBoundsBySlot[updatedSlot.slot] = undefined;
518
+ }
519
+ if (pending.contentChanged) this.indexMaterials(updatedSlot);
520
+ if (this.temporalTracking) this.dirtySinceSubmitted.add(updatedSlot.slot);
450
521
  updatedSlots.push(updatedSlot);
451
522
  if (pending.contentChanged) contentUpdatedSlots.push(updatedSlot);
523
+ else if (pending.instanceChanged) instanceUpdatedSlots.push(updatedSlot);
452
524
  updated += 1;
453
525
  }
454
526
 
455
527
  const changed = created + updated + removed + recreated > 0;
456
528
  if (changed) {
457
529
  this.revision += 1;
458
- this.invalidateSnapshots();
530
+ if (
531
+ created + removed + recreated > 0 ||
532
+ pendingOrder.some((pending) => pending.current !== pending.initial?.snapshot)
533
+ ) {
534
+ this.invalidateSnapshots();
535
+ }
459
536
  }
460
537
  return {
461
538
  created,
@@ -466,61 +543,31 @@ export class RenderScene {
466
543
  createdSlots,
467
544
  updatedSlots,
468
545
  contentUpdatedSlots,
546
+ instanceUpdatedSlots,
469
547
  removedSlots,
470
548
  recreatedSlots,
471
549
  resynced: 0,
472
550
  };
473
551
  }
474
552
 
475
- /** Apply version-selected GlobalTransform columns without allocating per-entity operations. */
476
- applyTransformSpans(spans: readonly WorldTransformSpan[]): RenderSceneApplyResult {
477
- this.deltaFrames += 1;
478
- let scanned = 0;
479
- let updated = 0;
480
- let ignoredLateUpdates = 0;
481
- const updatedSlots = this.transformUpdatedSlots;
482
- updatedSlots.length = 0;
483
- for (const span of spans) {
484
- scanned += span.entities.length;
485
- for (let row = 0; row < span.entities.length; row += 1) {
486
- const entity = span.entities[row];
487
- if (entity === undefined) continue;
488
- const slot = this.lookup(span.worldId, entity);
489
- if (slot === undefined) {
490
- ignoredLateUpdates += 1;
491
- continue;
492
- }
493
- const target = slot.snapshot.transform.world;
494
- const sourceStart = row * 16;
495
- for (let index = 0; index < 16; index += 1) {
496
- target[index] = span.worlds[sourceStart + index] ?? target[index] ?? 0;
497
- }
498
- if (slot.snapshot.instances === undefined) {
499
- const cached = this.worldBoundsBySlot[slot.slot];
500
- const bounds = worldBounds(slot.snapshot, cached ?? undefined);
501
- this.worldBoundsBySlot[slot.slot] = bounds ?? null;
502
- } else {
503
- this.worldBoundsBySlot[slot.slot] = undefined;
504
- }
505
- if (this.temporalTracking) this.dirtySinceSubmitted.add(slot.slot);
506
- updatedSlots.push(slot);
507
- updated += 1;
553
+ /** Rebind positional World indices while retaining stable slot and submission history. */
554
+ remapWorlds(indices: readonly number[]): void {
555
+ this.slotsByWorld.clear();
556
+ for (const slot of this.slots) {
557
+ if (slot === undefined) continue;
558
+ this.instanceBoundsCache.invalidate(slot.entityKey, slot.worldId);
559
+ const mapped = indices[slot.worldId];
560
+ const worldId = mapped === undefined || mapped < 0 ? -1 - slot.worldId : mapped;
561
+ const remapped = { ...slot, worldId, snapshot: { ...slot.snapshot, worldId } };
562
+ this.slots[slot.slot] = remapped;
563
+ let entities = this.slotsByWorld.get(worldId);
564
+ if (entities === undefined) {
565
+ entities = new Map<number, number>();
566
+ this.slotsByWorld.set(worldId, entities);
508
567
  }
568
+ entities.set(slot.entityKey, slot.slot);
509
569
  }
510
- this.renderableScans = scanned;
511
- if (updated > 0) this.revision += 1;
512
- return {
513
- created: 0,
514
- updated,
515
- removed: 0,
516
- recreated: 0,
517
- ignoredLateUpdates,
518
- createdSlots: [],
519
- updatedSlots,
520
- removedSlots: [],
521
- recreatedSlots: [],
522
- resynced: 0,
523
- };
570
+ this.invalidateSnapshots();
524
571
  }
525
572
 
526
573
  /** Reconcile the current identity set while retaining slots for survivors. */
@@ -545,7 +592,7 @@ export class RenderScene {
545
592
  for (const snapshot of desired.values()) {
546
593
  const existing = this.lookup(snapshot.worldId, snapshot.entityKey);
547
594
  if (existing === undefined) {
548
- const created = this.allocate(snapshot);
595
+ const created = this.allocate(ownSnapshot(snapshot));
549
596
  this.dirtySinceSubmitted.add(created.slot);
550
597
  continue;
551
598
  }
@@ -647,7 +694,6 @@ export class RenderScene {
647
694
  }
648
695
  this.lastSubmittedVisible = new Set(capture.visible);
649
696
  this.submittedEpoch += 1;
650
- this.invalidateSnapshots();
651
697
  return {
652
698
  epoch: this.submittedEpoch,
653
699
  slots: capture.slots.map((update) => update.slot),
@@ -729,6 +775,7 @@ export class RenderScene {
729
775
  fullRebuilds: this.fullRebuilds,
730
776
  resyncs: this.resyncs,
731
777
  lastResyncReason: this.lastResyncReason,
778
+ instanceBoundsCache: this.instanceBoundsCache.inspect(),
732
779
  };
733
780
  }
734
781
 
@@ -741,6 +788,8 @@ export class RenderScene {
741
788
  ignoredLateUpdates: 0,
742
789
  createdSlots: [],
743
790
  updatedSlots: [],
791
+ contentUpdatedSlots: [],
792
+ instanceUpdatedSlots: [],
744
793
  removedSlots: [],
745
794
  recreatedSlots: [],
746
795
  resynced: 0,
@@ -761,7 +810,7 @@ export class RenderScene {
761
810
  generation,
762
811
  worldId: snapshot.worldId,
763
812
  entityKey: snapshot.entityKey,
764
- snapshot: ownSnapshot(snapshot),
813
+ snapshot,
765
814
  };
766
815
  let previousWorld = this.previousWorldBySlot[slot];
767
816
  if (previousWorld === undefined) {
@@ -796,7 +845,23 @@ export class RenderScene {
796
845
  this.freeSlots.push(record.slot);
797
846
  }
798
847
 
848
+ addSharedRefConsumers(worldId: number, handle: number, target: Set<number>): void {
849
+ for (const index of [this.slotsByAsset, this.slotsByMaterial]) {
850
+ for (const slot of index.get(handle) ?? []) {
851
+ const record = this.slots[slot];
852
+ if (record?.worldId === worldId) target.add(record.entityKey);
853
+ }
854
+ }
855
+ }
856
+
799
857
  private indexMaterials(record: RenderSceneSlot): void {
858
+ const asset = record.snapshot.assetHandle;
859
+ let assetSlots = this.slotsByAsset.get(asset);
860
+ if (assetSlots === undefined) {
861
+ assetSlots = new Set<number>();
862
+ this.slotsByAsset.set(asset, assetSlots);
863
+ }
864
+ assetSlots.add(record.slot);
800
865
  for (const material of record.snapshot.materials) {
801
866
  const handle = material.materialHandle ?? 0;
802
867
  let slots = this.slotsByMaterial.get(handle);
@@ -809,6 +874,10 @@ export class RenderScene {
809
874
  }
810
875
 
811
876
  private unindexMaterials(record: RenderSceneSlot): void {
877
+ const asset = record.snapshot.assetHandle;
878
+ const assetSlots = this.slotsByAsset.get(asset);
879
+ assetSlots?.delete(record.slot);
880
+ if (assetSlots?.size === 0) this.slotsByAsset.delete(asset);
812
881
  for (const material of record.snapshot.materials) {
813
882
  const handle = material.materialHandle ?? 0;
814
883
  const slots = this.slotsByMaterial.get(handle);
@@ -914,9 +983,8 @@ export interface PersistentRenderSceneOptions {
914
983
  readonly onGpuError?: ((error: RhiError) => void) | undefined;
915
984
  readonly onSharedRefMutation?: ((worldId: number, handle: number) => void) | undefined;
916
985
  readonly configuredQueryBudget?: number | undefined;
917
- /** Rebuildable projection of World-owned instance matrices. */
918
- readonly instanceCollections?: InstanceProjectionStore | undefined;
919
- readonly getSkinPaletteAllocator?: (() => SkinPaletteAllocator | null) | undefined;
986
+ /** Producer-owned instance collections consumed by the same source update flow. */
987
+ readonly instanceCollections?: InstanceCollectionStore | undefined;
920
988
  }
921
989
 
922
990
  export interface PersistentGpuDrivenState {
@@ -925,6 +993,7 @@ export interface PersistentGpuDrivenState {
925
993
  readonly slots: readonly RenderSceneSlot[];
926
994
  /** Stable renderer-local World keys aligned with RenderSceneSlot.worldId. */
927
995
  readonly worldKeys?: readonly number[];
996
+ readonly structureMetrics?: GpuDrivenStructureMetrics;
928
997
  }
929
998
 
930
999
  export interface CpuDirectViewInput {
@@ -993,19 +1062,10 @@ type PersistentGpuSceneInspection = PersistentRenderSceneInspection['gpu'];
993
1062
  interface PersistentCompositionEntry {
994
1063
  readonly token: object;
995
1064
  readonly worlds: readonly World[];
996
- readonly cameraOwner: number;
997
- readonly resourceOwner: number;
998
1065
  readonly projection: RenderScene;
999
1066
  readonly topology: BatchTopology;
1000
1067
  readonly transmissionDemand: PersistentTransmissionDemandProjection;
1001
- structure: Pick<
1002
- ExtractedFrame,
1003
- | 'dispatch'
1004
- | 'visibilityStats'
1005
- | 'visibilitySnapshots'
1006
- | 'featureVisibilitySnapshots'
1007
- | 'hiddenEntityReports'
1008
- >;
1068
+ hiddenEntityReports: ExtractedFrame['hiddenEntityReports'];
1009
1069
  /** Stable renderer-local World keys; unlike RenderableSnapshot.worldId they survive reorder. */
1010
1070
  readonly worldKeys: readonly number[];
1011
1071
  readonly readVersions: RenderReadVersion[];
@@ -1014,53 +1074,22 @@ interface PersistentCompositionEntry {
1014
1074
  readonly renderChangeQueries: readonly ReadonlyMap<number, Query>[];
1015
1075
  readonly dispatchBySlot: Map<number, readonly DispatchEntry[]>;
1016
1076
  dispatchRevision: number;
1017
- dispatchCache:
1018
- | { readonly revision: number; readonly value: readonly DispatchEntry[] }
1019
- | undefined;
1077
+ dispatchCache: { readonly revision: number; readonly value: DispatchEntry[] } | undefined;
1020
1078
  readonly skinConsumersByJoint: Map<string, Set<number>>;
1021
1079
  /** Renderable identities whose partial producer still needs a successful publish. */
1022
1080
  readonly pendingRenderableEntitiesByWorld: Set<number>[];
1023
1081
  /** Slots retained for recovery but excluded from direct and indirect submission. */
1024
1082
  readonly unavailableSlots: Set<number>;
1025
1083
  catalogEpoch: number;
1084
+ instanceEpoch: number;
1026
1085
  readonly probeProjection: ProbeBlendSceneProjection;
1027
1086
  temporalDemanded: boolean;
1028
1087
  gpuDrivenSceneRequired: boolean;
1029
1088
  }
1030
1089
 
1031
- const RENDER_RELEVANT_COMPONENT_IDS = new Set([
1032
- componentId(GlobalTransform),
1033
- componentId(ChildOf),
1034
- componentId(MeshFilter),
1035
- componentId(MeshRenderer),
1036
- componentId(Instances),
1037
- componentId(SpriteInstances),
1038
- componentId(SpriteRegionOverride),
1039
- componentId(Skin),
1040
- componentId(Layer),
1041
- componentId(Visibility),
1042
- componentId(MorphWeights),
1043
- componentId(Points),
1044
- componentId(Lines),
1045
- componentId(SortKey),
1046
- componentId(Camera),
1047
- componentId(MotionBlur),
1048
- componentId(DirectionalLight),
1049
- componentId(LightProbe),
1050
- componentId(PointLight),
1051
- componentId(PointLightShadow),
1052
- componentId(SpotLight),
1053
- componentId(Skylight),
1054
- componentId(SkyboxBackground),
1055
- componentId(PostProcessParams),
1056
- componentId(ReflectionProbe),
1057
- componentId(VolumetricFog),
1058
- ]);
1059
-
1060
- // These components can change the retained renderable source. Other relevant
1061
- // components affect frame resources and still require a composition rebuild;
1062
- // they must not cause a renderable-only partial extraction to silently retain
1063
- // stale lights, cameras, or environment facts.
1090
+ // Retained geometry consumes these component changes. Camera, lighting and
1091
+ // environment facts are freshly extracted by deriveFramePlan on every draw;
1092
+ // their edits must not recreate unrelated geometry or its submission history.
1064
1093
  const RENDERABLE_SOURCE_COMPONENTS = [
1065
1094
  GlobalTransform,
1066
1095
  ChildOf,
@@ -1105,6 +1134,7 @@ function createGlobalTransformChangeQuery(world: World): GlobalTransformChangeQu
1105
1134
  function createRenderChangeQueries(world: World): ReadonlyMap<number, Query> {
1106
1135
  const queries = new Map<number, Query>();
1107
1136
  for (const component of RENDERABLE_SOURCE_COMPONENTS) {
1137
+ if (component === GlobalTransform) continue;
1108
1138
  const result = world.query({ read: [component], changed: [component] });
1109
1139
  if (!result.ok) throw result.error;
1110
1140
  const query = result.value as Query;
@@ -1190,7 +1220,7 @@ function dispatchForProjection(
1190
1220
  dispatchBySlot: ReadonlyMap<number, readonly DispatchEntry[]>,
1191
1221
  projection: RenderScene,
1192
1222
  unavailableSlots: ReadonlySet<number>,
1193
- ): readonly DispatchEntry[] {
1223
+ ): DispatchEntry[] {
1194
1224
  const dispatch: DispatchEntry[] = [];
1195
1225
  const renderables = projection.materialize();
1196
1226
  for (let renderableIndex = 0; renderableIndex < renderables.length; renderableIndex += 1) {
@@ -1200,7 +1230,9 @@ function dispatchForProjection(
1200
1230
  if (slot === undefined) continue;
1201
1231
  if (unavailableSlots.has(slot.slot)) continue;
1202
1232
  for (const entry of dispatchBySlot.get(slot.slot) ?? []) {
1203
- dispatch.push({ ...entry, renderableIndex });
1233
+ dispatch.push(
1234
+ renderableIndex === entry.renderableIndex ? entry : { ...entry, renderableIndex },
1235
+ );
1204
1236
  }
1205
1237
  }
1206
1238
  return dispatch;
@@ -1212,39 +1244,29 @@ function topologyDeltaForAvailable(
1212
1244
  newlyUnavailable: readonly RenderSceneSlot[],
1213
1245
  ): RenderSceneApplyResult {
1214
1246
  const updatedSlots = delta.updatedSlots.filter((slot) => !unavailableSlots.has(slot.slot));
1247
+ const unavailableUpdates = newlyUnavailable.map((slot) => ({
1248
+ ...slot,
1249
+ snapshot: { ...slot.snapshot, gpuDrivenDraws: [] },
1250
+ }));
1215
1251
  const contentUpdatedSlots = delta.contentUpdatedSlots?.filter(
1216
1252
  (slot) => !unavailableSlots.has(slot.slot),
1217
1253
  );
1254
+ const instanceUpdatedSlots = delta.instanceUpdatedSlots?.filter(
1255
+ (slot) => !unavailableSlots.has(slot.slot),
1256
+ );
1257
+ const contentBySlot =
1258
+ contentUpdatedSlots === undefined
1259
+ ? undefined
1260
+ : new Map(contentUpdatedSlots.map((slot) => [slot.slot, slot]));
1261
+ for (const slot of unavailableUpdates) contentBySlot?.set(slot.slot, slot);
1218
1262
  return {
1219
1263
  ...delta,
1220
- updatedSlots: [
1221
- ...updatedSlots,
1222
- ...newlyUnavailable.map((slot) => ({
1223
- ...slot,
1224
- snapshot: { ...slot.snapshot, gpuDrivenDraws: [] },
1225
- })),
1226
- ],
1227
- ...(contentUpdatedSlots === undefined ? {} : { contentUpdatedSlots }),
1264
+ updatedSlots: [...updatedSlots, ...unavailableUpdates],
1265
+ ...(contentBySlot === undefined ? {} : { contentUpdatedSlots: [...contentBySlot.values()] }),
1266
+ ...(instanceUpdatedSlots === undefined ? {} : { instanceUpdatedSlots }),
1228
1267
  };
1229
1268
  }
1230
1269
 
1231
- function addSharedRefConsumers(
1232
- projection: RenderScene,
1233
- worldId: number,
1234
- handle: number,
1235
- target: Set<number>,
1236
- ): void {
1237
- for (const slot of projection.slotsSnapshot()) {
1238
- if (
1239
- slot.worldId === worldId &&
1240
- (slot.snapshot.assetHandle === handle ||
1241
- slot.snapshot.materials.some((material) => material.materialHandle === handle))
1242
- ) {
1243
- target.add(slot.entityKey);
1244
- }
1245
- }
1246
- }
1247
-
1248
1270
  function frameDemandsTemporal(frame: ExtractedFrame): boolean {
1249
1271
  const camera = frame.cameras[0];
1250
1272
  return camera?.antialias === 'taa' || (camera?.motionBlur?.shutterAngle ?? 0) > 0;
@@ -1295,6 +1317,18 @@ function cullPersistentFrame(
1295
1317
  ) => worldBounds(snapshot),
1296
1318
  ): ExtractedFrame {
1297
1319
  const planes = cameraFrusta(frame.cameras);
1320
+ // Shadow casters are not constrained by the display camera. Keep the
1321
+ // primary camera cull intact, but admit a caster that is outside every
1322
+ // camera frustum when its bounds intersect one of the active light-space
1323
+ // frusta. This is especially important for point-light cube faces: the
1324
+ // caster can be behind the camera while still occluding the receiver.
1325
+ const casterIndices = new Set(
1326
+ frame.dispatch
1327
+ .filter((entry) => entry.tags.LightMode === 'ShadowCaster')
1328
+ .map((entry) => entry.renderableIndex),
1329
+ );
1330
+ const shadowFrusta = casterIndices.size === 0 ? [] : buildShadowFrusta(frame.lights);
1331
+ const shadowOnly = new Set<number>();
1298
1332
  const visible: RenderableSnapshot[] = [];
1299
1333
  if (cullProjectedIndexScratch.length < candidates.length) {
1300
1334
  cullProjectedIndexScratch = new Int32Array(candidates.length);
@@ -1313,6 +1347,7 @@ function cullPersistentFrame(
1313
1347
  continue;
1314
1348
  }
1315
1349
  let isVisible = true;
1350
+ let isShadowVisible = false;
1316
1351
  const candidateBounds = boundsOf(candidate, index);
1317
1352
  if (candidateBounds !== undefined) {
1318
1353
  total += 1;
@@ -1332,10 +1367,16 @@ function cullPersistentFrame(
1332
1367
  break;
1333
1368
  }
1334
1369
  }
1370
+ if (!isVisible && casterIndices.has(index)) {
1371
+ isShadowVisible = shadowFrusta.some((lightPlanes) =>
1372
+ frustum.intersectsBox(lightPlanes, worldAabb as box3.Box3Like),
1373
+ );
1374
+ }
1335
1375
  }
1336
1376
  if (!isVisible) {
1337
1377
  culled += 1;
1338
- continue;
1378
+ if (!isShadowVisible) continue;
1379
+ shadowOnly.add(index);
1339
1380
  }
1340
1381
  projectedIndex[index] = visible.length;
1341
1382
  // The culling candidate is the persistent projection snapshot used for
@@ -1349,9 +1390,15 @@ function cullPersistentFrame(
1349
1390
  const dispatch: DispatchEntry[] = [];
1350
1391
  for (const entry of frame.dispatch) {
1351
1392
  const candidateIndex = entry.renderableIndex;
1393
+ // Shadow-only candidates must never enter the main forward/transparent
1394
+ // dispatch. Their ShadowCaster entries remain available to the depth
1395
+ // passes, which consume the same validated ordered list.
1396
+ if (shadowOnly.has(candidateIndex) && entry.tags.LightMode !== 'ShadowCaster') continue;
1352
1397
  const renderableIndex = projectedIndex[candidateIndex];
1353
1398
  if (renderableIndex === undefined || renderableIndex < 0) continue;
1354
- dispatch.push({ ...entry, renderableIndex });
1399
+ dispatch.push(
1400
+ renderableIndex === entry.renderableIndex ? entry : { ...entry, renderableIndex },
1401
+ );
1355
1402
  }
1356
1403
  return {
1357
1404
  ...frame,
@@ -1391,12 +1438,15 @@ export class PersistentRenderScene {
1391
1438
  private readonly materialSnapshotCaches: MaterialSnapshotCachesByWorld = new WeakMap();
1392
1439
  private fullRebuilds = 0;
1393
1440
  private worldEntitiesScanned = 0;
1441
+ private sceneTableUploadBytes = 0;
1394
1442
  private noChangeFrames = 0;
1395
1443
  private deltaFrames = 0;
1396
1444
  private transformUpdates = 0;
1397
1445
  private lastResyncReason: RenderSceneResyncReason | undefined;
1398
1446
  private composition: PersistentCompositionEntry | undefined;
1399
1447
  private gpuScene: GpuScene | undefined;
1448
+ private readonly retiredGpuScenes = new Set<GpuScene>();
1449
+ private shadowProjection: PersistentShadowCasterProjection | undefined;
1400
1450
  private gpuOwner: object | undefined;
1401
1451
  private gpuDevice: RhiDevice | undefined;
1402
1452
  private gpuStatus: 'inactive' | 'unsupported' | 'resident' | 'rebuild-pending' | 'error' =
@@ -1424,6 +1474,7 @@ export class PersistentRenderScene {
1424
1474
  }
1425
1475
  | undefined;
1426
1476
  private visibilityProjectionRevision = 0;
1477
+ private projectedIndexScratch = new Int32Array(0);
1427
1478
  private lastActiveEntityKeys: ReadonlySet<number> | undefined;
1428
1479
  /** Stable renderer-world/entity lookup; rebuilt only with composition topology. */
1429
1480
  private readonly stableSlotByEntity = new Map<number, RenderSceneSlot>();
@@ -1473,6 +1524,10 @@ export class PersistentRenderScene {
1473
1524
  return this.composition?.projection.slotsSnapshot() ?? [];
1474
1525
  }
1475
1526
 
1527
+ shadowCasterProjection(): PersistentShadowCasterProjection | undefined {
1528
+ return this.shadowProjection;
1529
+ }
1530
+
1476
1531
  /** Stable renderer-world/entity lookup shared by query and final projection paths. */
1477
1532
  compositionSlotByStableEntity(): ReadonlyMap<number, RenderSceneSlot> {
1478
1533
  return this.stableSlotByEntity;
@@ -1517,7 +1572,7 @@ export class PersistentRenderScene {
1517
1572
  renderable.lods.length === 0 ||
1518
1573
  bounds === undefined ||
1519
1574
  bounds.length < 6 ||
1520
- [...bounds].some((value) => !Number.isFinite(value))
1575
+ !bounds.every(Number.isFinite)
1521
1576
  ) {
1522
1577
  continue;
1523
1578
  }
@@ -1594,15 +1649,19 @@ export class PersistentRenderScene {
1594
1649
  ) {
1595
1650
  return cached.projection;
1596
1651
  }
1597
- const projectedIndex = new Int32Array(renderables.length);
1598
- projectedIndex.fill(-1);
1652
+ if (this.projectedIndexScratch.length < renderables.length) {
1653
+ this.projectedIndexScratch = new Int32Array(renderables.length);
1654
+ }
1655
+ const projectedIndex = this.projectedIndexScratch;
1656
+ projectedIndex.fill(-1, 0, renderables.length);
1599
1657
  const projected: RenderableSnapshot[] = [];
1600
1658
  const activeEntityKeys = new Set<number>();
1601
1659
  let suppressed = 0;
1602
1660
  for (let index = 0; index < renderables.length; index += 1) {
1603
1661
  const renderable = renderables[index];
1604
1662
  if (renderable === undefined) continue;
1605
- const slot = this.stableSlotByEntity.get(this.stableEntityKey(worlds, renderable));
1663
+ const stableEntity = this.stableEntityKey(worlds, renderable);
1664
+ const slot = this.stableSlotByEntity.get(stableEntity);
1606
1665
  let draw = true;
1607
1666
  if (slot !== undefined && (renderable.lods?.length ?? 0) > 0) {
1608
1667
  const primitive = primitiveKey({
@@ -1613,9 +1672,7 @@ export class PersistentRenderScene {
1613
1672
  });
1614
1673
  const bounds = renderable.localAabb;
1615
1674
  const validBounds =
1616
- bounds !== undefined &&
1617
- bounds.length >= 6 &&
1618
- [...bounds].every((value) => Number.isFinite(value));
1675
+ bounds !== undefined && bounds.length >= 6 && bounds.every(Number.isFinite);
1619
1676
  draw = decideVisibility({
1620
1677
  authorVisible: renderable.authorVisible !== false,
1621
1678
  validBounds,
@@ -1631,13 +1688,15 @@ export class PersistentRenderScene {
1631
1688
  }
1632
1689
  projectedIndex[index] = projected.length;
1633
1690
  projected.push(renderable);
1634
- activeEntityKeys.add(this.stableEntityKey(worlds, renderable));
1691
+ activeEntityKeys.add(stableEntity);
1635
1692
  }
1636
1693
  const projectedDispatch: DispatchEntry[] = [];
1637
1694
  for (const entry of dispatch) {
1638
1695
  const renderableIndex = projectedIndex[entry.renderableIndex];
1639
1696
  if (renderableIndex === undefined || renderableIndex < 0) continue;
1640
- projectedDispatch.push({ ...entry, renderableIndex });
1697
+ projectedDispatch.push(
1698
+ renderableIndex === entry.renderableIndex ? entry : { ...entry, renderableIndex },
1699
+ );
1641
1700
  }
1642
1701
  const projection = {
1643
1702
  renderables: Object.freeze(projected),
@@ -1725,6 +1784,7 @@ export class PersistentRenderScene {
1725
1784
  leases?: readonly RenderReadLease[],
1726
1785
  ): ExtractedFrame {
1727
1786
  this.worldEntitiesScanned = 0;
1787
+ this.sceneTableUploadBytes = 0;
1728
1788
  const entry = this.composition;
1729
1789
  const sameWorldSet =
1730
1790
  entry !== undefined &&
@@ -1749,15 +1809,12 @@ export class PersistentRenderScene {
1749
1809
  leaseSetPreserved;
1750
1810
  if (
1751
1811
  entry === undefined ||
1752
- entry.catalogEpoch !== catalogEpoch ||
1753
- entry.cameraOwner !== owner.cameraOwner ||
1754
- entry.resourceOwner !== owner.resourceOwner ||
1755
1812
  entry.worlds.length !== worlds.length ||
1756
1813
  entry.worlds.some((world, index) => world !== worlds[index]) ||
1757
1814
  leases === undefined ||
1758
1815
  leases.length !== worlds.length
1759
1816
  ) {
1760
- return this.rebuildComposition(
1817
+ return this.reconcileComposition(
1761
1818
  worlds,
1762
1819
  owner,
1763
1820
  catalogEpoch,
@@ -1766,14 +1823,33 @@ export class PersistentRenderScene {
1766
1823
  preserveVisibilityOnReorder,
1767
1824
  );
1768
1825
  }
1769
- let requiresRebuild = false;
1826
+ let requiresSourceRefresh = false;
1770
1827
  const changedEntitiesByWorld = worlds.map(
1771
1828
  (_, worldId) => new Set(entry.pendingRenderableEntitiesByWorld[worldId] ?? []),
1772
1829
  );
1830
+ const contentChangedByWorld = worlds.map(
1831
+ (_, worldId) => new Set(entry.pendingRenderableEntitiesByWorld[worldId] ?? []),
1832
+ );
1833
+ if (entry.catalogEpoch !== catalogEpoch) {
1834
+ for (const slot of entry.projection.slotsSnapshot()) {
1835
+ changedEntitiesByWorld[slot.worldId]?.add(slot.entityKey);
1836
+ contentChangedByWorld[slot.worldId]?.add(slot.entityKey);
1837
+ }
1838
+ }
1839
+ const instanceEpoch = this.options.instanceCollections?.epoch ?? 0;
1840
+ if (entry.instanceEpoch !== instanceEpoch) {
1841
+ for (const slot of entry.projection.slotsSnapshot()) {
1842
+ const id = slot.snapshot.instances?.collectionId;
1843
+ if (id === undefined) continue;
1844
+ const current = this.options.instanceCollections?.inspect(id);
1845
+ if (!current?.ok || current.value.revision !== slot.snapshot.instances?.revision) {
1846
+ changedEntitiesByWorld[slot.worldId]?.add(slot.entityKey);
1847
+ }
1848
+ }
1849
+ }
1773
1850
  const nextReadVersions = entry.readVersions.slice();
1774
1851
  const structureChangedWorlds = new Set<number>();
1775
1852
  const structuralComponentIdsByWorld = worlds.map(() => new Set<number>());
1776
- let refreshStructureFacts = false;
1777
1853
  let currentRenderableEntitiesByWorld: readonly (ReadonlySet<number> | undefined)[] | undefined;
1778
1854
  const removedSlots: RenderSceneSlot[] = [];
1779
1855
  for (let worldId = 0; worldId < worlds.length; worldId += 1) {
@@ -1781,24 +1857,23 @@ export class PersistentRenderScene {
1781
1857
  if (world === undefined) continue;
1782
1858
  const lease = leases?.[worldId];
1783
1859
  if (lease === undefined || entry.leaseIdentities[worldId] !== lease.worldIdentity) {
1784
- requiresRebuild = true;
1860
+ requiresSourceRefresh = true;
1785
1861
  break;
1786
1862
  }
1787
1863
  const version = entry.readVersions[worldId];
1788
1864
  if (version === undefined) {
1789
- requiresRebuild = true;
1865
+ requiresSourceRefresh = true;
1790
1866
  break;
1791
1867
  }
1792
1868
  let read: ReturnType<RenderReadLease['readChanges']>;
1793
1869
  try {
1794
1870
  read = lease.readChanges(version);
1795
1871
  } catch {
1796
- requiresRebuild = true;
1872
+ requiresSourceRefresh = true;
1797
1873
  break;
1798
1874
  }
1799
1875
  if (read.status === 'rebuild') {
1800
1876
  structureChangedWorlds.add(worldId);
1801
- refreshStructureFacts = true;
1802
1877
  }
1803
1878
  const changes = read.world;
1804
1879
  const shared = read.sharedRefs;
@@ -1806,56 +1881,80 @@ export class PersistentRenderScene {
1806
1881
  for (const record of shared.records) {
1807
1882
  this.options.onSharedRefMutation?.(worldId, record.handle);
1808
1883
  const changed = changedEntitiesByWorld[worldId];
1809
- if (changed !== undefined)
1810
- addSharedRefConsumers(entry.projection, worldId, record.handle, changed);
1884
+ const content = contentChangedByWorld[worldId];
1885
+ if (changed !== undefined && content !== undefined) {
1886
+ entry.projection.addSharedRefConsumers(worldId, record.handle, content);
1887
+ for (const entity of content) changed.add(entity);
1888
+ }
1811
1889
  }
1812
1890
  }
1813
1891
  for (const changedComponentId of changes.changedComponentIds) {
1814
1892
  if (read.status === 'rebuild') {
1815
1893
  structuralComponentIdsByWorld[worldId]?.add(changedComponentId);
1816
1894
  }
1817
- if (!RENDER_RELEVANT_COMPONENT_IDS.has(changedComponentId)) continue;
1818
- if (!RENDERABLE_SOURCE_COMPONENT_IDS.has(changedComponentId)) {
1819
- requiresRebuild = true;
1820
- break;
1821
- }
1895
+ if (!RENDERABLE_SOURCE_COMPONENT_IDS.has(changedComponentId)) continue;
1822
1896
  const changed = changedEntitiesByWorld[worldId];
1823
1897
  if (changed === undefined) continue;
1824
- if (VISIBILITY_DEPENDENCY_COMPONENT_IDS.has(changedComponentId)) {
1825
- refreshStructureFacts = true;
1826
- // Visibility and parent changes affect the effective state of a
1827
- // subtree. The normal update path evaluates the current rows; the
1828
- // set is deliberately allowed to be broad, while empty work still
1829
- // exits naturally without a candidate loop.
1830
- for (const entity of collectRenderableEntities(world)) changed.add(entity);
1831
- continue;
1832
- }
1833
- if (changedComponentId === componentId(GlobalTransform)) {
1834
- const query = entry.renderChangeQueries[worldId]?.get(changedComponentId);
1835
- for (const row of query ?? []) {
1836
- const consumers = entry.skinConsumersByJoint.get(
1837
- skinJointKey(worldId, row.entity as number),
1838
- );
1839
- for (const consumer of consumers ?? []) changed.add(consumer);
1840
- }
1841
- continue;
1842
- }
1898
+ const otherChanged = contentChangedByWorld[worldId];
1843
1899
  const query = entry.renderChangeQueries[worldId]?.get(changedComponentId);
1844
1900
  if (query !== undefined) {
1845
- for (const row of query) changed.add(row.entity as number);
1901
+ for (const row of query) {
1902
+ const entity = row.entity as number;
1903
+ if (VISIBILITY_DEPENDENCY_COMPONENT_IDS.has(changedComponentId)) {
1904
+ const affected = collectSubtree(world, row.entity);
1905
+ for (const dependent of affected) {
1906
+ if (
1907
+ !entry.projection.has(worldId, dependent) &&
1908
+ !world.hasComponent(dependent as EntityHandle, MeshRenderer)
1909
+ )
1910
+ continue;
1911
+ changed.add(dependent);
1912
+ otherChanged?.add(dependent);
1913
+ }
1914
+ } else {
1915
+ changed.add(entity);
1916
+ if (changedComponentId !== componentId(Instances)) otherChanged?.add(entity);
1917
+ }
1918
+ }
1846
1919
  }
1847
1920
  }
1848
- if (requiresRebuild) break;
1921
+ if (requiresSourceRefresh) break;
1849
1922
  nextReadVersions[worldId] = read.version;
1850
1923
  }
1851
- if (requiresRebuild) {
1852
- return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
1924
+ if (requiresSourceRefresh) {
1925
+ return this.reconcileComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
1853
1926
  }
1854
1927
 
1928
+ const transformSpans: WorldTransformSpan[] = [];
1929
+ for (let worldId = 0; worldId < entry.transformQueries.length; worldId += 1) {
1930
+ const query = entry.transformQueries[worldId];
1931
+ if (query === undefined) continue;
1932
+ for (const span of query.spans().unwrap()) {
1933
+ const changedWorlds = span.get(GlobalTransform).world;
1934
+ if (!(changedWorlds instanceof Float32Array)) {
1935
+ requiresSourceRefresh = true;
1936
+ break;
1937
+ }
1938
+ transformSpans.push({ worldId, entities: span.entities, worlds: changedWorlds });
1939
+ for (const entity of span.entities) {
1940
+ for (const consumer of entry.skinConsumersByJoint.get(skinJointKey(worldId, entity)) ??
1941
+ []) {
1942
+ changedEntitiesByWorld[worldId]?.add(consumer);
1943
+ contentChangedByWorld[worldId]?.add(consumer);
1944
+ }
1945
+ }
1946
+ }
1947
+ if (requiresSourceRefresh) break;
1948
+ }
1949
+ if (requiresSourceRefresh) {
1950
+ return this.reconcileComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
1951
+ }
1855
1952
  if (structureChangedWorlds.size > 0) {
1856
1953
  this.visibilityFacets.clear();
1857
1954
  this.invalidateActiveEntityRevision();
1858
- const currentByWorld = worlds.map((world) => collectRenderableEntities(world));
1955
+ const currentByWorld = worlds.map((world, worldId) =>
1956
+ structureChangedWorlds.has(worldId) ? collectRenderableEntities(world) : undefined,
1957
+ );
1859
1958
  currentRenderableEntitiesByWorld = currentByWorld;
1860
1959
  for (let worldId = 0; worldId < currentByWorld.length; worldId += 1) {
1861
1960
  const world = worlds[worldId];
@@ -1873,7 +1972,10 @@ export class PersistentRenderScene {
1873
1972
  if (indexedWorldId !== worldId) continue;
1874
1973
  const jointEntity = Number(jointKey.slice(separator + 1));
1875
1974
  if (world.hasComponent(jointEntity as EntityHandle, GlobalTransform)) continue;
1876
- for (const consumer of consumers) changed.add(consumer);
1975
+ for (const consumer of consumers) {
1976
+ changed.add(consumer);
1977
+ contentChangedByWorld[worldId]?.add(consumer);
1978
+ }
1877
1979
  }
1878
1980
  for (const entity of current) {
1879
1981
  if (entry.projection.slot(worldId, entity) === undefined) changed.add(entity);
@@ -1889,26 +1991,14 @@ export class PersistentRenderScene {
1889
1991
  const membershipChanged = sourceMembershipChanged(world, slot);
1890
1992
  if (scanAllMembership || membershipChanged) {
1891
1993
  changed.add(slot.entityKey);
1892
- if (
1893
- slot.snapshot.instances !== undefined &&
1894
- !world.hasComponent(slot.entityKey as EntityHandle, Instances)
1895
- ) {
1896
- this.options.instanceCollections?.release(world, slot.entityKey);
1897
- }
1898
- if (
1899
- slot.snapshot.skin !== undefined &&
1900
- !world.hasComponent(slot.entityKey as EntityHandle, Skin)
1901
- ) {
1902
- this.options
1903
- .getSkinPaletteAllocator?.()
1904
- ?.releaseSliceFor(`${world.identity}:${slot.entityKey}`);
1905
- }
1994
+ contentChangedByWorld[worldId]?.add(slot.entityKey);
1906
1995
  }
1907
1996
  }
1908
1997
  }
1909
1998
  for (const slot of entry.projection.slotsSnapshot()) {
1910
1999
  if (slot.worldId !== worldId || current.has(slot.entityKey)) continue;
1911
2000
  removedSlots.push(slot);
2001
+ changedEntitiesByWorld[worldId]?.add(slot.entityKey);
1912
2002
  }
1913
2003
  this.worldEntitiesScanned += current.size;
1914
2004
  }
@@ -1925,7 +2015,7 @@ export class PersistentRenderScene {
1925
2015
 
1926
2016
  let changedDelta: RenderSceneApplyResult | undefined;
1927
2017
  const hasChangedEntities = changedEntitiesByWorld.some((entities) => entities.size > 0);
1928
- if (hasChangedEntities || removedSlots.length > 0) {
2018
+ if (hasChangedEntities || removedSlots.length > 0 || transformSpans.length > 0) {
1929
2019
  const operations: RenderSceneOperation[] = [];
1930
2020
  const removedDemand: TransmissionDemandOperation[] = [];
1931
2021
  const replacedSkinSnapshots: RenderableSnapshot[] = [];
@@ -1940,11 +2030,6 @@ export class PersistentRenderScene {
1940
2030
  if (slot.snapshot.skinJointEntities !== undefined) {
1941
2031
  removeSkinConsumer(entry.skinConsumersByJoint, slot.snapshot);
1942
2032
  }
1943
- const world = worlds[slot.worldId];
1944
- if (world !== undefined) this.options.instanceCollections?.release(world, slot.entityKey);
1945
- this.options
1946
- .getSkinPaletteAllocator?.()
1947
- ?.releaseSliceFor(`${world?.identity ?? slot.worldId}:${slot.entityKey}`);
1948
2033
  for (
1949
2034
  let materialIndex = 0;
1950
2035
  materialIndex < slot.snapshot.materials.length;
@@ -1969,15 +2054,39 @@ export class PersistentRenderScene {
1969
2054
  }
1970
2055
  }
1971
2056
  }
1972
- const partial = hasChangedEntities
1973
- ? buildCandidateFrame({ kind: 'partial', entitiesByWorld: changedEntitiesByWorld })
1974
- : undefined;
2057
+ let partial: ExtractedFrame | undefined;
2058
+ try {
2059
+ partial = hasChangedEntities
2060
+ ? buildCandidateFrame({ kind: 'partial', entitiesByWorld: changedEntitiesByWorld })
2061
+ : undefined;
2062
+ } catch (cause) {
2063
+ // Change queries have been observed, but this publication never ran.
2064
+ // Retain every consumed transform identity for the producer retry.
2065
+ for (const span of transformSpans) {
2066
+ const pending = entry.pendingRenderableEntitiesByWorld[span.worldId];
2067
+ for (const entity of span.entities) {
2068
+ if (entry.projection.has(span.worldId, entity)) pending?.add(entity);
2069
+ }
2070
+ }
2071
+ throw cause;
2072
+ }
1975
2073
  const publishedEntitiesByWorld = worlds.map(() => new Set<number>());
1976
2074
  for (const snapshot of partial?.renderables ?? []) {
1977
- operations.push({ kind: 'create', snapshot });
2075
+ const existing = entry.projection.slot(snapshot.worldId, snapshot.entityKey);
2076
+ const contentChanged =
2077
+ existing === undefined ||
2078
+ contentChangedByWorld[snapshot.worldId]?.has(snapshot.entityKey);
2079
+ operations.push({
2080
+ kind: 'update',
2081
+ worldId: snapshot.worldId,
2082
+ entityKey: snapshot.entityKey,
2083
+ ...(contentChanged || snapshot.instances === undefined
2084
+ ? { snapshot }
2085
+ : { instances: snapshot.instances }),
2086
+ });
1978
2087
  publishedEntitiesByWorld[snapshot.worldId]?.add(snapshot.entityKey);
1979
2088
  }
1980
- changedDelta = entry.projection.apply(operations);
2089
+ changedDelta = entry.projection.apply(operations, transformSpans);
1981
2090
  const publishedSkinKeys = new Set<string>();
1982
2091
  for (const snapshot of partial?.renderables ?? []) {
1983
2092
  publishedSkinKeys.add(`${snapshot.worldId}:${snapshot.entityKey}`);
@@ -2005,17 +2114,21 @@ export class PersistentRenderScene {
2005
2114
  for (const removed of changedDelta.removedSlots) entry.dispatchBySlot.delete(removed.slot);
2006
2115
  for (const recreated of changedDelta.recreatedSlots)
2007
2116
  entry.dispatchBySlot.delete(recreated.slot);
2117
+ if (partial !== undefined || removedSlots.length > 0) {
2118
+ const worldIndices = new Map(worlds.map((world, index) => [world, index]));
2119
+ entry.hiddenEntityReports = [
2120
+ ...entry.hiddenEntityReports.filter((report) => {
2121
+ const worldId = worldIndices.get(report.world);
2122
+ return (
2123
+ worldId !== undefined &&
2124
+ !changedEntitiesByWorld[worldId]?.has(report.entity) &&
2125
+ currentRenderableEntitiesByWorld?.[worldId]?.has(report.entity) !== false
2126
+ );
2127
+ }),
2128
+ ...(partial?.hiddenEntityReports ?? []),
2129
+ ];
2130
+ }
2008
2131
  if (partial !== undefined) {
2009
- if (refreshStructureFacts) {
2010
- entry.structure = {
2011
- ...entry.structure,
2012
- dispatch: partial.dispatch,
2013
- visibilityStats: partial.visibilityStats,
2014
- visibilitySnapshots: partial.visibilitySnapshots,
2015
- featureVisibilitySnapshots: partial.featureVisibilitySnapshots,
2016
- hiddenEntityReports: partial.hiddenEntityReports,
2017
- };
2018
- }
2019
2132
  const partialDispatch = dispatchEntriesBySlot(partial, entry.projection);
2020
2133
  for (const [slot, dispatch] of partialDispatch) entry.dispatchBySlot.set(slot, dispatch);
2021
2134
  // A changed renderable which now fails producer validation still owns
@@ -2067,8 +2180,10 @@ export class PersistentRenderScene {
2067
2180
  for (const removed of changedDelta.removedSlots) entry.unavailableSlots.delete(removed.slot);
2068
2181
  for (const recreated of changedDelta.recreatedSlots)
2069
2182
  entry.unavailableSlots.delete(recreated.slot);
2070
- entry.dispatchRevision += 1;
2071
- entry.dispatchCache = undefined;
2183
+ if (partial !== undefined || removedSlots.length > 0) {
2184
+ entry.dispatchRevision += 1;
2185
+ entry.dispatchCache = undefined;
2186
+ }
2072
2187
  entry.topology.apply(
2073
2188
  topologyDeltaForAvailable(changedDelta, entry.unavailableSlots, newlyUnavailable),
2074
2189
  );
@@ -2084,41 +2199,13 @@ export class PersistentRenderScene {
2084
2199
  this.transformUpdates += changedDelta.updated;
2085
2200
  }
2086
2201
 
2087
- const transformSpans: WorldTransformSpan[] = [];
2088
- for (let worldId = 0; worldId < entry.transformQueries.length; worldId += 1) {
2089
- const query = entry.transformQueries[worldId];
2090
- if (query === undefined) continue;
2091
- for (const span of query.spans().unwrap()) {
2092
- const changedWorlds = span.get(GlobalTransform).world;
2093
- if (!(changedWorlds instanceof Float32Array)) {
2094
- requiresRebuild = true;
2095
- break;
2096
- }
2097
- transformSpans.push({ worldId, entities: span.entities, worlds: changedWorlds });
2098
- }
2099
- if (requiresRebuild) break;
2100
- }
2101
- if (requiresRebuild) {
2102
- return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
2103
- }
2104
- if (transformSpans.length === 0 && changedDelta === undefined) {
2202
+ if (changedDelta === undefined) {
2105
2203
  this.noChangeFrames += 1;
2106
2204
  this.syncGpuOwner(entry.token, entry.projection);
2107
- const result = this.deriveFramePlan(entry, buildCandidateFrame);
2108
- for (let worldId = 0; worldId < nextReadVersions.length; worldId += 1) {
2109
- const version = nextReadVersions[worldId];
2110
- if (version !== undefined) entry.readVersions[worldId] = version;
2111
- }
2112
- return result;
2113
- }
2114
- if (transformSpans.length > 0) {
2115
- const delta = entry.projection.applyTransformSpans(transformSpans);
2116
- entry.topology.apply(topologyDeltaForAvailable(delta, entry.unavailableSlots, []));
2117
- this.syncGpuOwner(entry.token, entry.projection, delta);
2118
- this.deltaFrames += 1;
2119
- this.transformUpdates += delta.updated;
2120
2205
  }
2121
2206
  const result = this.deriveFramePlan(entry, buildCandidateFrame);
2207
+ entry.catalogEpoch = catalogEpoch;
2208
+ entry.instanceEpoch = instanceEpoch;
2122
2209
  for (let worldId = 0; worldId < nextReadVersions.length; worldId += 1) {
2123
2210
  const version = nextReadVersions[worldId];
2124
2211
  if (version !== undefined) entry.readVersions[worldId] = version;
@@ -2128,6 +2215,7 @@ export class PersistentRenderScene {
2128
2215
 
2129
2216
  invalidate(): void {
2130
2217
  this.composition = undefined;
2218
+ this.shadowProjection = undefined;
2131
2219
  this.temporalCapture = undefined;
2132
2220
  this.visibilityFacets.clear();
2133
2221
  this.invalidateActiveEntityRevision();
@@ -2150,16 +2238,9 @@ export class PersistentRenderScene {
2150
2238
  ? this.composition
2151
2239
  : undefined;
2152
2240
  if (detachedComposition !== undefined) {
2153
- for (const slot of detachedComposition.projection.slotsSnapshot()) {
2154
- if (detachedComposition.worlds[slot.worldId] !== world) continue;
2155
- this.options.instanceCollections?.release(world, slot.entityKey);
2156
- if (slot.snapshot.skin === undefined) continue;
2157
- this.options
2158
- .getSkinPaletteAllocator?.()
2159
- ?.releaseSliceFor(`${world.identity}:${slot.entityKey}`);
2160
- }
2241
+ this.composition = undefined;
2242
+ this.shadowProjection = undefined;
2161
2243
  }
2162
- if (detachedComposition !== undefined) this.composition = undefined;
2163
2244
  if (detachedComposition !== undefined) this.temporalCapture = undefined;
2164
2245
  if (detachedComposition !== undefined) {
2165
2246
  this.visibilityFacets.clear();
@@ -2171,7 +2252,7 @@ export class PersistentRenderScene {
2171
2252
  this.stableSlotByEntity.clear();
2172
2253
  }
2173
2254
  if (this.gpuOwner !== world && this.gpuOwner !== detachedComposition?.token) return;
2174
- this.gpuScene?.dispose();
2255
+ this.retireGpuScene(this.gpuScene);
2175
2256
  this.gpuScene = undefined;
2176
2257
  this.gpuOwner = undefined;
2177
2258
  this.gpuDevice = undefined;
@@ -2181,6 +2262,7 @@ export class PersistentRenderScene {
2181
2262
  /** Drop only device-owned tables; the CPU projection remains the recovery authority. */
2182
2263
  resetGpuForRecover(): void {
2183
2264
  if (this.composition !== undefined) this.composition.gpuDrivenSceneRequired = true;
2265
+ this.retireGpuScene(this.gpuScene);
2184
2266
  this.gpuScene = undefined;
2185
2267
  this.gpuOwner = undefined;
2186
2268
  this.gpuDevice = undefined;
@@ -2256,6 +2338,7 @@ export class PersistentRenderScene {
2256
2338
  publish: () => {
2257
2339
  if (published || released) return;
2258
2340
  published = true;
2341
+ this.retireGpuScene(this.gpuScene);
2259
2342
  this.gpuScene = scene;
2260
2343
  this.gpuOwner = composition.token;
2261
2344
  this.gpuDevice = device;
@@ -2301,12 +2384,13 @@ export class PersistentRenderScene {
2301
2384
  }
2302
2385
 
2303
2386
  dispose(): void {
2304
- this.gpuScene?.dispose();
2387
+ this.retireGpuScene(this.gpuScene);
2305
2388
  this.gpuScene = undefined;
2306
2389
  this.gpuOwner = undefined;
2307
2390
  this.gpuDevice = undefined;
2308
2391
  this.gpuStatus = 'inactive';
2309
2392
  this.composition = undefined;
2393
+ this.shadowProjection = undefined;
2310
2394
  this.temporalCapture = undefined;
2311
2395
  this.visibilityFacets.clear();
2312
2396
  this.invalidateActiveEntityRevision();
@@ -2316,6 +2400,55 @@ export class PersistentRenderScene {
2316
2400
  this.pointsLinesInspections = [];
2317
2401
  }
2318
2402
 
2403
+ private retireGpuScene(scene: GpuScene | undefined, device = this.gpuDevice): void {
2404
+ if (scene === undefined) return;
2405
+ if (device === undefined) {
2406
+ scene.dispose();
2407
+ return;
2408
+ }
2409
+ this.retiredGpuScenes.add(scene);
2410
+ const release = (): void => {
2411
+ if (!this.retiredGpuScenes.delete(scene)) return;
2412
+ scene.dispose();
2413
+ };
2414
+ // Retirement can happen while the current frame is still being encoded
2415
+ // (for example when a new composition token is published). Calling
2416
+ // onSubmittedWorkDone() synchronously at that point observes only work
2417
+ // already queued and may resolve before the frame that still references
2418
+ // this scene is submitted. Defer fence acquisition to the next
2419
+ // microtask, after the synchronous record/submit transaction has had a
2420
+ // chance to enqueue its command buffer.
2421
+ void Promise.resolve()
2422
+ .then(() => {
2423
+ try {
2424
+ return device.queue.onSubmittedWorkDone();
2425
+ } catch {
2426
+ return undefined;
2427
+ }
2428
+ })
2429
+ .then(release, release);
2430
+ }
2431
+
2432
+ /** Read existing culling bounds; never expose cached mutable arrays. */
2433
+ bounds(world: World, entity: number): RenderSceneBounds | undefined {
2434
+ const entry = this.composition;
2435
+ const worldId = entry?.worlds.indexOf(world) ?? -1;
2436
+ if (entry === undefined || worldId < 0) return undefined;
2437
+ const slot = entry.projection.slot(worldId, entity);
2438
+ if (slot === undefined) return undefined;
2439
+ const bounds = entry.projection.cullingWorldBoundsAt(slot);
2440
+ if (bounds === undefined) return undefined;
2441
+ for (const axis of [0, 1, 2] as const) {
2442
+ if (
2443
+ !Number.isFinite(bounds.min[axis]) ||
2444
+ !Number.isFinite(bounds.max[axis]) ||
2445
+ bounds.min[axis] > bounds.max[axis]
2446
+ )
2447
+ return undefined;
2448
+ }
2449
+ return { min: [...bounds.min], max: [...bounds.max] };
2450
+ }
2451
+
2319
2452
  inspect(): PersistentRenderSceneInspection {
2320
2453
  const entry = this.composition;
2321
2454
  const pointsLines = this.pointsLinesInspections;
@@ -2327,6 +2460,9 @@ export class PersistentRenderScene {
2327
2460
  transformUpdates: this.transformUpdates,
2328
2461
  lastResyncReason: this.lastResyncReason,
2329
2462
  projectionRecords: entry?.projection.inspect().records.length ?? 0,
2463
+ ...(entry === undefined
2464
+ ? {}
2465
+ : { instanceBoundsCache: entry.projection.inspect().instanceBoundsCache }),
2330
2466
  ...(entry === undefined ? {} : { probeBlend: entry.probeProjection.inspect() }),
2331
2467
  topology:
2332
2468
  entry?.topology.inspect() ??
@@ -2353,6 +2489,11 @@ export class PersistentRenderScene {
2353
2489
  plan: entry.topology.plan(),
2354
2490
  slots: entry.projection.slotsSnapshot(),
2355
2491
  worldKeys: entry.worldKeys,
2492
+ structureMetrics: {
2493
+ worldEntitiesScanned: this.worldEntitiesScanned,
2494
+ sceneTableUploadBytes: this.sceneTableUploadBytes,
2495
+ paletteUploadBytes: 0,
2496
+ },
2356
2497
  };
2357
2498
  }
2358
2499
 
@@ -2408,9 +2549,9 @@ export class PersistentRenderScene {
2408
2549
  return { status: 'inactive' };
2409
2550
  }
2410
2551
 
2411
- private rebuildComposition(
2552
+ private reconcileComposition(
2412
2553
  worlds: readonly World[],
2413
- owner: { readonly cameraOwner: number; readonly resourceOwner: number },
2554
+ _owner: { readonly cameraOwner: number; readonly resourceOwner: number },
2414
2555
  catalogEpoch: number,
2415
2556
  buildCandidateFrame: (request: PersistentRenderCandidateRequest) => ExtractedFrame,
2416
2557
  leases?: readonly RenderReadLease[],
@@ -2471,8 +2612,28 @@ export class PersistentRenderScene {
2471
2612
  renderables: sortedRenderables,
2472
2613
  dispatch: sortedDispatch,
2473
2614
  };
2474
- const projection = new RenderScene();
2475
- projection.reset(sortedRenderables);
2615
+ const projection = previousEntry?.projection ?? new RenderScene();
2616
+ if (previousEntry !== undefined) {
2617
+ projection.remapWorlds(previousEntry.worlds.map((world) => worlds.indexOf(world)));
2618
+ }
2619
+ const desired = new Set(
2620
+ sortedRenderables.map((snapshot) => identityKey(snapshot.worldId, snapshot.entityKey)),
2621
+ );
2622
+ const operations: RenderSceneOperation[] = [];
2623
+ for (const slot of projection.slotsSnapshot()) {
2624
+ if (!desired.has(identityKey(slot.worldId, slot.entityKey))) {
2625
+ operations.push({ kind: 'remove', worldId: slot.worldId, entityKey: slot.entityKey });
2626
+ }
2627
+ }
2628
+ for (const snapshot of sortedRenderables) {
2629
+ operations.push({
2630
+ kind: 'update',
2631
+ worldId: snapshot.worldId,
2632
+ entityKey: snapshot.entityKey,
2633
+ snapshot,
2634
+ });
2635
+ }
2636
+ const delta = projection.apply(operations);
2476
2637
  this.stableSlotByEntity.clear();
2477
2638
  for (const slot of projection.slotsSnapshot()) {
2478
2639
  const stableWorld = worldKeys[slot.worldId] ?? slot.worldId;
@@ -2480,28 +2641,20 @@ export class PersistentRenderScene {
2480
2641
  }
2481
2642
  const transmissionDemand = new PersistentTransmissionDemandProjection();
2482
2643
  transmissionDemand.apply(transmissionDemandOperations(candidateFrame.renderables));
2483
- const topology = new BatchTopology();
2484
- topology.rebuild(projection.slotsSnapshot());
2644
+ const topology = previousEntry?.topology ?? new BatchTopology();
2645
+ topology.apply(delta);
2485
2646
  const skinConsumersByJoint = new Map<string, Set<number>>();
2486
2647
  for (const snapshot of stableCandidateFrame.renderables) {
2487
2648
  if (snapshot.skinJointEntities !== undefined) addSkinConsumer(skinConsumersByJoint, snapshot);
2488
2649
  }
2489
- const token = {};
2650
+ const token = previousEntry?.token ?? {};
2490
2651
  const entry: PersistentCompositionEntry = {
2491
2652
  token,
2492
2653
  worlds: [...worlds],
2493
- cameraOwner: owner.cameraOwner,
2494
- resourceOwner: owner.resourceOwner,
2495
2654
  projection,
2496
2655
  topology,
2497
2656
  transmissionDemand,
2498
- structure: {
2499
- dispatch: stableCandidateFrame.dispatch,
2500
- visibilityStats: stableCandidateFrame.visibilityStats,
2501
- visibilitySnapshots: stableCandidateFrame.visibilitySnapshots,
2502
- featureVisibilitySnapshots: stableCandidateFrame.featureVisibilitySnapshots,
2503
- hiddenEntityReports: stableCandidateFrame.hiddenEntityReports,
2504
- },
2657
+ hiddenEntityReports: stableCandidateFrame.hiddenEntityReports,
2505
2658
  worldKeys,
2506
2659
  readVersions:
2507
2660
  leases === undefined || leases.length !== worlds.length
@@ -2526,6 +2679,7 @@ export class PersistentRenderScene {
2526
2679
  pendingRenderableEntitiesByWorld: worlds.map(() => new Set<number>()),
2527
2680
  unavailableSlots: new Set<number>(),
2528
2681
  catalogEpoch,
2682
+ instanceEpoch: this.options.instanceCollections?.epoch ?? 0,
2529
2683
  probeProjection: new ProbeBlendSceneProjection(),
2530
2684
  temporalDemanded: frameDemandsTemporal(candidateFrame),
2531
2685
  gpuDrivenSceneRequired: true,
@@ -2539,8 +2693,8 @@ export class PersistentRenderScene {
2539
2693
  // later rebuild.
2540
2694
  this.projectReflectionProbes(candidateFrame.reflectionProbes ?? []);
2541
2695
  this.projectProbes(entry, candidateFrame);
2542
- this.syncGpuOwner(token, projection, undefined, true);
2543
- this.fullRebuilds += 1;
2696
+ this.syncGpuOwner(token, projection, delta);
2697
+ if (previousEntry === undefined) this.fullRebuilds += 1;
2544
2698
  this.lastResyncReason = 'attach';
2545
2699
  const materialized = projection.materialize();
2546
2700
  const candidates = entry.temporalDemanded
@@ -2553,16 +2707,14 @@ export class PersistentRenderScene {
2553
2707
  entry.probeProjection,
2554
2708
  );
2555
2709
  const slots = projection.slotsSnapshot();
2556
- const result = cullPersistentFrame(
2557
- frameWithProbes,
2558
- frameWithProbes.renderables,
2559
- (snapshot, index) => {
2560
- const slot = slots[index];
2561
- return slot === undefined
2562
- ? projection.cullingWorldBounds(snapshot)
2563
- : projection.cullingWorldBoundsAt(slot);
2564
- },
2565
- );
2710
+ const ownedFrame = this.projectShadowOwnership(frameWithProbes);
2711
+ this.shadowProjection = { renderables: ownedFrame.renderables, dispatch: ownedFrame.dispatch };
2712
+ const result = cullPersistentFrame(ownedFrame, ownedFrame.renderables, (snapshot, index) => {
2713
+ const slot = slots[index];
2714
+ return slot === undefined
2715
+ ? projection.cullingWorldBounds(snapshot)
2716
+ : projection.cullingWorldBoundsAt(slot);
2717
+ });
2566
2718
  this.frameEpoch += 1;
2567
2719
  this.visibilityFacetFrame = undefined;
2568
2720
  this.visibilityProjectionCache = undefined;
@@ -2589,17 +2741,17 @@ export class PersistentRenderScene {
2589
2741
  ): ExtractedFrame {
2590
2742
  const resourceFrame = buildCandidateFrame('none');
2591
2743
  const cachedDispatch = entry.dispatchCache;
2592
- const dispatch: DispatchEntry[] = [
2593
- ...(cachedDispatch?.revision === entry.dispatchRevision
2744
+ const dispatch =
2745
+ cachedDispatch?.revision === entry.dispatchRevision
2594
2746
  ? cachedDispatch.value
2595
- : dispatchForProjection(entry.dispatchBySlot, entry.projection, entry.unavailableSlots)),
2596
- ];
2747
+ : dispatchForProjection(entry.dispatchBySlot, entry.projection, entry.unavailableSlots);
2597
2748
  if (cachedDispatch?.revision !== entry.dispatchRevision) {
2598
2749
  entry.dispatchCache = { revision: entry.dispatchRevision, value: dispatch };
2599
2750
  }
2600
2751
  const candidateFrame: ExtractedFrame = {
2601
2752
  ...resourceFrame,
2602
- ...entry.structure,
2753
+ hiddenEntityReports: entry.hiddenEntityReports,
2754
+ visibilityStats: { explicitlyHidden: entry.hiddenEntityReports.length },
2603
2755
  dispatch,
2604
2756
  };
2605
2757
  entry.temporalDemanded = frameDemandsTemporal(candidateFrame);
@@ -2626,22 +2778,55 @@ export class PersistentRenderScene {
2626
2778
  entry.probeProjection,
2627
2779
  );
2628
2780
  const slots = entry.projection.slotsSnapshot();
2629
- const result = cullPersistentFrame(
2630
- frameWithProbes,
2631
- frameWithProbes.renderables,
2632
- (snapshot, index) => {
2633
- const slot = slots[index];
2634
- return slot === undefined
2635
- ? entry.projection.cullingWorldBounds(snapshot)
2636
- : entry.projection.cullingWorldBoundsAt(slot);
2637
- },
2638
- );
2781
+ const ownedFrame = this.projectShadowOwnership(frameWithProbes);
2782
+ this.shadowProjection = { renderables: ownedFrame.renderables, dispatch: ownedFrame.dispatch };
2783
+ const result = cullPersistentFrame(ownedFrame, ownedFrame.renderables, (snapshot, index) => {
2784
+ const slot = slots[index];
2785
+ return slot === undefined
2786
+ ? entry.projection.cullingWorldBounds(snapshot)
2787
+ : entry.projection.cullingWorldBoundsAt(slot);
2788
+ });
2639
2789
  this.frameEpoch += 1;
2640
2790
  this.visibilityFacetFrame = undefined;
2641
2791
  this.visibilityProjectionCache = undefined;
2642
2792
  return result;
2643
2793
  }
2644
2794
 
2795
+ private projectShadowOwnership(frame: ExtractedFrame): ExtractedFrame {
2796
+ const shadowCasterEntityKeys = new Set<number>();
2797
+ const shadowCasterDrawKeys = new Set<string>();
2798
+ const shadowCasterMembership: NonNullable<ExtractedFrame['shadowCasterMembership']>[number][] =
2799
+ [];
2800
+ for (
2801
+ let renderableIndex = 0;
2802
+ renderableIndex < frame.renderables.length;
2803
+ renderableIndex += 1
2804
+ ) {
2805
+ const snapshot = frame.renderables[renderableIndex];
2806
+ if (snapshot === undefined || snapshot.authorVisible === false) continue;
2807
+ const worldEntity = worldEntityKey(snapshot.worldId, snapshot.entityKey);
2808
+ for (const pass of snapshot.shadowCasterPasses ?? []) {
2809
+ shadowCasterMembership.push({ ...pass, worldEntity, renderableIndex });
2810
+ shadowCasterEntityKeys.add(worldEntity);
2811
+ shadowCasterDrawKeys.add(
2812
+ gpuDrivenShadowDrawKey(
2813
+ worldEntity,
2814
+ pass.materialHandle,
2815
+ pass.drawItemIndex,
2816
+ pass.passIndex,
2817
+ ),
2818
+ );
2819
+ }
2820
+ }
2821
+ for (const draw of frame.dispatch) {
2822
+ if (draw.tags.LightMode !== 'ShadowCaster') continue;
2823
+ const snapshot = frame.renderables[draw.renderableIndex];
2824
+ if (snapshot !== undefined)
2825
+ shadowCasterEntityKeys.add(worldEntityKey(snapshot.worldId, snapshot.entityKey));
2826
+ }
2827
+ return { ...frame, shadowCasterEntityKeys, shadowCasterDrawKeys, shadowCasterMembership };
2828
+ }
2829
+
2645
2830
  private projectProbes(entry: PersistentCompositionEntry, frame: ExtractedFrame): void {
2646
2831
  const sky =
2647
2832
  frame.skylight === undefined
@@ -2692,14 +2877,19 @@ export class PersistentRenderScene {
2692
2877
  owner: object,
2693
2878
  projection: RenderScene,
2694
2879
  delta?: RenderSceneApplyResult,
2695
- full = false,
2696
2880
  ): void {
2697
2881
  if (this.composition?.token === owner && !this.composition.gpuDrivenSceneRequired) return;
2698
2882
  const acquired = this.acquireGpuScene(owner, projection);
2699
- if (acquired === undefined || acquired.rebuilt) return;
2700
- const result = full
2701
- ? acquired.scene.rebuild(projection.slotsSnapshot())
2702
- : delta === undefined
2883
+ if (acquired === undefined) {
2884
+ this.sceneTableUploadBytes = 0;
2885
+ return;
2886
+ }
2887
+ if (acquired.rebuilt) {
2888
+ this.sceneTableUploadBytes = acquired.uploadBytes;
2889
+ return;
2890
+ }
2891
+ const result =
2892
+ delta === undefined
2703
2893
  ? acquired.scene.sync({
2704
2894
  created: 0,
2705
2895
  updated: 0,
@@ -2708,22 +2898,32 @@ export class PersistentRenderScene {
2708
2898
  ignoredLateUpdates: 0,
2709
2899
  createdSlots: [],
2710
2900
  updatedSlots: [],
2901
+ contentUpdatedSlots: [],
2902
+ instanceUpdatedSlots: [],
2711
2903
  removedSlots: [],
2712
2904
  recreatedSlots: [],
2713
2905
  resynced: 0,
2714
2906
  })
2715
2907
  : acquired.scene.sync(delta);
2716
- if (!result.ok) this.failGpu(result.error);
2908
+ if (!result.ok) {
2909
+ this.sceneTableUploadBytes = 0;
2910
+ this.failGpu(result.error);
2911
+ return;
2912
+ }
2913
+ this.sceneTableUploadBytes = result.value.bytes;
2717
2914
  }
2718
2915
 
2719
2916
  private acquireGpuScene(
2720
2917
  owner: object,
2721
2918
  projection: RenderScene,
2722
- ): { readonly scene: GpuScene; readonly rebuilt: boolean } | undefined {
2919
+ ):
2920
+ | { readonly scene: GpuScene; readonly rebuilt: boolean; readonly uploadBytes: number }
2921
+ | undefined {
2723
2922
  const getDevice = this.options.getDevice;
2724
2923
  if (getDevice === undefined) return undefined;
2725
2924
  const device = getDevice();
2726
2925
  if (this.gpuDevice !== undefined && this.gpuDevice !== device) {
2926
+ this.retireGpuScene(this.gpuScene, this.gpuDevice);
2727
2927
  this.gpuScene = undefined;
2728
2928
  this.gpuOwner = undefined;
2729
2929
  this.gpuStatus = 'rebuild-pending';
@@ -2731,9 +2931,23 @@ export class PersistentRenderScene {
2731
2931
  if (this.gpuStatus === 'error' && this.gpuDevice === device) return undefined;
2732
2932
  if (this.gpuStatus === 'unsupported' && this.gpuDevice === device) return undefined;
2733
2933
  if (this.gpuScene !== undefined && this.gpuOwner === owner) {
2734
- return { scene: this.gpuScene, rebuilt: false };
2934
+ return { scene: this.gpuScene, rebuilt: false, uploadBytes: 0 };
2935
+ }
2936
+ if (this.gpuScene !== undefined && this.gpuDevice === device) {
2937
+ // Composition rebuilds replace the CPU projection token, but the GPU
2938
+ // table resource can be rebuilt in place. Keeping one scene identity
2939
+ // avoids destroying buffers that cached frame graphs still reference
2940
+ // and lets queue ordering preserve writes across the next submission.
2941
+ const rebuilt = this.gpuScene.rebuild(projection.slotsSnapshot());
2942
+ if (!rebuilt.ok) {
2943
+ this.failGpu(rebuilt.error);
2944
+ return undefined;
2945
+ }
2946
+ this.gpuOwner = owner;
2947
+ this.gpuStatus = 'resident';
2948
+ return { scene: this.gpuScene, rebuilt: true, uploadBytes: rebuilt.value.bytes };
2735
2949
  }
2736
- if (this.gpuScene !== undefined) this.gpuScene.dispose();
2950
+ this.retireGpuScene(this.gpuScene);
2737
2951
  this.gpuScene = undefined;
2738
2952
  this.gpuOwner = undefined;
2739
2953
  this.gpuDevice = device;
@@ -2756,11 +2970,11 @@ export class PersistentRenderScene {
2756
2970
  this.gpuScene = scene;
2757
2971
  this.gpuOwner = owner;
2758
2972
  this.gpuStatus = 'resident';
2759
- return { scene, rebuilt: true };
2973
+ return { scene, rebuilt: true, uploadBytes: rebuilt.value.bytes };
2760
2974
  }
2761
2975
 
2762
2976
  private failGpu(error: RhiError): void {
2763
- this.gpuScene?.dispose();
2977
+ this.retireGpuScene(this.gpuScene);
2764
2978
  this.gpuScene = undefined;
2765
2979
  this.gpuOwner = undefined;
2766
2980
  this.gpuStatus = 'error';