@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
@@ -16,13 +16,22 @@
16
16
  // .invert` (charter proposition 5: no math reinvention; render-system.test.ts
17
17
  // asserts `/@forgeax\/engine-math/` shows up in render-system.ts source).
18
18
 
19
- import { numMipLevels, resolveAssetHandle } from '@forgeax/engine-assets-runtime';
19
+ import {
20
+ type MipmapEncoderWork,
21
+ numMipLevels,
22
+ resolveAssetHandle,
23
+ } from '@forgeax/engine-assets-runtime';
20
24
  import type { World } from '@forgeax/engine-ecs';
21
25
  import type { RenderReadLease } from '@forgeax/engine-ecs/projection';
22
- import { mat4 } from '@forgeax/engine-math';
23
26
  import type { RecorderSession } from '@forgeax/engine-profiler';
24
27
  import type { CompiledRenderGraphInfo } from '@forgeax/engine-render-graph';
25
- import type { BindGroupEntry, BindGroupLayout, Buffer, RenderPipeline } from '@forgeax/engine-rhi';
28
+ import type {
29
+ BindGroupEntry,
30
+ BindGroupLayout,
31
+ Buffer,
32
+ RenderPipeline,
33
+ RhiDevice,
34
+ } from '@forgeax/engine-rhi';
26
35
  import { err, ok, type Result, RhiError } from '@forgeax/engine-rhi';
27
36
  import {
28
37
  derive,
@@ -30,7 +39,6 @@ import {
30
39
  type Handle,
31
40
  type MaterialAsset,
32
41
  type MaterialTextureValue,
33
- type MeshAsset,
34
42
  type RenderPipelineAsset,
35
43
  RenderQueue,
36
44
  type SamplerAsset,
@@ -43,14 +51,16 @@ import {
43
51
  projectDirectionalShadowInspectionSource,
44
52
  } from './assembly/directional-shadow-inspection';
45
53
  import { createClusterBinScratch } from './cluster-binner';
54
+ import type { DeviceScope, LifecycleResourceSpec } from './device/device-scope';
55
+ import type { EnvironmentGeneration } from './environment/generation';
46
56
  import { EnvironmentLifecycle } from './environment/lifecycle';
47
57
  import {
48
58
  type ObservationUnavailableError,
49
- PointsLinesMaterialUnsupportedError,
50
59
  type RenderError,
51
60
  TransmissionCapabilityMissingError,
52
61
  } from './errors/render';
53
62
  import {
63
+ type RenderFeatureHost,
54
64
  type RenderFeaturePreparedGraphicsResolverInput,
55
65
  runRenderFeatureFrame,
56
66
  settlePreparedGraphicsCompletion,
@@ -84,20 +94,14 @@ import type {
84
94
  TemporalTargetInspection,
85
95
  TransmissionInspection,
86
96
  } from './inspection-types';
87
- import { admitPointsLines } from './points-lines/admission';
88
- import { PointsLinesExpansionCache } from './points-lines/expansion-cache';
89
- import {
90
- inspectPointsLines,
91
- type PointsLinesInspection,
92
- type PointsLinesSourceError,
93
- } from './points-lines/inspection';
94
- import {
95
- createPointsLinesLanePreparationAdapter,
96
- type PointsLinesLanePreparationAdapter,
97
- } from './points-lines/prepare';
98
- import { createPointsLinesLaneAdapter, type PointsLinesBackend } from './points-lines/record';
97
+ import { buildOcclusionProxyVertices } from './occlusion-proxy';
99
98
  import type { PointsLinesRetainedSnapshot } from './points-lines/snapshot';
100
- import type { PointsLinesRecordOwner, PointsLinesRecordSubmission } from './record/render-context';
99
+ import { StandardPointsLinesOwner } from './points-lines/standard-owner';
100
+ import {
101
+ createRenderSystemRecovery,
102
+ materialSamplerHandles,
103
+ materialTextureHandles,
104
+ } from './recovery/render-system-candidate';
101
105
  import { ReflectionProbeRecordOwner } from './reflection/record-owner';
102
106
  import {
103
107
  OcclusionRenderRuntime,
@@ -108,18 +112,19 @@ import { inspectVolumetricFog } from './volume/inspection';
108
112
 
109
113
  export type { RenderSceneInspection } from './inspection-types';
110
114
 
111
- import { deriveVertexLayoutProjection } from '@forgeax/engine-geometry';
112
115
  import { createCubeCaptureScheduler } from './capture/scheduler';
116
+ import type { GpuResidencyCache } from './device/gpu-residency';
113
117
  import type { EnvironmentInspection } from './environment/inspection';
114
- import {
115
- GPU_BUFFER_USAGE_COPY_DST,
116
- GPU_BUFFER_USAGE_INDEX,
117
- GPU_BUFFER_USAGE_UNIFORM,
118
- GPU_BUFFER_USAGE_VERTEX,
119
- } from './gpu-usage';
118
+ import { GPU_BUFFER_USAGE_COPY_DST, GPU_BUFFER_USAGE_UNIFORM } from './gpu-usage';
120
119
  import { resetHdrpBuffers } from './hdrp-buffers';
121
120
  import { assembleMaterialWithSkylightEntries } from './ibl/skylight-bind-group';
122
- import { disposeInstanceBuffers, disposeTransientInstanceBuffers } from './instance-buffer-cache';
121
+ import {
122
+ disposeInstanceBufferChunks,
123
+ disposeInstanceBuffers,
124
+ disposeTransientInstanceBuffers,
125
+ } from './instance-buffer-cache';
126
+ import type { InstanceCollectionInspection } from './instances';
127
+ import { InstanceProjectionStore } from './instances';
123
128
  import {
124
129
  type MeshMaterialBindingObservation,
125
130
  type MeshMaterialBindingPreparationFailure,
@@ -158,9 +163,9 @@ import type {
158
163
  GpuPassTimingSession,
159
164
  } from './record/gpu-pass-timing/session';
160
165
  import type { GpuTimingCapture } from './record/gpu-timing';
161
- import { computeProjectionMatrix, computeViewMatrix } from './record/helpers';
162
166
  import { applyParamSnapshotToUbo, residentTextureView } from './record/main-pass-material';
163
- import type { RenderSystemInternals } from './record/render-context';
167
+ import type { RecoveryPipelineReadiness } from './record/recovery-pipeline';
168
+ import type { PipelineState, RenderSystemInternals } from './record/render-context';
164
169
  import type { CubeCaptureGraphState } from './record/typed-frame-graph';
165
170
  import {
166
171
  type RenderFeatureGraphCandidate,
@@ -183,7 +188,11 @@ import type {
183
188
  RenderableSnapshot,
184
189
  } from './render-system-extract';
185
190
  import { extractFrames } from './render-system-extract';
186
- import { PersistentRenderScene } from './scene/render-scene';
191
+ import {
192
+ type PersistentGpuDrivenCandidate,
193
+ type PersistentRenderCandidateRequest,
194
+ PersistentRenderScene,
195
+ } from './scene/render-scene';
187
196
  import { inspectLodOcclusion } from './scene/visibility/inspection';
188
197
  import { resolveSsaoParameters } from './ssao-config';
189
198
  import { projectSsrDependencies, resolveSsrAdmissionGeneration } from './ssr/admission';
@@ -210,6 +219,34 @@ import {
210
219
  inspectTransmissionFromAdmission,
211
220
  } from './transmission/inspection';
212
221
 
222
+ function canonicalizeWorldComposition(
223
+ worlds: readonly World[],
224
+ owners: Pick<DrawOwnerOptions, 'cameraOwner' | 'resourceOwner'>,
225
+ leases: readonly RenderReadLease[] | undefined,
226
+ ): {
227
+ readonly worlds: readonly World[];
228
+ readonly owners: Pick<DrawOwnerOptions, 'cameraOwner' | 'resourceOwner'>;
229
+ readonly leases: readonly RenderReadLease[] | undefined;
230
+ } {
231
+ const order = worlds
232
+ .map((world, index) => ({ world, index }))
233
+ .sort((left, right) => left.world.identity.localeCompare(right.world.identity));
234
+ const indexByOriginal = new Map(order.map((entry, index) => [entry.index, index]));
235
+ return {
236
+ worlds: Object.freeze(order.map((entry) => entry.world)),
237
+ owners: {
238
+ cameraOwner: indexByOriginal.get(owners.cameraOwner) ?? owners.cameraOwner,
239
+ resourceOwner: indexByOriginal.get(owners.resourceOwner) ?? owners.resourceOwner,
240
+ },
241
+ leases:
242
+ leases === undefined
243
+ ? undefined
244
+ : Object.freeze(
245
+ order.map((entry) => leases[entry.index]).filter((lease) => lease !== undefined),
246
+ ),
247
+ };
248
+ }
249
+
213
250
  interface LodInspectionQuerySummary {
214
251
  readonly used: number;
215
252
  readonly capacity: number;
@@ -361,6 +398,128 @@ export {
361
398
  selectSwapChainFormat,
362
399
  } from './record/render-context';
363
400
 
401
+ export type RendererProducerRootKind =
402
+ | 'backend-surface'
403
+ | 'shader-material-pipeline'
404
+ | 'mesh-texture-sampler'
405
+ | 'render-scene'
406
+ | 'gpu-driven'
407
+ | 'feature'
408
+ | 'external-source'
409
+ | 'render-graph'
410
+ | 'target-history'
411
+ | 'observation-lease';
412
+
413
+ export interface RendererProducerRoot {
414
+ readonly kind: RendererProducerRootKind;
415
+ readonly owner: string;
416
+ readonly candidateScope: 'device-scope';
417
+ readonly visibility: 'visible-workset' | 'non-visible-lazy';
418
+ readonly disabledWork: 'zero' | 'lazy';
419
+ }
420
+
421
+ /** Device-bound post-process resources prepared off to the side of the active renderer. */
422
+ export interface RecoveryPostProcessResources {
423
+ readonly device: RhiDevice;
424
+ readonly paramsBuffers: Map<string, Buffer>;
425
+ }
426
+
427
+ const PRODUCER_ROOT_MATRIX: readonly RendererProducerRoot[] = Object.freeze([
428
+ {
429
+ kind: 'backend-surface',
430
+ owner: 'backend-surface-owner',
431
+ candidateScope: 'device-scope',
432
+ visibility: 'visible-workset',
433
+ disabledWork: 'zero',
434
+ },
435
+ {
436
+ kind: 'shader-material-pipeline',
437
+ owner: 'shader-material-pipeline-owner',
438
+ candidateScope: 'device-scope',
439
+ visibility: 'visible-workset',
440
+ disabledWork: 'zero',
441
+ },
442
+ {
443
+ kind: 'mesh-texture-sampler',
444
+ owner: 'gpu-residency',
445
+ candidateScope: 'device-scope',
446
+ visibility: 'visible-workset',
447
+ disabledWork: 'lazy',
448
+ },
449
+ {
450
+ kind: 'render-scene',
451
+ owner: 'persistent-render-scene',
452
+ candidateScope: 'device-scope',
453
+ visibility: 'visible-workset',
454
+ disabledWork: 'zero',
455
+ },
456
+ {
457
+ kind: 'gpu-driven',
458
+ owner: 'gpu-driven-production',
459
+ candidateScope: 'device-scope',
460
+ visibility: 'visible-workset',
461
+ disabledWork: 'zero',
462
+ },
463
+ {
464
+ kind: 'feature',
465
+ owner: 'render-feature-host',
466
+ candidateScope: 'device-scope',
467
+ visibility: 'visible-workset',
468
+ disabledWork: 'lazy',
469
+ },
470
+ {
471
+ kind: 'external-source',
472
+ owner: 'external-source-owner',
473
+ candidateScope: 'device-scope',
474
+ visibility: 'non-visible-lazy',
475
+ disabledWork: 'zero',
476
+ },
477
+ {
478
+ kind: 'render-graph',
479
+ owner: 'typed-frame-graph',
480
+ candidateScope: 'device-scope',
481
+ visibility: 'visible-workset',
482
+ disabledWork: 'zero',
483
+ },
484
+ {
485
+ kind: 'target-history',
486
+ owner: 'temporal-target-owner',
487
+ candidateScope: 'device-scope',
488
+ visibility: 'visible-workset',
489
+ disabledWork: 'zero',
490
+ },
491
+ {
492
+ kind: 'observation-lease',
493
+ owner: 'render-observation-owner',
494
+ candidateScope: 'device-scope',
495
+ visibility: 'non-visible-lazy',
496
+ disabledWork: 'zero',
497
+ },
498
+ ]);
499
+
500
+ export function createRendererProducerRootMatrix(): readonly RendererProducerRoot[] {
501
+ return PRODUCER_ROOT_MATRIX;
502
+ }
503
+
504
+ /**
505
+ * Production-owned recovery facts. These values are projections of the live
506
+ * RenderSystem owners; they are not a test ledger or a second graph model.
507
+ */
508
+ export interface RecoveryProductionEvidence {
509
+ readonly producerRoots: readonly RendererProducerRoot[];
510
+ readonly graph: {
511
+ readonly ready: boolean;
512
+ readonly generation: number;
513
+ readonly passCount: number;
514
+ readonly resourceCount: number;
515
+ };
516
+ readonly residency: { readonly meshResidencyEpoch: number };
517
+ readonly submissions: {
518
+ readonly count: number;
519
+ readonly lastGeneration: number | undefined;
520
+ };
521
+ }
522
+
364
523
  /**
365
524
  * Unified transparent-queue sub-sort covering all four
366
525
  * {@link TransparentSortConfig} modes. Reorders the
@@ -508,6 +667,19 @@ export interface RenderSystem {
508
667
  renderReadLeases?: readonly RenderReadLease[],
509
668
  timingCapture?: GpuTimingCapture,
510
669
  ): boolean;
670
+ /** Prepare a detached graph candidate without entering the frame record path. */
671
+ prepareRecoveryGraphCandidate(
672
+ runtime: RecoveryGraphCandidateRuntime,
673
+ ): Promise<RecoveryGraphCandidatePreparation>;
674
+ /** Finish and submit detached candidate setup, never a frame receipt. */
675
+ submitCandidateSetup(
676
+ candidate: RecoveryGraphCandidate,
677
+ isValid: () => boolean,
678
+ ): Result<RecoveryGraphSetupSubmission, RhiError>;
679
+ /** Publish a detached graph candidate at the generation publication boundary. */
680
+ publishRecoveryGraphCandidate(candidate: RecoveryGraphCandidate): void;
681
+ /** Discard a detached graph candidate that did not reach publication. */
682
+ discardRecoveryGraphCandidate(candidate: RecoveryGraphCandidate): void;
511
683
  /** Release renderer-owned persistent state for one detached World. */
512
684
  detachScene(world: World): void;
513
685
  /** Release the profiler catalog contribution owned by this RenderSystem. */
@@ -524,6 +696,8 @@ export interface RenderSystem {
524
696
  readonly pipelineDispatchCounts: {
525
697
  readonly unlit: number;
526
698
  };
699
+ /** Detached residency and upload facts for explicit instance collections. */
700
+ readonly instanceCollectionsInspection: readonly InstanceCollectionInspection[];
527
701
  /**
528
702
  * feat-20260528-frustum-culling M5 / w14: per-frame frustum-culling counters.
529
703
  * Updated by `draw([world], { cameraOwner: 0, resourceOwner: 0 })` on every call from the Extract stage.
@@ -560,6 +734,8 @@ export interface RenderSystem {
560
734
  readonly pointsLinesSnapshots: readonly PointsLinesRetainedSnapshot[];
561
735
  /** Current mesh-slot provenance and active diagnostics from the last frame. */
562
736
  readonly meshMaterialBindings: readonly MeshMaterialBindingObservation[];
737
+ /** Final-submit IBL binding-chain receipt for the last diagnostic frame. */
738
+ readonly iblBinding: import('./mesh-material-bindings').IblBindingInspection | undefined;
563
739
  /**
564
740
  * feat-20260531-bloom-first-declarative-render-graph-pass M4 fix-up w19:
565
741
  * per-frame render-graph pass names in declaration order. Empty array
@@ -573,6 +749,8 @@ export interface RenderSystem {
573
749
  readonly lastSuccessfulCameraAntialias: CameraSnapshot['antialias'] | undefined;
574
750
  /** Detached compiled-graph facts used by the renderer inspection owner. */
575
751
  readonly perFrameGraphInfo: CompiledRenderGraphInfo | undefined;
752
+ /** Production-owner recovery facts used by Renderer.inspect(). */
753
+ readonly recoveryEvidence: RecoveryProductionEvidence;
576
754
  /**
577
755
  * feat-20260531-per-frame-bind-group-cache M1 / w4: per-frame
578
756
  * createBindGroup counter. Reset to 0 on every `draw([world], { cameraOwner: 0, resourceOwner: 0 })` entry,
@@ -600,6 +778,8 @@ export interface RenderSystem {
600
778
  configureStandard(config: RenderPipelineAsset['config']): void;
601
779
  /** Register one engine-owned post-process shader used by the Standard lane. */
602
780
  registerBuiltinPostProcess(id: string, entry: PostProcessShaderEntry): () => void;
781
+ /** Resolve one active built-in post-process declaration for a detached candidate. */
782
+ readonly lookupPostProcess: (id: string) => PostProcessShaderEntry | undefined;
603
783
  /**
604
784
  * feat-20260612-rhi-destroy-renderer-dispose-gpu-lifecycle / M5 / w21:
605
785
  * release the per-RenderSystem frame-state GPU bookkeeping during
@@ -623,9 +803,19 @@ export interface RenderSystem {
623
803
  * handles and are rebuilt after the replacement device is live.
624
804
  * Idempotent and graph-optional (no-op when nothing has compiled yet).
625
805
  */
626
- resetForRecover(): void;
806
+ resetForRecover(retiringPipelineState?: PipelineState, replacementDevice?: RhiDevice): void;
807
+ /** Build post-process parameter buffers without mutating the active bundle. */
808
+ prepareRecoveryPostProcessResources(
809
+ device: RhiDevice,
810
+ ): Result<RecoveryPostProcessResources, RhiError>;
811
+ /** Publish a previously prepared post-process bundle at the recovery boundary. */
812
+ publishRecoveryPostProcessResources(candidate: RecoveryPostProcessResources): void;
813
+ /** Release a candidate bundle that was never published. */
814
+ discardRecoveryPostProcessResources(candidate: RecoveryPostProcessResources): void;
627
815
  /** Recreate post-process GPU parameter resources after a device rebuild. */
628
816
  restorePostProcessResources(): void;
817
+ /** Build candidate roots from explicit candidate-owned device state. */
818
+ prepareRecoveryRoots(runtime: RecoveryRootRuntime): RecoveryRootBundle;
629
819
  }
630
820
 
631
821
  function isStructuredRendererError(error: unknown): error is RhiError | RenderError {
@@ -681,11 +871,12 @@ function makePreparedPipelinePendingError(): RhiError {
681
871
  });
682
872
  }
683
873
 
684
- type PointsLinesGpuResources = {
685
- readonly vertexBuffer: Buffer;
686
- readonly indexBuffer: Buffer;
687
- };
688
- type PointsLinesPreparationAdapter = PointsLinesLanePreparationAdapter<PointsLinesGpuResources>;
874
+ /** The single Points/Lines owner’s staged device-generation handoff. */
875
+ export interface RecoveryPointsLinesCandidate {
876
+ createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown>;
877
+ publish(): void;
878
+ release(): void;
879
+ }
689
880
 
690
881
  function preparationFailureFrom(error: unknown): MeshMaterialBindingPreparationFailure | undefined {
691
882
  if (typeof error !== 'object' || error === null) return undefined;
@@ -714,30 +905,56 @@ function preparationFailureFrom(error: unknown): MeshMaterialBindingPreparationF
714
905
  };
715
906
  }
716
907
 
717
- function materialTextureHandles(
718
- material: MaterialSnapshot,
719
- ): readonly Handle<'TextureAsset', 'shared'>[] {
720
- const handles = new Map<number, Handle<'TextureAsset', 'shared'>>();
721
- const add = (handle: Handle<'TextureAsset', 'shared'> | undefined): void => {
722
- if (handle !== undefined) handles.set(Number(handle), handle);
723
- };
724
- for (const handle of material.textureHandles?.values() ?? []) add(handle);
725
- add(material.baseColorTexture);
726
- add(material.metallicRoughnessTexture);
727
- add(material.normalTexture);
728
- add(material.emissiveTexture);
729
- add(material.occlusionTexture);
730
- return [...handles.values()];
908
+ /** Candidate-only device/runtime inputs used by the recovery prepare seam. */
909
+ export interface RecoveryGraphCandidateRuntime {
910
+ readonly internals: RenderSystemInternals;
911
+ readonly pipelineState: PipelineState;
731
912
  }
732
913
 
733
- function materialSamplerHandles(
734
- material: MaterialSnapshot,
735
- ): readonly Handle<'SamplerAsset', 'shared'>[] {
736
- const handles = new Map<number, Handle<'SamplerAsset', 'shared'>>();
737
- for (const handle of material.samplerHandles?.values() ?? []) {
738
- handles.set(Number(handle), handle);
739
- }
740
- return [...handles.values()];
914
+ /** Detached graph state prepared on a replacement device, before publication. */
915
+ export interface RecoveryGraphCandidate {
916
+ readonly frameState: RenderFrameState;
917
+ readonly setupWorks: readonly MipmapEncoderWork[];
918
+ readonly device: RhiDevice;
919
+ readonly generation: number;
920
+ readonly gpuDrivenProduction?: GpuDrivenProduction;
921
+ readonly gpuDrivenScene?: PersistentGpuDrivenCandidate;
922
+ /** Staged CPU-expanded Points/Lines buffers owned by the same RenderSystem. */
923
+ readonly pointsLines?: RecoveryPointsLinesCandidate;
924
+ readonly featureHost?: RenderFeatureHost;
925
+ readonly featureGpuWork?: RenderFeatureGpuWorkOwner;
926
+ readonly featureGraphCandidate?: RenderFeatureGraphCandidate;
927
+ readonly postProcessPipelines?: ReadonlyMap<string, RenderPipeline>;
928
+ readonly recoveryReadiness: RecoveryPipelineReadiness;
929
+ /** Release all candidate roots before publication; safe to call repeatedly. */
930
+ readonly release: () => void;
931
+ /** Transfer candidate-root ownership to the published RenderSystem. */
932
+ readonly markPublished: () => void;
933
+ }
934
+
935
+ /** Candidate setup completion; completion is bounded by the recovery owner. */
936
+ export interface RecoveryGraphSetupSubmission {
937
+ readonly completion?: PromiseLike<unknown>;
938
+ }
939
+
940
+ export type RecoveryGraphCandidatePreparation =
941
+ | { readonly kind: 'no-seed' }
942
+ | { readonly kind: 'ready'; readonly candidate: RecoveryGraphCandidate }
943
+ | { readonly kind: 'failed'; readonly reason: string; readonly cause?: unknown };
944
+
945
+ /** Explicit candidate-owned inputs for lifecycle-root preparation. */
946
+ export interface RecoveryRootRuntime {
947
+ readonly scope: DeviceScope;
948
+ readonly device: RhiDevice;
949
+ readonly gpuStore: GpuResidencyCache;
950
+ readonly graphCandidate?: RecoveryGraphCandidate | undefined;
951
+ }
952
+
953
+ /** Candidate roots plus the one synchronous owner switch at publication. */
954
+ export interface RecoveryRootBundle {
955
+ readonly roots: readonly LifecycleResourceSpec<unknown>[];
956
+ publish(): void;
957
+ discard(): void;
741
958
  }
742
959
 
743
960
  /**
@@ -856,267 +1073,6 @@ function observeMaterialResidency(
856
1073
  };
857
1074
  }
858
1075
 
859
- /**
860
- * The Standard renderer's single Points/Lines prepare owner.
861
- *
862
- * The retained snapshot is the identity boundary; this owner only resolves
863
- * the source assets, runs admission, and publishes the existing preparation
864
- * and record contracts to the main geometry loop. The expansion cache is
865
- * shared across lanes and this owner keeps the one prepared vertex/index
866
- * resource pair for each retained identity.
867
- */
868
- class StandardPointsLinesOwner implements PointsLinesRecordOwner {
869
- private readonly cache = new PointsLinesExpansionCache();
870
- private readonly preparations = new Map<string, PointsLinesPreparationAdapter>();
871
- private readonly active = new Map<string, PointsLinesInspection>();
872
- private readonly layoutProjection = deriveVertexLayoutProjection({
873
- position: new Float32Array(0),
874
- normal: new Float32Array(0),
875
- uv: new Float32Array(0),
876
- tangent: new Float32Array(0),
877
- });
878
-
879
- constructor(private readonly internals: RenderSystemInternals) {}
880
-
881
- beginFrame(): void {
882
- this.active.clear();
883
- }
884
-
885
- prepare(
886
- entry: import('./record/frame-snapshot').ValidatedRenderable,
887
- clustered: boolean,
888
- ): PointsLinesRecordSubmission | undefined {
889
- const snapshot = entry.source.pointsLines;
890
- if (snapshot === undefined || snapshot.component === undefined || entry.world === undefined) {
891
- return undefined;
892
- }
893
- const key = `${snapshot.worldId}:${snapshot.entityKey}`;
894
- const topology = snapshot.component === 'Points' ? 'point-list' : 'line-list';
895
- const backend = this.backend();
896
- const lane = backend === 'wgpu-webgl2' ? 'cpu-webgl2' : clustered ? 'clustered' : 'direct';
897
- const meshResult = resolveAssetHandle<MeshAsset>(
898
- entry.world,
899
- toShared<'MeshAsset'>(entry.source.assetHandle),
900
- );
901
- const materialResult = resolveAssetHandle<MaterialAsset>(
902
- entry.world,
903
- toShared<'MaterialAsset'>(snapshot.materialHandle),
904
- );
905
- if (!meshResult.ok || !materialResult.ok) {
906
- this.publishRefusal(
907
- snapshot,
908
- new PointsLinesMaterialUnsupportedError({
909
- entity: snapshot.entityKey,
910
- material: 'unresolved',
911
- pass: 'forward',
912
- module: 'asset-resolution',
913
- reason: 'source MeshAsset or MaterialAsset could not be resolved',
914
- }),
915
- );
916
- return undefined;
917
- }
918
- const admission = admitPointsLines({
919
- entity: snapshot.entityKey,
920
- mesh: meshResult.value,
921
- material: materialResult.value,
922
- ...(snapshot.style?.kind === 'points'
923
- ? {
924
- points: {
925
- sizePx: snapshot.style.sizePx,
926
- shape: snapshot.style.shape === 'circle' ? 1 : 0,
927
- },
928
- }
929
- : { lines: { widthPx: snapshot.style?.widthPx ?? 1 } }),
930
- });
931
- if (!admission.ok) {
932
- this.publishRefusal(snapshot, admission.error);
933
- return undefined;
934
- }
935
- const geometryKey = this.cache.getOrCreate(snapshot, meshResult.value).key;
936
- const prepKey = `${geometryKey}:${lane}:${backend}`;
937
- let preparation = this.preparations.get(prepKey);
938
- if (preparation === undefined) {
939
- preparation = createPointsLinesLanePreparationAdapter(lane, backend, {
940
- cache: this.cache,
941
- adapter: {
942
- create: (geometry) => {
943
- const vertex = this.internals.device.createBuffer({
944
- label: `points-lines-vertices:${geometryKey}`,
945
- size: Math.max(4, geometry.vertices.byteLength),
946
- usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
947
- mappedAtCreation: false,
948
- });
949
- if (!vertex.ok) return err(vertex.error);
950
- const index = this.internals.device.createBuffer({
951
- label: `points-lines-indices:${geometryKey}`,
952
- size: Math.max(4, geometry.indices.byteLength),
953
- usage: GPU_BUFFER_USAGE_INDEX | GPU_BUFFER_USAGE_COPY_DST,
954
- mappedAtCreation: false,
955
- });
956
- if (!index.ok) {
957
- this.internals.device.destroyBuffer(vertex.value);
958
- return err(index.error);
959
- }
960
- return ok({ vertexBuffer: vertex.value, indexBuffer: index.value });
961
- },
962
- upload: (resource, geometry) => {
963
- if (geometry.vertices.byteLength > 0) {
964
- const vertexWrite = this.internals.device.queue.writeBuffer(
965
- resource.vertexBuffer,
966
- 0,
967
- geometry.vertices,
968
- );
969
- if (!vertexWrite.ok) return err(vertexWrite.error);
970
- }
971
- if (geometry.indices.byteLength > 0) {
972
- const indexWrite = this.internals.device.queue.writeBuffer(
973
- resource.indexBuffer,
974
- 0,
975
- geometry.indices,
976
- );
977
- if (!indexWrite.ok) return err(indexWrite.error);
978
- }
979
- return ok(geometry.derivedBytes);
980
- },
981
- validate: (_resource, geometry) =>
982
- geometry.expandedVertexCount > 0 && geometry.expandedIndexCount > 0
983
- ? ok(undefined)
984
- : err(new Error('Points/Lines expansion contains no drawable triangles')),
985
- destroy: (resource) => {
986
- this.internals.device.destroyBuffer(resource.vertexBuffer);
987
- this.internals.device.destroyBuffer(resource.indexBuffer);
988
- },
989
- },
990
- });
991
- this.preparations.set(prepKey, preparation);
992
- }
993
- const prepared = preparation.prepare(snapshot, meshResult.value);
994
- if (!prepared.ok) {
995
- const lkg = preparation.lastKnownGood();
996
- if (lkg !== undefined) {
997
- const lkgPlan = createPointsLinesLaneAdapter(lane, backend).createRecordPlan(
998
- lkg.snapshot,
999
- lkg.geometry,
1000
- );
1001
- const state = preparation.inspect();
1002
- this.active.set(
1003
- key,
1004
- inspectPointsLines({
1005
- snapshot,
1006
- topology,
1007
- lane,
1008
- pointCount: lkg.geometry.pointCount,
1009
- segmentCount: lkg.geometry.segmentCount,
1010
- sourceBytes: lkg.geometry.sourceBytes,
1011
- derivedBytes: lkg.geometry.derivedBytes,
1012
- cache: { hit: true, rebuilds: state.rebuilds, evictions: 0 },
1013
- drawCount: lkgPlan.drawCount,
1014
- uploadBytes: 0,
1015
- lastKnownGood: true,
1016
- refusal: {
1017
- code: prepared.error.code,
1018
- expected: prepared.error.expected,
1019
- hint: prepared.error.hint,
1020
- detail: prepared.error.detail,
1021
- generation: prepared.error.detail.generation,
1022
- lastKnownGood: true,
1023
- },
1024
- }),
1025
- );
1026
- return {
1027
- plan: lkgPlan,
1028
- vertexBuffer: lkg.resource.vertexBuffer,
1029
- indexBuffer: lkg.resource.indexBuffer,
1030
- layoutProjection: this.layoutProjection,
1031
- };
1032
- }
1033
- this.publishRefusal(snapshot, prepared.error);
1034
- return undefined;
1035
- }
1036
- const plan = createPointsLinesLaneAdapter(lane, backend).createRecordPlan(
1037
- snapshot,
1038
- prepared.value.geometry,
1039
- );
1040
- const state = preparation.inspect();
1041
- this.active.set(
1042
- key,
1043
- inspectPointsLines({
1044
- snapshot,
1045
- topology,
1046
- lane,
1047
- pointCount: prepared.value.geometry.pointCount,
1048
- segmentCount: prepared.value.geometry.segmentCount,
1049
- sourceBytes: prepared.value.geometry.sourceBytes,
1050
- derivedBytes: prepared.value.geometry.derivedBytes,
1051
- cache: {
1052
- hit: prepared.value.uploadedBytes === 0,
1053
- rebuilds: state.rebuilds,
1054
- evictions: 0,
1055
- },
1056
- drawCount: plan.drawCount,
1057
- uploadBytes: prepared.value.uploadedBytes,
1058
- lastKnownGood: prepared.value.lastKnownGood,
1059
- }),
1060
- );
1061
- return {
1062
- plan,
1063
- vertexBuffer: prepared.value.resource.vertexBuffer,
1064
- indexBuffer: prepared.value.resource.indexBuffer,
1065
- layoutProjection: this.layoutProjection,
1066
- };
1067
- }
1068
-
1069
- resetForDeviceLoss(): void {
1070
- for (const preparation of this.preparations.values()) preparation.resetForDeviceLoss();
1071
- this.active.clear();
1072
- }
1073
-
1074
- inspections(): readonly PointsLinesInspection[] {
1075
- return [...this.active.values()];
1076
- }
1077
-
1078
- private backend(): PointsLinesBackend {
1079
- switch (this.internals.device.caps.backendKind) {
1080
- case 'wgpu-webgl2':
1081
- return 'wgpu-webgl2';
1082
- case 'null':
1083
- return 'null';
1084
- default:
1085
- return 'webgpu';
1086
- }
1087
- }
1088
-
1089
- private publishRefusal(
1090
- snapshot: PointsLinesRetainedSnapshot,
1091
- error: PointsLinesSourceError,
1092
- ): void {
1093
- this.active.set(
1094
- `${snapshot.worldId}:${snapshot.entityKey}`,
1095
- inspectPointsLines({
1096
- snapshot,
1097
- topology: snapshot.component === 'Points' ? 'point-list' : 'line-list',
1098
- lane: 'refused',
1099
- pointCount: 0,
1100
- segmentCount: 0,
1101
- sourceBytes: 0,
1102
- derivedBytes: 0,
1103
- cache: { hit: false, rebuilds: 0, evictions: 0 },
1104
- drawCount: 0,
1105
- uploadBytes: 0,
1106
- lastKnownGood: false,
1107
- refusal: {
1108
- code: error.code,
1109
- expected: error.expected,
1110
- hint: error.hint,
1111
- detail: error.detail,
1112
- generation: snapshot.meshGeneration,
1113
- lastKnownGood: false,
1114
- },
1115
- }),
1116
- );
1117
- }
1118
- }
1119
-
1120
1076
  export function createRenderSystem(internals: RenderSystemInternals): RenderSystem {
1121
1077
  const phaseCatalogRegistration = internals.profiler?.registerPhaseCatalog(
1122
1078
  'render',
@@ -1127,6 +1083,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1127
1083
  let preparedWorlds: readonly World[] = [];
1128
1084
  let latestCamera: CameraSnapshot | undefined;
1129
1085
  const pointsLinesOwner = new StandardPointsLinesOwner(internals);
1086
+ const instanceCollections = new InstanceProjectionStore();
1130
1087
  const reflectionProbeOwner = new ReflectionProbeRecordOwner(internals);
1131
1088
  let ssrFormatReceipt: import('@forgeax/engine-rhi').RhiTextureFormatCapabilityReceipt | undefined;
1132
1089
  let ssrFormatProbeGeneration = -1;
@@ -1168,8 +1125,10 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1168
1125
  onSharedRefMutation: (worldId, handle) => {
1169
1126
  internals.gpuStore.invalidateMesh(handle, preparedWorlds[worldId] ?? worldId);
1170
1127
  },
1128
+ instanceCollections,
1129
+ getSkinPaletteAllocator: () => internals.getPipelineState()?.skinPaletteAllocator ?? null,
1171
1130
  });
1172
- const environmentLifecycle = new EnvironmentLifecycle(internals.deviceScope);
1131
+ let environmentLifecycle = new EnvironmentLifecycle(internals.deviceScope);
1173
1132
  const transmissionAdmission = new TransmissionCandidateAdmission();
1174
1133
  let lastTransmissionInspection: TransmissionInspection | undefined;
1175
1134
  let lastTransmissionKey = '';
@@ -1294,8 +1253,13 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1294
1253
  });
1295
1254
  lastTransmissionKey = key;
1296
1255
  };
1256
+ // GPU-driven production uses the validated adapter for normal frames. The
1257
+ // assembly-supplied closure resolves its device-bound adapter at call time,
1258
+ // so recovery candidate preparation still follows candidateShaderState
1259
+ // rather than retaining a lost-generation module.
1297
1260
  const gpuDrivenShaderFactory =
1298
1261
  internals.shaderModuleFactory ??
1262
+ internals.immediateShaderModuleFactory ??
1299
1263
  ({
1300
1264
  createShaderModule: () =>
1301
1265
  err(
@@ -1306,7 +1270,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1306
1270
  }),
1307
1271
  ),
1308
1272
  } satisfies import('./pipeline-builder').PipelineBuilderShaderModuleFactory);
1309
- let gpuDrivenProduction = new GpuDrivenProduction(internals.device, gpuDrivenShaderFactory);
1273
+ let gpuDrivenProduction = GpuDrivenProduction.forDevice(internals.device, gpuDrivenShaderFactory);
1310
1274
  let occlusionRuntime: OcclusionRenderRuntime | undefined = new OcclusionRenderRuntime(
1311
1275
  internals.device,
1312
1276
  persistentRenderScene.visibilityFacetStore(),
@@ -1324,34 +1288,36 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1324
1288
  }
1325
1289
  return occlusionRuntime;
1326
1290
  };
1327
- const featureGpuWork: RenderFeatureGpuWorkOwner = createRenderFeatureGpuWorkOwner({
1328
- getDevice: () => internals.device,
1329
- getShaderModuleFactory: () =>
1330
- internals.shaderModuleFactory ??
1331
- ({
1332
- createShaderModule: () =>
1333
- err(
1334
- new RhiError({
1335
- code: 'rhi-not-available',
1336
- expected: 'the renderer backend exposes shader module creation',
1337
- hint: 'construct the renderer through the backend pack before activating GPU features',
1338
- }),
1339
- ),
1340
- } satisfies import('./pipeline-builder').PipelineBuilderShaderModuleFactory),
1341
- getImmediateShaderModuleFactory: () =>
1342
- internals.immediateShaderModuleFactory ??
1343
- internals.shaderModuleFactory ??
1344
- ({
1345
- createShaderModule: () =>
1346
- err(
1347
- new RhiError({
1348
- code: 'rhi-not-available',
1349
- expected: 'the renderer backend exposes shader module creation',
1350
- hint: 'construct the renderer through the backend pack before activating GPU features',
1351
- }),
1352
- ),
1353
- } satisfies import('./pipeline-builder').PipelineBuilderShaderModuleFactory),
1354
- });
1291
+ const createFeatureGpuWorkOwner = (runtime: RenderSystemInternals): RenderFeatureGpuWorkOwner =>
1292
+ createRenderFeatureGpuWorkOwner({
1293
+ getDevice: () => runtime.device,
1294
+ getShaderModuleFactory: () =>
1295
+ runtime.shaderModuleFactory ??
1296
+ ({
1297
+ createShaderModule: () =>
1298
+ err(
1299
+ new RhiError({
1300
+ code: 'rhi-not-available',
1301
+ expected: 'the renderer backend exposes shader module creation',
1302
+ hint: 'construct the renderer through the backend pack before activating GPU features',
1303
+ }),
1304
+ ),
1305
+ } satisfies import('./pipeline-builder').PipelineBuilderShaderModuleFactory),
1306
+ getImmediateShaderModuleFactory: () =>
1307
+ runtime.immediateShaderModuleFactory ??
1308
+ runtime.shaderModuleFactory ??
1309
+ ({
1310
+ createShaderModule: () =>
1311
+ err(
1312
+ new RhiError({
1313
+ code: 'rhi-not-available',
1314
+ expected: 'the renderer backend exposes shader module creation',
1315
+ hint: 'construct the renderer through the backend pack before activating GPU features',
1316
+ }),
1317
+ ),
1318
+ } satisfies import('./pipeline-builder').PipelineBuilderShaderModuleFactory),
1319
+ });
1320
+ let featureGpuWork = createFeatureGpuWorkOwner(internals);
1355
1321
  const disposeFeatureGpuWork = (): void => {
1356
1322
  const disposed = featureGpuWork.dispose();
1357
1323
  if (!disposed.ok) internals.errorRegistry.fire(disposed.error);
@@ -1411,6 +1377,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1411
1377
  currentDirectionalShadowView: null,
1412
1378
  currentSpotShadowView: null,
1413
1379
  instanceBuffers: new Map(),
1380
+ instanceResidency: new Map(),
1381
+ instanceBufferChunks: new Map(),
1414
1382
  probeBlendBuffers: new Map(),
1415
1383
  probeBlendRecordBufferCapacity: 0,
1416
1384
  morphBuffers: new Map(),
@@ -1493,6 +1461,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1493
1461
  spotShadowSnapshots: [],
1494
1462
  };
1495
1463
  let directFrameId = 0;
1464
+ let submittedFrameCount = 0;
1465
+ let lastSubmittedGeneration: number | undefined;
1496
1466
  let lastDirectionalShadowLights: ExtractedLights | undefined;
1497
1467
  let lastDirectionalShadowCandidate: 'accepted' | 'failed' = 'failed';
1498
1468
  let lastDirectionalShadowError: DirectionalShadowInspectionSource['error'];
@@ -1551,7 +1521,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1551
1521
  // D-3 / D-8: per-shader params UBO resource table (id -> GPU Buffer).
1552
1522
  // Eager-created at register time when entry.params is present (byteSize >= 16,
1553
1523
  // defaultValue.length === byteSize); reused frame-to-frame via queue.writeBuffer.
1554
- const postProcessParamsBuffers = new Map<string, Buffer>();
1524
+ let postProcessParamsBuffers = new Map<string, Buffer>();
1525
+ let postProcessParamsDevice = internals.device;
1555
1526
  const builtinPostProcessEntries = new Map<string, PostProcessShaderEntry>();
1556
1527
  let activeFeaturePostProcessEntries: ReadonlyMap<string, PostProcessShaderEntry> = new Map();
1557
1528
  const lookupPostProcess = (id: string): PostProcessShaderEntry | undefined =>
@@ -1567,6 +1538,76 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1567
1538
  // per-frame snapshot + bind it at group(1) binding(2).
1568
1539
  const getPostProcessParamsBuffer = (id: string): Buffer | undefined =>
1569
1540
  postProcessParamsBuffers.get(id);
1541
+ const postProcessDeclarations = (): readonly (readonly [string, PostProcessShaderEntry])[] => [
1542
+ ...builtinPostProcessEntries.entries(),
1543
+ ...activeFeaturePostProcessEntries.entries(),
1544
+ ];
1545
+ const disposePostProcessResources = (resources: RecoveryPostProcessResources): void => {
1546
+ for (const buffer of resources.paramsBuffers.values()) {
1547
+ const destroyed = resources.device.destroyBuffer(buffer);
1548
+ if (!destroyed.ok) internals.errorRegistry.fire(destroyed.error);
1549
+ }
1550
+ };
1551
+ const prepareRecoveryPostProcessResources = (
1552
+ device: RhiDevice,
1553
+ ): Result<RecoveryPostProcessResources, RhiError> => {
1554
+ const paramsBuffers = new Map<string, Buffer>();
1555
+ try {
1556
+ for (const [id, entry] of postProcessDeclarations()) {
1557
+ if (id === 'forgeax.taa-resolve') continue;
1558
+ let buffer: Buffer | undefined;
1559
+ if (entry.params !== undefined) {
1560
+ const created = device.createBuffer({
1561
+ label: `post-process-params-${id}`,
1562
+ size: entry.params.byteSize,
1563
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
1564
+ mappedAtCreation: false,
1565
+ });
1566
+ if (!created.ok) throw created.error;
1567
+ buffer = created.value;
1568
+ paramsBuffers.set(id, buffer);
1569
+ const written = device.queue.writeBuffer(buffer, 0, entry.params.defaultValue);
1570
+ if (!written.ok) throw written.error;
1571
+ } else if (entryHasDepthRead(entry)) {
1572
+ const created = device.createBuffer({
1573
+ label: `post-process-params-${id}`,
1574
+ size: DEPTH_MIN_PARAMS_BYTE_SIZE,
1575
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
1576
+ mappedAtCreation: false,
1577
+ });
1578
+ if (!created.ok) throw created.error;
1579
+ buffer = created.value;
1580
+ }
1581
+ if (buffer !== undefined) paramsBuffers.set(id, buffer);
1582
+ }
1583
+ } catch (cause) {
1584
+ disposePostProcessResources({ device, paramsBuffers });
1585
+ if (cause instanceof RhiError) return err(cause);
1586
+ return err(
1587
+ new RhiError({
1588
+ code: 'webgpu-runtime-error',
1589
+ expected: 'post-process recovery resources are created on the candidate device',
1590
+ hint: cause instanceof Error ? cause.message : String(cause),
1591
+ }),
1592
+ );
1593
+ }
1594
+ return ok({ device, paramsBuffers });
1595
+ };
1596
+ const publishRecoveryPostProcessResources = (candidate: RecoveryPostProcessResources): void => {
1597
+ if (candidate.paramsBuffers === postProcessParamsBuffers) return;
1598
+ const previous = { device: postProcessParamsDevice, paramsBuffers: postProcessParamsBuffers };
1599
+ postProcessParamsDevice = candidate.device;
1600
+ postProcessParamsBuffers = candidate.paramsBuffers;
1601
+ disposePostProcessResources(previous);
1602
+ };
1603
+ const discardRecoveryPostProcessResources = (candidate: RecoveryPostProcessResources): void => {
1604
+ if (candidate.paramsBuffers === postProcessParamsBuffers) {
1605
+ postProcessParamsBuffers = new Map();
1606
+ disposePostProcessResources(candidate);
1607
+ return;
1608
+ }
1609
+ disposePostProcessResources(candidate);
1610
+ };
1570
1611
  interface PostProcessParamsCandidate {
1571
1612
  readonly buffers: ReadonlyMap<string, Buffer>;
1572
1613
  readonly created: readonly Buffer[];
@@ -1666,6 +1707,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1666
1707
  const key = `${id}|${colorFormat}|${signature}`;
1667
1708
  const cached = postProcessPipelineCache.get(key);
1668
1709
  if (cached !== undefined) return cached;
1710
+ internals.recoveryColdWorkGuard?.notePipelineColdWork();
1669
1711
  const factory = internals.buildPostProcessPipeline;
1670
1712
  if (factory === undefined) return null;
1671
1713
  const built = factory(
@@ -1842,44 +1884,60 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1842
1884
  createBindGroup: 0,
1843
1885
  keys: [],
1844
1886
  };
1845
- const preparedPipelineIds = new WeakMap<object, string>();
1846
- const preparedMaterialPipelineShaders = new WeakMap<object, string>();
1847
- const preparedGroup0Pipelines = new WeakSet<object>();
1848
- const preparedViewOnlyPipelines = new WeakSet<object>();
1849
- const preparedRenderMaterialPipelines = new WeakSet<object>();
1850
- const preparedAssetHandles = new WeakMap<World, Map<string, number>>();
1887
+ type PreparedResolverCaches = {
1888
+ readonly preparedPipelineIds: WeakMap<object, string>;
1889
+ readonly preparedMaterialPipelineShaders: WeakMap<object, string>;
1890
+ readonly preparedGroup0Pipelines: WeakSet<object>;
1891
+ readonly preparedViewOnlyPipelines: WeakSet<object>;
1892
+ readonly preparedRenderMaterialPipelines: WeakSet<object>;
1893
+ readonly preparedAssetHandles: WeakMap<World, Map<string, number>>;
1894
+ };
1895
+ const createPreparedResolverCaches = (): PreparedResolverCaches => ({
1896
+ preparedPipelineIds: new WeakMap(),
1897
+ preparedMaterialPipelineShaders: new WeakMap(),
1898
+ preparedGroup0Pipelines: new WeakSet(),
1899
+ preparedViewOnlyPipelines: new WeakSet(),
1900
+ preparedRenderMaterialPipelines: new WeakSet(),
1901
+ preparedAssetHandles: new WeakMap(),
1902
+ });
1903
+ const activePreparedResolverCaches = createPreparedResolverCaches();
1851
1904
  const preparedHandle = <Brand extends string>(
1905
+ runtime: RenderSystemInternals,
1906
+ caches: PreparedResolverCaches,
1852
1907
  world: World,
1853
1908
  guid: string,
1854
1909
  brand: Brand,
1855
1910
  ): Handle<Brand, 'shared'> | undefined => {
1856
- let handles = preparedAssetHandles.get(world);
1911
+ let handles = caches.preparedAssetHandles.get(world);
1857
1912
  if (handles === undefined) {
1858
1913
  handles = new Map();
1859
- preparedAssetHandles.set(world, handles);
1914
+ caches.preparedAssetHandles.set(world, handles);
1860
1915
  }
1861
1916
  const key = `${brand}:${guid.toLowerCase()}`;
1862
1917
  const cached = handles.get(key);
1863
1918
  if (cached !== undefined) return cached as Handle<Brand, 'shared'>;
1864
- const asset = internals.assets.lookup(guid);
1919
+ const asset = runtime.assets.lookup(guid);
1865
1920
  if (asset === undefined) return undefined;
1866
1921
  const handle = world.allocSharedRef(brand, asset);
1867
1922
  handles.set(key, handle as number);
1868
1923
  return handle;
1869
1924
  };
1870
1925
  const preparedMaterialBindings = (
1926
+ runtime: RenderSystemInternals,
1927
+ caches: PreparedResolverCaches,
1928
+ worlds: readonly World[],
1871
1929
  materialShaderId: string,
1872
1930
  worldIndex: number,
1873
1931
  materialGuid: string,
1874
1932
  layout: BindGroupLayout,
1875
1933
  ) => {
1876
- const world = preparedWorlds[worldIndex];
1877
- const material = internals.assets.lookup(materialGuid) as MaterialAsset | undefined;
1878
- const pipelineState = internals.getPipelineState();
1934
+ const world = worlds[worldIndex];
1935
+ const material = runtime.assets.lookup(materialGuid) as MaterialAsset | undefined;
1936
+ const pipelineState = runtime.getPipelineState();
1879
1937
  if (world === undefined || material?.kind !== 'material' || pipelineState === null) {
1880
1938
  return err(new Error('prepared material asset is unavailable'));
1881
1939
  }
1882
- const schema = internals.getParamSchema?.(materialShaderId) ?? [];
1940
+ const schema = runtime.getParamSchema?.(materialShaderId) ?? [];
1883
1941
  const values = material.values ?? {};
1884
1942
  const derived = derive(schema);
1885
1943
  const fields = [...derived.textureFieldNames];
@@ -1892,7 +1950,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1892
1950
  schema,
1893
1951
  values as Readonly<Record<string, number | readonly number[]>>,
1894
1952
  );
1895
- const created = internals.device.createBuffer({
1953
+ const created = runtime.device.createBuffer({
1896
1954
  label: `prepared-material:${materialGuid}`,
1897
1955
  size: payload.byteLength,
1898
1956
  usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
@@ -1900,9 +1958,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1900
1958
  if (!created.ok) return created;
1901
1959
  buffer = created.value;
1902
1960
  payloadByteLength = payload.byteLength;
1903
- const written = internals.device.queue.writeBuffer(buffer, 0, payload);
1961
+ const written = runtime.device.queue.writeBuffer(buffer, 0, payload);
1904
1962
  if (!written.ok) {
1905
- internals.device.destroyBuffer(buffer);
1963
+ runtime.device.destroyBuffer(buffer);
1906
1964
  return written;
1907
1965
  }
1908
1966
  }
@@ -1919,20 +1977,32 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1919
1977
  : undefined;
1920
1978
  let sampler = pipelineState.defaultSampler;
1921
1979
  if (textureValue?.sampler !== undefined) {
1922
- const handle = preparedHandle(world, String(textureValue.sampler), 'SamplerAsset');
1980
+ const handle = preparedHandle(
1981
+ runtime,
1982
+ caches,
1983
+ world,
1984
+ String(textureValue.sampler),
1985
+ 'SamplerAsset',
1986
+ );
1923
1987
  if (handle !== undefined) {
1924
1988
  const asset = resolveAssetHandle<SamplerAsset>(world, handle);
1925
1989
  if (asset.ok) {
1926
- const resident = internals.gpuStore.ensureSamplerResident(handle, asset.value, world);
1990
+ const resident = runtime.gpuStore.ensureSamplerResident(handle, asset.value, world);
1927
1991
  if (resident.ok) sampler = resident.value;
1928
1992
  }
1929
1993
  }
1930
1994
  }
1931
1995
  let view = pipelineState.defaultWhiteTextureView;
1932
1996
  if (textureValue !== undefined) {
1933
- const handle = preparedHandle(world, String(textureValue.texture), 'TextureAsset');
1997
+ const handle = preparedHandle(
1998
+ runtime,
1999
+ caches,
2000
+ world,
2001
+ String(textureValue.texture),
2002
+ 'TextureAsset',
2003
+ );
1934
2004
  if (handle !== undefined) {
1935
- view = residentTextureView(world, internals.gpuStore, internals, handle) ?? view;
2005
+ view = residentTextureView(world, runtime.gpuStore, runtime, handle) ?? view;
1936
2006
  }
1937
2007
  }
1938
2008
  textureResources.push({ sampler, view });
@@ -1968,12 +2038,12 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1968
2038
  textureIndex += 1;
1969
2039
  continue;
1970
2040
  }
1971
- if (buffer !== undefined) internals.device.destroyBuffer(buffer);
2041
+ if (buffer !== undefined) runtime.device.destroyBuffer(buffer);
1972
2042
  return err(new Error('prepared material schema contains unsupported binding kinds'));
1973
2043
  }
1974
2044
  const fallback = pipelineState.skylightFallback;
1975
2045
  if (fallback === null) {
1976
- if (buffer !== undefined) internals.device.destroyBuffer(buffer);
2046
+ if (buffer !== undefined) runtime.device.destroyBuffer(buffer);
1977
2047
  return err(new Error('prepared material skylight fallback is unavailable'));
1978
2048
  }
1979
2049
  const completeEntries = assembleMaterialWithSkylightEntries(entries, {
@@ -1985,207 +2055,268 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1985
2055
  brdfLutSampler: fallback.sampler,
1986
2056
  intensityBuffer: fallback.intensityBuffer,
1987
2057
  });
1988
- const group = internals.device.createBindGroup({ layout, entries: completeEntries });
2058
+ const group = runtime.device.createBindGroup({ layout, entries: completeEntries });
1989
2059
  if (!group.ok) {
1990
- if (buffer !== undefined) internals.device.destroyBuffer(buffer);
2060
+ if (buffer !== undefined) runtime.device.destroyBuffer(buffer);
1991
2061
  return group;
1992
2062
  }
1993
2063
  if (buffer === undefined) return group;
1994
2064
  return ok({
1995
2065
  handle: group.value,
1996
2066
  dynamicOffsets: [0, 0],
1997
- release: () => internals.device.destroyBuffer(buffer),
2067
+ release: () => runtime.device.destroyBuffer(buffer),
1998
2068
  });
1999
2069
  };
2000
2070
  const lastFrustumStats: { culled: number; total: number } = { culled: 0, total: 0 };
2001
2071
  const lastVisibilityStats: { explicitlyHidden: number } = { explicitlyHidden: 0 };
2002
2072
  let lastMeshMaterialBindings: readonly MeshMaterialBindingObservation[] = [];
2003
2073
  let lastMeshMaterialBindingFrame: ExtractedFrame | undefined;
2004
- const preparedResolverFactory = (
2005
- input: RenderFeaturePreparedGraphicsResolverInput,
2006
- ): PreparedGraphicsResolver => {
2007
- return createPreparedGraphicsResolver({
2008
- device: internals.device,
2009
- featureIdentity: input.featureIdentity,
2010
- generation: input.generation,
2011
- capabilityAvailable: true,
2012
- featureOrder: input.order,
2013
- lookup: input.lookup,
2014
- resolveGpuBuffer: (reference) =>
2015
- featureGpuWork.resolveBuffer(input.featureIdentity, reference),
2016
- resolvePipeline: (descriptor) => {
2017
- const postProcessEntry =
2018
- input.fullscreenEffects.get(descriptor.shader) ??
2019
- builtinPostProcessEntries.get(descriptor.shader);
2020
- if (postProcessEntry !== undefined) {
2021
- const fullscreen = buildFullscreenPostProcessPass(
2022
- { device: internals.device, errorRegistry: internals.errorRegistry },
2023
- postProcessEntry,
2024
- );
2025
- if (fullscreen === null) return err(new Error('prepared post-process layout failed'));
2026
- const pipeline = internals.getPostProcessPipeline?.(
2074
+ const createPreparedResolverFactory =
2075
+ (
2076
+ runtime: RenderSystemInternals,
2077
+ gpuWork: RenderFeatureGpuWorkOwner,
2078
+ worlds: readonly World[],
2079
+ caches: PreparedResolverCaches,
2080
+ ) =>
2081
+ (input: RenderFeaturePreparedGraphicsResolverInput): PreparedGraphicsResolver => {
2082
+ const {
2083
+ preparedPipelineIds,
2084
+ preparedMaterialPipelineShaders,
2085
+ preparedGroup0Pipelines,
2086
+ preparedViewOnlyPipelines,
2087
+ preparedRenderMaterialPipelines,
2088
+ } = caches;
2089
+ return createPreparedGraphicsResolver({
2090
+ device: runtime.device,
2091
+ featureIdentity: input.featureIdentity,
2092
+ generation: input.generation,
2093
+ capabilityAvailable: true,
2094
+ featureOrder: input.order,
2095
+ lookup: input.lookup,
2096
+ resolveGpuBuffer: (reference) => gpuWork.resolveBuffer(input.featureIdentity, reference),
2097
+ resolvePipeline: (descriptor) => {
2098
+ const postProcessEntry =
2099
+ input.fullscreenEffects.get(descriptor.shader) ??
2100
+ builtinPostProcessEntries.get(descriptor.shader);
2101
+ if (postProcessEntry !== undefined) {
2102
+ const fullscreen = buildFullscreenPostProcessPass(
2103
+ { device: runtime.device, errorRegistry: runtime.errorRegistry },
2104
+ postProcessEntry,
2105
+ );
2106
+ if (fullscreen === null) return err(new Error('prepared post-process layout failed'));
2107
+ const pipeline = runtime.getPostProcessPipeline?.(
2108
+ descriptor.shader,
2109
+ fullscreen.bindGroupLayout,
2110
+ descriptor.colorFormats[0] as GPUTextureFormat,
2111
+ postProcessEntry,
2112
+ );
2113
+ if (pipeline !== null && pipeline !== undefined) {
2114
+ preparedPipelineIds.set(pipeline as object, descriptor.shader);
2115
+ if (runtime.getMaterialShaderBindingContract?.(descriptor.shader) === 'group-0') {
2116
+ preparedGroup0Pipelines.add(pipeline as object);
2117
+ }
2118
+ }
2119
+ return pipeline === null || pipeline === undefined
2120
+ ? err(makePreparedPipelinePendingError())
2121
+ : ok(pipeline);
2122
+ }
2123
+ const preparedPipeline = runtime.getMaterialShaderPipeline?.(
2027
2124
  descriptor.shader,
2028
- fullscreen.bindGroupLayout,
2125
+ descriptor.colorFormats[0] === 'rgba16float',
2126
+ descriptor.renderState,
2127
+ descriptor.topology,
2128
+ descriptor.indexFormat,
2129
+ undefined,
2130
+ 'forward',
2131
+ undefined,
2132
+ descriptor.sampleCount ?? 1,
2133
+ // Prepared graphics own their declared attachment format. Passing
2134
+ // it through keeps the PSO compatible with feature-target views
2135
+ // (the ordinary material path intentionally defaults to the
2136
+ // swap-chain view format).
2029
2137
  descriptor.colorFormats[0] as GPUTextureFormat,
2030
- postProcessEntry,
2138
+ undefined,
2139
+ descriptor.depthFormat === undefined
2140
+ ? null
2141
+ : (descriptor.depthFormat as GPUTextureFormat),
2142
+ descriptor.vertexLayout,
2143
+ undefined,
2144
+ input.shaderModuleMode,
2031
2145
  );
2032
- if (pipeline !== null && pipeline !== undefined) {
2033
- preparedPipelineIds.set(pipeline as object, descriptor.shader);
2034
- if (internals.getMaterialShaderBindingContract?.(descriptor.shader) === 'group-0') {
2146
+ // A requested material shader is an exact pipeline contract. During
2147
+ // async shader warmup, substituting the generic unlit pipeline can
2148
+ // mismatch vertex layouts and HDR attachment formats, turning a
2149
+ // retryable next-frame prepare into an invalid GPU command buffer.
2150
+ const pipeline = preparedPipeline ?? null;
2151
+ if (pipeline !== null) {
2152
+ preparedMaterialPipelineShaders.set(pipeline as object, descriptor.shader);
2153
+ const bindingContract = runtime.getMaterialShaderBindingContract?.(descriptor.shader);
2154
+ if (bindingContract === 'group-0') {
2035
2155
  preparedGroup0Pipelines.add(pipeline as object);
2156
+ } else if (bindingContract === 'view-only') {
2157
+ preparedViewOnlyPipelines.add(pipeline as object);
2158
+ } else {
2159
+ preparedRenderMaterialPipelines.add(pipeline as object);
2036
2160
  }
2037
2161
  }
2038
- return pipeline === null || pipeline === undefined
2039
- ? err(makePreparedPipelinePendingError())
2040
- : ok(pipeline);
2041
- }
2042
- const preparedPipeline = internals.getMaterialShaderPipeline?.(
2043
- descriptor.shader,
2044
- descriptor.colorFormats[0] === 'rgba16float',
2045
- descriptor.renderState,
2046
- descriptor.topology,
2047
- descriptor.indexFormat,
2048
- undefined,
2049
- 'forward',
2050
- undefined,
2051
- descriptor.sampleCount ?? 1,
2052
- // Prepared graphics own their declared attachment format. Passing
2053
- // it through keeps the PSO compatible with feature-target views
2054
- // (the ordinary material path intentionally defaults to the
2055
- // swap-chain view format).
2056
- descriptor.colorFormats[0] as GPUTextureFormat,
2057
- undefined,
2058
- descriptor.depthFormat === undefined
2059
- ? null
2060
- : (descriptor.depthFormat as GPUTextureFormat),
2061
- descriptor.vertexLayout,
2062
- undefined,
2063
- input.shaderModuleMode,
2064
- );
2065
- // A requested material shader is an exact pipeline contract. During
2066
- // async shader warmup, substituting the generic unlit pipeline can
2067
- // mismatch vertex layouts and HDR attachment formats, turning a
2068
- // retryable next-frame prepare into an invalid GPU command buffer.
2069
- const pipeline = preparedPipeline ?? null;
2070
- if (pipeline !== null) {
2071
- preparedMaterialPipelineShaders.set(pipeline as object, descriptor.shader);
2072
- const bindingContract = internals.getMaterialShaderBindingContract?.(descriptor.shader);
2073
- if (bindingContract === 'group-0') {
2074
- preparedGroup0Pipelines.add(pipeline as object);
2075
- } else if (bindingContract === 'view-only') {
2076
- preparedViewOnlyPipelines.add(pipeline as object);
2077
- } else {
2078
- preparedRenderMaterialPipelines.add(pipeline as object);
2162
+ return pipeline === null ? err(makePreparedPipelinePendingError()) : ok(pipeline);
2163
+ },
2164
+ resolveBindings: (descriptor, pipeline) => {
2165
+ const materialShaderId = preparedMaterialPipelineShaders.get(pipeline as object);
2166
+ const bindingContract =
2167
+ materialShaderId === undefined
2168
+ ? undefined
2169
+ : runtime.getMaterialShaderBindingContract?.(materialShaderId);
2170
+ if (bindingContract === 'group-0-resource') {
2171
+ return descriptor.values.sceneDepth === undefined
2172
+ ? err(new Error('prepared group-0 resource pipeline requires a scene target'))
2173
+ : ok(undefined);
2079
2174
  }
2080
- }
2081
- return pipeline === null ? err(makePreparedPipelinePendingError()) : ok(pipeline);
2082
- },
2083
- resolveBindings: (descriptor, pipeline) => {
2084
- const materialShaderId = preparedMaterialPipelineShaders.get(pipeline as object);
2085
- const bindingContract =
2086
- materialShaderId === undefined
2087
- ? undefined
2088
- : internals.getMaterialShaderBindingContract?.(materialShaderId);
2089
- if (bindingContract === 'group-0-resource') {
2090
- return descriptor.values.sceneDepth === undefined
2091
- ? err(new Error('prepared group-0 resource pipeline requires a scene target'))
2092
- : ok(undefined);
2093
- }
2094
- if (bindingContract === 'view-and-scene-depth') {
2095
- return ok(undefined);
2096
- }
2097
- if (preparedGroup0Pipelines.has(pipeline as object)) {
2175
+ if (bindingContract === 'view-and-scene-depth') {
2176
+ return ok(undefined);
2177
+ }
2178
+ if (preparedGroup0Pipelines.has(pipeline as object)) {
2179
+ const layout =
2180
+ (materialShaderId === undefined
2181
+ ? undefined
2182
+ : runtime.getMaterialBindGroupLayout?.(materialShaderId)) ??
2183
+ (
2184
+ pipeline as RenderPipeline & {
2185
+ getBindGroupLayout?: (index: number) => BindGroupLayout;
2186
+ }
2187
+ ).getBindGroupLayout?.(0);
2188
+ return layout === undefined
2189
+ ? err(new Error('prepared group-0 pipeline bind group layout is unavailable'))
2190
+ : runtime.device.createBindGroup({ layout, entries: [] });
2191
+ }
2192
+ if (
2193
+ preparedViewOnlyPipelines.has(pipeline as object) ||
2194
+ (preparedRenderMaterialPipelines.has(pipeline as object) &&
2195
+ descriptor.values.group === 0) ||
2196
+ pipeline === runtime.getPipelineState()?.unlitPipeline
2197
+ ) {
2198
+ return ok(undefined);
2199
+ }
2200
+ const group = descriptor.values.group;
2098
2201
  const layout =
2099
- (materialShaderId === undefined
2100
- ? undefined
2101
- : internals.getMaterialBindGroupLayout?.(materialShaderId)) ??
2202
+ (materialShaderId !== undefined
2203
+ ? (runtime.getMaterialBindGroupLayout?.(materialShaderId) ??
2204
+ (group === 0 ? runtime.getPipelineState()?.materialBindGroupLayout : undefined))
2205
+ : undefined) ??
2102
2206
  (
2103
2207
  pipeline as RenderPipeline & {
2104
2208
  getBindGroupLayout?: (index: number) => BindGroupLayout;
2105
2209
  }
2106
- ).getBindGroupLayout?.(0);
2107
- return layout === undefined
2108
- ? err(new Error('prepared group-0 pipeline bind group layout is unavailable'))
2109
- : internals.device.createBindGroup({ layout, entries: [] });
2110
- }
2111
- if (
2112
- preparedViewOnlyPipelines.has(pipeline as object) ||
2113
- (preparedRenderMaterialPipelines.has(pipeline as object) &&
2114
- descriptor.values.group === 0) ||
2115
- pipeline === internals.getPipelineState()?.unlitPipeline
2116
- ) {
2117
- return ok(undefined);
2118
- }
2119
- const group = descriptor.values.group;
2120
- const layout =
2121
- (materialShaderId !== undefined
2122
- ? (internals.getMaterialBindGroupLayout?.(materialShaderId) ??
2123
- (group === 0 ? internals.getPipelineState()?.materialBindGroupLayout : undefined))
2124
- : undefined) ??
2125
- (
2126
- pipeline as RenderPipeline & {
2127
- getBindGroupLayout?: (index: number) => BindGroupLayout;
2128
- }
2129
- ).getBindGroupLayout?.(group === 1 ? 1 : 0);
2130
- const material = descriptor.values.material;
2131
- if (
2132
- group === 1 &&
2133
- layout !== undefined &&
2134
- materialShaderId !== undefined &&
2135
- typeof material === 'object' &&
2136
- material !== null &&
2137
- 'world' in material &&
2138
- 'guid' in material &&
2139
- typeof material.world === 'number' &&
2140
- typeof material.guid === 'string'
2141
- ) {
2142
- return preparedMaterialBindings(materialShaderId, material.world, material.guid, layout);
2143
- }
2144
- return layout === undefined
2145
- ? err(new Error('prepared pipeline bind group layout is unavailable'))
2146
- : internals.device.createBindGroup({
2210
+ ).getBindGroupLayout?.(group === 1 ? 1 : 0);
2211
+ const material = descriptor.values.material;
2212
+ if (
2213
+ group === 1 &&
2214
+ layout !== undefined &&
2215
+ materialShaderId !== undefined &&
2216
+ typeof material === 'object' &&
2217
+ material !== null &&
2218
+ 'world' in material &&
2219
+ 'guid' in material &&
2220
+ typeof material.world === 'number' &&
2221
+ typeof material.guid === 'string'
2222
+ ) {
2223
+ return preparedMaterialBindings(
2224
+ runtime,
2225
+ caches,
2226
+ worlds,
2227
+ materialShaderId,
2228
+ material.world,
2229
+ material.guid,
2147
2230
  layout,
2148
- entries:
2149
- group === 1 && preparedPipelineIds.has(pipeline as object)
2150
- ? [
2151
- {
2152
- binding: 0,
2153
- resource: {
2154
- kind: 'textureView',
2155
- value: internals.getPipelineState()?.fallbackTextureView as never,
2231
+ );
2232
+ }
2233
+ return layout === undefined
2234
+ ? err(new Error('prepared pipeline bind group layout is unavailable'))
2235
+ : runtime.device.createBindGroup({
2236
+ layout,
2237
+ entries:
2238
+ group === 1 && preparedPipelineIds.has(pipeline as object)
2239
+ ? [
2240
+ {
2241
+ binding: 0,
2242
+ resource: {
2243
+ kind: 'textureView',
2244
+ value: runtime.getPipelineState()?.fallbackTextureView as never,
2245
+ },
2156
2246
  },
2157
- },
2158
- {
2159
- binding: 1,
2160
- resource: {
2161
- kind: 'sampler',
2162
- value: internals.getPipelineState()?.defaultSampler as never,
2247
+ {
2248
+ binding: 1,
2249
+ resource: {
2250
+ kind: 'sampler',
2251
+ value: runtime.getPipelineState()?.defaultSampler as never,
2252
+ },
2163
2253
  },
2164
- },
2165
- ...(internals.getPostProcessParamsBuffer?.(
2166
- preparedPipelineIds.get(pipeline as object) ?? '',
2167
- ) === undefined
2168
- ? []
2169
- : [
2170
- {
2171
- binding: 2,
2172
- resource: {
2173
- kind: 'buffer' as const,
2174
- value: {
2175
- buffer: internals.getPostProcessParamsBuffer?.(
2176
- preparedPipelineIds.get(pipeline as object) ?? '',
2177
- ) as never,
2254
+ ...(runtime.getPostProcessParamsBuffer?.(
2255
+ preparedPipelineIds.get(pipeline as object) ?? '',
2256
+ ) === undefined
2257
+ ? []
2258
+ : [
2259
+ {
2260
+ binding: 2,
2261
+ resource: {
2262
+ kind: 'buffer' as const,
2263
+ value: {
2264
+ buffer: runtime.getPostProcessParamsBuffer?.(
2265
+ preparedPipelineIds.get(pipeline as object) ?? '',
2266
+ ) as never,
2267
+ },
2178
2268
  },
2179
2269
  },
2180
- },
2181
- ]),
2182
- ]
2183
- : [],
2184
- });
2185
- },
2186
- });
2187
- };
2270
+ ]),
2271
+ ]
2272
+ : [],
2273
+ });
2274
+ },
2275
+ });
2276
+ };
2277
+ const preparedResolverFactory = (
2278
+ input: RenderFeaturePreparedGraphicsResolverInput,
2279
+ ): PreparedGraphicsResolver =>
2280
+ createPreparedResolverFactory(
2281
+ internals,
2282
+ featureGpuWork,
2283
+ preparedWorlds,
2284
+ activePreparedResolverCaches,
2285
+ )(input);
2286
+ const recoveryOwner = createRenderSystemRecovery({
2287
+ frameState,
2288
+ internals,
2289
+ getEnvironmentLifecycle: () => environmentLifecycle,
2290
+ getGpuDrivenProduction: () => gpuDrivenProduction,
2291
+ setGpuDrivenProduction: (production) => {
2292
+ gpuDrivenProduction = production;
2293
+ },
2294
+ disposeFeatureGpuWork,
2295
+ setFeatureGpuWork: (owner) => {
2296
+ featureGpuWork = owner;
2297
+ },
2298
+ getActiveFeaturePostProcessEntries: () => activeFeaturePostProcessEntries,
2299
+ setActiveFeaturePostProcessEntries: (entries) => {
2300
+ activeFeaturePostProcessEntries = entries;
2301
+ },
2302
+ postProcessPipelineCache,
2303
+ clearPostProcessPipelineCache,
2304
+ clearPostProcessPipelineEntry,
2305
+ invalidatePostProcessModule,
2306
+ persistentRenderScene,
2307
+ gpuDrivenShaderFactory,
2308
+ createFeatureGpuWorkOwner,
2309
+ pointsLinesOwner,
2310
+ createPreparedResolverFactory,
2311
+ createPreparedResolverCaches,
2312
+ });
2188
2313
  return {
2314
+ get instanceCollectionsInspection(): readonly InstanceCollectionInspection[] {
2315
+ return instanceCollections._inspections(
2316
+ frameState.instanceResidency,
2317
+ frameState.frameNumber - 1,
2318
+ );
2319
+ },
2189
2320
  releaseProfilerCatalog(): void {
2190
2321
  releaseProfilerCatalog?.();
2191
2322
  releaseProfilerCatalog = undefined;
@@ -2387,7 +2518,10 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2387
2518
  renderReadLeases?: readonly RenderReadLease[],
2388
2519
  timingCapture?: GpuTimingCapture,
2389
2520
  ): boolean {
2390
- preparedWorlds = worlds;
2521
+ const composition = canonicalizeWorldComposition(worlds, opts, renderReadLeases);
2522
+ const compositionWorlds = composition.worlds;
2523
+ const compositionLeases = composition.leases;
2524
+ preparedWorlds = compositionWorlds;
2391
2525
  const profileSession = internals.profiler?.activeSession();
2392
2526
  let ownsProfileFrame = false;
2393
2527
  let submitted = false;
@@ -2403,7 +2537,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2403
2537
  // cameraOwner drives the surfaced cameras + frustum
2404
2538
  // cull; resourceOwner drives skylight/skybox/postProcess + per-world
2405
2539
  // record config.
2406
- const { cameraOwner, resourceOwner } = opts;
2540
+ const { cameraOwner, resourceOwner } = composition.owners;
2407
2541
  // Keep the compiled graph as last-known-good until the candidate topology
2408
2542
  // compiles. The failed candidate frame does not execute it; successful
2409
2543
  // replacement retires it atomically in ensureCompiledFrameGraph.
@@ -2438,28 +2572,33 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2438
2572
  // the dedicated resourceOwner index. The variable is named
2439
2573
  // `resourceWorld` so the record-stage reads are self-describing (they
2440
2574
  // are resource-owner reads, not camera reads).
2441
- const resourceWorld = worlds[resourceOwner] as World;
2575
+ const resourceWorld = compositionWorlds[resourceOwner] as World;
2442
2576
  const frame = runProfiledRenderPhase(profileSession, 'extract', () => {
2443
2577
  return persistentRenderScene.extractComposition(
2444
- worlds,
2578
+ compositionWorlds,
2445
2579
  { cameraOwner, resourceOwner },
2446
2580
  internals.assets.catalogEpoch,
2447
- (renderables) =>
2581
+ (request: PersistentRenderCandidateRequest) =>
2448
2582
  extractFrames(
2449
- worlds,
2583
+ compositionWorlds,
2450
2584
  { cameraOwner, resourceOwner },
2451
2585
  internals.assets,
2452
2586
  internals.getPipelineState(),
2453
2587
  persistentRenderScene.materialSnapshotCacheStore(),
2454
2588
  {
2455
2589
  cull: 'none',
2456
- renderables,
2590
+ renderables: request === 'none' ? 'none' : 'full',
2591
+ ...(request === 'none' || request === 'full'
2592
+ ? {}
2593
+ : { renderableEntitiesByWorld: request.entitiesByWorld }),
2594
+ retainHidden: true,
2595
+ instanceCollections,
2457
2596
  ...(internals.getMaterialShaderArtifact === undefined
2458
2597
  ? {}
2459
2598
  : { getMaterialShaderArtifact: internals.getMaterialShaderArtifact }),
2460
2599
  },
2461
2600
  ),
2462
- renderReadLeases,
2601
+ compositionLeases,
2463
2602
  );
2464
2603
  });
2465
2604
  const {
@@ -2491,16 +2630,16 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2491
2630
  skylight,
2492
2631
  );
2493
2632
  latestCamera = cameras[0];
2494
- persistentRenderScene.updateVisibilityFacet(worlds, latestCamera, renderables);
2633
+ persistentRenderScene.updateVisibilityFacet(compositionWorlds, latestCamera, renderables);
2495
2634
  let occlusionProjection:
2496
2635
  | import('./scene/visibility/occlusion-runtime').OcclusionFrameProjection
2497
2636
  | undefined;
2498
2637
  let occlusionFallback: LodOcclusionInspection['fallback'] = { active: false };
2499
- const worldKeys = persistentRenderScene.visibilityWorldKeysFor(worlds);
2638
+ const worldKeys = persistentRenderScene.visibilityWorldKeysFor(compositionWorlds);
2500
2639
  const slotByEntity = persistentRenderScene.compositionSlotByStableEntity();
2501
2640
  if (latestCamera !== undefined) {
2502
2641
  const camera = latestCamera;
2503
- const cameraWorld = worlds[camera.worldId ?? cameraOwner];
2642
+ const cameraWorld = compositionWorlds[camera.worldId ?? cameraOwner];
2504
2643
  const view = viewKey({
2505
2644
  attachmentId: cameraWorld?.identity ?? 'missing-camera-world',
2506
2645
  cameraEntity: camera.entityKey ?? 0,
@@ -2514,7 +2653,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2514
2653
  cachedCandidates.cameraWorldIdentity === (cameraWorld?.identity ?? '')
2515
2654
  ? cachedCandidates.candidates
2516
2655
  : renderables.flatMap((renderable) => {
2517
- const world = worlds[renderable.worldId];
2656
+ const world = compositionWorlds[renderable.worldId];
2518
2657
  const aabb = renderable.localAabb;
2519
2658
  // Occlusion is a LOD transport concern. A plain mesh has no
2520
2659
  // lower-detail range to select, so keep it on the ordinary CPU
@@ -2599,7 +2738,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2599
2738
  latestCamera !== undefined &&
2600
2739
  ensureOcclusionRuntime().prepareUnavailable
2601
2740
  ) {
2602
- const cameraWorld = worlds[latestCamera.worldId ?? cameraOwner];
2741
+ const cameraWorld = compositionWorlds[latestCamera.worldId ?? cameraOwner];
2603
2742
  if (cameraWorld !== undefined) {
2604
2743
  const fallbackView = viewKey({
2605
2744
  attachmentId: cameraWorld.identity,
@@ -2609,7 +2748,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2609
2748
  });
2610
2749
  for (const renderable of renderables) {
2611
2750
  if ((renderable.lods?.length ?? 0) === 0) continue;
2612
- const world = worlds[renderable.worldId];
2751
+ const world = compositionWorlds[renderable.worldId];
2613
2752
  if (world === undefined) continue;
2614
2753
  const slot = slotByEntity.get(
2615
2754
  worldEntityKey(
@@ -2632,7 +2771,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2632
2771
  }
2633
2772
  }
2634
2773
  const visibilityProjection = persistentRenderScene.projectVisibility(
2635
- worlds,
2774
+ compositionWorlds,
2636
2775
  latestCamera,
2637
2776
  renderables,
2638
2777
  dispatch,
@@ -2678,7 +2817,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2678
2817
  const inspectionQuery = ensureOcclusionRuntime().inspect();
2679
2818
  const inspectionBudget = persistentRenderScene.visibilityBudgetValue();
2680
2819
  const inspectionWorlds = createLodWorldInspections(
2681
- worlds,
2820
+ compositionWorlds,
2682
2821
  renderables,
2683
2822
  submissionRenderables,
2684
2823
  inspectionCamera,
@@ -2697,8 +2836,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2697
2836
  : { active: false },
2698
2837
  );
2699
2838
  const attachmentByWorldKey = new Map<number, string>();
2700
- for (let worldId = 0; worldId < worlds.length; worldId += 1) {
2701
- const world = worlds[worldId];
2839
+ for (let worldId = 0; worldId < compositionWorlds.length; worldId += 1) {
2840
+ const world = compositionWorlds[worldId];
2702
2841
  if (world === undefined) continue;
2703
2842
  const worldKey = worldKeys[worldId] ?? worldId;
2704
2843
  if (attachmentByWorldKey.has(worldKey)) {
@@ -2717,7 +2856,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2717
2856
  sourceKey: `render-frame:${frameState.frameNumber}`,
2718
2857
  },
2719
2858
  view: {
2720
- attachmentId: worlds[inspectionCamera.worldId ?? cameraOwner]?.identity ?? 'none',
2859
+ attachmentId:
2860
+ compositionWorlds[inspectionCamera.worldId ?? cameraOwner]?.identity ?? 'none',
2721
2861
  cameraEntity: inspectionCamera.entityKey ?? 0,
2722
2862
  viewRole: 'main',
2723
2863
  viewGeneration: inspectionCamera.historyVersion ?? 0,
@@ -2859,7 +2999,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2859
2999
  const preparedResourceBatches = runProfiledRenderPhase(profileSession, 'features', () => {
2860
3000
  if (internals.featureHost === undefined) return [];
2861
3001
  const featureFrame = runRenderFeatureFrame(internals.featureHost, {
2862
- worlds,
3002
+ worlds: compositionWorlds,
2863
3003
  owner: resourceOwner,
2864
3004
  frameNumber: frameState.frameNumber,
2865
3005
  visibilitySnapshots: frame.featureVisibilitySnapshots,
@@ -3021,7 +3161,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3021
3161
  skybox,
3022
3162
  skyboxCount,
3023
3163
  postProcessParams,
3024
- worlds,
3164
+ compositionWorlds,
3025
3165
  recordProfilePhase,
3026
3166
  {
3027
3167
  owner: gpuDrivenProduction,
@@ -3034,7 +3174,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3034
3174
  telemetryCandidateCount: lodCandidateCountForFrame,
3035
3175
  ...(occlusionProjection === undefined ? {} : { occlusion: occlusionProjection }),
3036
3176
  },
3037
- renderReadLeases,
3177
+ compositionLeases,
3038
3178
  featureGraphCandidate,
3039
3179
  pointsLinesOwner,
3040
3180
  {
@@ -3051,6 +3191,14 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3051
3191
  timingCapture,
3052
3192
  ),
3053
3193
  );
3194
+ if (submitted) {
3195
+ try {
3196
+ internals.recoveryColdWorkGuard?.finish();
3197
+ } catch (cause) {
3198
+ submitted = false;
3199
+ throw cause;
3200
+ }
3201
+ }
3054
3202
  if (submitted && lastLodOcclusionInspection !== undefined) {
3055
3203
  const query = ensureOcclusionRuntime().inspect();
3056
3204
  lastLodOcclusionInspection = {
@@ -3090,6 +3238,16 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3090
3238
  };
3091
3239
  }
3092
3240
  if (submitted) {
3241
+ recoveryOwner.setLastSuccessfulFrameSeed({
3242
+ frame,
3243
+ worlds: compositionWorlds,
3244
+ cameraOwner: composition.owners.cameraOwner,
3245
+ resourceOwner: composition.owners.resourceOwner,
3246
+ width: Math.max(1, internals.canvas.width),
3247
+ height: Math.max(1, internals.canvas.height),
3248
+ });
3249
+ submittedFrameCount += 1;
3250
+ lastSubmittedGeneration = internals.deviceScope?.generation;
3093
3251
  if (occlusionProjection === undefined) {
3094
3252
  runProfiledRenderPhase(profileSession, 'record/occlusion-global-advance', () =>
3095
3253
  ensureOcclusionRuntime().advanceSuccessfulSubmit(frameState.frameNumber),
@@ -3226,6 +3384,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3226
3384
  get meshMaterialBindings(): readonly MeshMaterialBindingObservation[] {
3227
3385
  return lastMeshMaterialBindings;
3228
3386
  },
3387
+ get iblBinding(): import('./mesh-material-bindings').IblBindingInspection | undefined {
3388
+ return frameState.iblBindingInspection;
3389
+ },
3229
3390
  get perFramePassNames(): readonly string[] {
3230
3391
  return frameState.compiledFrameGraph?.inspect().passes.map((pass) => pass.name) ?? [];
3231
3392
  },
@@ -3235,6 +3396,25 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3235
3396
  get perFrameGraphInfo(): CompiledRenderGraphInfo | undefined {
3236
3397
  return frameState.compiledFrameGraph?.inspect();
3237
3398
  },
3399
+ get recoveryEvidence(): RecoveryProductionEvidence {
3400
+ const graph = frameState.compiledFrameGraph?.inspect();
3401
+ return Object.freeze({
3402
+ producerRoots: createRendererProducerRootMatrix(),
3403
+ graph: Object.freeze({
3404
+ ready: graph !== undefined,
3405
+ generation: frameState.graphGeneration,
3406
+ passCount: graph?.passes.length ?? 0,
3407
+ resourceCount: graph?.resources.length ?? 0,
3408
+ }),
3409
+ residency: Object.freeze({
3410
+ meshResidencyEpoch: internals.gpuStore.meshResidencyEpoch,
3411
+ }),
3412
+ submissions: Object.freeze({
3413
+ count: submittedFrameCount,
3414
+ lastGeneration: lastSubmittedGeneration,
3415
+ }),
3416
+ });
3417
+ },
3238
3418
  get volumetricFog(): import('./volume/inspection').VolumetricFogInspection {
3239
3419
  return frameState.volumetricFogInspection;
3240
3420
  },
@@ -3330,6 +3510,11 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3330
3510
  throw cause;
3331
3511
  }
3332
3512
  },
3513
+ lookupPostProcess,
3514
+ prepareRecoveryGraphCandidate: recoveryOwner.prepareRecoveryGraphCandidate,
3515
+ submitCandidateSetup: recoveryOwner.submitCandidateSetup,
3516
+ publishRecoveryGraphCandidate: recoveryOwner.publishRecoveryGraphCandidate,
3517
+ discardRecoveryGraphCandidate: recoveryOwner.discardRecoveryGraphCandidate,
3333
3518
  disposeFrameState(): void {
3334
3519
  (internals as TimingSessionHost).gpuPassTimingSession?.dispose();
3335
3520
  (internals as TimingSessionHost).gpuPassTimingSession = undefined;
@@ -3337,7 +3522,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3337
3522
  (internals as TimingSessionHost).gpuPassTimingSubmittedWork = undefined;
3338
3523
  (internals as TimingSessionHost).gpuPassTimingFrameIdentity = undefined;
3339
3524
  (internals as TimingSessionHost).gpuPassTimingBeginReason = undefined;
3525
+ pointsLinesOwner.dispose();
3340
3526
  persistentRenderScene.dispose();
3527
+ instanceCollections.dispose();
3341
3528
  gpuDrivenProduction.dispose();
3342
3529
  occlusionRuntime?.dispose();
3343
3530
  disposeFeatureGpuWork();
@@ -3407,6 +3594,10 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3407
3594
  // so destroy failures fire structured errors (unified per-frame +
3408
3595
  // dispose error strategy).
3409
3596
  disposeInstanceBuffers(frameState.instanceBuffers, internals.errorRegistry);
3597
+ frameState.instanceResidency?.clear();
3598
+ if (frameState.instanceBufferChunks !== undefined) {
3599
+ disposeInstanceBufferChunks(frameState.instanceBufferChunks, internals.errorRegistry);
3600
+ }
3410
3601
  disposeTransientInstanceBuffers(frameState.transientInstanceBuffers, internals.errorRegistry);
3411
3602
  if (frameState.probeBlendRecordBuffer !== undefined) {
3412
3603
  if (!frameState.probeBlendRecordBuffer.isDestroyed) {
@@ -3436,7 +3627,14 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3436
3627
  frameState.pointShadowAtlas = null;
3437
3628
  }
3438
3629
  },
3439
- resetForRecover(): void {
3630
+ resetForRecover(retiringPipelineState?: PipelineState, replacementDevice?: RhiDevice): void {
3631
+ frameState.lastSuccessfulTemporalView = undefined;
3632
+ frameState.successfulTemporalFrameIndex = 0;
3633
+ frameState.pendingTemporalCommit = { kind: 'none' };
3634
+ // The active expansion buffers belong to the lost generation. Abandon
3635
+ // their handles without invoking destroy on a device that can no longer
3636
+ // service cleanup; the staged candidate owns the replacement buffers.
3637
+ pointsLinesOwner.abandonForDeviceLoss();
3440
3638
  ssrFormatReceipt = undefined;
3441
3639
  ssrFormatProbeGeneration = -1;
3442
3640
  ssrFormatProbe = undefined;
@@ -3457,18 +3655,28 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3457
3655
  updateTransmissionInspection(lastTransmissionAntialias, false);
3458
3656
  persistentRenderScene.resetGpuForRecover();
3459
3657
  gpuDrivenProduction.dispose();
3460
- gpuDrivenProduction = new GpuDrivenProduction(internals.device, gpuDrivenShaderFactory);
3658
+ // The active renderer still points at the lost device until the caller
3659
+ // completes the synchronous publication boundary. Bind the replacement
3660
+ // explicitly so the first published draw never routes through an owner
3661
+ // recreated against the old physical device.
3662
+ gpuDrivenProduction = GpuDrivenProduction.forDevice(
3663
+ replacementDevice ?? internals.device,
3664
+ gpuDrivenShaderFactory,
3665
+ );
3461
3666
  occlusionRuntime?.dispose();
3462
3667
  // The replacement device is installed by the host after this hook. Do
3463
3668
  // not recreate device-bound query resources against the lost device;
3464
3669
  // the first post-recovery frame lazily binds the current device.
3465
3670
  occlusionRuntime = undefined;
3466
3671
  disposeFeatureGpuWork();
3672
+ featureGpuWork = createFeatureGpuWorkOwner(internals);
3467
3673
  reflectionProbeOwner.dispose();
3468
3674
  // A recovered device cannot retain handles from the lost device. The
3469
3675
  // Bloom owner abandons candidates and retires every committed generation
3470
3676
  // before the new PipelineState is rebuilt.
3471
- internals.getPipelineState()?.perPassResources.drainBloomResources?.();
3677
+ (
3678
+ retiringPipelineState ?? internals.getPipelineState()
3679
+ )?.perPassResources.drainBloomResources?.();
3472
3680
  // feat-20260622-s5 M3 / B-2 / w18: recover() rebuild drops device-bound
3473
3681
  // state minted by the lost device. The active graph and per-entity caches
3474
3682
  // must be discarded, not merely marked for destruction: their opaque
@@ -3531,6 +3739,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3531
3739
  }
3532
3740
  frameState.retiredCompiledFrameGraphs.clear();
3533
3741
  frameState.instanceBuffers.clear();
3742
+ frameState.instanceBufferChunks?.clear();
3743
+ frameState.instanceResidency?.clear();
3534
3744
  delete frameState.probeBlendRecordBuffer;
3535
3745
  frameState.probeBlendRecordBufferCapacity = 0;
3536
3746
  frameState.probeBlendBuffers.clear();
@@ -3554,113 +3764,147 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3554
3764
  // the lost device, so recovery must invalidate this cache alongside the
3555
3765
  // feature-host declarations and UBOs below.
3556
3766
  postProcessPipelineCache.clear();
3557
- // Logical declarations stay live in the feature host while a replacement
3558
- // device is being requested.
3559
- postProcessParamsBuffers.clear();
3767
+ // Logical declarations and the active params bundle stay live until the
3768
+ // candidate post-process bundle crosses the publication boundary.
3560
3769
  resetRenderFeatureGraphState(internals);
3561
3770
  },
3771
+ prepareRecoveryPostProcessResources(device: RhiDevice) {
3772
+ return prepareRecoveryPostProcessResources(device);
3773
+ },
3774
+ publishRecoveryPostProcessResources(candidate: RecoveryPostProcessResources): void {
3775
+ publishRecoveryPostProcessResources(candidate);
3776
+ },
3777
+ discardRecoveryPostProcessResources(candidate: RecoveryPostProcessResources): void {
3778
+ discardRecoveryPostProcessResources(candidate);
3779
+ },
3562
3780
  restorePostProcessResources(): void {
3563
- const declarations = [
3564
- ...builtinPostProcessEntries.entries(),
3565
- ...activeFeaturePostProcessEntries.entries(),
3566
- ];
3567
- for (const [id, entry] of declarations) {
3568
- if (id === 'forgeax.taa-resolve') continue;
3569
- let buffer: Buffer | undefined;
3570
- if (entry.params !== undefined) {
3571
- const created = internals.device.createBuffer({
3572
- label: `post-process-params-${id}`,
3573
- size: entry.params.byteSize,
3574
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
3575
- mappedAtCreation: false,
3576
- });
3577
- if (!created.ok) throw created.error;
3578
- buffer = created.value;
3579
- const written = internals.device.queue.writeBuffer(buffer, 0, entry.params.defaultValue);
3580
- if (!written.ok) {
3581
- internals.device.destroyBuffer(buffer);
3582
- throw written.error;
3583
- }
3584
- } else if (entryHasDepthRead(entry)) {
3585
- const created = internals.device.createBuffer({
3586
- label: `post-process-params-${id}`,
3587
- size: DEPTH_MIN_PARAMS_BYTE_SIZE,
3588
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
3589
- mappedAtCreation: false,
3781
+ const prepared = prepareRecoveryPostProcessResources(internals.device);
3782
+ if (!prepared.ok) throw prepared.error;
3783
+ publishRecoveryPostProcessResources(prepared.value);
3784
+ },
3785
+ prepareRecoveryRoots(runtime: RecoveryRootRuntime): RecoveryRootBundle {
3786
+ // Snapshot only CPU projections/declarations here. The root create
3787
+ // callbacks below must never close over a mutable active owner while
3788
+ // LifecycleTransaction is awaiting candidate construction.
3789
+ const candidateEnvironmentLifecycle = environmentLifecycle.createRecoveryCandidate(
3790
+ runtime.scope,
3791
+ );
3792
+ const roots: LifecycleResourceSpec<unknown>[] = [];
3793
+ let candidateEnvironmentGeneration: EnvironmentGeneration | undefined;
3794
+ let environmentRootCleaned = false;
3795
+ let published = false;
3796
+ if (candidateEnvironmentLifecycle.hasRecoveryFrame()) {
3797
+ const environmentRoot = candidateEnvironmentLifecycle.createRecoveryRoot(runtime.scope);
3798
+ roots.push({
3799
+ ...environmentRoot,
3800
+ create: async () => {
3801
+ const generation = (await environmentRoot.create()) as EnvironmentGeneration;
3802
+ candidateEnvironmentGeneration = generation;
3803
+ return generation;
3804
+ },
3805
+ cleanup: (value) => {
3806
+ if (environmentRootCleaned) return;
3807
+ environmentRootCleaned = true;
3808
+ if (candidateEnvironmentGeneration === value) {
3809
+ candidateEnvironmentGeneration = undefined;
3810
+ }
3811
+ if (published) {
3812
+ candidateEnvironmentLifecycle.retirePublishedGeneration(
3813
+ value as EnvironmentGeneration,
3814
+ );
3815
+ } else {
3816
+ environmentRoot.cleanup(value);
3817
+ }
3818
+ },
3819
+ });
3820
+ }
3821
+ const graphCandidate = runtime.graphCandidate;
3822
+ if (graphCandidate !== undefined) {
3823
+ const releaseGraphCandidate = (): void => graphCandidate.release();
3824
+ const compiledGraph = graphCandidate.frameState.compiledFrameGraph;
3825
+ if (compiledGraph !== null) {
3826
+ roots.push({
3827
+ kind: 'pipeline',
3828
+ create: () => {
3829
+ if (!runtime.scope.isAlive()) {
3830
+ throw new Error('Recovery graph candidate scope is not active.');
3831
+ }
3832
+ if (graphCandidate.frameState.compiledFrameGraph !== compiledGraph) {
3833
+ throw new Error('Recovery graph candidate was changed before aggregate assembly.');
3834
+ }
3835
+ return compiledGraph;
3836
+ },
3837
+ // All graph/feature/GPU-driven candidate resources share one
3838
+ // owner. The root is a real release seam, not a marker; the
3839
+ // candidate latch makes every root cleanup call harmless after
3840
+ // the first one.
3841
+ cleanup: releaseGraphCandidate,
3590
3842
  });
3591
- if (!created.ok) throw created.error;
3592
- buffer = created.value;
3593
3843
  }
3594
- if (buffer !== undefined && lookupPostProcess(id) === entry) {
3595
- postProcessParamsBuffers.set(id, buffer);
3596
- } else if (buffer !== undefined) {
3597
- internals.device.destroyBuffer(buffer);
3844
+ if (graphCandidate.featureHost !== undefined) {
3845
+ const featureRoot = graphCandidate.featureHost.createRecoveryRoot(runtime.scope);
3846
+ roots.push({ ...featureRoot, cleanup: releaseGraphCandidate });
3847
+ }
3848
+ if (graphCandidate.featureGpuWork !== undefined) {
3849
+ const featureWorkRoot = graphCandidate.featureGpuWork.createRecoveryRoot(runtime.scope);
3850
+ roots.push({ ...featureWorkRoot, cleanup: releaseGraphCandidate });
3851
+ }
3852
+ if (graphCandidate.gpuDrivenProduction !== undefined) {
3853
+ const productionRoot = graphCandidate.gpuDrivenProduction.createRecoveryRoot(
3854
+ runtime.scope,
3855
+ );
3856
+ roots.push({ ...productionRoot, cleanup: releaseGraphCandidate });
3857
+ }
3858
+ if (graphCandidate.gpuDrivenScene?.state !== undefined) {
3859
+ const sceneRoot = graphCandidate.gpuDrivenScene.createRecoveryRoot(runtime.scope);
3860
+ roots.push({ ...sceneRoot, cleanup: releaseGraphCandidate });
3861
+ }
3862
+ if (graphCandidate.pointsLines !== undefined) {
3863
+ const pointsLinesRoot = graphCandidate.pointsLines.createRecoveryRoot(runtime.scope);
3864
+ roots.push({ ...pointsLinesRoot, cleanup: releaseGraphCandidate });
3865
+ }
3866
+ if (runtime.gpuStore.recoveryResourceCount() > 0) {
3867
+ const gpuStoreRoot = runtime.gpuStore.createRecoveryRoot(runtime.scope);
3868
+ roots.push({ ...gpuStoreRoot, cleanup: releaseGraphCandidate });
3598
3869
  }
3599
3870
  }
3871
+ let discarded = false;
3872
+ return {
3873
+ roots: Object.freeze(roots),
3874
+ publish: () => {
3875
+ if (published || discarded) return;
3876
+ if (
3877
+ candidateEnvironmentGeneration !== undefined &&
3878
+ !candidateEnvironmentGeneration.retired
3879
+ ) {
3880
+ environmentLifecycle.retireActiveForReplacement();
3881
+ candidateEnvironmentLifecycle.publish(candidateEnvironmentGeneration);
3882
+ }
3883
+ published = true;
3884
+ environmentLifecycle = candidateEnvironmentLifecycle;
3885
+ Object.assign(frameState, { environmentLifecycle });
3886
+ },
3887
+ discard: () => {
3888
+ if (published || discarded) return;
3889
+ discarded = true;
3890
+ if (candidateEnvironmentGeneration !== undefined) {
3891
+ candidateEnvironmentLifecycle.discard(candidateEnvironmentGeneration);
3892
+ candidateEnvironmentGeneration = undefined;
3893
+ }
3894
+ candidateEnvironmentLifecycle.discardRecoveryCandidates();
3895
+ // The transaction can fail before it reaches any graph root (for
3896
+ // example while the environment root is created). Keep the graph,
3897
+ // feature, GPU-driven, and residency resources on the same real
3898
+ // candidate owner in that case; later scope/factory cleanup is a
3899
+ // harmless repeat through the candidate latch.
3900
+ graphCandidate?.release();
3901
+ runtime.scope.abandon();
3902
+ },
3903
+ };
3600
3904
  },
3601
3905
  };
3602
3906
  }
3603
3907
 
3604
- function buildOcclusionProxyVertices(
3605
- aabb: ArrayLike<number>,
3606
- world: ArrayLike<number>,
3607
- camera: import('./render-contract').CameraSnapshot,
3608
- ): Float32Array {
3609
- const view = computeViewMatrix(camera);
3610
- const projection = computeProjectionMatrix(camera);
3611
- const viewProjection = mat4.create();
3612
- mat4.multiply(viewProjection, projection, view);
3613
- const corners = [
3614
- [aabb[0] ?? 0, aabb[1] ?? 0, aabb[2] ?? 0],
3615
- [aabb[3] ?? 0, aabb[1] ?? 0, aabb[2] ?? 0],
3616
- [aabb[3] ?? 0, aabb[4] ?? 0, aabb[2] ?? 0],
3617
- [aabb[0] ?? 0, aabb[4] ?? 0, aabb[2] ?? 0],
3618
- [aabb[0] ?? 0, aabb[1] ?? 0, aabb[5] ?? 0],
3619
- [aabb[3] ?? 0, aabb[1] ?? 0, aabb[5] ?? 0],
3620
- [aabb[3] ?? 0, aabb[4] ?? 0, aabb[5] ?? 0],
3621
- [aabb[0] ?? 0, aabb[4] ?? 0, aabb[5] ?? 0],
3622
- ];
3623
- const triangles = [
3624
- 0, 1, 2, 0, 2, 3, 4, 6, 5, 4, 7, 6, 0, 4, 5, 0, 5, 1, 3, 2, 6, 3, 6, 7, 0, 3, 7, 0, 7, 4, 1, 5,
3625
- 6, 1, 6, 2,
3626
- ];
3627
- const output = new Float32Array(triangles.length * 4);
3628
- for (let index = 0; index < triangles.length; index += 1) {
3629
- const corner = corners[triangles[index] ?? 0] ?? [0, 0, 0];
3630
- const cx = corner[0] ?? 0;
3631
- const cy = corner[1] ?? 0;
3632
- const cz = corner[2] ?? 0;
3633
- const x = (world[0] ?? 0) * cx + (world[4] ?? 0) * cy + (world[8] ?? 0) * cz + (world[12] ?? 0);
3634
- const y = (world[1] ?? 0) * cx + (world[5] ?? 0) * cy + (world[9] ?? 0) * cz + (world[13] ?? 0);
3635
- const z =
3636
- (world[2] ?? 0) * cx + (world[6] ?? 0) * cy + (world[10] ?? 0) * cz + (world[14] ?? 0);
3637
- const w =
3638
- (world[3] ?? 0) * cx + (world[7] ?? 0) * cy + (world[11] ?? 0) * cz + (world[15] ?? 1);
3639
- const base = index * 4;
3640
- output[base] =
3641
- (viewProjection[0] ?? 0) * x +
3642
- (viewProjection[4] ?? 0) * y +
3643
- (viewProjection[8] ?? 0) * z +
3644
- (viewProjection[12] ?? 0) * w;
3645
- output[base + 1] =
3646
- (viewProjection[1] ?? 0) * x +
3647
- (viewProjection[5] ?? 0) * y +
3648
- (viewProjection[9] ?? 0) * z +
3649
- (viewProjection[13] ?? 0) * w;
3650
- output[base + 2] =
3651
- (viewProjection[2] ?? 0) * x +
3652
- (viewProjection[6] ?? 0) * y +
3653
- (viewProjection[10] ?? 0) * z +
3654
- (viewProjection[14] ?? 0) * w;
3655
- output[base + 3] =
3656
- (viewProjection[3] ?? 0) * x +
3657
- (viewProjection[7] ?? 0) * y +
3658
- (viewProjection[11] ?? 0) * z +
3659
- (viewProjection[15] ?? 0) * w;
3660
- }
3661
- return output;
3662
- }
3663
-
3664
3908
  /** Resolve the build identity used by every inspection submit. */
3665
3909
  export function resolveInspectionBuild(internals: Pick<RenderSystemInternals, 'build'>): string {
3666
3910
  return internals.build ?? 'render-system';