@forgeax/engine-render 0.1.23 → 0.1.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (271) hide show
  1. package/README.md +131 -9
  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-shader-policy.d.ts +168 -0
  15. package/dist/assembly/material-shader-policy.d.ts.map +1 -0
  16. package/dist/assembly/material-shader-prewarm.d.ts +21 -0
  17. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -0
  18. package/dist/assembly/mesh-ssbo-grow.d.ts +88 -0
  19. package/dist/assembly/mesh-ssbo-grow.d.ts.map +1 -0
  20. package/dist/assembly/recovery/device-loss-fanout.d.ts +14 -0
  21. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -0
  22. package/dist/assembly/recovery/generation.d.ts +46 -0
  23. package/dist/assembly/recovery/generation.d.ts.map +1 -0
  24. package/dist/assembly/recovery/recovery-attempt.d.ts +63 -0
  25. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -0
  26. package/dist/assembly/recovery/renderer-recover.d.ts +115 -0
  27. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -0
  28. package/dist/assembly/renderer-facade.d.ts +10 -0
  29. package/dist/assembly/renderer-facade.d.ts.map +1 -0
  30. package/dist/assembly/renderer-frame-transaction.d.ts +17 -0
  31. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  32. package/dist/assembly/renderer-helpers.d.ts +19 -0
  33. package/dist/assembly/renderer-helpers.d.ts.map +1 -0
  34. package/dist/assembly/renderer-lifecycle.d.ts +34 -0
  35. package/dist/assembly/renderer-lifecycle.d.ts.map +1 -1
  36. package/dist/assembly/shader-prewarm-policy.d.ts +15 -0
  37. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -0
  38. package/dist/assembly/webgpu-ready-contract.d.ts +9 -0
  39. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -0
  40. package/dist/assembly/webgpu-ready.d.ts +55 -0
  41. package/dist/assembly/webgpu-ready.d.ts.map +1 -0
  42. package/dist/assembly/webgpu-renderer.d.ts +152 -0
  43. package/dist/assembly/webgpu-renderer.d.ts.map +1 -0
  44. package/dist/authoring.mjs +1 -1
  45. package/dist/{chunk-4L3P6DT5.mjs → chunk-GE3SDD72.mjs} +37 -5
  46. package/dist/chunk-GE3SDD72.mjs.map +1 -0
  47. package/dist/{chunk-HZUPSYW7.mjs → chunk-KZFTK6DO.mjs} +102 -15
  48. package/dist/chunk-KZFTK6DO.mjs.map +1 -0
  49. package/dist/{chunk-FF7K3FSO.mjs → chunk-Q752SHI5.mjs} +89 -89
  50. package/dist/{chunk-FF7K3FSO.mjs.map → chunk-Q752SHI5.mjs.map} +1 -1
  51. package/dist/{chunk-QODA5UKU.mjs → chunk-RR5L4SRS.mjs} +2 -2
  52. package/dist/chunk-RR5L4SRS.mjs.map +1 -0
  53. package/dist/{chunk-YALPS44A.mjs → chunk-TFH5I7JJ.mjs} +47 -47
  54. package/dist/chunk-TFH5I7JJ.mjs.map +1 -0
  55. package/dist/{chunk-FBC4R44J.mjs → chunk-UQYS7FRM.mjs} +57 -57
  56. package/dist/chunk-UQYS7FRM.mjs.map +1 -0
  57. package/dist/{chunk-4NPUASZR.mjs → chunk-YKB5DQV7.mjs} +161 -19
  58. package/dist/chunk-YKB5DQV7.mjs.map +1 -0
  59. package/dist/{chunk-ZUWKAZ2I.mjs → chunk-YRUYGA4A.mjs} +615 -237
  60. package/dist/chunk-YRUYGA4A.mjs.map +1 -0
  61. package/dist/{chunk-DIYW3M5D.mjs → chunk-Z3YYH2MB.mjs} +2210 -2213
  62. package/dist/chunk-Z3YYH2MB.mjs.map +1 -0
  63. package/dist/construct-renderer.mjs +19841 -16190
  64. package/dist/construct-renderer.mjs.map +1 -1
  65. package/dist/device/device-scope.d.ts +2 -0
  66. package/dist/device/device-scope.d.ts.map +1 -1
  67. package/dist/device/gpu-residency.d.ts +24 -4
  68. package/dist/device/gpu-residency.d.ts.map +1 -1
  69. package/dist/environment/frame.d.ts +5 -0
  70. package/dist/environment/frame.d.ts.map +1 -1
  71. package/dist/environment/inspection.d.ts +7 -0
  72. package/dist/environment/inspection.d.ts.map +1 -1
  73. package/dist/environment/lifecycle.d.ts +20 -2
  74. package/dist/environment/lifecycle.d.ts.map +1 -1
  75. package/dist/errors/recover.d.ts +58 -2
  76. package/dist/errors/recover.d.ts.map +1 -1
  77. package/dist/errors/render.d.ts +13 -0
  78. package/dist/errors/render.d.ts.map +1 -1
  79. package/dist/extract/contracts.d.ts +899 -0
  80. package/dist/extract/contracts.d.ts.map +1 -0
  81. package/dist/features/host.d.ts +2 -0
  82. package/dist/features/host.d.ts.map +1 -1
  83. package/dist/features/prepared-gpu-work.d.ts +3 -0
  84. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  85. package/dist/features/prepared-graphics-store.d.ts +2 -0
  86. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  87. package/dist/gpu-driven/production-raster.d.ts +28 -0
  88. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  89. package/dist/gpu-scene.d.ts +1 -0
  90. package/dist/gpu-scene.d.ts.map +1 -1
  91. package/dist/ibl/IblPipelineCache.d.ts +7 -6
  92. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  93. package/dist/ibl/skylight-bind-group.d.ts +7 -1
  94. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  95. package/dist/index.d.ts +1 -1
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.mjs +6 -6
  98. package/dist/internal.mjs +6 -6
  99. package/dist/light-buffer-layout.d.ts +3 -0
  100. package/dist/light-buffer-layout.d.ts.map +1 -1
  101. package/dist/materials.d.ts +34 -10
  102. package/dist/materials.d.ts.map +1 -1
  103. package/dist/mesh-material-bindings.d.ts +56 -0
  104. package/dist/mesh-material-bindings.d.ts.map +1 -1
  105. package/dist/occlusion-proxy.d.ts +4 -0
  106. package/dist/occlusion-proxy.d.ts.map +1 -0
  107. package/dist/pbr-pipeline.d.ts +32 -9
  108. package/dist/pbr-pipeline.d.ts.map +1 -1
  109. package/dist/points-lines/prepare.d.ts +3 -0
  110. package/dist/points-lines/prepare.d.ts.map +1 -1
  111. package/dist/points-lines/standard-owner.d.ts +33 -0
  112. package/dist/points-lines/standard-owner.d.ts.map +1 -0
  113. package/dist/record/frame-snapshot.d.ts +5 -0
  114. package/dist/record/frame-snapshot.d.ts.map +1 -1
  115. package/dist/record/frame-targets.d.ts.map +1 -1
  116. package/dist/record/frame.d.ts +14 -2
  117. package/dist/record/frame.d.ts.map +1 -1
  118. package/dist/record/main-pass-geometry.d.ts +14 -1
  119. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  120. package/dist/record/main-pass-material.d.ts +7 -17
  121. package/dist/record/main-pass-material.d.ts.map +1 -1
  122. package/dist/record/main-pass.d.ts.map +1 -1
  123. package/dist/record/mesh-ssbo.d.ts +11 -3
  124. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  125. package/dist/record/recovery-pipeline.d.ts +30 -0
  126. package/dist/record/recovery-pipeline.d.ts.map +1 -0
  127. package/dist/record/render-context.d.ts +26 -4
  128. package/dist/record/render-context.d.ts.map +1 -1
  129. package/dist/record/typed-frame-graph.d.ts +1 -1
  130. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  131. package/dist/recovery/render-system-candidate.d.ts +65 -0
  132. package/dist/recovery/render-system-candidate.d.ts.map +1 -0
  133. package/dist/render-contract.d.ts +64 -7
  134. package/dist/render-contract.d.ts.map +1 -1
  135. package/dist/render-system-extract-tail.d.ts +12 -0
  136. package/dist/render-system-extract-tail.d.ts.map +1 -0
  137. package/dist/render-system-extract.d.ts +156 -22
  138. package/dist/render-system-extract.d.ts.map +1 -1
  139. package/dist/render-system.d.ts +128 -4
  140. package/dist/render-system.d.ts.map +1 -1
  141. package/dist/scene/render-scene.d.ts +19 -0
  142. package/dist/scene/render-scene.d.ts.map +1 -1
  143. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  144. package/dist/temporal/index.mjs +6 -6
  145. package/dist/temporal/taa-history-store.d.ts +15 -1
  146. package/dist/temporal/taa-history-store.d.ts.map +1 -1
  147. package/dist/temporal/transaction.d.ts +5 -0
  148. package/dist/temporal/transaction.d.ts.map +1 -1
  149. package/package.json +20 -20
  150. package/src/__tests__/device-feature-admission.unit.test.ts +6 -0
  151. package/src/__tests__/device-loss-fanout.unit.test.ts +107 -0
  152. package/src/__tests__/device-scope-lifecycle.unit.test.ts +21 -0
  153. package/src/__tests__/environment-generation.integration.test.ts +23 -0
  154. package/src/__tests__/expose-renderer-errors.unit.test.ts +34 -0
  155. package/src/__tests__/factory-contract.integration.test.ts +122 -7
  156. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +4 -1
  157. package/src/__tests__/ibl/face-uniforms.unit.test.ts +2 -0
  158. package/src/__tests__/ibl-residency-publication.unit.test.ts +113 -0
  159. package/src/__tests__/material-contract-inventory.unit.test.ts +24 -9
  160. package/src/__tests__/material-cooked-projection.unit.test.ts +19 -1
  161. package/src/__tests__/material-static-dynamic-route.unit.test.ts +29 -0
  162. package/src/__tests__/material-stride-surface.unit.test.ts +7 -3
  163. package/src/__tests__/materials-standard-contract.unit.test.ts +104 -0
  164. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
  165. package/src/__tests__/physical-clearcoat.integration.test.ts +89 -0
  166. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +1 -1
  167. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +1 -1
  168. package/src/__tests__/recover-error-policy-owner.unit.test.ts +6 -3
  169. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +140 -0
  170. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +85 -18
  171. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +47 -0
  172. package/src/__tests__/renderer-generation-assembly.unit.test.ts +77 -0
  173. package/src/__tests__/renderer-generation-failure.unit.test.ts +108 -0
  174. package/src/__tests__/renderer-producer-root-matrix.integration.test.ts +33 -0
  175. package/src/__tests__/renderer-recovery-capability-matrix.unit.test.ts +59 -0
  176. package/src/__tests__/renderer-recovery-contract.unit.test.ts +73 -0
  177. package/src/__tests__/renderer-recovery-lifecycle.unit.test.ts +216 -0
  178. package/src/__tests__/renderer-target-history-recovery.integration.test.ts +80 -0
  179. package/src/__tests__/shader-manifest-fixture.ts +13 -16
  180. package/src/__tests__/shader-stage-surface.unit.test.ts +1 -1
  181. package/src/__tests__/standard-layer-projection.unit.test.ts +23 -0
  182. package/src/__tests__/transmission-prewarm.unit.test.ts +95 -85
  183. package/src/__tests__/volumetric-fog-topology.unit.test.ts +3 -1
  184. package/src/assembly/bundler-contract.ts +9 -0
  185. package/src/assembly/device-feature-admission.ts +15 -5
  186. package/src/assembly/extended-lighting-fallback.ts +216 -0
  187. package/src/assembly/factory.ts +81 -10088
  188. package/src/assembly/material/assembly.ts +18 -2
  189. package/src/assembly/material/standard-layer-projection.ts +16 -0
  190. package/src/assembly/material-shader-policy.ts +854 -0
  191. package/src/assembly/material-shader-prewarm.ts +88 -0
  192. package/src/assembly/mesh-ssbo-grow.ts +270 -0
  193. package/src/assembly/recovery/device-loss-fanout.ts +89 -0
  194. package/src/assembly/recovery/generation.ts +141 -0
  195. package/src/assembly/recovery/recovery-attempt.ts +800 -0
  196. package/src/assembly/recovery/renderer-recover.ts +906 -0
  197. package/src/assembly/renderer-facade.ts +342 -0
  198. package/src/assembly/renderer-frame-transaction.ts +45 -0
  199. package/src/assembly/renderer-helpers.ts +99 -0
  200. package/src/assembly/renderer-lifecycle.ts +159 -0
  201. package/src/assembly/shader-prewarm-policy.ts +157 -0
  202. package/src/assembly/webgpu-ready-contract.ts +14 -0
  203. package/src/assembly/webgpu-ready.ts +4079 -0
  204. package/src/assembly/webgpu-renderer.ts +4095 -0
  205. package/src/components/camera.ts +1 -1
  206. package/src/debug-draw-glue.ts +1 -1
  207. package/src/device/device-scope.ts +35 -6
  208. package/src/device/gpu-residency.ts +194 -26
  209. package/src/environment/frame.ts +6 -0
  210. package/src/environment/inspection.ts +10 -0
  211. package/src/environment/lifecycle.ts +114 -8
  212. package/src/errors/recover.ts +76 -5
  213. package/src/errors/render.ts +17 -0
  214. package/src/extract/contracts.ts +974 -0
  215. package/src/features/host.ts +16 -0
  216. package/src/features/prepared-gpu-work.ts +20 -2
  217. package/src/features/prepared-graphics-store.ts +17 -0
  218. package/src/gpu-driven/production-raster.ts +97 -2
  219. package/src/gpu-scene.ts +3 -2
  220. package/src/ibl/IblPipelineCache.ts +134 -50
  221. package/src/ibl/skylight-bind-group.ts +26 -0
  222. package/src/index.ts +9 -1
  223. package/src/light-buffer-layout.ts +3 -0
  224. package/src/materials.ts +262 -45
  225. package/src/mesh-material-bindings.ts +59 -0
  226. package/src/occlusion-proxy.ts +64 -0
  227. package/src/pbr-pipeline.ts +139 -15
  228. package/src/points-lines/__tests__/cache-recovery.unit.test.ts +22 -0
  229. package/src/points-lines/prepare.ts +9 -0
  230. package/src/points-lines/standard-owner.ts +403 -0
  231. package/src/record/__tests__/frame-targets-recovery.unit.test.ts +60 -0
  232. package/src/record/__tests__/main-pass-geometry.unit.test.ts +26 -1
  233. package/src/record/__tests__/mesh-ssbo.unit.test.ts +38 -0
  234. package/src/record/__tests__/recovery-pipeline.unit.test.ts +124 -0
  235. package/src/record/__tests__/sprite-material-abi.unit.test.ts +28 -13
  236. package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +47 -4
  237. package/src/record/__tests__/temporal-payload.unit.test.ts +22 -43
  238. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -1
  239. package/src/record/frame-snapshot.ts +15 -0
  240. package/src/record/frame-targets.ts +11 -1
  241. package/src/record/frame.ts +6 -2
  242. package/src/record/helpers.ts +1 -1
  243. package/src/record/main-pass-geometry.ts +230 -5
  244. package/src/record/main-pass-material.ts +316 -209
  245. package/src/record/main-pass-sprite-draws.ts +6 -7
  246. package/src/record/main-pass.ts +94 -6
  247. package/src/record/mesh-ssbo.ts +32 -10
  248. package/src/record/recovery-pipeline.ts +695 -0
  249. package/src/record/render-context.ts +35 -4
  250. package/src/record/typed-frame-graph.ts +45 -1
  251. package/src/record/view-ubo.ts +3 -3
  252. package/src/recovery/render-system-candidate.ts +839 -0
  253. package/src/render-contract.ts +65 -7
  254. package/src/render-system-extract-tail.ts +2983 -0
  255. package/src/render-system-extract.ts +124 -2977
  256. package/src/render-system.ts +868 -659
  257. package/src/scene/render-scene.ts +124 -1
  258. package/src/temporal/frame-coordinator.ts +4 -1
  259. package/src/temporal/taa-history-store.ts +46 -2
  260. package/src/temporal/temporal-view.ts +1 -1
  261. package/src/temporal/transaction.ts +9 -0
  262. package/src/tilemap-chunk-extract-system.ts +1 -1
  263. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +19 -12
  264. package/dist/chunk-4L3P6DT5.mjs.map +0 -1
  265. package/dist/chunk-4NPUASZR.mjs.map +0 -1
  266. package/dist/chunk-DIYW3M5D.mjs.map +0 -1
  267. package/dist/chunk-FBC4R44J.mjs.map +0 -1
  268. package/dist/chunk-HZUPSYW7.mjs.map +0 -1
  269. package/dist/chunk-QODA5UKU.mjs.map +0 -1
  270. package/dist/chunk-YALPS44A.mjs.map +0 -1
  271. 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,15 +112,10 @@ 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
121
  import { disposeInstanceBuffers, disposeTransientInstanceBuffers } from './instance-buffer-cache';
@@ -158,9 +157,9 @@ import type {
158
157
  GpuPassTimingSession,
159
158
  } from './record/gpu-pass-timing/session';
160
159
  import type { GpuTimingCapture } from './record/gpu-timing';
161
- import { computeProjectionMatrix, computeViewMatrix } from './record/helpers';
162
160
  import { applyParamSnapshotToUbo, residentTextureView } from './record/main-pass-material';
163
- import type { RenderSystemInternals } from './record/render-context';
161
+ import type { RecoveryPipelineReadiness } from './record/recovery-pipeline';
162
+ import type { PipelineState, RenderSystemInternals } from './record/render-context';
164
163
  import type { CubeCaptureGraphState } from './record/typed-frame-graph';
165
164
  import {
166
165
  type RenderFeatureGraphCandidate,
@@ -183,7 +182,7 @@ import type {
183
182
  RenderableSnapshot,
184
183
  } from './render-system-extract';
185
184
  import { extractFrames } from './render-system-extract';
186
- import { PersistentRenderScene } from './scene/render-scene';
185
+ import { type PersistentGpuDrivenCandidate, PersistentRenderScene } from './scene/render-scene';
187
186
  import { inspectLodOcclusion } from './scene/visibility/inspection';
188
187
  import { resolveSsaoParameters } from './ssao-config';
189
188
  import { projectSsrDependencies, resolveSsrAdmissionGeneration } from './ssr/admission';
@@ -210,6 +209,34 @@ import {
210
209
  inspectTransmissionFromAdmission,
211
210
  } from './transmission/inspection';
212
211
 
212
+ function canonicalizeWorldComposition(
213
+ worlds: readonly World[],
214
+ owners: Pick<DrawOwnerOptions, 'cameraOwner' | 'resourceOwner'>,
215
+ leases: readonly RenderReadLease[] | undefined,
216
+ ): {
217
+ readonly worlds: readonly World[];
218
+ readonly owners: Pick<DrawOwnerOptions, 'cameraOwner' | 'resourceOwner'>;
219
+ readonly leases: readonly RenderReadLease[] | undefined;
220
+ } {
221
+ const order = worlds
222
+ .map((world, index) => ({ world, index }))
223
+ .sort((left, right) => left.world.identity.localeCompare(right.world.identity));
224
+ const indexByOriginal = new Map(order.map((entry, index) => [entry.index, index]));
225
+ return {
226
+ worlds: Object.freeze(order.map((entry) => entry.world)),
227
+ owners: {
228
+ cameraOwner: indexByOriginal.get(owners.cameraOwner) ?? owners.cameraOwner,
229
+ resourceOwner: indexByOriginal.get(owners.resourceOwner) ?? owners.resourceOwner,
230
+ },
231
+ leases:
232
+ leases === undefined
233
+ ? undefined
234
+ : Object.freeze(
235
+ order.map((entry) => leases[entry.index]).filter((lease) => lease !== undefined),
236
+ ),
237
+ };
238
+ }
239
+
213
240
  interface LodInspectionQuerySummary {
214
241
  readonly used: number;
215
242
  readonly capacity: number;
@@ -361,6 +388,128 @@ export {
361
388
  selectSwapChainFormat,
362
389
  } from './record/render-context';
363
390
 
391
+ export type RendererProducerRootKind =
392
+ | 'backend-surface'
393
+ | 'shader-material-pipeline'
394
+ | 'mesh-texture-sampler'
395
+ | 'render-scene'
396
+ | 'gpu-driven'
397
+ | 'feature'
398
+ | 'external-source'
399
+ | 'render-graph'
400
+ | 'target-history'
401
+ | 'observation-lease';
402
+
403
+ export interface RendererProducerRoot {
404
+ readonly kind: RendererProducerRootKind;
405
+ readonly owner: string;
406
+ readonly candidateScope: 'device-scope';
407
+ readonly visibility: 'visible-workset' | 'non-visible-lazy';
408
+ readonly disabledWork: 'zero' | 'lazy';
409
+ }
410
+
411
+ /** Device-bound post-process resources prepared off to the side of the active renderer. */
412
+ export interface RecoveryPostProcessResources {
413
+ readonly device: RhiDevice;
414
+ readonly paramsBuffers: Map<string, Buffer>;
415
+ }
416
+
417
+ const PRODUCER_ROOT_MATRIX: readonly RendererProducerRoot[] = Object.freeze([
418
+ {
419
+ kind: 'backend-surface',
420
+ owner: 'backend-surface-owner',
421
+ candidateScope: 'device-scope',
422
+ visibility: 'visible-workset',
423
+ disabledWork: 'zero',
424
+ },
425
+ {
426
+ kind: 'shader-material-pipeline',
427
+ owner: 'shader-material-pipeline-owner',
428
+ candidateScope: 'device-scope',
429
+ visibility: 'visible-workset',
430
+ disabledWork: 'zero',
431
+ },
432
+ {
433
+ kind: 'mesh-texture-sampler',
434
+ owner: 'gpu-residency',
435
+ candidateScope: 'device-scope',
436
+ visibility: 'visible-workset',
437
+ disabledWork: 'lazy',
438
+ },
439
+ {
440
+ kind: 'render-scene',
441
+ owner: 'persistent-render-scene',
442
+ candidateScope: 'device-scope',
443
+ visibility: 'visible-workset',
444
+ disabledWork: 'zero',
445
+ },
446
+ {
447
+ kind: 'gpu-driven',
448
+ owner: 'gpu-driven-production',
449
+ candidateScope: 'device-scope',
450
+ visibility: 'visible-workset',
451
+ disabledWork: 'zero',
452
+ },
453
+ {
454
+ kind: 'feature',
455
+ owner: 'render-feature-host',
456
+ candidateScope: 'device-scope',
457
+ visibility: 'visible-workset',
458
+ disabledWork: 'lazy',
459
+ },
460
+ {
461
+ kind: 'external-source',
462
+ owner: 'external-source-owner',
463
+ candidateScope: 'device-scope',
464
+ visibility: 'non-visible-lazy',
465
+ disabledWork: 'zero',
466
+ },
467
+ {
468
+ kind: 'render-graph',
469
+ owner: 'typed-frame-graph',
470
+ candidateScope: 'device-scope',
471
+ visibility: 'visible-workset',
472
+ disabledWork: 'zero',
473
+ },
474
+ {
475
+ kind: 'target-history',
476
+ owner: 'temporal-target-owner',
477
+ candidateScope: 'device-scope',
478
+ visibility: 'visible-workset',
479
+ disabledWork: 'zero',
480
+ },
481
+ {
482
+ kind: 'observation-lease',
483
+ owner: 'render-observation-owner',
484
+ candidateScope: 'device-scope',
485
+ visibility: 'non-visible-lazy',
486
+ disabledWork: 'zero',
487
+ },
488
+ ]);
489
+
490
+ export function createRendererProducerRootMatrix(): readonly RendererProducerRoot[] {
491
+ return PRODUCER_ROOT_MATRIX;
492
+ }
493
+
494
+ /**
495
+ * Production-owned recovery facts. These values are projections of the live
496
+ * RenderSystem owners; they are not a test ledger or a second graph model.
497
+ */
498
+ export interface RecoveryProductionEvidence {
499
+ readonly producerRoots: readonly RendererProducerRoot[];
500
+ readonly graph: {
501
+ readonly ready: boolean;
502
+ readonly generation: number;
503
+ readonly passCount: number;
504
+ readonly resourceCount: number;
505
+ };
506
+ readonly residency: { readonly meshResidencyEpoch: number };
507
+ readonly submissions: {
508
+ readonly count: number;
509
+ readonly lastGeneration: number | undefined;
510
+ };
511
+ }
512
+
364
513
  /**
365
514
  * Unified transparent-queue sub-sort covering all four
366
515
  * {@link TransparentSortConfig} modes. Reorders the
@@ -508,6 +657,19 @@ export interface RenderSystem {
508
657
  renderReadLeases?: readonly RenderReadLease[],
509
658
  timingCapture?: GpuTimingCapture,
510
659
  ): boolean;
660
+ /** Prepare a detached graph candidate without entering the frame record path. */
661
+ prepareRecoveryGraphCandidate(
662
+ runtime: RecoveryGraphCandidateRuntime,
663
+ ): RecoveryGraphCandidatePreparation;
664
+ /** Finish and submit detached candidate setup, never a frame receipt. */
665
+ submitCandidateSetup(
666
+ candidate: RecoveryGraphCandidate,
667
+ isValid: () => boolean,
668
+ ): Result<RecoveryGraphSetupSubmission, RhiError>;
669
+ /** Publish a detached graph candidate at the generation publication boundary. */
670
+ publishRecoveryGraphCandidate(candidate: RecoveryGraphCandidate): void;
671
+ /** Discard a detached graph candidate that did not reach publication. */
672
+ discardRecoveryGraphCandidate(candidate: RecoveryGraphCandidate): void;
511
673
  /** Release renderer-owned persistent state for one detached World. */
512
674
  detachScene(world: World): void;
513
675
  /** Release the profiler catalog contribution owned by this RenderSystem. */
@@ -560,6 +722,8 @@ export interface RenderSystem {
560
722
  readonly pointsLinesSnapshots: readonly PointsLinesRetainedSnapshot[];
561
723
  /** Current mesh-slot provenance and active diagnostics from the last frame. */
562
724
  readonly meshMaterialBindings: readonly MeshMaterialBindingObservation[];
725
+ /** Final-submit IBL binding-chain receipt for the last diagnostic frame. */
726
+ readonly iblBinding: import('./mesh-material-bindings').IblBindingInspection | undefined;
563
727
  /**
564
728
  * feat-20260531-bloom-first-declarative-render-graph-pass M4 fix-up w19:
565
729
  * per-frame render-graph pass names in declaration order. Empty array
@@ -573,6 +737,8 @@ export interface RenderSystem {
573
737
  readonly lastSuccessfulCameraAntialias: CameraSnapshot['antialias'] | undefined;
574
738
  /** Detached compiled-graph facts used by the renderer inspection owner. */
575
739
  readonly perFrameGraphInfo: CompiledRenderGraphInfo | undefined;
740
+ /** Production-owner recovery facts used by Renderer.inspect(). */
741
+ readonly recoveryEvidence: RecoveryProductionEvidence;
576
742
  /**
577
743
  * feat-20260531-per-frame-bind-group-cache M1 / w4: per-frame
578
744
  * createBindGroup counter. Reset to 0 on every `draw([world], { cameraOwner: 0, resourceOwner: 0 })` entry,
@@ -600,6 +766,8 @@ export interface RenderSystem {
600
766
  configureStandard(config: RenderPipelineAsset['config']): void;
601
767
  /** Register one engine-owned post-process shader used by the Standard lane. */
602
768
  registerBuiltinPostProcess(id: string, entry: PostProcessShaderEntry): () => void;
769
+ /** Resolve one active built-in post-process declaration for a detached candidate. */
770
+ readonly lookupPostProcess: (id: string) => PostProcessShaderEntry | undefined;
603
771
  /**
604
772
  * feat-20260612-rhi-destroy-renderer-dispose-gpu-lifecycle / M5 / w21:
605
773
  * release the per-RenderSystem frame-state GPU bookkeeping during
@@ -623,9 +791,19 @@ export interface RenderSystem {
623
791
  * handles and are rebuilt after the replacement device is live.
624
792
  * Idempotent and graph-optional (no-op when nothing has compiled yet).
625
793
  */
626
- resetForRecover(): void;
794
+ resetForRecover(retiringPipelineState?: PipelineState, replacementDevice?: RhiDevice): void;
795
+ /** Build post-process parameter buffers without mutating the active bundle. */
796
+ prepareRecoveryPostProcessResources(
797
+ device: RhiDevice,
798
+ ): Result<RecoveryPostProcessResources, RhiError>;
799
+ /** Publish a previously prepared post-process bundle at the recovery boundary. */
800
+ publishRecoveryPostProcessResources(candidate: RecoveryPostProcessResources): void;
801
+ /** Release a candidate bundle that was never published. */
802
+ discardRecoveryPostProcessResources(candidate: RecoveryPostProcessResources): void;
627
803
  /** Recreate post-process GPU parameter resources after a device rebuild. */
628
804
  restorePostProcessResources(): void;
805
+ /** Build candidate roots from explicit candidate-owned device state. */
806
+ prepareRecoveryRoots(runtime: RecoveryRootRuntime): RecoveryRootBundle;
629
807
  }
630
808
 
631
809
  function isStructuredRendererError(error: unknown): error is RhiError | RenderError {
@@ -681,11 +859,12 @@ function makePreparedPipelinePendingError(): RhiError {
681
859
  });
682
860
  }
683
861
 
684
- type PointsLinesGpuResources = {
685
- readonly vertexBuffer: Buffer;
686
- readonly indexBuffer: Buffer;
687
- };
688
- type PointsLinesPreparationAdapter = PointsLinesLanePreparationAdapter<PointsLinesGpuResources>;
862
+ /** The single Points/Lines owner’s staged device-generation handoff. */
863
+ export interface RecoveryPointsLinesCandidate {
864
+ createRecoveryRoot(scope: DeviceScope): LifecycleResourceSpec<unknown>;
865
+ publish(): void;
866
+ release(): void;
867
+ }
689
868
 
690
869
  function preparationFailureFrom(error: unknown): MeshMaterialBindingPreparationFailure | undefined {
691
870
  if (typeof error !== 'object' || error === null) return undefined;
@@ -714,30 +893,56 @@ function preparationFailureFrom(error: unknown): MeshMaterialBindingPreparationF
714
893
  };
715
894
  }
716
895
 
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()];
896
+ /** Candidate-only device/runtime inputs used by the recovery prepare seam. */
897
+ export interface RecoveryGraphCandidateRuntime {
898
+ readonly internals: RenderSystemInternals;
899
+ readonly pipelineState: PipelineState;
731
900
  }
732
901
 
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()];
902
+ /** Detached graph state prepared on a replacement device, before publication. */
903
+ export interface RecoveryGraphCandidate {
904
+ readonly frameState: RenderFrameState;
905
+ readonly setupWorks: readonly MipmapEncoderWork[];
906
+ readonly device: RhiDevice;
907
+ readonly generation: number;
908
+ readonly gpuDrivenProduction?: GpuDrivenProduction;
909
+ readonly gpuDrivenScene?: PersistentGpuDrivenCandidate;
910
+ /** Staged CPU-expanded Points/Lines buffers owned by the same RenderSystem. */
911
+ readonly pointsLines?: RecoveryPointsLinesCandidate;
912
+ readonly featureHost?: RenderFeatureHost;
913
+ readonly featureGpuWork?: RenderFeatureGpuWorkOwner;
914
+ readonly featureGraphCandidate?: RenderFeatureGraphCandidate;
915
+ readonly postProcessPipelines?: ReadonlyMap<string, RenderPipeline>;
916
+ readonly recoveryReadiness: RecoveryPipelineReadiness;
917
+ /** Release all candidate roots before publication; safe to call repeatedly. */
918
+ readonly release: () => void;
919
+ /** Transfer candidate-root ownership to the published RenderSystem. */
920
+ readonly markPublished: () => void;
921
+ }
922
+
923
+ /** Candidate setup completion; completion is bounded by the recovery owner. */
924
+ export interface RecoveryGraphSetupSubmission {
925
+ readonly completion?: PromiseLike<unknown>;
926
+ }
927
+
928
+ export type RecoveryGraphCandidatePreparation =
929
+ | { readonly kind: 'no-seed' }
930
+ | { readonly kind: 'ready'; readonly candidate: RecoveryGraphCandidate }
931
+ | { readonly kind: 'failed'; readonly reason: string; readonly cause?: unknown };
932
+
933
+ /** Explicit candidate-owned inputs for lifecycle-root preparation. */
934
+ export interface RecoveryRootRuntime {
935
+ readonly scope: DeviceScope;
936
+ readonly device: RhiDevice;
937
+ readonly gpuStore: GpuResidencyCache;
938
+ readonly graphCandidate?: RecoveryGraphCandidate | undefined;
939
+ }
940
+
941
+ /** Candidate roots plus the one synchronous owner switch at publication. */
942
+ export interface RecoveryRootBundle {
943
+ readonly roots: readonly LifecycleResourceSpec<unknown>[];
944
+ publish(): void;
945
+ discard(): void;
741
946
  }
742
947
 
743
948
  /**
@@ -856,267 +1061,6 @@ function observeMaterialResidency(
856
1061
  };
857
1062
  }
858
1063
 
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
1064
  export function createRenderSystem(internals: RenderSystemInternals): RenderSystem {
1121
1065
  const phaseCatalogRegistration = internals.profiler?.registerPhaseCatalog(
1122
1066
  'render',
@@ -1169,7 +1113,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1169
1113
  internals.gpuStore.invalidateMesh(handle, preparedWorlds[worldId] ?? worldId);
1170
1114
  },
1171
1115
  });
1172
- const environmentLifecycle = new EnvironmentLifecycle(internals.deviceScope);
1116
+ let environmentLifecycle = new EnvironmentLifecycle(internals.deviceScope);
1173
1117
  const transmissionAdmission = new TransmissionCandidateAdmission();
1174
1118
  let lastTransmissionInspection: TransmissionInspection | undefined;
1175
1119
  let lastTransmissionKey = '';
@@ -1294,8 +1238,13 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1294
1238
  });
1295
1239
  lastTransmissionKey = key;
1296
1240
  };
1241
+ // GPU-driven production uses the validated adapter for normal frames. The
1242
+ // assembly-supplied closure resolves its device-bound adapter at call time,
1243
+ // so recovery candidate preparation still follows candidateShaderState
1244
+ // rather than retaining a lost-generation module.
1297
1245
  const gpuDrivenShaderFactory =
1298
1246
  internals.shaderModuleFactory ??
1247
+ internals.immediateShaderModuleFactory ??
1299
1248
  ({
1300
1249
  createShaderModule: () =>
1301
1250
  err(
@@ -1306,7 +1255,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1306
1255
  }),
1307
1256
  ),
1308
1257
  } satisfies import('./pipeline-builder').PipelineBuilderShaderModuleFactory);
1309
- let gpuDrivenProduction = new GpuDrivenProduction(internals.device, gpuDrivenShaderFactory);
1258
+ let gpuDrivenProduction = GpuDrivenProduction.forDevice(internals.device, gpuDrivenShaderFactory);
1310
1259
  let occlusionRuntime: OcclusionRenderRuntime | undefined = new OcclusionRenderRuntime(
1311
1260
  internals.device,
1312
1261
  persistentRenderScene.visibilityFacetStore(),
@@ -1324,34 +1273,36 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1324
1273
  }
1325
1274
  return occlusionRuntime;
1326
1275
  };
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
- });
1276
+ const createFeatureGpuWorkOwner = (runtime: RenderSystemInternals): RenderFeatureGpuWorkOwner =>
1277
+ createRenderFeatureGpuWorkOwner({
1278
+ getDevice: () => runtime.device,
1279
+ getShaderModuleFactory: () =>
1280
+ runtime.shaderModuleFactory ??
1281
+ ({
1282
+ createShaderModule: () =>
1283
+ err(
1284
+ new RhiError({
1285
+ code: 'rhi-not-available',
1286
+ expected: 'the renderer backend exposes shader module creation',
1287
+ hint: 'construct the renderer through the backend pack before activating GPU features',
1288
+ }),
1289
+ ),
1290
+ } satisfies import('./pipeline-builder').PipelineBuilderShaderModuleFactory),
1291
+ getImmediateShaderModuleFactory: () =>
1292
+ runtime.immediateShaderModuleFactory ??
1293
+ runtime.shaderModuleFactory ??
1294
+ ({
1295
+ createShaderModule: () =>
1296
+ err(
1297
+ new RhiError({
1298
+ code: 'rhi-not-available',
1299
+ expected: 'the renderer backend exposes shader module creation',
1300
+ hint: 'construct the renderer through the backend pack before activating GPU features',
1301
+ }),
1302
+ ),
1303
+ } satisfies import('./pipeline-builder').PipelineBuilderShaderModuleFactory),
1304
+ });
1305
+ let featureGpuWork = createFeatureGpuWorkOwner(internals);
1355
1306
  const disposeFeatureGpuWork = (): void => {
1356
1307
  const disposed = featureGpuWork.dispose();
1357
1308
  if (!disposed.ok) internals.errorRegistry.fire(disposed.error);
@@ -1493,6 +1444,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1493
1444
  spotShadowSnapshots: [],
1494
1445
  };
1495
1446
  let directFrameId = 0;
1447
+ let submittedFrameCount = 0;
1448
+ let lastSubmittedGeneration: number | undefined;
1496
1449
  let lastDirectionalShadowLights: ExtractedLights | undefined;
1497
1450
  let lastDirectionalShadowCandidate: 'accepted' | 'failed' = 'failed';
1498
1451
  let lastDirectionalShadowError: DirectionalShadowInspectionSource['error'];
@@ -1551,7 +1504,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1551
1504
  // D-3 / D-8: per-shader params UBO resource table (id -> GPU Buffer).
1552
1505
  // Eager-created at register time when entry.params is present (byteSize >= 16,
1553
1506
  // defaultValue.length === byteSize); reused frame-to-frame via queue.writeBuffer.
1554
- const postProcessParamsBuffers = new Map<string, Buffer>();
1507
+ let postProcessParamsBuffers = new Map<string, Buffer>();
1508
+ let postProcessParamsDevice = internals.device;
1555
1509
  const builtinPostProcessEntries = new Map<string, PostProcessShaderEntry>();
1556
1510
  let activeFeaturePostProcessEntries: ReadonlyMap<string, PostProcessShaderEntry> = new Map();
1557
1511
  const lookupPostProcess = (id: string): PostProcessShaderEntry | undefined =>
@@ -1567,6 +1521,76 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1567
1521
  // per-frame snapshot + bind it at group(1) binding(2).
1568
1522
  const getPostProcessParamsBuffer = (id: string): Buffer | undefined =>
1569
1523
  postProcessParamsBuffers.get(id);
1524
+ const postProcessDeclarations = (): readonly (readonly [string, PostProcessShaderEntry])[] => [
1525
+ ...builtinPostProcessEntries.entries(),
1526
+ ...activeFeaturePostProcessEntries.entries(),
1527
+ ];
1528
+ const disposePostProcessResources = (resources: RecoveryPostProcessResources): void => {
1529
+ for (const buffer of resources.paramsBuffers.values()) {
1530
+ const destroyed = resources.device.destroyBuffer(buffer);
1531
+ if (!destroyed.ok) internals.errorRegistry.fire(destroyed.error);
1532
+ }
1533
+ };
1534
+ const prepareRecoveryPostProcessResources = (
1535
+ device: RhiDevice,
1536
+ ): Result<RecoveryPostProcessResources, RhiError> => {
1537
+ const paramsBuffers = new Map<string, Buffer>();
1538
+ try {
1539
+ for (const [id, entry] of postProcessDeclarations()) {
1540
+ if (id === 'forgeax.taa-resolve') continue;
1541
+ let buffer: Buffer | undefined;
1542
+ if (entry.params !== undefined) {
1543
+ const created = device.createBuffer({
1544
+ label: `post-process-params-${id}`,
1545
+ size: entry.params.byteSize,
1546
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
1547
+ mappedAtCreation: false,
1548
+ });
1549
+ if (!created.ok) throw created.error;
1550
+ buffer = created.value;
1551
+ paramsBuffers.set(id, buffer);
1552
+ const written = device.queue.writeBuffer(buffer, 0, entry.params.defaultValue);
1553
+ if (!written.ok) throw written.error;
1554
+ } else if (entryHasDepthRead(entry)) {
1555
+ const created = device.createBuffer({
1556
+ label: `post-process-params-${id}`,
1557
+ size: DEPTH_MIN_PARAMS_BYTE_SIZE,
1558
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
1559
+ mappedAtCreation: false,
1560
+ });
1561
+ if (!created.ok) throw created.error;
1562
+ buffer = created.value;
1563
+ }
1564
+ if (buffer !== undefined) paramsBuffers.set(id, buffer);
1565
+ }
1566
+ } catch (cause) {
1567
+ disposePostProcessResources({ device, paramsBuffers });
1568
+ if (cause instanceof RhiError) return err(cause);
1569
+ return err(
1570
+ new RhiError({
1571
+ code: 'webgpu-runtime-error',
1572
+ expected: 'post-process recovery resources are created on the candidate device',
1573
+ hint: cause instanceof Error ? cause.message : String(cause),
1574
+ }),
1575
+ );
1576
+ }
1577
+ return ok({ device, paramsBuffers });
1578
+ };
1579
+ const publishRecoveryPostProcessResources = (candidate: RecoveryPostProcessResources): void => {
1580
+ if (candidate.paramsBuffers === postProcessParamsBuffers) return;
1581
+ const previous = { device: postProcessParamsDevice, paramsBuffers: postProcessParamsBuffers };
1582
+ postProcessParamsDevice = candidate.device;
1583
+ postProcessParamsBuffers = candidate.paramsBuffers;
1584
+ disposePostProcessResources(previous);
1585
+ };
1586
+ const discardRecoveryPostProcessResources = (candidate: RecoveryPostProcessResources): void => {
1587
+ if (candidate.paramsBuffers === postProcessParamsBuffers) {
1588
+ postProcessParamsBuffers = new Map();
1589
+ disposePostProcessResources(candidate);
1590
+ return;
1591
+ }
1592
+ disposePostProcessResources(candidate);
1593
+ };
1570
1594
  interface PostProcessParamsCandidate {
1571
1595
  readonly buffers: ReadonlyMap<string, Buffer>;
1572
1596
  readonly created: readonly Buffer[];
@@ -1666,6 +1690,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1666
1690
  const key = `${id}|${colorFormat}|${signature}`;
1667
1691
  const cached = postProcessPipelineCache.get(key);
1668
1692
  if (cached !== undefined) return cached;
1693
+ internals.recoveryColdWorkGuard?.notePipelineColdWork();
1669
1694
  const factory = internals.buildPostProcessPipeline;
1670
1695
  if (factory === undefined) return null;
1671
1696
  const built = factory(
@@ -1842,44 +1867,60 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1842
1867
  createBindGroup: 0,
1843
1868
  keys: [],
1844
1869
  };
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>>();
1870
+ type PreparedResolverCaches = {
1871
+ readonly preparedPipelineIds: WeakMap<object, string>;
1872
+ readonly preparedMaterialPipelineShaders: WeakMap<object, string>;
1873
+ readonly preparedGroup0Pipelines: WeakSet<object>;
1874
+ readonly preparedViewOnlyPipelines: WeakSet<object>;
1875
+ readonly preparedRenderMaterialPipelines: WeakSet<object>;
1876
+ readonly preparedAssetHandles: WeakMap<World, Map<string, number>>;
1877
+ };
1878
+ const createPreparedResolverCaches = (): PreparedResolverCaches => ({
1879
+ preparedPipelineIds: new WeakMap(),
1880
+ preparedMaterialPipelineShaders: new WeakMap(),
1881
+ preparedGroup0Pipelines: new WeakSet(),
1882
+ preparedViewOnlyPipelines: new WeakSet(),
1883
+ preparedRenderMaterialPipelines: new WeakSet(),
1884
+ preparedAssetHandles: new WeakMap(),
1885
+ });
1886
+ const activePreparedResolverCaches = createPreparedResolverCaches();
1851
1887
  const preparedHandle = <Brand extends string>(
1888
+ runtime: RenderSystemInternals,
1889
+ caches: PreparedResolverCaches,
1852
1890
  world: World,
1853
1891
  guid: string,
1854
1892
  brand: Brand,
1855
1893
  ): Handle<Brand, 'shared'> | undefined => {
1856
- let handles = preparedAssetHandles.get(world);
1894
+ let handles = caches.preparedAssetHandles.get(world);
1857
1895
  if (handles === undefined) {
1858
1896
  handles = new Map();
1859
- preparedAssetHandles.set(world, handles);
1897
+ caches.preparedAssetHandles.set(world, handles);
1860
1898
  }
1861
1899
  const key = `${brand}:${guid.toLowerCase()}`;
1862
1900
  const cached = handles.get(key);
1863
1901
  if (cached !== undefined) return cached as Handle<Brand, 'shared'>;
1864
- const asset = internals.assets.lookup(guid);
1902
+ const asset = runtime.assets.lookup(guid);
1865
1903
  if (asset === undefined) return undefined;
1866
1904
  const handle = world.allocSharedRef(brand, asset);
1867
1905
  handles.set(key, handle as number);
1868
1906
  return handle;
1869
1907
  };
1870
1908
  const preparedMaterialBindings = (
1909
+ runtime: RenderSystemInternals,
1910
+ caches: PreparedResolverCaches,
1911
+ worlds: readonly World[],
1871
1912
  materialShaderId: string,
1872
1913
  worldIndex: number,
1873
1914
  materialGuid: string,
1874
1915
  layout: BindGroupLayout,
1875
1916
  ) => {
1876
- const world = preparedWorlds[worldIndex];
1877
- const material = internals.assets.lookup(materialGuid) as MaterialAsset | undefined;
1878
- const pipelineState = internals.getPipelineState();
1917
+ const world = worlds[worldIndex];
1918
+ const material = runtime.assets.lookup(materialGuid) as MaterialAsset | undefined;
1919
+ const pipelineState = runtime.getPipelineState();
1879
1920
  if (world === undefined || material?.kind !== 'material' || pipelineState === null) {
1880
1921
  return err(new Error('prepared material asset is unavailable'));
1881
1922
  }
1882
- const schema = internals.getParamSchema?.(materialShaderId) ?? [];
1923
+ const schema = runtime.getParamSchema?.(materialShaderId) ?? [];
1883
1924
  const values = material.values ?? {};
1884
1925
  const derived = derive(schema);
1885
1926
  const fields = [...derived.textureFieldNames];
@@ -1892,7 +1933,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1892
1933
  schema,
1893
1934
  values as Readonly<Record<string, number | readonly number[]>>,
1894
1935
  );
1895
- const created = internals.device.createBuffer({
1936
+ const created = runtime.device.createBuffer({
1896
1937
  label: `prepared-material:${materialGuid}`,
1897
1938
  size: payload.byteLength,
1898
1939
  usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
@@ -1900,9 +1941,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1900
1941
  if (!created.ok) return created;
1901
1942
  buffer = created.value;
1902
1943
  payloadByteLength = payload.byteLength;
1903
- const written = internals.device.queue.writeBuffer(buffer, 0, payload);
1944
+ const written = runtime.device.queue.writeBuffer(buffer, 0, payload);
1904
1945
  if (!written.ok) {
1905
- internals.device.destroyBuffer(buffer);
1946
+ runtime.device.destroyBuffer(buffer);
1906
1947
  return written;
1907
1948
  }
1908
1949
  }
@@ -1919,20 +1960,32 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1919
1960
  : undefined;
1920
1961
  let sampler = pipelineState.defaultSampler;
1921
1962
  if (textureValue?.sampler !== undefined) {
1922
- const handle = preparedHandle(world, String(textureValue.sampler), 'SamplerAsset');
1963
+ const handle = preparedHandle(
1964
+ runtime,
1965
+ caches,
1966
+ world,
1967
+ String(textureValue.sampler),
1968
+ 'SamplerAsset',
1969
+ );
1923
1970
  if (handle !== undefined) {
1924
1971
  const asset = resolveAssetHandle<SamplerAsset>(world, handle);
1925
1972
  if (asset.ok) {
1926
- const resident = internals.gpuStore.ensureSamplerResident(handle, asset.value, world);
1973
+ const resident = runtime.gpuStore.ensureSamplerResident(handle, asset.value, world);
1927
1974
  if (resident.ok) sampler = resident.value;
1928
1975
  }
1929
1976
  }
1930
1977
  }
1931
1978
  let view = pipelineState.defaultWhiteTextureView;
1932
1979
  if (textureValue !== undefined) {
1933
- const handle = preparedHandle(world, String(textureValue.texture), 'TextureAsset');
1980
+ const handle = preparedHandle(
1981
+ runtime,
1982
+ caches,
1983
+ world,
1984
+ String(textureValue.texture),
1985
+ 'TextureAsset',
1986
+ );
1934
1987
  if (handle !== undefined) {
1935
- view = residentTextureView(world, internals.gpuStore, internals, handle) ?? view;
1988
+ view = residentTextureView(world, runtime.gpuStore, runtime, handle) ?? view;
1936
1989
  }
1937
1990
  }
1938
1991
  textureResources.push({ sampler, view });
@@ -1968,12 +2021,12 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1968
2021
  textureIndex += 1;
1969
2022
  continue;
1970
2023
  }
1971
- if (buffer !== undefined) internals.device.destroyBuffer(buffer);
2024
+ if (buffer !== undefined) runtime.device.destroyBuffer(buffer);
1972
2025
  return err(new Error('prepared material schema contains unsupported binding kinds'));
1973
2026
  }
1974
2027
  const fallback = pipelineState.skylightFallback;
1975
2028
  if (fallback === null) {
1976
- if (buffer !== undefined) internals.device.destroyBuffer(buffer);
2029
+ if (buffer !== undefined) runtime.device.destroyBuffer(buffer);
1977
2030
  return err(new Error('prepared material skylight fallback is unavailable'));
1978
2031
  }
1979
2032
  const completeEntries = assembleMaterialWithSkylightEntries(entries, {
@@ -1985,206 +2038,261 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
1985
2038
  brdfLutSampler: fallback.sampler,
1986
2039
  intensityBuffer: fallback.intensityBuffer,
1987
2040
  });
1988
- const group = internals.device.createBindGroup({ layout, entries: completeEntries });
2041
+ const group = runtime.device.createBindGroup({ layout, entries: completeEntries });
1989
2042
  if (!group.ok) {
1990
- if (buffer !== undefined) internals.device.destroyBuffer(buffer);
2043
+ if (buffer !== undefined) runtime.device.destroyBuffer(buffer);
1991
2044
  return group;
1992
2045
  }
1993
2046
  if (buffer === undefined) return group;
1994
2047
  return ok({
1995
2048
  handle: group.value,
1996
2049
  dynamicOffsets: [0, 0],
1997
- release: () => internals.device.destroyBuffer(buffer),
2050
+ release: () => runtime.device.destroyBuffer(buffer),
1998
2051
  });
1999
2052
  };
2000
2053
  const lastFrustumStats: { culled: number; total: number } = { culled: 0, total: 0 };
2001
2054
  const lastVisibilityStats: { explicitlyHidden: number } = { explicitlyHidden: 0 };
2002
2055
  let lastMeshMaterialBindings: readonly MeshMaterialBindingObservation[] = [];
2003
2056
  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?.(
2057
+ const createPreparedResolverFactory =
2058
+ (
2059
+ runtime: RenderSystemInternals,
2060
+ gpuWork: RenderFeatureGpuWorkOwner,
2061
+ worlds: readonly World[],
2062
+ caches: PreparedResolverCaches,
2063
+ ) =>
2064
+ (input: RenderFeaturePreparedGraphicsResolverInput): PreparedGraphicsResolver => {
2065
+ const {
2066
+ preparedPipelineIds,
2067
+ preparedMaterialPipelineShaders,
2068
+ preparedGroup0Pipelines,
2069
+ preparedViewOnlyPipelines,
2070
+ preparedRenderMaterialPipelines,
2071
+ } = caches;
2072
+ return createPreparedGraphicsResolver({
2073
+ device: runtime.device,
2074
+ featureIdentity: input.featureIdentity,
2075
+ generation: input.generation,
2076
+ capabilityAvailable: true,
2077
+ featureOrder: input.order,
2078
+ lookup: input.lookup,
2079
+ resolveGpuBuffer: (reference) => gpuWork.resolveBuffer(input.featureIdentity, reference),
2080
+ resolvePipeline: (descriptor) => {
2081
+ const postProcessEntry =
2082
+ input.fullscreenEffects.get(descriptor.shader) ??
2083
+ builtinPostProcessEntries.get(descriptor.shader);
2084
+ if (postProcessEntry !== undefined) {
2085
+ const fullscreen = buildFullscreenPostProcessPass(
2086
+ { device: runtime.device, errorRegistry: runtime.errorRegistry },
2087
+ postProcessEntry,
2088
+ );
2089
+ if (fullscreen === null) return err(new Error('prepared post-process layout failed'));
2090
+ const pipeline = runtime.getPostProcessPipeline?.(
2091
+ descriptor.shader,
2092
+ fullscreen.bindGroupLayout,
2093
+ descriptor.colorFormats[0] as GPUTextureFormat,
2094
+ postProcessEntry,
2095
+ );
2096
+ if (pipeline !== null && pipeline !== undefined) {
2097
+ preparedPipelineIds.set(pipeline as object, descriptor.shader);
2098
+ if (runtime.getMaterialShaderBindingContract?.(descriptor.shader) === 'group-0') {
2099
+ preparedGroup0Pipelines.add(pipeline as object);
2100
+ }
2101
+ }
2102
+ return pipeline === null || pipeline === undefined
2103
+ ? err(makePreparedPipelinePendingError())
2104
+ : ok(pipeline);
2105
+ }
2106
+ const preparedPipeline = runtime.getMaterialShaderPipeline?.(
2027
2107
  descriptor.shader,
2028
- fullscreen.bindGroupLayout,
2108
+ descriptor.colorFormats[0] === 'rgba16float',
2109
+ descriptor.renderState,
2110
+ descriptor.topology,
2111
+ descriptor.indexFormat,
2112
+ undefined,
2113
+ 'forward',
2114
+ undefined,
2115
+ descriptor.sampleCount ?? 1,
2116
+ // Prepared graphics own their declared attachment format. Passing
2117
+ // it through keeps the PSO compatible with feature-target views
2118
+ // (the ordinary material path intentionally defaults to the
2119
+ // swap-chain view format).
2029
2120
  descriptor.colorFormats[0] as GPUTextureFormat,
2030
- postProcessEntry,
2121
+ undefined,
2122
+ descriptor.depthFormat === undefined
2123
+ ? null
2124
+ : (descriptor.depthFormat as GPUTextureFormat),
2125
+ descriptor.vertexLayout,
2126
+ undefined,
2127
+ input.shaderModuleMode,
2031
2128
  );
2032
- if (pipeline !== null && pipeline !== undefined) {
2033
- preparedPipelineIds.set(pipeline as object, descriptor.shader);
2034
- if (internals.getMaterialShaderBindingContract?.(descriptor.shader) === 'group-0') {
2129
+ // A requested material shader is an exact pipeline contract. During
2130
+ // async shader warmup, substituting the generic unlit pipeline can
2131
+ // mismatch vertex layouts and HDR attachment formats, turning a
2132
+ // retryable next-frame prepare into an invalid GPU command buffer.
2133
+ const pipeline = preparedPipeline ?? null;
2134
+ if (pipeline !== null) {
2135
+ preparedMaterialPipelineShaders.set(pipeline as object, descriptor.shader);
2136
+ const bindingContract = runtime.getMaterialShaderBindingContract?.(descriptor.shader);
2137
+ if (bindingContract === 'group-0') {
2035
2138
  preparedGroup0Pipelines.add(pipeline as object);
2139
+ } else if (bindingContract === 'view-only') {
2140
+ preparedViewOnlyPipelines.add(pipeline as object);
2141
+ } else {
2142
+ preparedRenderMaterialPipelines.add(pipeline as object);
2036
2143
  }
2037
2144
  }
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);
2145
+ return pipeline === null ? err(makePreparedPipelinePendingError()) : ok(pipeline);
2146
+ },
2147
+ resolveBindings: (descriptor, pipeline) => {
2148
+ const materialShaderId = preparedMaterialPipelineShaders.get(pipeline as object);
2149
+ const bindingContract =
2150
+ materialShaderId === undefined
2151
+ ? undefined
2152
+ : runtime.getMaterialShaderBindingContract?.(materialShaderId);
2153
+ if (bindingContract === 'group-0-resource') {
2154
+ return descriptor.values.sceneDepth === undefined
2155
+ ? err(new Error('prepared group-0 resource pipeline requires a scene target'))
2156
+ : ok(undefined);
2079
2157
  }
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)) {
2158
+ if (bindingContract === 'view-and-scene-depth') {
2159
+ return ok(undefined);
2160
+ }
2161
+ if (preparedGroup0Pipelines.has(pipeline as object)) {
2162
+ const layout =
2163
+ (materialShaderId === undefined
2164
+ ? undefined
2165
+ : runtime.getMaterialBindGroupLayout?.(materialShaderId)) ??
2166
+ (
2167
+ pipeline as RenderPipeline & {
2168
+ getBindGroupLayout?: (index: number) => BindGroupLayout;
2169
+ }
2170
+ ).getBindGroupLayout?.(0);
2171
+ return layout === undefined
2172
+ ? err(new Error('prepared group-0 pipeline bind group layout is unavailable'))
2173
+ : runtime.device.createBindGroup({ layout, entries: [] });
2174
+ }
2175
+ if (
2176
+ preparedViewOnlyPipelines.has(pipeline as object) ||
2177
+ (preparedRenderMaterialPipelines.has(pipeline as object) &&
2178
+ descriptor.values.group === 0) ||
2179
+ pipeline === runtime.getPipelineState()?.unlitPipeline
2180
+ ) {
2181
+ return ok(undefined);
2182
+ }
2183
+ const group = descriptor.values.group;
2098
2184
  const layout =
2099
- (materialShaderId === undefined
2100
- ? undefined
2101
- : internals.getMaterialBindGroupLayout?.(materialShaderId)) ??
2185
+ (materialShaderId !== undefined
2186
+ ? (runtime.getMaterialBindGroupLayout?.(materialShaderId) ??
2187
+ (group === 0 ? runtime.getPipelineState()?.materialBindGroupLayout : undefined))
2188
+ : undefined) ??
2102
2189
  (
2103
2190
  pipeline as RenderPipeline & {
2104
2191
  getBindGroupLayout?: (index: number) => BindGroupLayout;
2105
2192
  }
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({
2193
+ ).getBindGroupLayout?.(group === 1 ? 1 : 0);
2194
+ const material = descriptor.values.material;
2195
+ if (
2196
+ group === 1 &&
2197
+ layout !== undefined &&
2198
+ materialShaderId !== undefined &&
2199
+ typeof material === 'object' &&
2200
+ material !== null &&
2201
+ 'world' in material &&
2202
+ 'guid' in material &&
2203
+ typeof material.world === 'number' &&
2204
+ typeof material.guid === 'string'
2205
+ ) {
2206
+ return preparedMaterialBindings(
2207
+ runtime,
2208
+ caches,
2209
+ worlds,
2210
+ materialShaderId,
2211
+ material.world,
2212
+ material.guid,
2147
2213
  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,
2214
+ );
2215
+ }
2216
+ return layout === undefined
2217
+ ? err(new Error('prepared pipeline bind group layout is unavailable'))
2218
+ : runtime.device.createBindGroup({
2219
+ layout,
2220
+ entries:
2221
+ group === 1 && preparedPipelineIds.has(pipeline as object)
2222
+ ? [
2223
+ {
2224
+ binding: 0,
2225
+ resource: {
2226
+ kind: 'textureView',
2227
+ value: runtime.getPipelineState()?.fallbackTextureView as never,
2228
+ },
2156
2229
  },
2157
- },
2158
- {
2159
- binding: 1,
2160
- resource: {
2161
- kind: 'sampler',
2162
- value: internals.getPipelineState()?.defaultSampler as never,
2230
+ {
2231
+ binding: 1,
2232
+ resource: {
2233
+ kind: 'sampler',
2234
+ value: runtime.getPipelineState()?.defaultSampler as never,
2235
+ },
2163
2236
  },
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,
2237
+ ...(runtime.getPostProcessParamsBuffer?.(
2238
+ preparedPipelineIds.get(pipeline as object) ?? '',
2239
+ ) === undefined
2240
+ ? []
2241
+ : [
2242
+ {
2243
+ binding: 2,
2244
+ resource: {
2245
+ kind: 'buffer' as const,
2246
+ value: {
2247
+ buffer: runtime.getPostProcessParamsBuffer?.(
2248
+ preparedPipelineIds.get(pipeline as object) ?? '',
2249
+ ) as never,
2250
+ },
2178
2251
  },
2179
2252
  },
2180
- },
2181
- ]),
2182
- ]
2183
- : [],
2184
- });
2185
- },
2186
- });
2187
- };
2253
+ ]),
2254
+ ]
2255
+ : [],
2256
+ });
2257
+ },
2258
+ });
2259
+ };
2260
+ const preparedResolverFactory = (
2261
+ input: RenderFeaturePreparedGraphicsResolverInput,
2262
+ ): PreparedGraphicsResolver =>
2263
+ createPreparedResolverFactory(
2264
+ internals,
2265
+ featureGpuWork,
2266
+ preparedWorlds,
2267
+ activePreparedResolverCaches,
2268
+ )(input);
2269
+ const recoveryOwner = createRenderSystemRecovery({
2270
+ frameState,
2271
+ internals,
2272
+ getEnvironmentLifecycle: () => environmentLifecycle,
2273
+ getGpuDrivenProduction: () => gpuDrivenProduction,
2274
+ setGpuDrivenProduction: (production) => {
2275
+ gpuDrivenProduction = production;
2276
+ },
2277
+ disposeFeatureGpuWork,
2278
+ setFeatureGpuWork: (owner) => {
2279
+ featureGpuWork = owner;
2280
+ },
2281
+ getActiveFeaturePostProcessEntries: () => activeFeaturePostProcessEntries,
2282
+ setActiveFeaturePostProcessEntries: (entries) => {
2283
+ activeFeaturePostProcessEntries = entries;
2284
+ },
2285
+ postProcessPipelineCache,
2286
+ clearPostProcessPipelineCache,
2287
+ clearPostProcessPipelineEntry,
2288
+ invalidatePostProcessModule,
2289
+ persistentRenderScene,
2290
+ gpuDrivenShaderFactory,
2291
+ createFeatureGpuWorkOwner,
2292
+ pointsLinesOwner,
2293
+ createPreparedResolverFactory,
2294
+ createPreparedResolverCaches,
2295
+ });
2188
2296
  return {
2189
2297
  releaseProfilerCatalog(): void {
2190
2298
  releaseProfilerCatalog?.();
@@ -2387,7 +2495,10 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2387
2495
  renderReadLeases?: readonly RenderReadLease[],
2388
2496
  timingCapture?: GpuTimingCapture,
2389
2497
  ): boolean {
2390
- preparedWorlds = worlds;
2498
+ const composition = canonicalizeWorldComposition(worlds, opts, renderReadLeases);
2499
+ const compositionWorlds = composition.worlds;
2500
+ const compositionLeases = composition.leases;
2501
+ preparedWorlds = compositionWorlds;
2391
2502
  const profileSession = internals.profiler?.activeSession();
2392
2503
  let ownsProfileFrame = false;
2393
2504
  let submitted = false;
@@ -2403,7 +2514,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2403
2514
  // cameraOwner drives the surfaced cameras + frustum
2404
2515
  // cull; resourceOwner drives skylight/skybox/postProcess + per-world
2405
2516
  // record config.
2406
- const { cameraOwner, resourceOwner } = opts;
2517
+ const { cameraOwner, resourceOwner } = composition.owners;
2407
2518
  // Keep the compiled graph as last-known-good until the candidate topology
2408
2519
  // compiles. The failed candidate frame does not execute it; successful
2409
2520
  // replacement retires it atomically in ensureCompiledFrameGraph.
@@ -2438,15 +2549,15 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2438
2549
  // the dedicated resourceOwner index. The variable is named
2439
2550
  // `resourceWorld` so the record-stage reads are self-describing (they
2440
2551
  // are resource-owner reads, not camera reads).
2441
- const resourceWorld = worlds[resourceOwner] as World;
2552
+ const resourceWorld = compositionWorlds[resourceOwner] as World;
2442
2553
  const frame = runProfiledRenderPhase(profileSession, 'extract', () => {
2443
2554
  return persistentRenderScene.extractComposition(
2444
- worlds,
2555
+ compositionWorlds,
2445
2556
  { cameraOwner, resourceOwner },
2446
2557
  internals.assets.catalogEpoch,
2447
2558
  (renderables) =>
2448
2559
  extractFrames(
2449
- worlds,
2560
+ compositionWorlds,
2450
2561
  { cameraOwner, resourceOwner },
2451
2562
  internals.assets,
2452
2563
  internals.getPipelineState(),
@@ -2459,7 +2570,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2459
2570
  : { getMaterialShaderArtifact: internals.getMaterialShaderArtifact }),
2460
2571
  },
2461
2572
  ),
2462
- renderReadLeases,
2573
+ compositionLeases,
2463
2574
  );
2464
2575
  });
2465
2576
  const {
@@ -2491,16 +2602,16 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2491
2602
  skylight,
2492
2603
  );
2493
2604
  latestCamera = cameras[0];
2494
- persistentRenderScene.updateVisibilityFacet(worlds, latestCamera, renderables);
2605
+ persistentRenderScene.updateVisibilityFacet(compositionWorlds, latestCamera, renderables);
2495
2606
  let occlusionProjection:
2496
2607
  | import('./scene/visibility/occlusion-runtime').OcclusionFrameProjection
2497
2608
  | undefined;
2498
2609
  let occlusionFallback: LodOcclusionInspection['fallback'] = { active: false };
2499
- const worldKeys = persistentRenderScene.visibilityWorldKeysFor(worlds);
2610
+ const worldKeys = persistentRenderScene.visibilityWorldKeysFor(compositionWorlds);
2500
2611
  const slotByEntity = persistentRenderScene.compositionSlotByStableEntity();
2501
2612
  if (latestCamera !== undefined) {
2502
2613
  const camera = latestCamera;
2503
- const cameraWorld = worlds[camera.worldId ?? cameraOwner];
2614
+ const cameraWorld = compositionWorlds[camera.worldId ?? cameraOwner];
2504
2615
  const view = viewKey({
2505
2616
  attachmentId: cameraWorld?.identity ?? 'missing-camera-world',
2506
2617
  cameraEntity: camera.entityKey ?? 0,
@@ -2514,7 +2625,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2514
2625
  cachedCandidates.cameraWorldIdentity === (cameraWorld?.identity ?? '')
2515
2626
  ? cachedCandidates.candidates
2516
2627
  : renderables.flatMap((renderable) => {
2517
- const world = worlds[renderable.worldId];
2628
+ const world = compositionWorlds[renderable.worldId];
2518
2629
  const aabb = renderable.localAabb;
2519
2630
  // Occlusion is a LOD transport concern. A plain mesh has no
2520
2631
  // lower-detail range to select, so keep it on the ordinary CPU
@@ -2599,7 +2710,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2599
2710
  latestCamera !== undefined &&
2600
2711
  ensureOcclusionRuntime().prepareUnavailable
2601
2712
  ) {
2602
- const cameraWorld = worlds[latestCamera.worldId ?? cameraOwner];
2713
+ const cameraWorld = compositionWorlds[latestCamera.worldId ?? cameraOwner];
2603
2714
  if (cameraWorld !== undefined) {
2604
2715
  const fallbackView = viewKey({
2605
2716
  attachmentId: cameraWorld.identity,
@@ -2609,7 +2720,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2609
2720
  });
2610
2721
  for (const renderable of renderables) {
2611
2722
  if ((renderable.lods?.length ?? 0) === 0) continue;
2612
- const world = worlds[renderable.worldId];
2723
+ const world = compositionWorlds[renderable.worldId];
2613
2724
  if (world === undefined) continue;
2614
2725
  const slot = slotByEntity.get(
2615
2726
  worldEntityKey(
@@ -2632,7 +2743,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2632
2743
  }
2633
2744
  }
2634
2745
  const visibilityProjection = persistentRenderScene.projectVisibility(
2635
- worlds,
2746
+ compositionWorlds,
2636
2747
  latestCamera,
2637
2748
  renderables,
2638
2749
  dispatch,
@@ -2678,7 +2789,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2678
2789
  const inspectionQuery = ensureOcclusionRuntime().inspect();
2679
2790
  const inspectionBudget = persistentRenderScene.visibilityBudgetValue();
2680
2791
  const inspectionWorlds = createLodWorldInspections(
2681
- worlds,
2792
+ compositionWorlds,
2682
2793
  renderables,
2683
2794
  submissionRenderables,
2684
2795
  inspectionCamera,
@@ -2697,8 +2808,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2697
2808
  : { active: false },
2698
2809
  );
2699
2810
  const attachmentByWorldKey = new Map<number, string>();
2700
- for (let worldId = 0; worldId < worlds.length; worldId += 1) {
2701
- const world = worlds[worldId];
2811
+ for (let worldId = 0; worldId < compositionWorlds.length; worldId += 1) {
2812
+ const world = compositionWorlds[worldId];
2702
2813
  if (world === undefined) continue;
2703
2814
  const worldKey = worldKeys[worldId] ?? worldId;
2704
2815
  if (attachmentByWorldKey.has(worldKey)) {
@@ -2717,7 +2828,8 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2717
2828
  sourceKey: `render-frame:${frameState.frameNumber}`,
2718
2829
  },
2719
2830
  view: {
2720
- attachmentId: worlds[inspectionCamera.worldId ?? cameraOwner]?.identity ?? 'none',
2831
+ attachmentId:
2832
+ compositionWorlds[inspectionCamera.worldId ?? cameraOwner]?.identity ?? 'none',
2721
2833
  cameraEntity: inspectionCamera.entityKey ?? 0,
2722
2834
  viewRole: 'main',
2723
2835
  viewGeneration: inspectionCamera.historyVersion ?? 0,
@@ -2859,7 +2971,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
2859
2971
  const preparedResourceBatches = runProfiledRenderPhase(profileSession, 'features', () => {
2860
2972
  if (internals.featureHost === undefined) return [];
2861
2973
  const featureFrame = runRenderFeatureFrame(internals.featureHost, {
2862
- worlds,
2974
+ worlds: compositionWorlds,
2863
2975
  owner: resourceOwner,
2864
2976
  frameNumber: frameState.frameNumber,
2865
2977
  visibilitySnapshots: frame.featureVisibilitySnapshots,
@@ -3021,7 +3133,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3021
3133
  skybox,
3022
3134
  skyboxCount,
3023
3135
  postProcessParams,
3024
- worlds,
3136
+ compositionWorlds,
3025
3137
  recordProfilePhase,
3026
3138
  {
3027
3139
  owner: gpuDrivenProduction,
@@ -3034,7 +3146,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3034
3146
  telemetryCandidateCount: lodCandidateCountForFrame,
3035
3147
  ...(occlusionProjection === undefined ? {} : { occlusion: occlusionProjection }),
3036
3148
  },
3037
- renderReadLeases,
3149
+ compositionLeases,
3038
3150
  featureGraphCandidate,
3039
3151
  pointsLinesOwner,
3040
3152
  {
@@ -3051,6 +3163,14 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3051
3163
  timingCapture,
3052
3164
  ),
3053
3165
  );
3166
+ if (submitted) {
3167
+ try {
3168
+ internals.recoveryColdWorkGuard?.finish();
3169
+ } catch (cause) {
3170
+ submitted = false;
3171
+ throw cause;
3172
+ }
3173
+ }
3054
3174
  if (submitted && lastLodOcclusionInspection !== undefined) {
3055
3175
  const query = ensureOcclusionRuntime().inspect();
3056
3176
  lastLodOcclusionInspection = {
@@ -3090,6 +3210,16 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3090
3210
  };
3091
3211
  }
3092
3212
  if (submitted) {
3213
+ recoveryOwner.setLastSuccessfulFrameSeed({
3214
+ frame,
3215
+ worlds: compositionWorlds,
3216
+ cameraOwner: composition.owners.cameraOwner,
3217
+ resourceOwner: composition.owners.resourceOwner,
3218
+ width: Math.max(1, internals.canvas.width),
3219
+ height: Math.max(1, internals.canvas.height),
3220
+ });
3221
+ submittedFrameCount += 1;
3222
+ lastSubmittedGeneration = internals.deviceScope?.generation;
3093
3223
  if (occlusionProjection === undefined) {
3094
3224
  runProfiledRenderPhase(profileSession, 'record/occlusion-global-advance', () =>
3095
3225
  ensureOcclusionRuntime().advanceSuccessfulSubmit(frameState.frameNumber),
@@ -3226,6 +3356,9 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3226
3356
  get meshMaterialBindings(): readonly MeshMaterialBindingObservation[] {
3227
3357
  return lastMeshMaterialBindings;
3228
3358
  },
3359
+ get iblBinding(): import('./mesh-material-bindings').IblBindingInspection | undefined {
3360
+ return frameState.iblBindingInspection;
3361
+ },
3229
3362
  get perFramePassNames(): readonly string[] {
3230
3363
  return frameState.compiledFrameGraph?.inspect().passes.map((pass) => pass.name) ?? [];
3231
3364
  },
@@ -3235,6 +3368,25 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3235
3368
  get perFrameGraphInfo(): CompiledRenderGraphInfo | undefined {
3236
3369
  return frameState.compiledFrameGraph?.inspect();
3237
3370
  },
3371
+ get recoveryEvidence(): RecoveryProductionEvidence {
3372
+ const graph = frameState.compiledFrameGraph?.inspect();
3373
+ return Object.freeze({
3374
+ producerRoots: createRendererProducerRootMatrix(),
3375
+ graph: Object.freeze({
3376
+ ready: graph !== undefined,
3377
+ generation: frameState.graphGeneration,
3378
+ passCount: graph?.passes.length ?? 0,
3379
+ resourceCount: graph?.resources.length ?? 0,
3380
+ }),
3381
+ residency: Object.freeze({
3382
+ meshResidencyEpoch: internals.gpuStore.meshResidencyEpoch,
3383
+ }),
3384
+ submissions: Object.freeze({
3385
+ count: submittedFrameCount,
3386
+ lastGeneration: lastSubmittedGeneration,
3387
+ }),
3388
+ });
3389
+ },
3238
3390
  get volumetricFog(): import('./volume/inspection').VolumetricFogInspection {
3239
3391
  return frameState.volumetricFogInspection;
3240
3392
  },
@@ -3330,6 +3482,11 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3330
3482
  throw cause;
3331
3483
  }
3332
3484
  },
3485
+ lookupPostProcess,
3486
+ prepareRecoveryGraphCandidate: recoveryOwner.prepareRecoveryGraphCandidate,
3487
+ submitCandidateSetup: recoveryOwner.submitCandidateSetup,
3488
+ publishRecoveryGraphCandidate: recoveryOwner.publishRecoveryGraphCandidate,
3489
+ discardRecoveryGraphCandidate: recoveryOwner.discardRecoveryGraphCandidate,
3333
3490
  disposeFrameState(): void {
3334
3491
  (internals as TimingSessionHost).gpuPassTimingSession?.dispose();
3335
3492
  (internals as TimingSessionHost).gpuPassTimingSession = undefined;
@@ -3337,6 +3494,7 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3337
3494
  (internals as TimingSessionHost).gpuPassTimingSubmittedWork = undefined;
3338
3495
  (internals as TimingSessionHost).gpuPassTimingFrameIdentity = undefined;
3339
3496
  (internals as TimingSessionHost).gpuPassTimingBeginReason = undefined;
3497
+ pointsLinesOwner.dispose();
3340
3498
  persistentRenderScene.dispose();
3341
3499
  gpuDrivenProduction.dispose();
3342
3500
  occlusionRuntime?.dispose();
@@ -3436,7 +3594,14 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3436
3594
  frameState.pointShadowAtlas = null;
3437
3595
  }
3438
3596
  },
3439
- resetForRecover(): void {
3597
+ resetForRecover(retiringPipelineState?: PipelineState, replacementDevice?: RhiDevice): void {
3598
+ frameState.lastSuccessfulTemporalView = undefined;
3599
+ frameState.successfulTemporalFrameIndex = 0;
3600
+ frameState.pendingTemporalCommit = { kind: 'none' };
3601
+ // The active expansion buffers belong to the lost generation. Abandon
3602
+ // their handles without invoking destroy on a device that can no longer
3603
+ // service cleanup; the staged candidate owns the replacement buffers.
3604
+ pointsLinesOwner.abandonForDeviceLoss();
3440
3605
  ssrFormatReceipt = undefined;
3441
3606
  ssrFormatProbeGeneration = -1;
3442
3607
  ssrFormatProbe = undefined;
@@ -3457,18 +3622,28 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3457
3622
  updateTransmissionInspection(lastTransmissionAntialias, false);
3458
3623
  persistentRenderScene.resetGpuForRecover();
3459
3624
  gpuDrivenProduction.dispose();
3460
- gpuDrivenProduction = new GpuDrivenProduction(internals.device, gpuDrivenShaderFactory);
3625
+ // The active renderer still points at the lost device until the caller
3626
+ // completes the synchronous publication boundary. Bind the replacement
3627
+ // explicitly so the first published draw never routes through an owner
3628
+ // recreated against the old physical device.
3629
+ gpuDrivenProduction = GpuDrivenProduction.forDevice(
3630
+ replacementDevice ?? internals.device,
3631
+ gpuDrivenShaderFactory,
3632
+ );
3461
3633
  occlusionRuntime?.dispose();
3462
3634
  // The replacement device is installed by the host after this hook. Do
3463
3635
  // not recreate device-bound query resources against the lost device;
3464
3636
  // the first post-recovery frame lazily binds the current device.
3465
3637
  occlusionRuntime = undefined;
3466
3638
  disposeFeatureGpuWork();
3639
+ featureGpuWork = createFeatureGpuWorkOwner(internals);
3467
3640
  reflectionProbeOwner.dispose();
3468
3641
  // A recovered device cannot retain handles from the lost device. The
3469
3642
  // Bloom owner abandons candidates and retires every committed generation
3470
3643
  // before the new PipelineState is rebuilt.
3471
- internals.getPipelineState()?.perPassResources.drainBloomResources?.();
3644
+ (
3645
+ retiringPipelineState ?? internals.getPipelineState()
3646
+ )?.perPassResources.drainBloomResources?.();
3472
3647
  // feat-20260622-s5 M3 / B-2 / w18: recover() rebuild drops device-bound
3473
3648
  // state minted by the lost device. The active graph and per-entity caches
3474
3649
  // must be discarded, not merely marked for destruction: their opaque
@@ -3554,113 +3729,147 @@ export function createRenderSystem(internals: RenderSystemInternals): RenderSyst
3554
3729
  // the lost device, so recovery must invalidate this cache alongside the
3555
3730
  // feature-host declarations and UBOs below.
3556
3731
  postProcessPipelineCache.clear();
3557
- // Logical declarations stay live in the feature host while a replacement
3558
- // device is being requested.
3559
- postProcessParamsBuffers.clear();
3732
+ // Logical declarations and the active params bundle stay live until the
3733
+ // candidate post-process bundle crosses the publication boundary.
3560
3734
  resetRenderFeatureGraphState(internals);
3561
3735
  },
3736
+ prepareRecoveryPostProcessResources(device: RhiDevice) {
3737
+ return prepareRecoveryPostProcessResources(device);
3738
+ },
3739
+ publishRecoveryPostProcessResources(candidate: RecoveryPostProcessResources): void {
3740
+ publishRecoveryPostProcessResources(candidate);
3741
+ },
3742
+ discardRecoveryPostProcessResources(candidate: RecoveryPostProcessResources): void {
3743
+ discardRecoveryPostProcessResources(candidate);
3744
+ },
3562
3745
  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,
3746
+ const prepared = prepareRecoveryPostProcessResources(internals.device);
3747
+ if (!prepared.ok) throw prepared.error;
3748
+ publishRecoveryPostProcessResources(prepared.value);
3749
+ },
3750
+ prepareRecoveryRoots(runtime: RecoveryRootRuntime): RecoveryRootBundle {
3751
+ // Snapshot only CPU projections/declarations here. The root create
3752
+ // callbacks below must never close over a mutable active owner while
3753
+ // LifecycleTransaction is awaiting candidate construction.
3754
+ const candidateEnvironmentLifecycle = environmentLifecycle.createRecoveryCandidate(
3755
+ runtime.scope,
3756
+ );
3757
+ const roots: LifecycleResourceSpec<unknown>[] = [];
3758
+ let candidateEnvironmentGeneration: EnvironmentGeneration | undefined;
3759
+ let environmentRootCleaned = false;
3760
+ let published = false;
3761
+ if (candidateEnvironmentLifecycle.hasRecoveryFrame()) {
3762
+ const environmentRoot = candidateEnvironmentLifecycle.createRecoveryRoot(runtime.scope);
3763
+ roots.push({
3764
+ ...environmentRoot,
3765
+ create: async () => {
3766
+ const generation = (await environmentRoot.create()) as EnvironmentGeneration;
3767
+ candidateEnvironmentGeneration = generation;
3768
+ return generation;
3769
+ },
3770
+ cleanup: (value) => {
3771
+ if (environmentRootCleaned) return;
3772
+ environmentRootCleaned = true;
3773
+ if (candidateEnvironmentGeneration === value) {
3774
+ candidateEnvironmentGeneration = undefined;
3775
+ }
3776
+ if (published) {
3777
+ candidateEnvironmentLifecycle.retirePublishedGeneration(
3778
+ value as EnvironmentGeneration,
3779
+ );
3780
+ } else {
3781
+ environmentRoot.cleanup(value);
3782
+ }
3783
+ },
3784
+ });
3785
+ }
3786
+ const graphCandidate = runtime.graphCandidate;
3787
+ if (graphCandidate !== undefined) {
3788
+ const releaseGraphCandidate = (): void => graphCandidate.release();
3789
+ const compiledGraph = graphCandidate.frameState.compiledFrameGraph;
3790
+ if (compiledGraph !== null) {
3791
+ roots.push({
3792
+ kind: 'pipeline',
3793
+ create: () => {
3794
+ if (!runtime.scope.isAlive()) {
3795
+ throw new Error('Recovery graph candidate scope is not active.');
3796
+ }
3797
+ if (graphCandidate.frameState.compiledFrameGraph !== compiledGraph) {
3798
+ throw new Error('Recovery graph candidate was changed before aggregate assembly.');
3799
+ }
3800
+ return compiledGraph;
3801
+ },
3802
+ // All graph/feature/GPU-driven candidate resources share one
3803
+ // owner. The root is a real release seam, not a marker; the
3804
+ // candidate latch makes every root cleanup call harmless after
3805
+ // the first one.
3806
+ cleanup: releaseGraphCandidate,
3590
3807
  });
3591
- if (!created.ok) throw created.error;
3592
- buffer = created.value;
3593
3808
  }
3594
- if (buffer !== undefined && lookupPostProcess(id) === entry) {
3595
- postProcessParamsBuffers.set(id, buffer);
3596
- } else if (buffer !== undefined) {
3597
- internals.device.destroyBuffer(buffer);
3809
+ if (graphCandidate.featureHost !== undefined) {
3810
+ const featureRoot = graphCandidate.featureHost.createRecoveryRoot(runtime.scope);
3811
+ roots.push({ ...featureRoot, cleanup: releaseGraphCandidate });
3812
+ }
3813
+ if (graphCandidate.featureGpuWork !== undefined) {
3814
+ const featureWorkRoot = graphCandidate.featureGpuWork.createRecoveryRoot(runtime.scope);
3815
+ roots.push({ ...featureWorkRoot, cleanup: releaseGraphCandidate });
3816
+ }
3817
+ if (graphCandidate.gpuDrivenProduction !== undefined) {
3818
+ const productionRoot = graphCandidate.gpuDrivenProduction.createRecoveryRoot(
3819
+ runtime.scope,
3820
+ );
3821
+ roots.push({ ...productionRoot, cleanup: releaseGraphCandidate });
3822
+ }
3823
+ if (graphCandidate.gpuDrivenScene?.state !== undefined) {
3824
+ const sceneRoot = graphCandidate.gpuDrivenScene.createRecoveryRoot(runtime.scope);
3825
+ roots.push({ ...sceneRoot, cleanup: releaseGraphCandidate });
3826
+ }
3827
+ if (graphCandidate.pointsLines !== undefined) {
3828
+ const pointsLinesRoot = graphCandidate.pointsLines.createRecoveryRoot(runtime.scope);
3829
+ roots.push({ ...pointsLinesRoot, cleanup: releaseGraphCandidate });
3830
+ }
3831
+ if (runtime.gpuStore.recoveryResourceCount() > 0) {
3832
+ const gpuStoreRoot = runtime.gpuStore.createRecoveryRoot(runtime.scope);
3833
+ roots.push({ ...gpuStoreRoot, cleanup: releaseGraphCandidate });
3598
3834
  }
3599
3835
  }
3836
+ let discarded = false;
3837
+ return {
3838
+ roots: Object.freeze(roots),
3839
+ publish: () => {
3840
+ if (published || discarded) return;
3841
+ if (
3842
+ candidateEnvironmentGeneration !== undefined &&
3843
+ !candidateEnvironmentGeneration.retired
3844
+ ) {
3845
+ environmentLifecycle.retireActiveForReplacement();
3846
+ candidateEnvironmentLifecycle.publish(candidateEnvironmentGeneration);
3847
+ }
3848
+ published = true;
3849
+ environmentLifecycle = candidateEnvironmentLifecycle;
3850
+ Object.assign(frameState, { environmentLifecycle });
3851
+ },
3852
+ discard: () => {
3853
+ if (published || discarded) return;
3854
+ discarded = true;
3855
+ if (candidateEnvironmentGeneration !== undefined) {
3856
+ candidateEnvironmentLifecycle.discard(candidateEnvironmentGeneration);
3857
+ candidateEnvironmentGeneration = undefined;
3858
+ }
3859
+ candidateEnvironmentLifecycle.discardRecoveryCandidates();
3860
+ // The transaction can fail before it reaches any graph root (for
3861
+ // example while the environment root is created). Keep the graph,
3862
+ // feature, GPU-driven, and residency resources on the same real
3863
+ // candidate owner in that case; later scope/factory cleanup is a
3864
+ // harmless repeat through the candidate latch.
3865
+ graphCandidate?.release();
3866
+ runtime.scope.abandon();
3867
+ },
3868
+ };
3600
3869
  },
3601
3870
  };
3602
3871
  }
3603
3872
 
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
3873
  /** Resolve the build identity used by every inspection submit. */
3665
3874
  export function resolveInspectionBuild(internals: Pick<RenderSystemInternals, 'build'>): string {
3666
3875
  return internals.build ?? 'render-system';