@forgeax/engine-render 0.1.23 → 0.1.25

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 (322) hide show
  1. package/README.md +231 -10
  2. package/dist/assembly/bundler-contract.d.ts +9 -0
  3. package/dist/assembly/bundler-contract.d.ts.map +1 -0
  4. package/dist/assembly/device-feature-admission.d.ts +8 -0
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -1
  6. package/dist/assembly/extended-lighting-fallback.d.ts +13 -0
  7. package/dist/assembly/extended-lighting-fallback.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +7 -243
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/material/assembly.d.ts +3 -2
  11. package/dist/assembly/material/assembly.d.ts.map +1 -1
  12. package/dist/assembly/material/standard-layer-projection.d.ts +10 -0
  13. package/dist/assembly/material/standard-layer-projection.d.ts.map +1 -0
  14. package/dist/assembly/material/surface-projection.d.ts +24 -0
  15. package/dist/assembly/material/surface-projection.d.ts.map +1 -0
  16. package/dist/assembly/material-shader-policy.d.ts +188 -0
  17. package/dist/assembly/material-shader-policy.d.ts.map +1 -0
  18. package/dist/assembly/material-shader-prewarm.d.ts +21 -0
  19. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
  20. package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
  21. package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
  22. package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
  23. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
  24. package/dist/assembly/recovery/generation.d.ts +46 -0
  25. package/dist/assembly/recovery/generation.d.ts.map +1 -0
  26. package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
  27. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
  28. package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
  29. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
  30. package/dist/assembly/renderer-facade.d.ts +10 -0
  31. package/dist/assembly/renderer-facade.d.ts.map +1 -0
  32. package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
  33. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  34. package/dist/assembly/renderer-helpers.d.ts +19 -0
  35. package/dist/assembly/renderer-helpers.d.ts.map +1 -0
  36. package/dist/assembly/renderer-lifecycle.d.ts +34 -0
  37. package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
  38. package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
  39. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
  40. package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
  41. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
  42. package/dist/assembly/webgpu-ready.d.ts +55 -0
  43. package/dist/assembly/webgpu-ready.d.ts.map +1 -0
  44. package/dist/assembly/webgpu-renderer.d.ts +152 -0
  45. package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
  46. package/dist/assembly/webgpu-vertex-layouts.d.ts +89 -0
  47. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -0
  48. package/dist/assets/asset-decoders.d.ts.map +1 -1
  49. package/dist/authoring.mjs +1 -1
  50. package/dist/{chunk-FF7K3FSO.mjs → chunk-ADHHVYLW.mjs} +89 -89
  51. package/dist/chunk-ADHHVYLW.mjs.map +1 -0
  52. package/dist/{chunk-YALPS44A.mjs → chunk-E34VL5VI.mjs} +47 -47
  53. package/dist/chunk-E34VL5VI.mjs.map +1 -0
  54. package/dist/{chunk-4L3P6DT5.mjs → chunk-GE3SDD72.mjs} +37 -5
  55. package/dist/chunk-GE3SDD72.mjs.map +1 -0
  56. package/dist/{chunk-HZUPSYW7.mjs → chunk-HKXTW355.mjs} +105 -15
  57. package/dist/chunk-HKXTW355.mjs.map +1 -0
  58. package/dist/chunk-JSEDTZYS.mjs +413 -0
  59. package/dist/chunk-JSEDTZYS.mjs.map +1 -0
  60. package/dist/{chunk-FBC4R44J.mjs → chunk-KM2NOX2I.mjs} +57 -57
  61. package/dist/chunk-KM2NOX2I.mjs.map +1 -0
  62. package/dist/{chunk-DIYW3M5D.mjs → chunk-N3RT2EUR.mjs} +2288 -2238
  63. package/dist/chunk-N3RT2EUR.mjs.map +1 -0
  64. package/dist/{chunk-ZUWKAZ2I.mjs → chunk-TCLDX7SZ.mjs} +1167 -368
  65. package/dist/chunk-TCLDX7SZ.mjs.map +1 -0
  66. package/dist/{chunk-QODA5UKU.mjs → chunk-ZZ4YQ474.mjs} +2 -2
  67. package/dist/chunk-ZZ4YQ474.mjs.map +1 -0
  68. package/dist/components/instances.d.ts +13 -31
  69. package/dist/components/instances.d.ts.map +1 -1
  70. package/dist/construct-renderer.mjs +20688 -16314
  71. package/dist/construct-renderer.mjs.map +1 -1
  72. package/dist/device/device-scope.d.ts +2 -0
  73. package/dist/device/device-scope.d.ts.map +1 -1
  74. package/dist/device/gpu-residency.d.ts +24 -4
  75. package/dist/device/gpu-residency.d.ts.map +1 -1
  76. package/dist/environment/frame.d.ts +5 -0
  77. package/dist/environment/frame.d.ts.map +1 -1
  78. package/dist/environment/inspection.d.ts +7 -0
  79. package/dist/environment/inspection.d.ts.map +1 -1
  80. package/dist/environment/lifecycle.d.ts +20 -2
  81. package/dist/environment/lifecycle.d.ts.map +1 -1
  82. package/dist/errors/recover.d.ts +58 -2
  83. package/dist/errors/recover.d.ts.map +1 -1
  84. package/dist/errors/render.d.ts +13 -0
  85. package/dist/errors/render.d.ts.map +1 -1
  86. package/dist/extract/contracts.d.ts +899 -0
  87. package/dist/extract/contracts.d.ts.map +1 -0
  88. package/dist/features/host.d.ts +2 -0
  89. package/dist/features/host.d.ts.map +1 -1
  90. package/dist/features/prepared-gpu-work.d.ts +3 -0
  91. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  92. package/dist/features/prepared-graphics-store.d.ts +2 -0
  93. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  94. package/dist/gpu-driven/production-raster.d.ts +34 -1
  95. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  96. package/dist/gpu-scene.d.ts +5 -0
  97. package/dist/gpu-scene.d.ts.map +1 -1
  98. package/dist/ibl/IblPipelineCache.d.ts +7 -6
  99. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  100. package/dist/ibl/skylight-bind-group.d.ts +7 -1
  101. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  102. package/dist/index.d.ts +2 -1
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.mjs +27 -9
  105. package/dist/index.mjs.map +1 -1
  106. package/dist/instance-buffer-cache.d.ts +7 -1
  107. package/dist/instance-buffer-cache.d.ts.map +1 -1
  108. package/dist/instances.d.ts +101 -0
  109. package/dist/instances.d.ts.map +1 -0
  110. package/dist/internal.mjs +6 -6
  111. package/dist/light-buffer-layout.d.ts +3 -0
  112. package/dist/light-buffer-layout.d.ts.map +1 -1
  113. package/dist/materials.d.ts +96 -11
  114. package/dist/materials.d.ts.map +1 -1
  115. package/dist/mesh-material-bindings.d.ts +56 -0
  116. package/dist/mesh-material-bindings.d.ts.map +1 -1
  117. package/dist/occlusion-proxy.d.ts +4 -0
  118. package/dist/occlusion-proxy.d.ts.map +1 -0
  119. package/dist/pbr-pipeline.d.ts +32 -9
  120. package/dist/pbr-pipeline.d.ts.map +1 -1
  121. package/dist/pipeline-builder.d.ts.map +1 -1
  122. package/dist/pipeline-spec.d.ts.map +1 -1
  123. package/dist/points-lines/prepare.d.ts +3 -0
  124. package/dist/points-lines/prepare.d.ts.map +1 -1
  125. package/dist/points-lines/standard-owner.d.ts +33 -0
  126. package/dist/points-lines/standard-owner.d.ts.map +1 -0
  127. package/dist/record/frame-snapshot.d.ts +18 -2
  128. package/dist/record/frame-snapshot.d.ts.map +1 -1
  129. package/dist/record/frame-targets.d.ts.map +1 -1
  130. package/dist/record/frame.d.ts +14 -2
  131. package/dist/record/frame.d.ts.map +1 -1
  132. package/dist/record/main-pass-geometry.d.ts +36 -4
  133. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  134. package/dist/record/main-pass-material.d.ts +7 -17
  135. package/dist/record/main-pass-material.d.ts.map +1 -1
  136. package/dist/record/main-pass.d.ts.map +1 -1
  137. package/dist/record/mesh-ssbo.d.ts +11 -3
  138. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  139. package/dist/record/recovery-pipeline.d.ts +30 -0
  140. package/dist/record/recovery-pipeline.d.ts.map +1 -0
  141. package/dist/record/render-context.d.ts +28 -5
  142. package/dist/record/render-context.d.ts.map +1 -1
  143. package/dist/record/shadow-pass.d.ts +11 -1
  144. package/dist/record/shadow-pass.d.ts.map +1 -1
  145. package/dist/record/typed-frame-graph.d.ts +1 -1
  146. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  147. package/dist/recovery/render-system-candidate.d.ts +65 -0
  148. package/dist/recovery/render-system-candidate.d.ts.map +1 -0
  149. package/dist/render-contract.d.ts +68 -7
  150. package/dist/render-contract.d.ts.map +1 -1
  151. package/dist/render-system-extract-tail.d.ts +16 -0
  152. package/dist/render-system-extract-tail.d.ts.map +1 -0
  153. package/dist/render-system-extract.d.ts +180 -22
  154. package/dist/render-system-extract.d.ts.map +1 -1
  155. package/dist/render-system.d.ts +131 -4
  156. package/dist/render-system.d.ts.map +1 -1
  157. package/dist/scene/render-scene-types.d.ts +2 -0
  158. package/dist/scene/render-scene-types.d.ts.map +1 -1
  159. package/dist/scene/render-scene.d.ts +31 -1
  160. package/dist/scene/render-scene.d.ts.map +1 -1
  161. package/dist/systems/skin-palette-allocator.d.ts +6 -0
  162. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  163. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  164. package/dist/temporal/index.mjs +6 -6
  165. package/dist/temporal/taa-history-store.d.ts +15 -1
  166. package/dist/temporal/taa-history-store.d.ts.map +1 -1
  167. package/dist/temporal/transaction.d.ts +5 -0
  168. package/dist/temporal/transaction.d.ts.map +1 -1
  169. package/package.json +20 -20
  170. package/src/__tests__/device-feature-admission.unit.test.ts +6 -0
  171. package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
  172. package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
  173. package/src/__tests__/environment-generation.integration.test.ts +23 -0
  174. package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
  175. package/src/__tests__/factory-contract.integration.test.ts +122 -7
  176. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
  177. package/src/__tests__/gpu-scene.unit.test.ts +38 -2
  178. package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
  179. package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
  180. package/src/__tests__/instances-store.unit.test.ts +46 -0
  181. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  182. package/src/__tests__/material-contract-inventory.unit.test.ts +29 -22
  183. package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
  184. package/src/__tests__/material-pass-policy.unit.test.ts +69 -0
  185. package/src/__tests__/material-snapshot-mutation.integration.test.ts +69 -0
  186. package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
  187. package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
  188. package/src/__tests__/material-surface-authoring.test-d.ts +62 -0
  189. package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
  190. package/src/__tests__/materials-transmission-validation.unit.test.ts +24 -0
  191. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -5
  192. package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
  193. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +1 -1
  194. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
  195. package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
  196. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
  197. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +127 -18
  198. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
  199. package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
  200. package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
  201. package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
  202. package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
  203. package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
  204. package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
  205. package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
  206. package/src/__tests__/shader-manifest-fixture.ts +13 -16
  207. package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
  208. package/src/__tests__/skin-motion-regression.unit.test.ts +33 -0
  209. package/src/__tests__/skinned-shadow-caster.test.ts +30 -2
  210. package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
  211. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +60 -0
  212. package/src/__tests__/surface-standard-pipeline.browser.test.ts +20 -0
  213. package/src/__tests__/surface-standard-pipeline.dawn.test.ts +20 -0
  214. package/src/__tests__/surface-standard-pipeline.fixture.ts +132 -0
  215. package/src/__tests__/surface-variant-provenance.integration.test.ts +33 -0
  216. package/src/__tests__/transmission-prewarm.unit.test.ts +95 -85
  217. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +1 -1
  218. package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
  219. package/src/assembly/bundler-contract.ts +9 -0
  220. package/src/assembly/device-feature-admission.ts +15 -5
  221. package/src/assembly/extended-lighting-fallback.ts +216 -0
  222. package/src/assembly/factory.ts +83 -10088
  223. package/src/assembly/material/__tests__/standard-layer-plan-projection.integration.test.ts +18 -0
  224. package/src/assembly/material/__tests__/surface-pass-projection.integration.test.ts +62 -0
  225. package/src/assembly/material/assembly.ts +18 -2
  226. package/src/assembly/material/standard-layer-projection.ts +16 -0
  227. package/src/assembly/material/surface-projection.ts +90 -0
  228. package/src/assembly/material-shader-policy.ts +932 -0
  229. package/src/assembly/material-shader-prewarm.ts +88 -0
  230. package/src/assembly/mesh-ssbo-grow.ts +270 -0
  231. package/src/assembly/recovery/device-loss-fanout.ts +89 -0
  232. package/src/assembly/recovery/generation.ts +141 -0
  233. package/src/assembly/recovery/recovery-attempt.ts +800 -0
  234. package/src/assembly/recovery/renderer-recover.ts +906 -0
  235. package/src/assembly/renderer-facade.ts +342 -0
  236. package/src/assembly/renderer-frame-transaction.ts +45 -0
  237. package/src/assembly/renderer-helpers.ts +99 -0
  238. package/src/assembly/renderer-lifecycle.ts +159 -0
  239. package/src/assembly/shader-prewarm-policy.ts +157 -0
  240. package/src/assembly/webgpu-ready-contract.ts +14 -0
  241. package/src/assembly/webgpu-ready.ts +4066 -0
  242. package/src/assembly/webgpu-renderer.ts +4027 -0
  243. package/src/assembly/webgpu-vertex-layouts.ts +132 -0
  244. package/src/assets/asset-decoders.ts +27 -4
  245. package/src/components/camera.ts +1 -1
  246. package/src/components/instances.ts +14 -197
  247. package/src/debug-draw-glue.ts +1 -1
  248. package/src/device/device-scope.ts +35 -6
  249. package/src/device/gpu-residency.ts +194 -26
  250. package/src/environment/frame.ts +6 -0
  251. package/src/environment/inspection.ts +10 -0
  252. package/src/environment/lifecycle.ts +114 -8
  253. package/src/errors/recover.ts +76 -5
  254. package/src/errors/render.ts +17 -0
  255. package/src/extract/contracts.ts +974 -0
  256. package/src/features/host.ts +16 -0
  257. package/src/features/prepared-gpu-work.ts +20 -2
  258. package/src/features/prepared-graphics-store.ts +17 -0
  259. package/src/gpu-driven/production-raster.ts +131 -3
  260. package/src/gpu-scene.ts +87 -31
  261. package/src/ibl/IblPipelineCache.ts +134 -50
  262. package/src/ibl/skylight-bind-group.ts +26 -0
  263. package/src/index.ts +10 -1
  264. package/src/instance-buffer-cache.ts +14 -1
  265. package/src/instances.ts +288 -0
  266. package/src/light-buffer-layout.ts +3 -0
  267. package/src/materials.ts +365 -53
  268. package/src/mesh-material-bindings.ts +59 -0
  269. package/src/occlusion-proxy.ts +64 -0
  270. package/src/pbr-pipeline.ts +139 -15
  271. package/src/pipeline-builder.ts +14 -10
  272. package/src/pipeline-spec.ts +6 -14
  273. package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
  274. package/src/points-lines/prepare.ts +9 -0
  275. package/src/points-lines/standard-owner.ts +403 -0
  276. package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
  277. package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
  278. package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
  279. package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
  280. package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
  281. package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
  282. package/src/record/__tests__/temporal-payload.unit.test.ts +22 -43
  283. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -1
  284. package/src/record/frame-snapshot.ts +34 -2
  285. package/src/record/frame-targets.ts +11 -1
  286. package/src/record/frame.ts +37 -4
  287. package/src/record/helpers.ts +1 -1
  288. package/src/record/main-pass-geometry.ts +727 -63
  289. package/src/record/main-pass-material.ts +316 -209
  290. package/src/record/main-pass-sprite-draws.ts +7 -8
  291. package/src/record/main-pass.ts +94 -6
  292. package/src/record/mesh-ssbo.ts +32 -10
  293. package/src/record/recovery-pipeline.ts +695 -0
  294. package/src/record/render-context.ts +39 -4
  295. package/src/record/shadow-pass.ts +46 -99
  296. package/src/record/typed-frame-graph.ts +45 -1
  297. package/src/record/view-ubo.ts +3 -3
  298. package/src/recovery/render-system-candidate.ts +876 -0
  299. package/src/render-contract.ts +69 -7
  300. package/src/render-system-extract-tail.ts +3039 -0
  301. package/src/render-system-extract.ts +177 -3003
  302. package/src/render-system.ts +906 -662
  303. package/src/scene/render-scene-types.ts +2 -0
  304. package/src/scene/render-scene.ts +703 -37
  305. package/src/systems/skin-palette-allocator.ts +129 -7
  306. package/src/temporal/frame-coordinator.ts +4 -1
  307. package/src/temporal/taa-history-store.ts +46 -2
  308. package/src/temporal/temporal-view.ts +1 -1
  309. package/src/temporal/transaction.ts +9 -0
  310. package/src/tilemap-chunk-extract-system.ts +1 -1
  311. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
  312. package/src/transmission/__tests__/surface-compatibility.integration.test.ts +32 -0
  313. package/dist/chunk-4L3P6DT5.mjs.map +0 -1
  314. package/dist/chunk-4NPUASZR.mjs +0 -203
  315. package/dist/chunk-4NPUASZR.mjs.map +0 -1
  316. package/dist/chunk-DIYW3M5D.mjs.map +0 -1
  317. package/dist/chunk-FBC4R44J.mjs.map +0 -1
  318. package/dist/chunk-FF7K3FSO.mjs.map +0 -1
  319. package/dist/chunk-HZUPSYW7.mjs.map +0 -1
  320. package/dist/chunk-QODA5UKU.mjs.map +0 -1
  321. package/dist/chunk-YALPS44A.mjs.map +0 -1
  322. package/dist/chunk-ZUWKAZ2I.mjs.map +0 -1
@@ -1,9 +1,9 @@
1
- import type { Query, World } from '@forgeax/engine-ecs';
1
+ import type { EntityHandle, Query, World } from '@forgeax/engine-ecs';
2
2
  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, Transform } from '@forgeax/engine-scene';
6
+ import { ChildOf, GlobalTransform, MorphWeights, Transform } from '@forgeax/engine-scene';
7
7
  import { Skin } from '@forgeax/engine-skinning';
8
8
  import { err, ok, type Result } from '@forgeax/engine-types';
9
9
  import {
@@ -12,20 +12,24 @@ import {
12
12
  Instances,
13
13
  Layer,
14
14
  LightProbe,
15
+ Lines,
15
16
  MeshFilter,
16
17
  MeshRenderer,
17
18
  MotionBlur,
18
19
  PointLight,
19
20
  PointLightShadow,
21
+ Points,
20
22
  PostProcessParams,
21
23
  ReflectionProbe,
22
24
  SkyboxBackground,
23
25
  Skylight,
26
+ SortKey,
24
27
  SpotLight,
25
28
  SpriteInstances,
26
29
  SpriteRegionOverride,
27
30
  Visibility,
28
31
  } from '../components';
32
+ import type { DeviceScope, LifecycleResourceSpec } from '../device/device-scope';
29
33
  import {
30
34
  BatchTopology,
31
35
  type BatchTopologyInspection,
@@ -33,6 +37,7 @@ import {
33
37
  } from '../gpu-driven/batch-topology';
34
38
  import { GpuScene } from '../gpu-scene';
35
39
  import type { PersistentRenderSceneInspection } from '../inspection-types';
40
+ import type { InstanceProjectionStore } from '../instances';
36
41
  import { fingerprintNumericArray, InstanceBoundsCache } from '../instances-derived-bounds';
37
42
  import type { PointsLinesInspection } from '../points-lines/inspection';
38
43
  import type { PointsLinesRetainedSnapshot } from '../points-lines/snapshot';
@@ -52,6 +57,7 @@ import type {
52
57
  RenderableSnapshot,
53
58
  RenderableTemporalSnapshot,
54
59
  } from '../render-system-extract';
60
+ import type { SkinPaletteAllocator } from '../systems/skin-palette-allocator';
55
61
  import {
56
62
  PersistentTransmissionDemandProjection,
57
63
  type TransmissionDemand,
@@ -96,6 +102,7 @@ interface PendingIdentity {
96
102
  readonly initial: RenderSceneSlot | undefined;
97
103
  current: RenderableSnapshot | undefined;
98
104
  removed: boolean;
105
+ contentChanged: boolean;
99
106
  }
100
107
 
101
108
  interface WorldTransformSpan {
@@ -106,6 +113,16 @@ interface WorldTransformSpan {
106
113
 
107
114
  type GlobalTransformChangeQuery = Query<readonly [typeof GlobalTransform]>;
108
115
 
116
+ /** The normal extract request used by the persistent scene owner. */
117
+ export type PersistentRenderCandidateRequest =
118
+ | 'full'
119
+ | 'none'
120
+ | {
121
+ readonly kind: 'partial';
122
+ /** Entity keys are local to the corresponding World entry. */
123
+ readonly entitiesByWorld: readonly (ReadonlySet<number> | undefined)[];
124
+ };
125
+
109
126
  export interface RenderSceneSubmissionCapture {
110
127
  readonly revision: number;
111
128
  readonly slots: readonly {
@@ -187,6 +204,9 @@ function ownSnapshot(snapshot: RenderableSnapshot): RenderableSnapshot {
187
204
  ...(snapshot.pointsLines === undefined
188
205
  ? {}
189
206
  : { pointsLines: ownPointsLinesSnapshot(snapshot.pointsLines) }),
207
+ ...(snapshot.skinJointEntities === undefined
208
+ ? {}
209
+ : { skinJointEntities: [...snapshot.skinJointEntities] }),
190
210
  };
191
211
  }
192
212
 
@@ -356,6 +376,7 @@ export class RenderScene {
356
376
  initial,
357
377
  current: initial?.snapshot,
358
378
  removed: false,
379
+ contentChanged: false,
359
380
  };
360
381
  entities.set(entityKey, pending);
361
382
  pendingOrder.push(pending);
@@ -367,6 +388,7 @@ export class RenderScene {
367
388
  const { worldId, entityKey } = operation.snapshot;
368
389
  const pending = pendingFor(worldId, entityKey);
369
390
  pending.current = ownSnapshot(operation.snapshot);
391
+ pending.contentChanged = true;
370
392
  continue;
371
393
  }
372
394
  const pending = pendingFor(operation.worldId, operation.entityKey);
@@ -388,6 +410,7 @@ export class RenderScene {
388
410
  let recreated = 0;
389
411
  const createdSlots: RenderSceneSlot[] = [];
390
412
  const updatedSlots: RenderSceneSlot[] = [];
413
+ const contentUpdatedSlots: RenderSceneSlot[] = [];
391
414
  const removedSlots: RenderSceneRecord[] = [];
392
415
  const recreatedSlots: RenderSceneSlot[] = [];
393
416
  for (const pending of pendingOrder) {
@@ -425,6 +448,7 @@ export class RenderScene {
425
448
  this.indexMaterials(updatedSlot);
426
449
  this.dirtySinceSubmitted.add(updatedSlot.slot);
427
450
  updatedSlots.push(updatedSlot);
451
+ if (pending.contentChanged) contentUpdatedSlots.push(updatedSlot);
428
452
  updated += 1;
429
453
  }
430
454
 
@@ -441,6 +465,7 @@ export class RenderScene {
441
465
  ignoredLateUpdates,
442
466
  createdSlots,
443
467
  updatedSlots,
468
+ contentUpdatedSlots,
444
469
  removedSlots,
445
470
  recreatedSlots,
446
471
  resynced: 0,
@@ -865,7 +890,7 @@ export class RenderScene {
865
890
  entityKey: snapshot.entityKey,
866
891
  meshGeneration: fingerprintNumericArray(snapshot.localAabb) ^ (snapshot.assetHandle >>> 0),
867
892
  transformGeneration: fingerprintNumericArray(snapshot.transform.world),
868
- matrixGeneration: snapshot.instances.archVersion,
893
+ matrixGeneration: snapshot.instances.revision ?? snapshot.instances.archVersion,
869
894
  meshAabb: snapshot.localAabb,
870
895
  entityWorld: snapshot.transform.world,
871
896
  transforms: snapshot.instances.transforms,
@@ -889,6 +914,9 @@ export interface PersistentRenderSceneOptions {
889
914
  readonly onGpuError?: ((error: RhiError) => void) | undefined;
890
915
  readonly onSharedRefMutation?: ((worldId: number, handle: number) => void) | undefined;
891
916
  readonly configuredQueryBudget?: number | undefined;
917
+ /** Rebuildable projection of World-owned instance matrices. */
918
+ readonly instanceCollections?: InstanceProjectionStore | undefined;
919
+ readonly getSkinPaletteAllocator?: (() => SkinPaletteAllocator | null) | undefined;
892
920
  }
893
921
 
894
922
  export interface PersistentGpuDrivenState {
@@ -943,6 +971,23 @@ export function projectCpuDirectCandidates(
943
971
  return candidates;
944
972
  }
945
973
 
974
+ /** Detached GPU scene prepared from the retained CPU composition. */
975
+ export interface PersistentGpuDrivenCandidate {
976
+ readonly state: PersistentGpuDrivenState | undefined;
977
+ /** Candidate-owned scene root; never references the active GPU scene. */
978
+ createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown>;
979
+ publish(): void;
980
+ discard(): void;
981
+ /** Release the candidate scene even after it has crossed publication. */
982
+ release(): void;
983
+ }
984
+
985
+ export interface RenderSceneRecoveryRoot {
986
+ readonly generation: number;
987
+ readonly revision: number;
988
+ readonly visibleSlots: number;
989
+ }
990
+
946
991
  type PersistentGpuSceneInspection = PersistentRenderSceneInspection['gpu'];
947
992
 
948
993
  interface PersistentCompositionEntry {
@@ -953,7 +998,7 @@ interface PersistentCompositionEntry {
953
998
  readonly projection: RenderScene;
954
999
  readonly topology: BatchTopology;
955
1000
  readonly transmissionDemand: PersistentTransmissionDemandProjection;
956
- readonly structure: Pick<
1001
+ structure: Pick<
957
1002
  ExtractedFrame,
958
1003
  | 'dispatch'
959
1004
  | 'visibilityStats'
@@ -966,15 +1011,24 @@ interface PersistentCompositionEntry {
966
1011
  readonly readVersions: RenderReadVersion[];
967
1012
  readonly leaseIdentities: readonly string[];
968
1013
  readonly transformQueries: readonly GlobalTransformChangeQuery[];
1014
+ readonly renderChangeQueries: readonly ReadonlyMap<number, Query>[];
1015
+ readonly dispatchBySlot: Map<number, readonly DispatchEntry[]>;
1016
+ dispatchRevision: number;
1017
+ dispatchCache:
1018
+ | { readonly revision: number; readonly value: readonly DispatchEntry[] }
1019
+ | undefined;
1020
+ readonly skinConsumersByJoint: Map<string, Set<number>>;
1021
+ /** Renderable identities whose partial producer still needs a successful publish. */
1022
+ readonly pendingRenderableEntitiesByWorld: Set<number>[];
1023
+ /** Slots retained for recovery but excluded from direct and indirect submission. */
1024
+ readonly unavailableSlots: Set<number>;
969
1025
  catalogEpoch: number;
970
- readonly rigidOnly: boolean;
971
1026
  readonly probeProjection: ProbeBlendSceneProjection;
972
1027
  temporalDemanded: boolean;
973
1028
  gpuDrivenSceneRequired: boolean;
974
1029
  }
975
1030
 
976
1031
  const RENDER_RELEVANT_COMPONENT_IDS = new Set([
977
- componentId(Transform),
978
1032
  componentId(GlobalTransform),
979
1033
  componentId(ChildOf),
980
1034
  componentId(MeshFilter),
@@ -985,6 +1039,10 @@ const RENDER_RELEVANT_COMPONENT_IDS = new Set([
985
1039
  componentId(Skin),
986
1040
  componentId(Layer),
987
1041
  componentId(Visibility),
1042
+ componentId(MorphWeights),
1043
+ componentId(Points),
1044
+ componentId(Lines),
1045
+ componentId(SortKey),
988
1046
  componentId(Camera),
989
1047
  componentId(MotionBlur),
990
1048
  componentId(DirectionalLight),
@@ -999,6 +1057,41 @@ const RENDER_RELEVANT_COMPONENT_IDS = new Set([
999
1057
  componentId(VolumetricFog),
1000
1058
  ]);
1001
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.
1064
+ const RENDERABLE_SOURCE_COMPONENTS = [
1065
+ GlobalTransform,
1066
+ ChildOf,
1067
+ MeshFilter,
1068
+ MeshRenderer,
1069
+ Instances,
1070
+ SpriteInstances,
1071
+ SpriteRegionOverride,
1072
+ Skin,
1073
+ Layer,
1074
+ Visibility,
1075
+ MorphWeights,
1076
+ Points,
1077
+ Lines,
1078
+ SortKey,
1079
+ ] as const;
1080
+ const RENDERABLE_SOURCE_COMPONENT_IDS = new Set(
1081
+ RENDERABLE_SOURCE_COMPONENTS.map((component) => componentId(component)),
1082
+ );
1083
+ const VISIBILITY_DEPENDENCY_COMPONENT_IDS = new Set([
1084
+ componentId(ChildOf),
1085
+ componentId(Visibility),
1086
+ ]);
1087
+ const STRUCTURAL_MEMBERSHIP_SCAN_COMPONENT_IDS = new Set([
1088
+ componentId(ChildOf),
1089
+ componentId(Layer),
1090
+ componentId(Visibility),
1091
+ componentId(SpriteRegionOverride),
1092
+ componentId(SortKey),
1093
+ ]);
1094
+
1002
1095
  function createGlobalTransformChangeQuery(world: World): GlobalTransformChangeQuery {
1003
1096
  const result = world.query({ read: [GlobalTransform], changed: [GlobalTransform] });
1004
1097
  if (!result.ok) throw result.error;
@@ -1009,6 +1102,149 @@ function createGlobalTransformChangeQuery(world: World): GlobalTransformChangeQu
1009
1102
  return query;
1010
1103
  }
1011
1104
 
1105
+ function createRenderChangeQueries(world: World): ReadonlyMap<number, Query> {
1106
+ const queries = new Map<number, Query>();
1107
+ for (const component of RENDERABLE_SOURCE_COMPONENTS) {
1108
+ const result = world.query({ read: [component], changed: [component] });
1109
+ if (!result.ok) throw result.error;
1110
+ const query = result.value as Query;
1111
+ // The initial full extraction has already consumed this change evidence.
1112
+ // Keep the query compiled so subsequent frames iterate only rows whose
1113
+ // component version changed.
1114
+ for (const _span of query.spans().unwrap()) {
1115
+ // Establish the observation baseline.
1116
+ }
1117
+ queries.set(componentId(component), query);
1118
+ }
1119
+ return queries;
1120
+ }
1121
+
1122
+ function collectRenderableEntities(world: World): ReadonlySet<number> {
1123
+ const result = new Set<number>();
1124
+ const query = world.query({ read: [MeshRenderer], with: [Transform, MeshFilter] }).unwrap();
1125
+ for (const row of query) result.add(row.entity as number);
1126
+ return result;
1127
+ }
1128
+
1129
+ function sourceMembershipChanged(world: World, slot: RenderSceneSlot): boolean {
1130
+ const entity = slot.entityKey as EntityHandle;
1131
+ const snapshot = slot.snapshot;
1132
+ return (
1133
+ !world.hasComponent(entity, GlobalTransform) ||
1134
+ (snapshot.instances !== undefined) !== world.hasComponent(entity, Instances) ||
1135
+ (snapshot.spriteInstances !== undefined) !== world.hasComponent(entity, SpriteInstances) ||
1136
+ (snapshot.skin !== undefined) !== world.hasComponent(entity, Skin) ||
1137
+ (snapshot.morph !== undefined) !== world.hasComponent(entity, MorphWeights) ||
1138
+ (snapshot.pointsLines?.component === 'Points') !== world.hasComponent(entity, Points) ||
1139
+ (snapshot.pointsLines?.component === 'Lines') !== world.hasComponent(entity, Lines)
1140
+ );
1141
+ }
1142
+
1143
+ function skinJointKey(worldId: number, jointEntity: number): string {
1144
+ return `${worldId}:${jointEntity}`;
1145
+ }
1146
+
1147
+ function addSkinConsumer(index: Map<string, Set<number>>, snapshot: RenderableSnapshot): void {
1148
+ for (const jointEntity of snapshot.skinJointEntities ?? []) {
1149
+ const key = skinJointKey(snapshot.worldId, jointEntity);
1150
+ let consumers = index.get(key);
1151
+ if (consumers === undefined) {
1152
+ consumers = new Set<number>();
1153
+ index.set(key, consumers);
1154
+ }
1155
+ consumers.add(snapshot.entityKey);
1156
+ }
1157
+ }
1158
+
1159
+ function removeSkinConsumer(index: Map<string, Set<number>>, snapshot: RenderableSnapshot): void {
1160
+ for (const jointEntity of snapshot.skinJointEntities ?? []) {
1161
+ const key = skinJointKey(snapshot.worldId, jointEntity);
1162
+ const consumers = index.get(key);
1163
+ consumers?.delete(snapshot.entityKey);
1164
+ if (consumers?.size === 0) index.delete(key);
1165
+ }
1166
+ }
1167
+
1168
+ function dispatchEntriesBySlot(
1169
+ frame: ExtractedFrame,
1170
+ projection: RenderScene,
1171
+ ): Map<number, readonly DispatchEntry[]> {
1172
+ const bySlot = new Map<number, DispatchEntry[]>();
1173
+ const byRenderable = new Map<number, DispatchEntry[]>();
1174
+ for (const entry of frame.dispatch) {
1175
+ const list = byRenderable.get(entry.renderableIndex);
1176
+ if (list === undefined) byRenderable.set(entry.renderableIndex, [entry]);
1177
+ else list.push(entry);
1178
+ }
1179
+ for (let index = 0; index < frame.renderables.length; index += 1) {
1180
+ const renderable = frame.renderables[index];
1181
+ if (renderable === undefined) continue;
1182
+ const slot = projection.slot(renderable.worldId, renderable.entityKey);
1183
+ if (slot === undefined) continue;
1184
+ bySlot.set(slot.slot, byRenderable.get(index) ?? []);
1185
+ }
1186
+ return bySlot;
1187
+ }
1188
+
1189
+ function dispatchForProjection(
1190
+ dispatchBySlot: ReadonlyMap<number, readonly DispatchEntry[]>,
1191
+ projection: RenderScene,
1192
+ unavailableSlots: ReadonlySet<number>,
1193
+ ): readonly DispatchEntry[] {
1194
+ const dispatch: DispatchEntry[] = [];
1195
+ const renderables = projection.materialize();
1196
+ for (let renderableIndex = 0; renderableIndex < renderables.length; renderableIndex += 1) {
1197
+ const renderable = renderables[renderableIndex];
1198
+ if (renderable === undefined) continue;
1199
+ const slot = projection.slot(renderable.worldId, renderable.entityKey);
1200
+ if (slot === undefined) continue;
1201
+ if (unavailableSlots.has(slot.slot)) continue;
1202
+ for (const entry of dispatchBySlot.get(slot.slot) ?? []) {
1203
+ dispatch.push({ ...entry, renderableIndex });
1204
+ }
1205
+ }
1206
+ return dispatch;
1207
+ }
1208
+
1209
+ function topologyDeltaForAvailable(
1210
+ delta: RenderSceneApplyResult,
1211
+ unavailableSlots: ReadonlySet<number>,
1212
+ newlyUnavailable: readonly RenderSceneSlot[],
1213
+ ): RenderSceneApplyResult {
1214
+ const updatedSlots = delta.updatedSlots.filter((slot) => !unavailableSlots.has(slot.slot));
1215
+ const contentUpdatedSlots = delta.contentUpdatedSlots?.filter(
1216
+ (slot) => !unavailableSlots.has(slot.slot),
1217
+ );
1218
+ return {
1219
+ ...delta,
1220
+ updatedSlots: [
1221
+ ...updatedSlots,
1222
+ ...newlyUnavailable.map((slot) => ({
1223
+ ...slot,
1224
+ snapshot: { ...slot.snapshot, gpuDrivenDraws: [] },
1225
+ })),
1226
+ ],
1227
+ ...(contentUpdatedSlots === undefined ? {} : { contentUpdatedSlots }),
1228
+ };
1229
+ }
1230
+
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
+
1012
1248
  function frameDemandsTemporal(frame: ExtractedFrame): boolean {
1013
1249
  const camera = frame.cameras[0];
1014
1250
  return camera?.antialias === 'taa' || (camera?.motionBlur?.shutterAngle ?? 0) > 0;
@@ -1030,8 +1266,8 @@ function cameraFrusta(cameras: readonly CameraSnapshot[]): readonly Float32Array
1030
1266
  projection,
1031
1267
  camera.orthoLeft,
1032
1268
  camera.orthoRight,
1033
- camera.orthoBottom,
1034
1269
  camera.orthoTop,
1270
+ camera.orthoBottom,
1035
1271
  camera.near,
1036
1272
  camera.far,
1037
1273
  );
@@ -1072,6 +1308,10 @@ function cullPersistentFrame(
1072
1308
  for (let index = 0; index < candidates.length; index += 1) {
1073
1309
  const candidate = candidates[index];
1074
1310
  if (candidate === undefined) continue;
1311
+ if (candidate.authorVisible === false) {
1312
+ culled += 1;
1313
+ continue;
1314
+ }
1075
1315
  let isVisible = true;
1076
1316
  const candidateBounds = boundsOf(candidate, index);
1077
1317
  if (candidateBounds !== undefined) {
@@ -1377,7 +1617,7 @@ export class PersistentRenderScene {
1377
1617
  bounds.length >= 6 &&
1378
1618
  [...bounds].every((value) => Number.isFinite(value));
1379
1619
  draw = decideVisibility({
1380
- authorVisible: true,
1620
+ authorVisible: renderable.authorVisible !== false,
1381
1621
  validBounds,
1382
1622
  frustumVisible: true,
1383
1623
  lodReady: true,
@@ -1481,7 +1721,7 @@ export class PersistentRenderScene {
1481
1721
  worlds: readonly World[],
1482
1722
  owner: { readonly cameraOwner: number; readonly resourceOwner: number },
1483
1723
  catalogEpoch: number,
1484
- buildCandidateFrame: (renderables: 'full' | 'none') => ExtractedFrame,
1724
+ buildCandidateFrame: (request: PersistentRenderCandidateRequest) => ExtractedFrame,
1485
1725
  leases?: readonly RenderReadLease[],
1486
1726
  ): ExtractedFrame {
1487
1727
  this.worldEntitiesScanned = 0;
@@ -1526,11 +1766,16 @@ export class PersistentRenderScene {
1526
1766
  preserveVisibilityOnReorder,
1527
1767
  );
1528
1768
  }
1529
- if (!entry.rigidOnly) {
1530
- return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
1531
- }
1532
-
1533
1769
  let requiresRebuild = false;
1770
+ const changedEntitiesByWorld = worlds.map(
1771
+ (_, worldId) => new Set(entry.pendingRenderableEntitiesByWorld[worldId] ?? []),
1772
+ );
1773
+ const nextReadVersions = entry.readVersions.slice();
1774
+ const structureChangedWorlds = new Set<number>();
1775
+ const structuralComponentIdsByWorld = worlds.map(() => new Set<number>());
1776
+ let refreshStructureFacts = false;
1777
+ let currentRenderableEntitiesByWorld: readonly (ReadonlySet<number> | undefined)[] | undefined;
1778
+ const removedSlots: RenderSceneSlot[] = [];
1534
1779
  for (let worldId = 0; worldId < worlds.length; worldId += 1) {
1535
1780
  const world = worlds[worldId];
1536
1781
  if (world === undefined) continue;
@@ -1551,35 +1796,294 @@ export class PersistentRenderScene {
1551
1796
  requiresRebuild = true;
1552
1797
  break;
1553
1798
  }
1554
- if (read.status !== 'ok') {
1555
- requiresRebuild = true;
1556
- break;
1799
+ if (read.status === 'rebuild') {
1800
+ structureChangedWorlds.add(worldId);
1801
+ refreshStructureFacts = true;
1557
1802
  }
1558
1803
  const changes = read.world;
1559
1804
  const shared = read.sharedRefs;
1560
1805
  if (shared.records.length > 0) {
1561
1806
  for (const record of shared.records) {
1562
1807
  this.options.onSharedRefMutation?.(worldId, record.handle);
1808
+ const changed = changedEntitiesByWorld[worldId];
1809
+ if (changed !== undefined)
1810
+ addSharedRefConsumers(entry.projection, worldId, record.handle, changed);
1563
1811
  }
1564
- requiresRebuild = true;
1565
- break;
1566
1812
  }
1567
1813
  for (const changedComponentId of changes.changedComponentIds) {
1814
+ if (read.status === 'rebuild') {
1815
+ structuralComponentIdsByWorld[worldId]?.add(changedComponentId);
1816
+ }
1568
1817
  if (!RENDER_RELEVANT_COMPONENT_IDS.has(changedComponentId)) continue;
1569
- if (
1570
- changedComponentId !== componentId(Transform) &&
1571
- changedComponentId !== componentId(GlobalTransform)
1572
- ) {
1818
+ if (!RENDERABLE_SOURCE_COMPONENT_IDS.has(changedComponentId)) {
1573
1819
  requiresRebuild = true;
1574
1820
  break;
1575
1821
  }
1822
+ const changed = changedEntitiesByWorld[worldId];
1823
+ 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
+ }
1843
+ const query = entry.renderChangeQueries[worldId]?.get(changedComponentId);
1844
+ if (query !== undefined) {
1845
+ for (const row of query) changed.add(row.entity as number);
1846
+ }
1576
1847
  }
1577
1848
  if (requiresRebuild) break;
1578
- entry.readVersions[worldId] = read.version;
1849
+ nextReadVersions[worldId] = read.version;
1579
1850
  }
1580
1851
  if (requiresRebuild) {
1581
1852
  return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
1582
1853
  }
1854
+
1855
+ if (structureChangedWorlds.size > 0) {
1856
+ this.visibilityFacets.clear();
1857
+ this.invalidateActiveEntityRevision();
1858
+ const currentByWorld = worlds.map((world) => collectRenderableEntities(world));
1859
+ currentRenderableEntitiesByWorld = currentByWorld;
1860
+ for (let worldId = 0; worldId < currentByWorld.length; worldId += 1) {
1861
+ const world = worlds[worldId];
1862
+ const current = currentByWorld[worldId];
1863
+ if (world === undefined || current === undefined) continue;
1864
+ const changed = changedEntitiesByWorld[worldId];
1865
+ if (changed !== undefined && structureChangedWorlds.has(worldId)) {
1866
+ // A joint can disappear without leaving a row in the joint's
1867
+ // GlobalTransform change query. Wake every dependent renderable so
1868
+ // the next partial extraction validates the dangling reference and
1869
+ // either republishes or retires its palette data.
1870
+ for (const [jointKey, consumers] of entry.skinConsumersByJoint) {
1871
+ const separator = jointKey.indexOf(':');
1872
+ const indexedWorldId = Number(jointKey.slice(0, separator));
1873
+ if (indexedWorldId !== worldId) continue;
1874
+ const jointEntity = Number(jointKey.slice(separator + 1));
1875
+ if (world.hasComponent(jointEntity as EntityHandle, GlobalTransform)) continue;
1876
+ for (const consumer of consumers) changed.add(consumer);
1877
+ }
1878
+ for (const entity of current) {
1879
+ if (entry.projection.slot(worldId, entity) === undefined) changed.add(entity);
1880
+ }
1881
+ const structuralComponentIds = structuralComponentIdsByWorld[worldId];
1882
+ const scanAllMembership =
1883
+ structuralComponentIds !== undefined &&
1884
+ [...structuralComponentIds].some((id) =>
1885
+ STRUCTURAL_MEMBERSHIP_SCAN_COMPONENT_IDS.has(id),
1886
+ );
1887
+ for (const slot of entry.projection.slotsSnapshot()) {
1888
+ if (slot.worldId !== worldId || !current.has(slot.entityKey)) continue;
1889
+ const membershipChanged = sourceMembershipChanged(world, slot);
1890
+ if (scanAllMembership || membershipChanged) {
1891
+ 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
+ }
1906
+ }
1907
+ }
1908
+ }
1909
+ for (const slot of entry.projection.slotsSnapshot()) {
1910
+ if (slot.worldId !== worldId || current.has(slot.entityKey)) continue;
1911
+ removedSlots.push(slot);
1912
+ }
1913
+ this.worldEntitiesScanned += current.size;
1914
+ }
1915
+ }
1916
+
1917
+ // Query iteration is an observation step, not publication. Persist every
1918
+ // requested identity before invoking any producer so a thrown or omitted
1919
+ // partial extraction is retried even after ECS change evidence is drained.
1920
+ for (let worldId = 0; worldId < changedEntitiesByWorld.length; worldId += 1) {
1921
+ const pending = entry.pendingRenderableEntitiesByWorld[worldId];
1922
+ if (pending === undefined) continue;
1923
+ for (const entityKey of changedEntitiesByWorld[worldId] ?? []) pending.add(entityKey);
1924
+ }
1925
+
1926
+ let changedDelta: RenderSceneApplyResult | undefined;
1927
+ const hasChangedEntities = changedEntitiesByWorld.some((entities) => entities.size > 0);
1928
+ if (hasChangedEntities || removedSlots.length > 0) {
1929
+ const operations: RenderSceneOperation[] = [];
1930
+ const removedDemand: TransmissionDemandOperation[] = [];
1931
+ const replacedSkinSnapshots: RenderableSnapshot[] = [];
1932
+ for (let worldId = 0; worldId < changedEntitiesByWorld.length; worldId += 1) {
1933
+ for (const entityKey of changedEntitiesByWorld[worldId] ?? []) {
1934
+ const slot = entry.projection.slot(worldId, entityKey);
1935
+ if (slot !== undefined) replacedSkinSnapshots.push(slot.snapshot);
1936
+ }
1937
+ }
1938
+ for (const slot of removedSlots) {
1939
+ operations.push({ kind: 'remove', worldId: slot.worldId, entityKey: slot.entityKey });
1940
+ if (slot.snapshot.skinJointEntities !== undefined) {
1941
+ removeSkinConsumer(entry.skinConsumersByJoint, slot.snapshot);
1942
+ }
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
+ for (
1949
+ let materialIndex = 0;
1950
+ materialIndex < slot.snapshot.materials.length;
1951
+ materialIndex += 1
1952
+ ) {
1953
+ removedDemand.push({
1954
+ kind: 'remove',
1955
+ key: `${slot.worldId}:${slot.entityKey}:${materialIndex}`,
1956
+ });
1957
+ }
1958
+ }
1959
+ for (let worldId = 0; worldId < changedEntitiesByWorld.length; worldId += 1) {
1960
+ for (const entityKey of changedEntitiesByWorld[worldId] ?? []) {
1961
+ const slot = entry.projection.slot(worldId, entityKey);
1962
+ if (slot === undefined) continue;
1963
+ for (
1964
+ let materialIndex = 0;
1965
+ materialIndex < slot.snapshot.materials.length;
1966
+ materialIndex += 1
1967
+ ) {
1968
+ removedDemand.push({ kind: 'remove', key: `${worldId}:${entityKey}:${materialIndex}` });
1969
+ }
1970
+ }
1971
+ }
1972
+ const partial = hasChangedEntities
1973
+ ? buildCandidateFrame({ kind: 'partial', entitiesByWorld: changedEntitiesByWorld })
1974
+ : undefined;
1975
+ const publishedEntitiesByWorld = worlds.map(() => new Set<number>());
1976
+ for (const snapshot of partial?.renderables ?? []) {
1977
+ operations.push({ kind: 'create', snapshot });
1978
+ publishedEntitiesByWorld[snapshot.worldId]?.add(snapshot.entityKey);
1979
+ }
1980
+ changedDelta = entry.projection.apply(operations);
1981
+ const publishedSkinKeys = new Set<string>();
1982
+ for (const snapshot of partial?.renderables ?? []) {
1983
+ publishedSkinKeys.add(`${snapshot.worldId}:${snapshot.entityKey}`);
1984
+ }
1985
+ for (const snapshot of replacedSkinSnapshots) {
1986
+ // Keep the old dependency while a failed/omitted partial publication
1987
+ // retains its CPU slot. The pending identity must still be woken by
1988
+ // a later joint transform change so the producer gets another chance.
1989
+ if (
1990
+ snapshot.skinJointEntities !== undefined &&
1991
+ publishedSkinKeys.has(`${snapshot.worldId}:${snapshot.entityKey}`)
1992
+ ) {
1993
+ removeSkinConsumer(entry.skinConsumersByJoint, snapshot);
1994
+ }
1995
+ }
1996
+ for (const snapshot of partial?.renderables ?? []) {
1997
+ if (snapshot.skinJointEntities !== undefined) {
1998
+ addSkinConsumer(entry.skinConsumersByJoint, snapshot);
1999
+ }
2000
+ }
2001
+ entry.transmissionDemand.apply([
2002
+ ...removedDemand,
2003
+ ...transmissionDemandOperations(partial?.renderables ?? []),
2004
+ ]);
2005
+ for (const removed of changedDelta.removedSlots) entry.dispatchBySlot.delete(removed.slot);
2006
+ for (const recreated of changedDelta.recreatedSlots)
2007
+ entry.dispatchBySlot.delete(recreated.slot);
2008
+ 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
+ const partialDispatch = dispatchEntriesBySlot(partial, entry.projection);
2020
+ for (const [slot, dispatch] of partialDispatch) entry.dispatchBySlot.set(slot, dispatch);
2021
+ // A changed renderable which now fails producer validation still owns
2022
+ // its retained identity; replace its old dispatch with an empty list.
2023
+ for (const snapshot of partial.renderables) {
2024
+ const slot = entry.projection.slot(snapshot.worldId, snapshot.entityKey);
2025
+ if (slot !== undefined && !partialDispatch.has(slot.slot))
2026
+ entry.dispatchBySlot.set(slot.slot, []);
2027
+ }
2028
+ }
2029
+ for (let worldId = 0; worldId < changedEntitiesByWorld.length; worldId += 1) {
2030
+ const requested = changedEntitiesByWorld[worldId];
2031
+ const published = publishedEntitiesByWorld[worldId];
2032
+ if (requested === undefined || published === undefined) continue;
2033
+ const pending = entry.pendingRenderableEntitiesByWorld[worldId];
2034
+ for (const entityKey of requested) {
2035
+ const slot = entry.projection.slot(worldId, entityKey);
2036
+ if (published.has(entityKey)) {
2037
+ pending?.delete(entityKey);
2038
+ continue;
2039
+ }
2040
+ if (slot === undefined) {
2041
+ const current = currentRenderableEntitiesByWorld?.[worldId];
2042
+ if (current !== undefined && !current.has(entityKey)) pending?.delete(entityKey);
2043
+ continue;
2044
+ }
2045
+ // The producer did not publish this requested identity. Keep the
2046
+ // old CPU slot for recovery, but stop submitting stale dispatch and
2047
+ // retry the same identity even if its World version stays still.
2048
+ pending?.add(entityKey);
2049
+ if (slot !== undefined) entry.dispatchBySlot.set(slot.slot, []);
2050
+ }
2051
+ }
2052
+ const newlyUnavailable: RenderSceneSlot[] = [];
2053
+ for (let worldId = 0; worldId < changedEntitiesByWorld.length; worldId += 1) {
2054
+ const requested = changedEntitiesByWorld[worldId];
2055
+ const published = publishedEntitiesByWorld[worldId];
2056
+ if (requested === undefined || published === undefined) continue;
2057
+ for (const entityKey of requested) {
2058
+ const slot = entry.projection.slot(worldId, entityKey);
2059
+ if (slot === undefined || published.has(entityKey)) {
2060
+ if (slot !== undefined) entry.unavailableSlots.delete(slot.slot);
2061
+ continue;
2062
+ }
2063
+ if (!entry.unavailableSlots.has(slot.slot)) newlyUnavailable.push(slot);
2064
+ entry.unavailableSlots.add(slot.slot);
2065
+ }
2066
+ }
2067
+ for (const removed of changedDelta.removedSlots) entry.unavailableSlots.delete(removed.slot);
2068
+ for (const recreated of changedDelta.recreatedSlots)
2069
+ entry.unavailableSlots.delete(recreated.slot);
2070
+ entry.dispatchRevision += 1;
2071
+ entry.dispatchCache = undefined;
2072
+ entry.topology.apply(
2073
+ topologyDeltaForAvailable(changedDelta, entry.unavailableSlots, newlyUnavailable),
2074
+ );
2075
+ this.syncGpuOwner(entry.token, entry.projection, changedDelta);
2076
+ this.deltaFrames +=
2077
+ changedDelta.created +
2078
+ changedDelta.updated +
2079
+ changedDelta.removed +
2080
+ changedDelta.recreated >
2081
+ 0
2082
+ ? 1
2083
+ : 0;
2084
+ this.transformUpdates += changedDelta.updated;
2085
+ }
2086
+
1583
2087
  const transformSpans: WorldTransformSpan[] = [];
1584
2088
  for (let worldId = 0; worldId < entry.transformQueries.length; worldId += 1) {
1585
2089
  const query = entry.transformQueries[worldId];
@@ -1597,17 +2101,29 @@ export class PersistentRenderScene {
1597
2101
  if (requiresRebuild) {
1598
2102
  return this.rebuildComposition(worlds, owner, catalogEpoch, buildCandidateFrame, leases);
1599
2103
  }
1600
- if (transformSpans.length === 0) {
2104
+ if (transformSpans.length === 0 && changedDelta === undefined) {
1601
2105
  this.noChangeFrames += 1;
1602
2106
  this.syncGpuOwner(entry.token, entry.projection);
1603
- return this.deriveFramePlan(entry, buildCandidateFrame);
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;
1604
2113
  }
1605
- const delta = entry.projection.applyTransformSpans(transformSpans);
1606
- entry.topology.apply(delta);
1607
- this.syncGpuOwner(entry.token, entry.projection, delta);
1608
- this.deltaFrames += 1;
1609
- this.transformUpdates += delta.updated;
1610
- return this.deriveFramePlan(entry, buildCandidateFrame);
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
+ }
2121
+ const result = this.deriveFramePlan(entry, buildCandidateFrame);
2122
+ for (let worldId = 0; worldId < nextReadVersions.length; worldId += 1) {
2123
+ const version = nextReadVersions[worldId];
2124
+ if (version !== undefined) entry.readVersions[worldId] = version;
2125
+ }
2126
+ return result;
1611
2127
  }
1612
2128
 
1613
2129
  invalidate(): void {
@@ -1633,6 +2149,16 @@ export class PersistentRenderScene {
1633
2149
  const detachedComposition = this.composition?.worlds.includes(world)
1634
2150
  ? this.composition
1635
2151
  : undefined;
2152
+ 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
+ }
2161
+ }
1636
2162
  if (detachedComposition !== undefined) this.composition = undefined;
1637
2163
  if (detachedComposition !== undefined) this.temporalCapture = undefined;
1638
2164
  if (detachedComposition !== undefined) {
@@ -1661,6 +2187,111 @@ export class PersistentRenderScene {
1661
2187
  this.gpuStatus = this.options.getDevice === undefined ? 'inactive' : 'rebuild-pending';
1662
2188
  }
1663
2189
 
2190
+ /** Prepare a detached GPU scene without changing the active scene owner. */
2191
+ prepareRecoveryGpuDrivenCandidate(
2192
+ device: RhiDevice,
2193
+ ): Result<PersistentGpuDrivenCandidate, RhiError> {
2194
+ const composition = this.composition;
2195
+ if (composition === undefined) {
2196
+ return ok({
2197
+ state: undefined,
2198
+ createRecoveryRoot: (scope) => ({
2199
+ kind: 'scene-table',
2200
+ create: () => {
2201
+ if (!scope.isAlive()) throw new Error('GPU-driven candidate scope is not active.');
2202
+ throw new Error('GPU-driven candidate has no scene resource.');
2203
+ },
2204
+ cleanup: () => undefined,
2205
+ }),
2206
+ publish: () => undefined,
2207
+ discard: () => undefined,
2208
+ release: () => undefined,
2209
+ });
2210
+ }
2211
+ const created = GpuScene.create(
2212
+ device,
2213
+ Math.max(256, composition.projection.inspect().slotCapacity),
2214
+ );
2215
+ if (!created.ok) return created;
2216
+ if (created.value.status === 'unavailable') {
2217
+ return ok({
2218
+ state: undefined,
2219
+ createRecoveryRoot: (scope) => ({
2220
+ kind: 'scene-table',
2221
+ create: () => {
2222
+ if (!scope.isAlive()) throw new Error('GPU-driven candidate scope is not active.');
2223
+ throw new Error('GPU-driven candidate has no scene resource.');
2224
+ },
2225
+ cleanup: () => undefined,
2226
+ }),
2227
+ publish: () => undefined,
2228
+ discard: () => undefined,
2229
+ release: () => undefined,
2230
+ });
2231
+ }
2232
+ const scene = created.value.scene;
2233
+ const slots = composition.projection.slotsSnapshot();
2234
+ const rebuilt = scene.rebuild(slots);
2235
+ if (!rebuilt.ok) {
2236
+ scene.dispose();
2237
+ return rebuilt;
2238
+ }
2239
+ const state: PersistentGpuDrivenState = Object.freeze({
2240
+ scene,
2241
+ plan: composition.topology.plan(),
2242
+ slots,
2243
+ });
2244
+ let published = false;
2245
+ let released = false;
2246
+ return ok({
2247
+ state,
2248
+ createRecoveryRoot: (scope) => ({
2249
+ kind: 'scene-table',
2250
+ create: () => {
2251
+ if (!scope.isAlive()) throw new Error('GPU-driven candidate scope is not active.');
2252
+ return scene;
2253
+ },
2254
+ cleanup: () => undefined,
2255
+ }),
2256
+ publish: () => {
2257
+ if (published || released) return;
2258
+ published = true;
2259
+ this.gpuScene = scene;
2260
+ this.gpuOwner = composition.token;
2261
+ this.gpuDevice = device;
2262
+ this.gpuStatus = 'resident';
2263
+ },
2264
+ discard: () => {
2265
+ if (published || released) return;
2266
+ released = true;
2267
+ scene.dispose();
2268
+ },
2269
+ release: () => {
2270
+ if (published || released) return;
2271
+ released = true;
2272
+ scene.dispose();
2273
+ },
2274
+ });
2275
+ }
2276
+
2277
+ createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown> {
2278
+ return {
2279
+ kind: 'scene-table',
2280
+ create: () => {
2281
+ const composition = this.composition;
2282
+ if (!scope.isAlive() || composition === undefined) {
2283
+ throw new Error('RenderScene CPU projection is not ready for recovery.');
2284
+ }
2285
+ return Object.freeze({
2286
+ generation: scope.generation,
2287
+ revision: composition.projection.inspect().revision,
2288
+ visibleSlots: composition.projection.slotsSnapshot().length,
2289
+ });
2290
+ },
2291
+ cleanup: () => undefined,
2292
+ };
2293
+ }
2294
+
1664
2295
  setPointsLinesInspections(inspections: readonly PointsLinesInspection[]): void {
1665
2296
  this.pointsLinesInspections = inspections.map((inspection) => ({
1666
2297
  ...inspection,
@@ -1781,7 +2412,7 @@ export class PersistentRenderScene {
1781
2412
  worlds: readonly World[],
1782
2413
  owner: { readonly cameraOwner: number; readonly resourceOwner: number },
1783
2414
  catalogEpoch: number,
1784
- buildCandidateFrame: (renderables: 'full' | 'none') => ExtractedFrame,
2415
+ buildCandidateFrame: (request: PersistentRenderCandidateRequest) => ExtractedFrame,
1785
2416
  leases?: readonly RenderReadLease[],
1786
2417
  preserveVisibilityOnReorder = false,
1787
2418
  ): ExtractedFrame {
@@ -1851,6 +2482,10 @@ export class PersistentRenderScene {
1851
2482
  transmissionDemand.apply(transmissionDemandOperations(candidateFrame.renderables));
1852
2483
  const topology = new BatchTopology();
1853
2484
  topology.rebuild(projection.slotsSnapshot());
2485
+ const skinConsumersByJoint = new Map<string, Set<number>>();
2486
+ for (const snapshot of stableCandidateFrame.renderables) {
2487
+ if (snapshot.skinJointEntities !== undefined) addSkinConsumer(skinConsumersByJoint, snapshot);
2488
+ }
1854
2489
  const token = {};
1855
2490
  const entry: PersistentCompositionEntry = {
1856
2491
  token,
@@ -1880,8 +2515,17 @@ export class PersistentRenderScene {
1880
2515
  leases === undefined || leases.length !== worlds.length
1881
2516
  ? []
1882
2517
  : worlds.map(createGlobalTransformChangeQuery),
2518
+ renderChangeQueries:
2519
+ leases === undefined || leases.length !== worlds.length
2520
+ ? []
2521
+ : worlds.map(createRenderChangeQueries),
2522
+ dispatchBySlot: dispatchEntriesBySlot(stableCandidateFrame, projection),
2523
+ dispatchRevision: 0,
2524
+ dispatchCache: undefined,
2525
+ skinConsumersByJoint,
2526
+ pendingRenderableEntitiesByWorld: worlds.map(() => new Set<number>()),
2527
+ unavailableSlots: new Set<number>(),
1883
2528
  catalogEpoch,
1884
- rigidOnly: candidateFrame.renderables.every((renderable) => renderable.skin === undefined),
1885
2529
  probeProjection: new ProbeBlendSceneProjection(),
1886
2530
  temporalDemanded: frameDemandsTemporal(candidateFrame),
1887
2531
  gpuDrivenSceneRequired: true,
@@ -1941,10 +2585,23 @@ export class PersistentRenderScene {
1941
2585
  */
1942
2586
  private deriveFramePlan(
1943
2587
  entry: PersistentCompositionEntry,
1944
- buildCandidateFrame: (renderables: 'full' | 'none') => ExtractedFrame,
2588
+ buildCandidateFrame: (request: PersistentRenderCandidateRequest) => ExtractedFrame,
1945
2589
  ): ExtractedFrame {
1946
2590
  const resourceFrame = buildCandidateFrame('none');
1947
- const candidateFrame: ExtractedFrame = { ...resourceFrame, ...entry.structure };
2591
+ const cachedDispatch = entry.dispatchCache;
2592
+ const dispatch: DispatchEntry[] = [
2593
+ ...(cachedDispatch?.revision === entry.dispatchRevision
2594
+ ? cachedDispatch.value
2595
+ : dispatchForProjection(entry.dispatchBySlot, entry.projection, entry.unavailableSlots)),
2596
+ ];
2597
+ if (cachedDispatch?.revision !== entry.dispatchRevision) {
2598
+ entry.dispatchCache = { revision: entry.dispatchRevision, value: dispatch };
2599
+ }
2600
+ const candidateFrame: ExtractedFrame = {
2601
+ ...resourceFrame,
2602
+ ...entry.structure,
2603
+ dispatch,
2604
+ };
1948
2605
  entry.temporalDemanded = frameDemandsTemporal(candidateFrame);
1949
2606
  entry.projection.setTemporalTracking(entry.temporalDemanded);
1950
2607
  this.projectProbes(entry, candidateFrame);
@@ -1953,9 +2610,18 @@ export class PersistentRenderScene {
1953
2610
  const candidates = entry.temporalDemanded
1954
2611
  ? materialized.map((snapshot) => entry.projection.withTemporal(snapshot))
1955
2612
  : materialized;
2613
+ const drawableCandidates =
2614
+ entry.unavailableSlots.size === 0
2615
+ ? candidates
2616
+ : candidates.map((snapshot) => {
2617
+ const slot = entry.projection.slot(snapshot.worldId, snapshot.entityKey);
2618
+ return slot !== undefined && entry.unavailableSlots.has(slot.slot)
2619
+ ? { ...snapshot, authorVisible: false }
2620
+ : snapshot;
2621
+ });
1956
2622
  const frameWithProbes = attachProbeRecords(
1957
- { ...candidateFrame, renderables: candidates },
1958
- candidates,
2623
+ { ...candidateFrame, renderables: drawableCandidates },
2624
+ drawableCandidates,
1959
2625
  entry.projection.slotsSnapshot(),
1960
2626
  entry.probeProjection,
1961
2627
  );