@forgeax/engine-render 0.1.28 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (642) hide show
  1. package/README.md +627 -124
  2. package/dist/assembly/atmosphere-shader-prewarm.d.ts +6 -0
  3. package/dist/assembly/atmosphere-shader-prewarm.d.ts.map +1 -0
  4. package/dist/assembly/dynamic-geometry-host.d.ts +21 -0
  5. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  6. package/dist/assembly/dynamic-geometry-runtime.d.ts +20 -0
  7. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +3 -1
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/gpu-driven-owner.d.ts +10 -0
  11. package/dist/assembly/gpu-driven-owner.d.ts.map +1 -0
  12. package/dist/assembly/host-contract.d.ts +8 -0
  13. package/dist/assembly/host-contract.d.ts.map +1 -1
  14. package/dist/assembly/material/assembly.d.ts +7 -0
  15. package/dist/assembly/material/assembly.d.ts.map +1 -1
  16. package/dist/assembly/material-shader-policy.d.ts +3 -1
  17. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  18. package/dist/assembly/recovery/failure-location.d.ts +12 -0
  19. package/dist/assembly/recovery/failure-location.d.ts.map +1 -0
  20. package/dist/assembly/recovery/renderer-recover.d.ts +6 -4
  21. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  22. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  23. package/dist/assembly/renderer-feature-inspection.d.ts +5 -0
  24. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -0
  25. package/dist/assembly/renderer-frame-transaction.d.ts +19 -0
  26. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  27. package/dist/assembly/renderer-helpers.d.ts +3 -0
  28. package/dist/assembly/renderer-helpers.d.ts.map +1 -1
  29. package/dist/assembly/renderer-inspection.d.ts +4 -0
  30. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  31. package/dist/assembly/shader-prewarm-policy.d.ts +6 -0
  32. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -1
  33. package/dist/assembly/skin-palette-owner.d.ts +8 -0
  34. package/dist/assembly/skin-palette-owner.d.ts.map +1 -0
  35. package/dist/assembly/ssr-shader-prewarm.d.ts +6 -0
  36. package/dist/assembly/ssr-shader-prewarm.d.ts.map +1 -0
  37. package/dist/assembly/webgpu-device-loss.d.ts +15 -0
  38. package/dist/assembly/webgpu-device-loss.d.ts.map +1 -0
  39. package/dist/assembly/webgpu-pbr-ready.d.ts +38 -0
  40. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -0
  41. package/dist/assembly/webgpu-ready-mipmap.d.ts +11 -0
  42. package/dist/assembly/webgpu-ready-mipmap.d.ts.map +1 -0
  43. package/dist/assembly/webgpu-ready.d.ts +7 -3
  44. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  45. package/dist/assembly/webgpu-renderer-bootstrap.d.ts +10 -0
  46. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -0
  47. package/dist/assembly/webgpu-renderer-contract.d.ts +62 -0
  48. package/dist/assembly/webgpu-renderer-contract.d.ts.map +1 -0
  49. package/dist/assembly/webgpu-renderer-guards.d.ts +9 -0
  50. package/dist/assembly/webgpu-renderer-guards.d.ts.map +1 -0
  51. package/dist/assembly/webgpu-renderer-observation.d.ts +6 -0
  52. package/dist/assembly/webgpu-renderer-observation.d.ts.map +1 -0
  53. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts +12 -0
  54. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts.map +1 -0
  55. package/dist/assembly/webgpu-renderer.d.ts +7 -141
  56. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  57. package/dist/assembly/webgpu-vertex-layouts.d.ts +28 -84
  58. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -1
  59. package/dist/authoring.mjs +2 -2
  60. package/dist/{chunk-JAASCMXA.mjs → chunk-33XZFTQU.mjs} +245 -39
  61. package/dist/chunk-33XZFTQU.mjs.map +1 -0
  62. package/dist/{chunk-GF523LHF.mjs → chunk-BLXGONR2.mjs} +30 -12
  63. package/dist/chunk-BLXGONR2.mjs.map +1 -0
  64. package/dist/{chunk-J52NMQKR.mjs → chunk-BQVQOQJH.mjs} +1086 -838
  65. package/dist/chunk-BQVQOQJH.mjs.map +1 -0
  66. package/dist/{chunk-IQMKJLNU.mjs → chunk-BTWH7BP4.mjs} +386 -17
  67. package/dist/chunk-BTWH7BP4.mjs.map +1 -0
  68. package/dist/{chunk-MBU4ZVE7.mjs → chunk-FUODTTIA.mjs} +3921 -671
  69. package/dist/chunk-FUODTTIA.mjs.map +1 -0
  70. package/dist/{chunk-MYJ5RPJO.mjs → chunk-JQTSEI3Z.mjs} +3 -3
  71. package/dist/{chunk-MYJ5RPJO.mjs.map → chunk-JQTSEI3Z.mjs.map} +1 -1
  72. package/dist/{chunk-GE3SDD72.mjs → chunk-NICXSHAT.mjs} +16 -38
  73. package/dist/chunk-NICXSHAT.mjs.map +1 -0
  74. package/dist/{chunk-J6KACNOH.mjs → chunk-NRNXBLIJ.mjs} +577 -7
  75. package/dist/chunk-NRNXBLIJ.mjs.map +1 -0
  76. package/dist/{chunk-RBDBZ5R7.mjs → chunk-PA5DRJNI.mjs} +2 -2
  77. package/dist/chunk-PA5DRJNI.mjs.map +1 -0
  78. package/dist/{chunk-VSL23LZE.mjs → chunk-S4DNKQYQ.mjs} +1117 -445
  79. package/dist/chunk-S4DNKQYQ.mjs.map +1 -0
  80. package/dist/components/camera.d.ts +62 -2
  81. package/dist/components/camera.d.ts.map +1 -1
  82. package/dist/components/index.d.ts +1 -0
  83. package/dist/components/index.d.ts.map +1 -1
  84. package/dist/components/instances.d.ts +6 -20
  85. package/dist/components/instances.d.ts.map +1 -1
  86. package/dist/components/scene-instance.d.ts +2 -1
  87. package/dist/components/scene-instance.d.ts.map +1 -1
  88. package/dist/components/screen-space-reflection.d.ts +14 -0
  89. package/dist/components/screen-space-reflection.d.ts.map +1 -0
  90. package/dist/construct-renderer.mjs +12888 -7025
  91. package/dist/construct-renderer.mjs.map +1 -1
  92. package/dist/device/gpu-residency.d.ts +1 -12
  93. package/dist/device/gpu-residency.d.ts.map +1 -1
  94. package/dist/environment/background.d.ts +6 -0
  95. package/dist/environment/background.d.ts.map +1 -0
  96. package/dist/errors/gpu-driven.d.ts +6 -3
  97. package/dist/errors/gpu-driven.d.ts.map +1 -1
  98. package/dist/errors/render.d.ts +7 -4
  99. package/dist/errors/render.d.ts.map +1 -1
  100. package/dist/extract/camera.d.ts.map +1 -1
  101. package/dist/extract/gpu-driven.d.ts +33 -3
  102. package/dist/extract/gpu-driven.d.ts.map +1 -1
  103. package/dist/features/host.d.ts +23 -1
  104. package/dist/features/host.d.ts.map +1 -1
  105. package/dist/features/noise-texture.d.ts +17 -0
  106. package/dist/features/noise-texture.d.ts.map +1 -0
  107. package/dist/features/plan.d.ts +83 -2
  108. package/dist/features/plan.d.ts.map +1 -1
  109. package/dist/features/prepared-gpu-work.d.ts +37 -4
  110. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  111. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  112. package/dist/features/prepared-graphics.d.ts +5 -0
  113. package/dist/features/prepared-graphics.d.ts.map +1 -1
  114. package/dist/features/render-graph-compute.d.ts +5 -3
  115. package/dist/features/render-graph-compute.d.ts.map +1 -1
  116. package/dist/features/render-graph-contribution.d.ts +11 -3
  117. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  118. package/dist/features/render-graph-raster.d.ts +19 -3
  119. package/dist/features/render-graph-raster.d.ts.map +1 -1
  120. package/dist/features/types.d.ts +11 -0
  121. package/dist/features/types.d.ts.map +1 -1
  122. package/dist/fullscreen-post-process-pass.d.ts +2 -0
  123. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  124. package/dist/gpu-driven/batch-topology.d.ts +34 -1
  125. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  126. package/dist/gpu-driven/pbr-program.d.ts +10 -0
  127. package/dist/gpu-driven/pbr-program.d.ts.map +1 -0
  128. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  129. package/dist/gpu-driven/production-raster.d.ts +144 -23
  130. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  131. package/dist/gpu-driven/shadow-views.d.ts +126 -0
  132. package/dist/gpu-driven/shadow-views.d.ts.map +1 -0
  133. package/dist/gpu-driven/view-gpu.d.ts +18 -2
  134. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  135. package/dist/gpu-scene-schema.d.ts +2 -8
  136. package/dist/gpu-scene-schema.d.ts.map +1 -1
  137. package/dist/gpu-scene.d.ts +14 -0
  138. package/dist/gpu-scene.d.ts.map +1 -1
  139. package/dist/gpu-texture-usage.d.ts +1 -0
  140. package/dist/gpu-texture-usage.d.ts.map +1 -1
  141. package/dist/hdrp-buffers.d.ts +6 -0
  142. package/dist/hdrp-buffers.d.ts.map +1 -1
  143. package/dist/ibl/frame-resources.d.ts +8 -0
  144. package/dist/ibl/frame-resources.d.ts.map +1 -0
  145. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  146. package/dist/index.d.ts +18 -8
  147. package/dist/index.d.ts.map +1 -1
  148. package/dist/index.mjs +8 -8
  149. package/dist/inspection-types.d.ts +164 -0
  150. package/dist/inspection-types.d.ts.map +1 -1
  151. package/dist/instances-derived-bounds.d.ts +11 -0
  152. package/dist/instances-derived-bounds.d.ts.map +1 -1
  153. package/dist/instances.d.ts +89 -44
  154. package/dist/instances.d.ts.map +1 -1
  155. package/dist/internal.d.ts +3 -0
  156. package/dist/internal.d.ts.map +1 -1
  157. package/dist/internal.mjs +117 -9
  158. package/dist/internal.mjs.map +1 -1
  159. package/dist/lifecycle.d.ts +2 -1
  160. package/dist/lifecycle.d.ts.map +1 -1
  161. package/dist/material-row.d.ts +5 -0
  162. package/dist/material-row.d.ts.map +1 -0
  163. package/dist/materials.d.ts +15 -0
  164. package/dist/materials.d.ts.map +1 -1
  165. package/dist/pbr-pipeline.d.ts +21 -5
  166. package/dist/pbr-pipeline.d.ts.map +1 -1
  167. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  168. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts +12 -0
  169. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts.map +1 -0
  170. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts +70 -0
  171. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts.map +1 -0
  172. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts +75 -0
  173. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts.map +1 -0
  174. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts +111 -0
  175. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts.map +1 -0
  176. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts +16 -0
  177. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts.map +1 -0
  178. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts +14 -0
  179. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts.map +1 -0
  180. package/dist/pipeline/standard-output/auto-exposure/state.d.ts +46 -0
  181. package/dist/pipeline/standard-output/auto-exposure/state.d.ts.map +1 -0
  182. package/dist/pipeline/standard-output/color-transform.d.ts +27 -0
  183. package/dist/pipeline/standard-output/color-transform.d.ts.map +1 -0
  184. package/dist/pipeline/standard-output/graph.d.ts +15 -0
  185. package/dist/pipeline/standard-output/graph.d.ts.map +1 -0
  186. package/dist/pipeline/standard-output/lut-admission.d.ts +29 -0
  187. package/dist/pipeline/standard-output/lut-admission.d.ts.map +1 -0
  188. package/dist/pipeline/standard-output/lut-gpu.d.ts +48 -0
  189. package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -0
  190. package/dist/pipeline/standard-output/lut-state.d.ts +68 -0
  191. package/dist/pipeline/standard-output/lut-state.d.ts.map +1 -0
  192. package/dist/pipeline/standard-output/resources.d.ts +10 -0
  193. package/dist/pipeline/standard-output/resources.d.ts.map +1 -0
  194. package/dist/pipeline/standard-output/types.d.ts +39 -0
  195. package/dist/pipeline/standard-output/types.d.ts.map +1 -0
  196. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  197. package/dist/pipeline/standard-post.d.ts +2 -2
  198. package/dist/pipeline/standard-post.d.ts.map +1 -1
  199. package/dist/pipeline-builder.d.ts +2 -1
  200. package/dist/pipeline-builder.d.ts.map +1 -1
  201. package/dist/pipeline-spec-types.d.ts +4 -0
  202. package/dist/pipeline-spec-types.d.ts.map +1 -1
  203. package/dist/pipeline-spec.d.ts +1 -9
  204. package/dist/pipeline-spec.d.ts.map +1 -1
  205. package/dist/points-lines/standard-owner.d.ts +1 -1
  206. package/dist/points-lines/standard-owner.d.ts.map +1 -1
  207. package/dist/prepare/prepared-graphics-resolver.d.ts +6 -1
  208. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  209. package/dist/record/dynamic-geometry-consumption.d.ts +15 -0
  210. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -0
  211. package/dist/record/frame-lighting.d.ts +3 -3
  212. package/dist/record/frame-lighting.d.ts.map +1 -1
  213. package/dist/record/frame-snapshot.d.ts +106 -5
  214. package/dist/record/frame-snapshot.d.ts.map +1 -1
  215. package/dist/record/frame-targets.d.ts.map +1 -1
  216. package/dist/record/frame.d.ts +39 -4
  217. package/dist/record/frame.d.ts.map +1 -1
  218. package/dist/record/main-pass-geometry.d.ts +7 -8
  219. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  220. package/dist/record/main-pass-material.d.ts +38 -1
  221. package/dist/record/main-pass-material.d.ts.map +1 -1
  222. package/dist/record/main-pass-sprite-draws.d.ts +2 -10
  223. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  224. package/dist/record/main-pass.d.ts +12 -2
  225. package/dist/record/main-pass.d.ts.map +1 -1
  226. package/dist/record/mesh-ssbo.d.ts +15 -6
  227. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  228. package/dist/record/prepared-material-bindings.d.ts +19 -0
  229. package/dist/record/prepared-material-bindings.d.ts.map +1 -0
  230. package/dist/record/recovery-pipeline.d.ts +2 -9
  231. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  232. package/dist/record/render-context.d.ts +74 -26
  233. package/dist/record/render-context.d.ts.map +1 -1
  234. package/dist/record/shadow-pass.d.ts +62 -7
  235. package/dist/record/shadow-pass.d.ts.map +1 -1
  236. package/dist/record/skybox-post-pass.d.ts +1 -1
  237. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  238. package/dist/record/typed-frame-graph.d.ts +35 -7
  239. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  240. package/dist/record/view-ubo.d.ts +1 -0
  241. package/dist/record/view-ubo.d.ts.map +1 -1
  242. package/dist/recovery/render-system-candidate.d.ts +10 -2
  243. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  244. package/dist/recovery/render-system-roots.d.ts +11 -0
  245. package/dist/recovery/render-system-roots.d.ts.map +1 -0
  246. package/dist/recovery/types.d.ts +76 -0
  247. package/dist/recovery/types.d.ts.map +1 -0
  248. package/dist/reflection/record-owner.d.ts +0 -2
  249. package/dist/reflection/record-owner.d.ts.map +1 -1
  250. package/dist/render-contract.d.ts +173 -30
  251. package/dist/render-contract.d.ts.map +1 -1
  252. package/dist/render-graph-primitives.d.ts +1 -0
  253. package/dist/render-graph-primitives.d.ts.map +1 -1
  254. package/dist/render-pipeline.d.ts +64 -5
  255. package/dist/render-pipeline.d.ts.map +1 -1
  256. package/dist/render-system-extract-tail.d.ts +7 -5
  257. package/dist/render-system-extract-tail.d.ts.map +1 -1
  258. package/dist/render-system-extract.d.ts +101 -41
  259. package/dist/render-system-extract.d.ts.map +1 -1
  260. package/dist/render-system-projections.d.ts +22 -0
  261. package/dist/render-system-projections.d.ts.map +1 -0
  262. package/dist/render-system.d.ts +27 -77
  263. package/dist/render-system.d.ts.map +1 -1
  264. package/dist/scene/render-scene-types.d.ts +14 -7
  265. package/dist/scene/render-scene-types.d.ts.map +1 -1
  266. package/dist/scene/render-scene.d.ts +29 -11
  267. package/dist/scene/render-scene.d.ts.map +1 -1
  268. package/dist/scene/shadow-visibility.d.ts +5 -0
  269. package/dist/scene/shadow-visibility.d.ts.map +1 -0
  270. package/dist/ssr/admission.d.ts +70 -1
  271. package/dist/ssr/admission.d.ts.map +1 -1
  272. package/dist/ssr/compose.d.ts +13 -0
  273. package/dist/ssr/compose.d.ts.map +1 -0
  274. package/dist/ssr/errors.d.ts +35 -0
  275. package/dist/ssr/errors.d.ts.map +1 -1
  276. package/dist/ssr/graph.d.ts +60 -0
  277. package/dist/ssr/graph.d.ts.map +1 -0
  278. package/dist/ssr/history.d.ts +97 -0
  279. package/dist/ssr/history.d.ts.map +1 -0
  280. package/dist/ssr/hiz.d.ts +37 -0
  281. package/dist/ssr/hiz.d.ts.map +1 -0
  282. package/dist/ssr/inspection.d.ts +42 -0
  283. package/dist/ssr/inspection.d.ts.map +1 -0
  284. package/dist/ssr/resources.d.ts +58 -0
  285. package/dist/ssr/resources.d.ts.map +1 -0
  286. package/dist/ssr/temporal.d.ts +38 -0
  287. package/dist/ssr/temporal.d.ts.map +1 -0
  288. package/dist/systems/skin-palette-allocator.d.ts +32 -10
  289. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  290. package/dist/systems/skin-palette-types.d.ts +15 -0
  291. package/dist/systems/skin-palette-types.d.ts.map +1 -1
  292. package/dist/systems/transparent-dispatch.d.ts +9 -0
  293. package/dist/systems/transparent-dispatch.d.ts.map +1 -0
  294. package/dist/targets/material-source.d.ts +7 -0
  295. package/dist/targets/material-source.d.ts.map +1 -1
  296. package/dist/temporal/frame-coordinator.d.ts +7 -0
  297. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  298. package/dist/temporal/gpu.d.ts +8 -0
  299. package/dist/temporal/gpu.d.ts.map +1 -1
  300. package/dist/temporal/history.d.ts.map +1 -1
  301. package/dist/temporal/index.mjs +6 -6
  302. package/dist/temporal/inspection.d.ts.map +1 -1
  303. package/dist/temporal/standard-scene-data.d.ts +2 -1
  304. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  305. package/dist/temporal/temporal-view.d.ts +2 -0
  306. package/dist/temporal/temporal-view.d.ts.map +1 -1
  307. package/dist/typed-render-graph-primitives.d.ts +35 -1
  308. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  309. package/dist/typed-shadow-passes.d.ts +3 -1
  310. package/dist/typed-shadow-passes.d.ts.map +1 -1
  311. package/dist/volume/temporal.d.ts +6 -0
  312. package/dist/volume/temporal.d.ts.map +1 -1
  313. package/package.json +21 -20
  314. package/src/__tests__/auto-exposure-gpu-evidence.ts +291 -0
  315. package/src/__tests__/auto-exposure-inspection.unit.test.ts +80 -0
  316. package/src/__tests__/auto-exposure-transaction.integration.test.ts +99 -0
  317. package/src/__tests__/auto-exposure.browser.test.ts +33 -0
  318. package/src/__tests__/auto-exposure.dawn.test.ts +33 -0
  319. package/src/__tests__/detached-inspection.integration.test.ts +5 -0
  320. package/src/__tests__/device-scope-lifecycle.unit.test.ts +18 -0
  321. package/src/__tests__/device-scope-stale-matrix.unit.test.ts +18 -0
  322. package/src/__tests__/docs-gate-green.unit.test.ts +3 -3
  323. package/src/__tests__/factory-contract.integration.test.ts +67 -8
  324. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +19 -0
  325. package/src/__tests__/feature-depth-input.dawn.test.ts +121 -0
  326. package/src/__tests__/feature-noise.unit.test.ts +38 -0
  327. package/src/__tests__/fixtures/taa-resolve-pinned-baseline.wgsl +301 -0
  328. package/src/__tests__/frame-camera-selection.unit.test.ts +131 -0
  329. package/src/__tests__/frame-plan-contract.unit.test.ts +24 -0
  330. package/src/__tests__/frame-targets.unit.test.ts +24 -0
  331. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +3 -0
  332. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +79 -0
  333. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +287 -6
  334. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +122 -142
  335. package/src/__tests__/gpu-driven-indirect-raster.browser.test.ts +10 -1
  336. package/src/__tests__/gpu-driven-lod-cache.unit.test.ts +264 -0
  337. package/src/__tests__/gpu-driven-mesh-world-identity.unit.test.ts +349 -0
  338. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +1417 -0
  339. package/src/__tests__/gpu-driven-pbr.integration.test.ts +21 -0
  340. package/src/__tests__/gpu-driven-production-fixture.ts +96 -0
  341. package/src/__tests__/gpu-driven-production.integration.test.ts +1075 -93
  342. package/src/__tests__/gpu-driven-scaling.unit.test.ts +1 -3
  343. package/src/__tests__/gpu-driven-shadow-views.browser.test.ts +63 -0
  344. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +294 -0
  345. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +97 -0
  346. package/src/__tests__/gpu-driven-shadow-views.unit.test.ts +168 -0
  347. package/src/__tests__/gpu-driven-skin-contract.test-d.ts +11 -0
  348. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +72 -20
  349. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +30 -9
  350. package/src/__tests__/gpu-driven-view.unit.test.ts +159 -52
  351. package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -1
  352. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +42 -0
  353. package/src/__tests__/gpu-scene-render-graph-gpu.ts +19 -5
  354. package/src/__tests__/gpu-scene.dawn.test.ts +40 -13
  355. package/src/__tests__/gpu-scene.unit.test.ts +559 -33
  356. package/src/__tests__/instances-culling.integration.test.ts +36 -10
  357. package/src/__tests__/instances-derived-bounds.unit.test.ts +54 -0
  358. package/src/__tests__/instances-store.unit.test.ts +242 -39
  359. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +6 -0
  360. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +13 -16
  361. package/src/__tests__/lut-lkg-transaction.integration.test.ts +122 -0
  362. package/src/__tests__/material-contract-inventory.unit.test.ts +5 -6
  363. package/src/__tests__/material-dispatch-shader-id.unit.test.ts +51 -0
  364. package/src/__tests__/material-lane-selection.unit.test.ts +60 -0
  365. package/src/__tests__/material-numeric-upload.unit.test.ts +53 -0
  366. package/src/__tests__/material-texture-handles.unit.test.ts +18 -0
  367. package/src/__tests__/material-texture-source.unit.test.ts +87 -0
  368. package/src/__tests__/materials-authoring-validation.unit.test.ts +41 -0
  369. package/src/__tests__/materials-standard-contract.unit.test.ts +52 -2
  370. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +4 -0
  371. package/src/__tests__/normal-fallback.dawn.test.ts +122 -0
  372. package/src/__tests__/particle-material-input-layout.unit.test.ts +104 -0
  373. package/src/__tests__/physical-clearcoat.integration.test.ts +37 -3
  374. package/src/__tests__/pipeline-texture-specialization.unit.test.ts +138 -0
  375. package/src/__tests__/point-shadow-inspection.unit.test.ts +2 -0
  376. package/src/__tests__/prepared-gpu-driven-pbr-contract.test-d.ts +31 -0
  377. package/src/__tests__/prepared-gpu-driven-pbr-contract.unit.test.ts +39 -0
  378. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +35 -4
  379. package/src/__tests__/prepared-graphics-capability.unit.test.ts +21 -0
  380. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +6 -2
  381. package/src/__tests__/profiler-phase-catalog.test.ts +5 -0
  382. package/src/__tests__/public-surface.test-d.ts +3 -0
  383. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +30 -12
  384. package/src/__tests__/reflection-fallback-async-publication.unit.test.ts +104 -0
  385. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +2 -1
  386. package/src/__tests__/render-error-code-owner.test-d.ts +4 -0
  387. package/src/__tests__/render-error-detail-ssot.test-d.ts +22 -0
  388. package/src/__tests__/render-error-exhaustive.test-d.ts +15 -0
  389. package/src/__tests__/render-feature-gpu-work.integration.test.ts +285 -2
  390. package/src/__tests__/render-feature-plan.unit.test.ts +245 -0
  391. package/src/__tests__/render-feature-shadow-order.unit.test.ts +228 -0
  392. package/src/__tests__/render-feature-zero-work.unit.test.ts +35 -1
  393. package/src/__tests__/render-graph-fallback.integration.test.ts +2 -2
  394. package/src/__tests__/render-owner-cohesion.unit.test.ts +55 -0
  395. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +426 -0
  396. package/src/__tests__/render-scene-projection.unit.test.ts +914 -23
  397. package/src/__tests__/render-scene-resource-updates.integration.test.ts +214 -0
  398. package/src/__tests__/render-scene-skin-lifecycle.integration.test.ts +173 -0
  399. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +133 -0
  400. package/src/__tests__/render-target-material-source.test-d.ts +27 -3
  401. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +60 -0
  402. package/src/__tests__/renderer-frame-transaction.integration.test.ts +923 -5
  403. package/src/__tests__/shader-manifest-fixture.ts +39 -0
  404. package/src/__tests__/skin-motion-regression.unit.test.ts +0 -33
  405. package/src/__tests__/skin-palette-persistent.integration.test.ts +28 -0
  406. package/src/__tests__/skin-palette-persistent.unit.test.ts +247 -0
  407. package/src/__tests__/skinned-shadow-caster.test.ts +118 -14
  408. package/src/__tests__/specular-environment-scale.dawn.test.ts +156 -0
  409. package/src/__tests__/ssr-admission-matrix.integration.test.ts +7 -1
  410. package/src/__tests__/ssr-confidence-compose.unit.test.ts +84 -0
  411. package/src/__tests__/ssr-coordinate-oracle.unit.test.ts +28 -0
  412. package/src/__tests__/ssr-format-probe-gating.integration.test.ts +82 -0
  413. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +91 -0
  414. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +112 -0
  415. package/src/__tests__/ssr-gpu-dispatch.ts +946 -0
  416. package/src/__tests__/ssr-graph-topology.unit.test.ts +104 -0
  417. package/src/__tests__/ssr-history-filter.dawn.test.ts +198 -0
  418. package/src/__tests__/ssr-hit-color.dawn.test.ts +273 -0
  419. package/src/__tests__/ssr-hiz-odd-reduction.dawn.test.ts +569 -0
  420. package/src/__tests__/ssr-hiz-rescue.dawn.test.ts +279 -0
  421. package/src/__tests__/ssr-hiz.browser.test.ts +150 -0
  422. package/src/__tests__/ssr-hiz.dawn.test.ts +150 -0
  423. package/src/__tests__/ssr-hiz.unit.test.ts +56 -0
  424. package/src/__tests__/ssr-inspection.unit.test.ts +105 -0
  425. package/src/__tests__/ssr-memory-budget.unit.test.ts +45 -0
  426. package/src/__tests__/ssr-performance-sequence-aggregation.unit.test.ts +15 -0
  427. package/src/__tests__/ssr-public-surface.test-d.ts +37 -0
  428. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +387 -0
  429. package/src/__tests__/ssr-reflection-mip-reducer.dawn.test.ts +309 -0
  430. package/src/__tests__/ssr-reprojection-probe.ts +90 -0
  431. package/src/__tests__/ssr-roughness-mip.dawn.test.ts +163 -0
  432. package/src/__tests__/ssr-shader-prewarm.unit.test.ts +59 -0
  433. package/src/__tests__/ssr-spatial.integration.test.ts +141 -0
  434. package/src/__tests__/ssr-stable-lattice.dawn.test.ts +475 -0
  435. package/src/__tests__/ssr-temporal-history.unit.test.ts +202 -0
  436. package/src/__tests__/ssr-texel-crossing.dawn.test.ts +448 -0
  437. package/src/__tests__/ssr-zero-work.integration.test.ts +87 -0
  438. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +44 -12
  439. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +29 -0
  440. package/src/__tests__/standard-output-chain-contract.unit.test.ts +99 -0
  441. package/src/__tests__/standard-output-chain.integration.test.ts +73 -0
  442. package/src/__tests__/standard-output-gpu-evidence.ts +205 -0
  443. package/src/__tests__/standard-output.browser.test.ts +15 -0
  444. package/src/__tests__/standard-output.dawn.test.ts +15 -0
  445. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +38 -0
  446. package/src/__tests__/standard-pipeline.integration.test.ts +320 -3
  447. package/src/__tests__/standard-profile-oracle.unit.test.ts +22 -0
  448. package/src/__tests__/standard-surface-lighting.unit.test.ts +41 -0
  449. package/src/__tests__/standard-texture-specialization.dawn.test.ts +141 -0
  450. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +6 -3
  451. package/src/__tests__/taa-coverage-edge.dawn.test.ts +193 -0
  452. package/src/__tests__/taa-history-clipping.dawn.test.ts +328 -0
  453. package/src/__tests__/taa-neighborhood-equivalence.dawn.test.ts +778 -0
  454. package/src/__tests__/taa-stability-feedback.browser.test.ts +23 -0
  455. package/src/__tests__/taa-stability-feedback.dawn.test.ts +68 -0
  456. package/src/__tests__/taa-stability-feedback.ts +341 -0
  457. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +49 -7
  458. package/src/__tests__/temporal-reset-matrix.unit.test.ts +9 -0
  459. package/src/__tests__/temporal-transaction.integration.test.ts +28 -0
  460. package/src/__tests__/texture-usage-surface.unit.test.ts +4 -2
  461. package/src/__tests__/typed-pipeline-topology.unit.test.ts +109 -4
  462. package/src/__tests__/typed-scene-pass-material-cache.integration.test.ts +126 -0
  463. package/src/__tests__/vertex-layouts-ssot.unit.test.ts +47 -0
  464. package/src/__tests__/visibility-extract.perf.test.ts +20 -8
  465. package/src/__tests__/visibility-instances.integration.test.ts +19 -7
  466. package/src/__tests__/visibility-producer-matrix.integration.test.ts +20 -9
  467. package/src/assembly/atmosphere-shader-prewarm.ts +38 -0
  468. package/src/assembly/dynamic-geometry-host.ts +48 -0
  469. package/src/assembly/dynamic-geometry-runtime.ts +71 -0
  470. package/src/assembly/factory.ts +4 -0
  471. package/src/assembly/gpu-driven-owner.ts +29 -0
  472. package/src/assembly/host-contract.ts +10 -0
  473. package/src/assembly/material/assembly.ts +38 -0
  474. package/src/assembly/material-shader-policy.ts +71 -28
  475. package/src/assembly/recovery/failure-location.ts +62 -0
  476. package/src/assembly/recovery/renderer-recover.ts +19 -17
  477. package/src/assembly/renderer-facade.ts +12 -0
  478. package/src/assembly/renderer-feature-inspection.ts +13 -0
  479. package/src/assembly/renderer-frame-transaction.ts +63 -0
  480. package/src/assembly/renderer-helpers.ts +26 -0
  481. package/src/assembly/renderer-inspection.ts +6 -0
  482. package/src/assembly/shader-prewarm-policy.ts +32 -0
  483. package/src/assembly/skin-palette-owner.ts +21 -0
  484. package/src/assembly/ssr-shader-prewarm.ts +41 -0
  485. package/src/assembly/webgpu-device-loss.ts +75 -0
  486. package/src/assembly/webgpu-pbr-ready.ts +232 -0
  487. package/src/assembly/webgpu-ready-mipmap.ts +26 -0
  488. package/src/assembly/webgpu-ready.ts +142 -121
  489. package/src/assembly/webgpu-renderer-bootstrap.ts +85 -0
  490. package/src/assembly/webgpu-renderer-contract.ts +69 -0
  491. package/src/assembly/webgpu-renderer-guards.ts +32 -0
  492. package/src/assembly/webgpu-renderer-observation.ts +43 -0
  493. package/src/assembly/webgpu-renderer-recovery-failure.ts +62 -0
  494. package/src/assembly/webgpu-renderer.ts +666 -652
  495. package/src/assembly/webgpu-vertex-layouts.ts +233 -87
  496. package/src/components/__tests__/camera-exposure.contract.unit.test.ts +153 -0
  497. package/src/components/camera.ts +287 -7
  498. package/src/components/index.ts +1 -0
  499. package/src/components/instances.ts +45 -18
  500. package/src/components/scene-instance.ts +2 -1
  501. package/src/components/screen-space-reflection.ts +21 -0
  502. package/src/components/sprite-animation.ts +2 -2
  503. package/src/components/sprite-instances.ts +1 -1
  504. package/src/device/gpu-residency.ts +34 -31
  505. package/src/environment/background.ts +263 -0
  506. package/src/errors/gpu-driven.ts +39 -5
  507. package/src/errors/render.ts +27 -3
  508. package/src/extract/camera.ts +26 -1
  509. package/src/extract/gpu-driven.ts +185 -10
  510. package/src/features/host.ts +237 -19
  511. package/src/features/noise-texture.ts +91 -0
  512. package/src/features/plan.ts +553 -24
  513. package/src/features/prepared-gpu-work.ts +631 -49
  514. package/src/features/prepared-graphics-store.ts +31 -0
  515. package/src/features/prepared-graphics.ts +5 -0
  516. package/src/features/render-graph-compute.ts +41 -8
  517. package/src/features/render-graph-contribution.ts +141 -3
  518. package/src/features/render-graph-raster.ts +143 -30
  519. package/src/features/types.ts +11 -0
  520. package/src/fullscreen-post-process-pass.ts +3 -0
  521. package/src/gpu-driven/batch-topology.ts +424 -102
  522. package/src/gpu-driven/pbr-program.ts +31 -0
  523. package/src/gpu-driven/prepared-draw.ts +29 -4
  524. package/src/gpu-driven/production-raster.ts +2267 -694
  525. package/src/gpu-driven/shadow-views.ts +539 -0
  526. package/src/gpu-driven/view-gpu.ts +188 -41
  527. package/src/gpu-scene-schema.ts +52 -11
  528. package/src/gpu-scene.ts +186 -99
  529. package/src/gpu-texture-usage.ts +1 -0
  530. package/src/hdrp-buffers.ts +72 -36
  531. package/src/ibl/frame-resources.ts +38 -0
  532. package/src/ibl/skylight-bind-group.ts +5 -4
  533. package/src/index.ts +109 -1
  534. package/src/inspection-types.ts +191 -0
  535. package/src/instances-derived-bounds.ts +258 -34
  536. package/src/instances.ts +459 -150
  537. package/src/internal.ts +22 -0
  538. package/src/lifecycle.ts +2 -0
  539. package/src/material-row.ts +145 -0
  540. package/src/materials.ts +44 -8
  541. package/src/pbr-pipeline.ts +159 -17
  542. package/src/pipeline/standard-forward-lane.ts +52 -24
  543. package/src/pipeline/standard-output/__tests__/lut-admission.unit.test.ts +103 -0
  544. package/src/pipeline/standard-output/auto-exposure/__tests__/capability.unit.test.ts +40 -0
  545. package/src/pipeline/standard-output/auto-exposure/__tests__/gpu.contract.unit.test.ts +125 -0
  546. package/src/pipeline/standard-output/auto-exposure/__tests__/graph.contract.unit.test.ts +94 -0
  547. package/src/pipeline/standard-output/auto-exposure/__tests__/oracle.test.ts +108 -0
  548. package/src/pipeline/standard-output/auto-exposure/__tests__/preset.test.ts +26 -0
  549. package/src/pipeline/standard-output/auto-exposure/capability.ts +38 -0
  550. package/src/pipeline/standard-output/auto-exposure/gpu.ts +567 -0
  551. package/src/pipeline/standard-output/auto-exposure/graph.ts +221 -0
  552. package/src/pipeline/standard-output/auto-exposure/inspection.ts +231 -0
  553. package/src/pipeline/standard-output/auto-exposure/oracle.ts +291 -0
  554. package/src/pipeline/standard-output/auto-exposure/preset.ts +28 -0
  555. package/src/pipeline/standard-output/auto-exposure/state.ts +156 -0
  556. package/src/pipeline/standard-output/color-transform.ts +124 -0
  557. package/src/pipeline/standard-output/graph.ts +139 -0
  558. package/src/pipeline/standard-output/lut-admission.ts +121 -0
  559. package/src/pipeline/standard-output/lut-gpu.ts +399 -0
  560. package/src/pipeline/standard-output/lut-state.ts +176 -0
  561. package/src/pipeline/standard-output/resources.ts +121 -0
  562. package/src/pipeline/standard-output/types.ts +58 -0
  563. package/src/pipeline/standard-pipeline.ts +192 -59
  564. package/src/pipeline/standard-post.ts +186 -27
  565. package/src/pipeline-builder.ts +16 -6
  566. package/src/pipeline-spec-types.ts +4 -0
  567. package/src/pipeline-spec.ts +37 -29
  568. package/src/points-lines/standard-owner.ts +6 -14
  569. package/src/prepare/prepared-graphics-resolver.ts +9 -2
  570. package/src/record/__tests__/material-slot-plan.unit.test.ts +48 -0
  571. package/src/record/__tests__/mesh-ssbo.unit.test.ts +40 -1
  572. package/src/record/__tests__/recovery-pipeline.unit.test.ts +2 -29
  573. package/src/record/__tests__/sprite-material-abi.unit.test.ts +36 -5
  574. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +45 -0
  575. package/src/record/__tests__/view-ubo-layout.unit.test.ts +5 -0
  576. package/src/record/dynamic-geometry-consumption.ts +59 -0
  577. package/src/record/frame-lighting.ts +12 -11
  578. package/src/record/frame-snapshot.ts +133 -23
  579. package/src/record/frame-targets.ts +9 -0
  580. package/src/record/frame.ts +725 -133
  581. package/src/record/main-pass-geometry.ts +149 -94
  582. package/src/record/main-pass-material.ts +115 -26
  583. package/src/record/main-pass-sprite-draws.ts +16 -107
  584. package/src/record/main-pass.ts +78 -8
  585. package/src/record/material-uniforms.ts +1 -1
  586. package/src/record/mesh-ssbo.ts +59 -15
  587. package/src/record/prepared-material-bindings.ts +234 -0
  588. package/src/record/recovery-pipeline.ts +59 -136
  589. package/src/record/render-context.ts +90 -22
  590. package/src/record/shadow-pass.ts +1231 -209
  591. package/src/record/skybox-post-pass.ts +14 -3
  592. package/src/record/typed-frame-graph.ts +1004 -44
  593. package/src/record/view-ubo.ts +11 -0
  594. package/src/recovery/render-system-candidate.ts +90 -66
  595. package/src/recovery/render-system-roots.ts +113 -0
  596. package/src/recovery/types.ts +77 -0
  597. package/src/reflection/record-owner.ts +26 -54
  598. package/src/render-contract.ts +195 -25
  599. package/src/render-graph-primitives.ts +6 -2
  600. package/src/render-pipeline.ts +70 -1
  601. package/src/render-system-extract-tail.ts +409 -160
  602. package/src/render-system-extract.ts +376 -102
  603. package/src/render-system-projections.ts +241 -0
  604. package/src/render-system.ts +681 -822
  605. package/src/scene/render-scene-types.ts +13 -4
  606. package/src/scene/render-scene.ts +569 -355
  607. package/src/scene/shadow-visibility.ts +21 -0
  608. package/src/ssr/admission.ts +380 -14
  609. package/src/ssr/compose.ts +144 -0
  610. package/src/ssr/errors.ts +48 -0
  611. package/src/ssr/graph.ts +777 -0
  612. package/src/ssr/history.ts +436 -0
  613. package/src/ssr/hiz.ts +124 -0
  614. package/src/ssr/inspection.ts +124 -0
  615. package/src/ssr/resources.ts +202 -0
  616. package/src/ssr/temporal.ts +164 -0
  617. package/src/systems/skin-palette-allocator.ts +495 -148
  618. package/src/systems/skin-palette-types.ts +17 -0
  619. package/src/systems/transparent-dispatch.ts +90 -0
  620. package/src/targets/material-source.ts +34 -0
  621. package/src/temporal/coverage.ts +1 -1
  622. package/src/temporal/frame-coordinator.ts +23 -2
  623. package/src/temporal/gpu.ts +74 -12
  624. package/src/temporal/history.ts +3 -1
  625. package/src/temporal/inspection.ts +2 -1
  626. package/src/temporal/standard-scene-data.ts +3 -1
  627. package/src/temporal/temporal-view.ts +3 -0
  628. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +204 -139
  629. package/src/typed-render-graph-primitives.ts +266 -74
  630. package/src/typed-shadow-passes.ts +122 -12
  631. package/src/volume/temporal.ts +10 -0
  632. package/dist/chunk-GE3SDD72.mjs.map +0 -1
  633. package/dist/chunk-GF523LHF.mjs.map +0 -1
  634. package/dist/chunk-IQMKJLNU.mjs.map +0 -1
  635. package/dist/chunk-J52NMQKR.mjs.map +0 -1
  636. package/dist/chunk-J6KACNOH.mjs.map +0 -1
  637. package/dist/chunk-JAASCMXA.mjs.map +0 -1
  638. package/dist/chunk-MBU4ZVE7.mjs.map +0 -1
  639. package/dist/chunk-RBDBZ5R7.mjs.map +0 -1
  640. package/dist/chunk-VSL23LZE.mjs.map +0 -1
  641. package/src/__tests__/instances-world-ownership.integration.test.ts +0 -62
  642. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +0 -36
@@ -1,24 +1,6 @@
1
- // @forgeax/engine-render — concrete Renderer lifecycle and frame interpreter.
2
- //
3
- // The Runtime host selects one typed backend pack before this assembly runs.
4
- //
5
- // **D-P4 three-channel error propagation** (RK-04 + plan-strategy §7.3):
6
- // (a) Construction-time → `createRenderer(...)` rejects with
7
- // `EngineEnvironmentError` whose `detail.webgpuError` is the Channel 2
8
- // `RhiError` and `detail.wgpuError` is the Channel 3 failure (when both
9
- // channels fail; AI users `try { await createRenderer(...) } catch (e) {
10
- // switch (e.detail.webgpuError?.code) { ... } }`).
11
- // (b) Run-time pipeline build → `renderer.initialization` rejects with the
12
- // structured `RhiError` (`shader-compile-failed` / `limit-exceeded` /
13
- // `webgpu-runtime-error` / ...); AI users `await renderer.initialization`.
14
- // (c) error fan-out → `renderer.subscribe(listener)` captures the same
15
- // errors as a fallback observability channel (charter proposition 4
16
- // structured + proposition 9 graceful degradation).
17
- //
18
- // AC-15 source-level: this file uses `globalThis.navigator` rather than
19
- // `navigator` directly; it never touches `window` or `document`.
1
+ // @forgeax/engine-render — WebGPU renderer lifecycle/frame interpreter.
2
+ // Runtime selects the backend; structured RHI errors and browser isolation stay intact.
20
3
 
21
- import type { MipmapShaderModuleFactory } from '@forgeax/engine-assets-runtime';
22
4
  import {
23
5
  AssetRegistry,
24
6
  adaptDynamicTextureDevice,
@@ -28,12 +10,12 @@ import { audioLoader } from '@forgeax/engine-audio-webaudio';
28
10
  import { Update, type World } from '@forgeax/engine-ecs';
29
11
  import { createRenderReadLease, type RenderReadLease } from '@forgeax/engine-ecs/projection';
30
12
  import {
31
- deriveVertexBufferLayout,
32
- deriveVertexBufferLayoutFromProjection,
13
+ DEFAULT_VERTEX_ATTRIBUTE_MAP,
33
14
  type VertexLayoutProjection,
34
15
  } from '@forgeax/engine-geometry';
35
16
  import type {
36
17
  BindGroupLayout,
18
+ Buffer,
37
19
  PipelineLayout,
38
20
  RenderPipeline,
39
21
  Result,
@@ -50,19 +32,16 @@ import {
50
32
  standardPhysicalTextureFields,
51
33
  } from '@forgeax/engine-shader';
52
34
  import type {
53
- ImportTransport,
54
35
  MaterialRenderState,
55
36
  ParamSchemaEntry,
56
37
  PassKind,
57
38
  PrimitiveTopology,
58
39
  VertexAttributeMap,
59
40
  } from '@forgeax/engine-types';
60
- import { createDynamicGeometryLifecycle } from '../dynamic-geometry';
61
41
 
62
42
  export type { MaterialShaderManifestEntry } from '@forgeax/engine-shader';
63
43
 
64
44
  import { GpuResidencyCache } from '../device/gpu-residency';
65
- import type { DeviceResourceKind } from '../device/resource-types';
66
45
  import { createEngineMetrics } from '../engine-metrics';
67
46
  import { createRecoveryFailedError, RecoverError, type RecoverFailure } from '../errors/recover';
68
47
  import {
@@ -74,7 +53,7 @@ import {
74
53
  RenderFeatureCapabilityMissingError,
75
54
  RenderFeatureStageFailedError,
76
55
  } from '../errors/render';
77
- import { createRenderFeatureHost, type RenderFeatureHost } from '../features/host';
56
+ import { createRenderFeatureHost } from '../features/host';
78
57
  import { RENDER_FEATURE_VERTEX_LAYOUTS } from '../features/prepared-graphics';
79
58
  import type { RenderFeature, RenderFeatureShaderModuleMode } from '../features/types';
80
59
  import {
@@ -82,15 +61,12 @@ import {
82
61
  postProcessShaderModuleLabel,
83
62
  } from '../fullscreen-post-process-pass';
84
63
  import { glyphTextLayoutSystem } from '../glyph-text-layout-system';
64
+ import { resolveStandardPbrProgram } from '../gpu-driven/pbr-program';
85
65
  import { GPU_SHADER_STAGE_FRAGMENT, GPU_SHADER_STAGE_VERTEX } from '../gpu-stage';
86
66
  import type { BloomInspection } from '../inspection-types';
67
+ import { DeviceScope } from '../lifecycle';
87
68
  import {
88
- DeviceScope,
89
- HealthListenerRegistry,
90
- LostListenerRegistry,
91
- RhiErrorListenerRegistry,
92
- } from '../lifecycle';
93
- import {
69
+ createHdrpBindGroupLayoutDescriptor,
94
70
  isCanonicalStandardPbrMaterialShader,
95
71
  isStandardPbrMaterialShader,
96
72
  type PipelineGroup2Contract,
@@ -105,6 +81,7 @@ import {
105
81
  cacheKeyOf,
106
82
  colorFormatsForPassKind,
107
83
  type PipelineSpec,
84
+ PipelineSpecError,
108
85
  passKindPolicyTable,
109
86
  } from '../pipeline-spec';
110
87
  import type { ExtendedLightingResourceCandidate } from '../prepare/extended-lighting/resources';
@@ -122,6 +99,7 @@ import {
122
99
  promoteExtendedLightingCandidate,
123
100
  } from '../prepare/extended-lighting/state';
124
101
  import type { FrameObservationOptions } from '../record/frame';
102
+ import { getTextureIdentity } from '../record/frame-snapshot';
125
103
  import type { GpuPassTimingReason } from '../record/gpu-pass-timing/errors.js';
126
104
  import {
127
105
  createGpuPassTimingSession,
@@ -131,14 +109,14 @@ import {
131
109
  type GpuPassTimingSession,
132
110
  } from '../record/gpu-pass-timing/index.js';
133
111
  import { GpuTimingCapture, type VolumeTimingObservation } from '../record/gpu-timing';
134
- import type { RecoveryColdWorkGuard } from '../record/render-context';
135
112
  import type {
113
+ AtmosphereShaderSources,
136
114
  DrawOwnerOptions,
115
+ FrameObservationDomain,
137
116
  FrameObservationRequest,
138
117
  FrameReceipt,
139
118
  FrameReceiptObservation,
140
119
  HealthSnapshot,
141
- RenderDebugOverlay,
142
120
  RendererErrorListener,
143
121
  RendererLostListener,
144
122
  RendererOptions,
@@ -147,6 +125,7 @@ import type {
147
125
  RenderProfile,
148
126
  RenderResult,
149
127
  RenderWorldLease,
128
+ SsrShaderSources,
150
129
  VolumetricFogShaderSources,
151
130
  } from '../render-contract';
152
131
  import { FXAA_POST_PROCESS_ID, STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from '../render-contract';
@@ -163,10 +142,10 @@ import { createGpuPassTimingObservationStore, observeGpuPassTimingDisabled } fro
163
142
  import { postSpawnResolveJoints } from '../scene-instances/post-spawn-resolve-joints';
164
143
  import type { RenderTarget } from '../targets/contracts';
165
144
  import { tilemapChunkExtractSystem } from '../tilemap-chunk-extract-system';
145
+ import type { TypedFrameObservationCapture } from '../typed-render-graph-primitives';
166
146
  import type { RhiBackendPack } from './backend-contract';
167
147
  import type { BundlerOptions } from './bundler-contract';
168
- import { deviceOptionsForAdapter } from './device-feature-admission';
169
- import { createDynamicGeometryHost } from './dynamic-geometry-host';
148
+ import { createRendererDynamicGeometryController } from './dynamic-geometry-host';
170
149
  import type { RendererAssemblyImplementation } from './host-contract';
171
150
  import {
172
151
  allowsUnlitPreparedFallback,
@@ -188,7 +167,6 @@ import {
188
167
  shouldDeferMissingPreparedMaterialShader,
189
168
  } from './material-shader-policy';
190
169
  import type { MeshSsboGrowResult, MeshSsboState } from './mesh-ssbo-grow';
191
- import { attachDeviceLostFanout } from './recovery/device-loss-fanout';
192
170
  import {
193
171
  type GenerationAggregate,
194
172
  type GenerationPublication,
@@ -201,32 +179,48 @@ import {
201
179
  structuredRendererCause,
202
180
  validateRenderProfile,
203
181
  } from './renderer-facade';
182
+ import { projectRendererFeatureInspection } from './renderer-feature-inspection';
204
183
  import {
205
184
  type ContinuationTerminator,
206
185
  createContinuationTerminator,
207
186
  } from './renderer-frame-transaction';
208
- import { ensureContextConfigured, wrapDisposeError } from './renderer-helpers';
187
+ import {
188
+ ensureContextConfigured,
189
+ rendererInitializationError,
190
+ rendererInitializationPipelineError,
191
+ wrapDisposeError,
192
+ } from './renderer-helpers';
209
193
  import { projectRendererOutputInspection } from './renderer-inspection';
210
194
  import {
211
195
  createRecoveryContinuation,
212
196
  createRecoveryDeadline,
213
197
  createSingleFlight,
214
- type RecoveryGuidance,
215
198
  type RecoveryPhase,
216
199
  } from './renderer-lifecycle';
200
+ import { derivedPhysicsFrameError } from './webgpu-renderer-guards';
201
+ import {
202
+ createRecoveryFailureLocation,
203
+ type RecoveryFailureLocation,
204
+ } from './webgpu-renderer-recovery-failure';
205
+
206
+ export type { RecoveryFailureLocation } from './webgpu-renderer-recovery-failure';
207
+
217
208
  import { STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES } from './shader-prewarm-policy';
218
209
  import { buildReadyWebGPU } from './webgpu-ready';
219
210
  import { DEPTH_TEXTURE_FORMAT, HDR_COLOR_ATTACHMENT_FORMAT } from './webgpu-ready-contract';
211
+ import { adaptMipmapShaderModuleFactory } from './webgpu-ready-mipmap';
212
+ import {
213
+ tryCreateWebGPURenderer as tryCreateWebGPURendererBootstrap,
214
+ type WebGPURendererInternals,
215
+ } from './webgpu-renderer-bootstrap';
216
+ import type { WebGPUOutcome } from './webgpu-renderer-contract';
217
+ import { disposeObservationCaptureSet } from './webgpu-renderer-observation';
220
218
  import {
221
- BILLBOARD_MATERIAL_INSTANCE_VERTEX_BUFFERS,
222
- DEFAULT_VERTEX_ATTRS,
223
- MESH_GEOMETRY_MATERIAL_INSTANCE_VERTEX_BUFFERS,
224
- PBR_SKIN_SENTINEL_ATTR_BUFFER,
225
- POSITION_SIZE_COLOR_INSTANCE_VERTEX_BUFFERS,
219
+ isPreparedMaterialVertexLayout,
226
220
  PREPARED_INSTANCE_VERTEX_ATTRS,
227
221
  PREPARED_MATERIAL_INSTANCE_VERTEX_ATTRS,
228
- TOPOLOGY_SEGMENT_INSTANCE_VERTEX_BUFFERS,
229
- toGpuVertexBufferLayouts,
222
+ particleMaterialInputVertexBuffers,
223
+ resolveWebGPUVertexBufferLayouts,
230
224
  } from './webgpu-vertex-layouts';
231
225
 
232
226
  export type { BundlerOptions } from './bundler-contract';
@@ -249,362 +243,41 @@ export {
249
243
  selectPipelineLayoutForVariant,
250
244
  shouldDeferMissingPreparedMaterialShader,
251
245
  } from './material-shader-policy';
252
- export type {
253
- MeshSsboBufferWrapper,
254
- MeshSsboGrowController,
255
- MeshSsboGrowControllerInit,
256
- MeshSsboGrowDevice,
257
- MeshSsboGrowErrorRegistry,
258
- MeshSsboGrowResult,
259
- MeshSsboState,
260
- } from './mesh-ssbo-grow';
246
+
261
247
  export {
262
248
  createMeshSsboGrowController,
263
249
  deriveStorageBufferCeiling,
264
250
  INITIAL_MESH_SSBO_SLOT_COUNT,
251
+ type MeshSsboBufferWrapper,
252
+ type MeshSsboGrowController,
253
+ type MeshSsboGrowControllerInit,
254
+ type MeshSsboGrowDevice,
255
+ type MeshSsboGrowErrorRegistry,
256
+ type MeshSsboGrowResult,
257
+ type MeshSsboState,
265
258
  requireMeshSsboBuffer,
266
259
  } from './mesh-ssbo-grow';
267
260
  export { exposeRenderer } from './renderer-facade';
268
261
  export {
262
+ selectGpuDrivenSceneIndexVariant,
269
263
  selectHdrpPbrPrewarmVariants,
270
264
  selectProbePrewarmVariants,
271
265
  selectSkinPrewarmVariants,
272
266
  selectStandardPbrTransmissionPrewarmVariants,
273
267
  } from './shader-prewarm-policy';
268
+ export type { WebGPUOutcome, WebGPURendererInternals } from './webgpu-renderer-contract';
269
+ export { disposeObservationCaptureSet } from './webgpu-renderer-observation';
274
270
 
275
- function adaptMipmapShaderModuleFactory(
276
- factory: RhiBackendPack['createShaderModule'],
277
- ): MipmapShaderModuleFactory | undefined {
278
- if (factory === undefined) return undefined;
279
- return (device, descriptor) => factory(device as RhiDevice, descriptor);
280
- }
281
-
282
- // Re-export registerAdvanceAnimationPlayer so consumers can wire it.
283
- // Re-export registerPropagateTransforms so consumers can wire the
284
- // Transform.world mat4 derivation (audio listener sync + picking read the
285
- // derived Transform.world from scripts).
286
-
287
- /**
288
- * Bundler-layer injection accepted by `createRenderer` (and proxied by
289
- * `createApp`) as the optional third argument.
290
- *
291
- * feat-20260608-create-app-param-surface-trim / M2 / D-3: aggregates the two
292
- * host-injected build-tool channels:
293
- *
294
- * - shaderManifestUrl: the URL the host's vite-plugin-shader emit step
295
- * wrote `manifest.json` to. When this field is omitted (or `bundler`
296
- * itself is omitted), createRenderer falls back to
297
- * '/shaders/manifest.json' (D-2 q5-A) so the LO 1.1 zero-config takeoff
298
- * path keeps working without explicit injection. Tests can inject via a
299
- * `data:application/json,...` URL to bypass fetch.
300
- *
301
- * - importTransport: dev-only ImportTransport forwarded verbatim to the
302
- * AssetRegistry third ctor slot so DDC-miss assets can lazy-import.
303
- * Absent => shipped form (a DDC miss fails fast with `asset-not-imported`).
304
- *
305
- * The interface is structurally shared with `@forgeax/engine-app` while this
306
- * package keeps the renderer independent from the host package.
307
- */
308
- // ─── Backend pack — M3 auto-select internal shape ───────────────────────────
309
-
310
- // ─── WebGPU branch ──────────────────────────────────────────────────────────
311
-
312
- /**
313
- * Wire the spec `device.lost` Promise into the lost / error / health channels
314
- * (research §F-4 / R2). A genuine device loss fans out through `errorRegistry`
315
- * so the public `renderer.subscribe` error event triggers, and
316
- * through `healthRegistry` so `health().reason` flips to `'device-lost'`
317
- * (feat-20260622-s5 M1/M2). Explicit renderer teardown is observable through
318
- * `onLost`, but is not a runtime error and must not manufacture an RhiError.
319
- *
320
- * Extracted to a single helper (SSOT) so the createRenderer assembly path AND
321
- * the recover() rebuild path attach byte-identical fan-out to whichever device
322
- * is current — recover() mints a fresh device whose own `lost` Promise must be
323
- * re-wired to the SAME registries the host already subscribed to.
324
- */
325
- // Device loss wiring lives in the recovery assembly seam so boot and recovery
326
- // cannot drift into separate lifecycle event paths.
327
-
328
- /**
329
- * tryCreateWebGPURenderer returns three kinds of outcome (fix-f1 / w16):
330
- * - `ok` → Renderer created successfully
331
- * - `rhi-err` → RHI Result.err path (preserves the original RhiError
332
- * for AI consumers to read; now includes context acquisition
333
- * failures via pack.rhi.acquireCanvasContext)
334
- * - `throw` → exception path (caught and wrapped by the caller as
335
- * an Error)
336
- */
337
- export type WebGPUOutcome =
338
- | { kind: 'ok'; renderer: RendererAssemblyImplementation }
339
- | { kind: 'rhi-err'; error: RhiError }
340
- | { kind: 'throw'; error: Error };
341
-
342
- /**
343
- * Goes through the strict two-step path exposed by the injected backend pack:
344
- * `rhi.requestAdapter()` -> `adapter.requestDevice()`
345
- * (M6 fix-up [w51] retires the legacy single-step factory per plan-strategy
346
- * §6 M3 break-point #2 + AGENTS.md break-point list 2026-05-10 #2). Returns
347
- * `Result<RhiAdapter, RhiError>` then `Result<RhiDevice, RhiError>`; each
348
- * failure / failure to acquire the canvas WebGPU context returns a
349
- * structured outcome so the caller can decide on fallback and error-
350
- * preservation strategy (plan-strategy §7.3).
351
- *
352
- * fix-f1: on either Result.err path the original `RhiError` is preserved
353
- * via the `rhi-err` outcome — when fallback also fails, AI consumers read
354
- * it via `EngineEnvironmentError.detail.webgpuError.code`.
355
- *
356
- * w19 / M4: context acquisition goes through `pack.rhi.acquireCanvasContext(canvas)`
357
- * — a polymorphic entry owned by the selected backend adapter.
358
- * The backend pack is selected BEFORE this call (in `loadBackendPack` /
359
- * the Channel 2→3 fallback in `createRenderer`), so the canvas context
360
- * is never type-locked by a wrong backend's acquisition attempt.
361
- */
271
+ /** Bundler injection and the WebGPU renderer construction entry points. */
362
272
  let nextDerivedSystemId = 0;
363
-
364
273
  export async function tryCreateWebGPURenderer(
365
274
  canvas: HTMLCanvasElement | OffscreenCanvas,
366
275
  options: RendererOptions | undefined,
367
276
  pack: RhiBackendPack,
368
- // feat-20260608 M2 / D-3: BundlerOptions threaded verbatim. Carries
369
- // shaderManifestUrl (consumed in getShader fallback ~30 lines below) and
370
- // importTransport (forwarded to the AssetRegistry third ctor slot;
371
- // undefined keeps the shipped form, AC-08).
372
277
  bundler: BundlerOptions | undefined,
373
278
  ): Promise<WebGPUOutcome> {
374
- const importTransport = bundler?.importTransport;
375
- // M6 fix-up [w51]: spec-aligned two-step path. Step 1 - requestAdapter()
376
- // surfaces adapter.features / adapter.limits (capability pre-screen,
377
- // charter proposition 4 forward-reachable). Step 2 - adapter.requestDevice()
378
- // requests the actual device. M3 D-P4: `pack.rhi` is the auto-selected
379
- // backend singleton (rhi-webgpu / rhi-wgpu / explicit escape hatch).
380
- // w19: thread canvas as compatibleSurface so the wgpu GL backend can
381
- // enumerate adapters against it (escape hatch, capability-gated).
382
- const adapterResult = await pack.rhi.requestAdapter(undefined, canvas);
383
- if (!adapterResult.ok) {
384
- return { kind: 'rhi-err', error: adapterResult.error };
385
- }
386
- const adapter = adapterResult.value;
387
- // M4 w28: filter compression features from adapter.features (AC-07). The
388
- // same helper is used by recover() below so both paths share one profile.
389
- const deviceOpts = deviceOptionsForAdapter(adapter, options);
390
- const result = await adapter.requestDevice(deviceOpts);
391
- if (!result.ok) {
392
- return { kind: 'rhi-err', error: result.error };
393
- }
394
- const device: RhiDevice = result.value;
395
-
396
- // Acquire the canvas context through the backend pack (M3 / w16).
397
- // rhi-webgpu: internally calls canvas.getContext('webgpu') + branded wrap.
398
- // rhi-wgpu: internally calls wasm createSurface(canvas) + configure.
399
- // When context acquisition fails (null from canvas.getContext or wasm
400
- // createSurface throws), the pack returns Result.err(RhiError).
401
- const ctxResult = pack.rhi.acquireCanvasContext(canvas);
402
- if (!ctxResult.ok) {
403
- return { kind: 'rhi-err', error: ctxResult.error };
404
- }
405
- const context: RhiCanvasContext = ctxResult.value;
406
-
407
- const lostRegistry = new LostListenerRegistry();
408
- const errorRegistry = new RhiErrorListenerRegistry();
409
- const healthRegistry = new HealthListenerRegistry();
410
- const lossObserver: {
411
- current?: (detail: string) => void;
412
- } = {};
413
- const generationState: {
414
- current: number;
415
- onStaleLoss?: () => void;
416
- } = { current: 0 };
417
- // device.lost dual-track (research §F-4 / R2 countermeasure): the spec
418
- // Promise is passed through to the lost-fan-out registry (extracted to
419
- // attachDeviceLostFanout so the recover() rebuild path re-attaches the
420
- // SAME wiring to the freshly-minted device — SSOT, one fan-out shape).
421
- attachDeviceLostFanout(device, pack, {
422
- lostRegistry,
423
- errorRegistry,
424
- healthRegistry,
425
- generation: 0,
426
- currentGeneration: () => generationState.current,
427
- onStaleLoss: () => generationState.onStaleLoss?.(),
428
- onDeviceLost: (detail) => lossObserver.current?.(detail),
429
- });
430
-
431
- // D-VD2 Round 2 wire-up part 2: register the spec `onuncapturederror`
432
- // listener on the raw GPUDevice so GPUUncapturedErrorEvent (validation /
433
- // oom / internal) is translated to the 17-member RhiErrorCode union +
434
- // dispatched through the public Renderer event subscription. When the pack does not expose a
435
- // raw-device escape hatch (rhi-wgpu wasm path, custom escape hatch), the
436
- // listener registration is skipped — the device.lost dual-channel above
437
- // still fires through `RhiDevice.lost` Promise on every path (graceful
438
- // degradation per charter proposition 9; per-path coverage matrix in
439
- // packages/engine/README.md `## Error model`).
440
- if (pack._internal_getRawDevice && pack.translateErrorEventToRhiError) {
441
- const backendDevice = pack._internal_getRawDevice(device);
442
- if (backendDevice && typeof backendDevice === 'object') {
443
- const target = backendDevice as {
444
- onuncapturederror?: ((event: unknown) => void) | null;
445
- };
446
- // Spec form: `device.onuncapturederror = (event) => ...`. The listener
447
- // sets a property handler (spec normative GPUDevice extends EventTarget
448
- // and exposes `onuncapturederror` as a settable callback property);
449
- // engine writes through the property so each pre-existing handler that
450
- // an AI user assigned would be replaced (last-write-wins; spec lifetime
451
- // is device-scoped so engine ownership is OK — AI users observe via
452
- // the Renderer event subscription, not the raw property).
453
- target.onuncapturederror = (event: unknown): void => {
454
- const translated = pack.translateErrorEventToRhiError?.(event);
455
- if (translated && !translated.ok) {
456
- errorRegistry.fire(translated.error);
457
- }
458
- };
459
- }
460
- }
461
- return {
462
- kind: 'ok',
463
- renderer: await makeWebGPURenderer({
464
- canvas,
465
- device,
466
- context,
467
- options,
468
- bundler,
469
- lostRegistry,
470
- errorRegistry,
471
- healthRegistry,
472
- pack,
473
- importTransport,
474
- lossObserver,
475
- generationState,
476
- }),
477
- };
279
+ return tryCreateWebGPURendererBootstrap(canvas, options, pack, bundler, makeWebGPURenderer);
478
280
  }
479
-
480
- export interface WebGPURendererInternals {
481
- canvas: HTMLCanvasElement | OffscreenCanvas;
482
- device: RhiDevice;
483
- // M6 / w41 (feat-20260510-rhi-resource-creation): forgeax RhiCanvasContext
484
- // brand; RenderSystem + ensureContextConfigured both go through the RHI
485
- // surface (charter proposition 5 consistent abstraction red line).
486
- context: RhiCanvasContext;
487
- options: RendererOptions | undefined;
488
- /**
489
- * feat-20260608-create-app-param-surface-trim / M2 / D-3: BundlerOptions
490
- * forwarded verbatim from createRenderer's third arg. Carries
491
- * shaderManifestUrl (host-injected vite-plugin-shader emit URL; absent =>
492
- * createRenderer falls back to '/shaders/manifest.json') and the
493
- * importTransport that previously rode a dedicated internal slot.
494
- */
495
- bundler: BundlerOptions | undefined;
496
- lostRegistry: LostListenerRegistry;
497
- errorRegistry: RhiErrorListenerRegistry;
498
- healthRegistry: HealthListenerRegistry;
499
- /** M3 D-P4 auto-select pack — carries the dynamic-imported rhi-webgpu / rhi-wgpu singleton + optional async shader factory. */
500
- pack: RhiBackendPack;
501
- /**
502
- * feat-20260604-hdr-equirect-cube-importer-loader M4 / w16 (D-3): the
503
- * dev-only ImportTransport, forwarded verbatim from createRenderer to the
504
- * AssetRegistry third ctor slot. `undefined` keeps the shipped form (DDC
505
- * miss fails fast with `asset-not-imported`, AC-08).
506
- */
507
- importTransport?: ImportTransport | undefined;
508
- /**
509
- * feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M2 / T-M2-05:
510
- * mesh-SSBO grow hook surfaced by `makeWebGPURenderer` after the controller
511
- * is constructed. Undefined before `makeWebGPURenderer` runs; populated by
512
- * the time the renderer is returned to the caller. M3's record stage reads
513
- * this field via the runtime path that bridges into render-system-record.ts
514
- * (the bridging plumbing is in M3's targetFiles).
515
- *
516
- * Returns `{ ok: true }` when the grow completed (or short-circuited
517
- * idempotently); `{ ok: false, code }` with a fired structured error
518
- * (`MeshSsboCeilingReachedError` or `MeshSsboCapacityExceededError`) when
519
- * the request cannot be satisfied. Never throws (D-5).
520
- */
521
- growMeshSsbo?: (neededSlots: number) => MeshSsboGrowResult;
522
- /**
523
- * feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M3 / T-M3-04:
524
- * read-only handle to the grow controller's state (slotCount + wrapper
525
- * refs), surfaced alongside `growMeshSsbo` so `ensureMeshSsboCapacity`
526
- * (record-stage wiring) can short-circuit when slotCount already covers the
527
- * frame's renderable count and so the dev-mode info log can report the
528
- * before/after slot count transition. Undefined until buildReadyWebGPU
529
- * has wired the controller; identity stable across grow events.
530
- */
531
- meshSsboState?: MeshSsboState;
532
- /** Host-owned producer features; created before device-bound assembly starts. */
533
- featureHost?: RenderFeatureHost;
534
- /** App-owned overlay capability read by the typed graph at frame time. */
535
- debugOverlay?: RenderDebugOverlay;
536
- /** Composed renderer utility sources installed atomically at build-ready. */
537
- volumetricFogShaders?: VolumetricFogShaderSources | undefined;
538
- gpuPassTimingSubmittedWork?: Promise<void> | undefined;
539
- lossObserver: { current?: (detail: string) => void };
540
- generationState: {
541
- current: number;
542
- onStaleLoss?: () => void;
543
- };
544
- recoveryColdWorkGuard?: RecoveryColdWorkGuard | undefined;
545
- getMaterialShaderUvSetCount?: (materialShaderId: string) => number | undefined;
546
- }
547
-
548
- export interface RecoveryFailureLocation {
549
- readonly phase: RecoveryPhase;
550
- readonly retryable: boolean;
551
- readonly guidance: RecoveryGuidance;
552
- readonly owner: string;
553
- readonly resourceKind: DeviceResourceKind;
554
- readonly cause: unknown;
555
- }
556
-
557
- function recoveryCauseCode(cause: unknown): string | undefined {
558
- if (typeof cause !== 'object' || cause === null) return undefined;
559
- const directCode = (cause as { readonly code?: unknown }).code;
560
- if (typeof directCode === 'string') return directCode;
561
- const detail = (cause as { readonly detail?: unknown }).detail;
562
- if (typeof detail !== 'object' || detail === null) return undefined;
563
- const detailCode = (detail as { readonly code?: unknown }).code;
564
- if (typeof detailCode === 'string') return detailCode;
565
- return recoveryCauseCode((detail as { readonly cause?: unknown }).cause);
566
- }
567
-
568
- function createRecoveryFailureLocation(
569
- phase: RecoveryPhase,
570
- cause: unknown,
571
- overrides: Partial<Omit<RecoveryFailureLocation, 'phase' | 'cause'>> = {},
572
- ): RecoveryFailureLocation {
573
- const code = recoveryCauseCode(cause) ?? '';
574
- const resourceKind: DeviceResourceKind = code.includes('shader')
575
- ? 'shader'
576
- : code.includes('buffer')
577
- ? 'buffer'
578
- : code.includes('texture')
579
- ? 'texture'
580
- : code.includes('binding')
581
- ? 'binding'
582
- : phase === 'rehydrate'
583
- ? 'shader'
584
- : phase === 'acquire-adapter' || phase === 'acquire-device'
585
- ? 'surface'
586
- : phase === 'publish' || phase === 'cleanup'
587
- ? 'surface'
588
- : 'pipeline';
589
- const owner =
590
- code.includes('shader') || phase === 'rehydrate'
591
- ? 'shader'
592
- : phase === 'acquire-adapter' || phase === 'acquire-device'
593
- ? 'backend'
594
- : phase === 'compile-graph'
595
- ? 'pipeline'
596
- : 'renderer';
597
- return {
598
- phase,
599
- retryable: false,
600
- guidance: 'repair-owner',
601
- owner,
602
- resourceKind,
603
- cause,
604
- ...overrides,
605
- };
606
- }
607
-
608
281
  async function makeWebGPURenderer(
609
282
  internals: WebGPURendererInternals,
610
283
  ): Promise<RendererAssemblyImplementation> {
@@ -893,16 +566,13 @@ async function makeWebGPURenderer(
893
566
  // looks up MeshFilter.assetHandle here per frame; buildReadyWebGPU step 3
894
567
  // uploads the registry's builtin mesh geometry to GPU buffers so the
895
568
  // RenderSystem can route MeshFilter.assetHandle to a real (vbo, ibo) pair.
896
- //
897
569
  // feat-20260527 M1 / w1: ShaderCatalog is now eager-constructed and
898
570
  // constructor-injected into AssetRegistry (plan-strategy D-1). The
899
571
  // ShaderCatalogDevice adapter (shared between the catalog and per-MaterialShader
900
572
  // pipeline cache) is already available at this point.
901
573
  const shaderCatalog = getShader();
902
-
903
574
  // feat-20260528-material-shader-registration-unification M3 / w14:
904
575
  // placeholder hardcoded installMaterialArtifact calls deleted.
905
- //
906
576
  // bug-20260601-hello-tonemap-material-register M1 (plan-strategy D-1):
907
577
  // All material-shader entries are now registered from the manifest
908
578
  // BEFORE the renderer is returned, so `register<MaterialAsset>` referencing
@@ -945,8 +615,7 @@ async function makeWebGPURenderer(
945
615
  renderSystemForTargetPromotion?.isCubeCapturePending(target) !== true,
946
616
  });
947
617
  // feat-20260527-sprite-nineslice M4 / w16 (D-5): detached host-owned
948
- // EngineMetrics counter, threaded to the record
949
- // stage via `RenderSystemRuntime.metrics` so soft-warns
618
+ // EngineMetrics counter reaches the record stage via `RenderSystemRuntime.metrics`.
950
619
  // (`nineslice.scale-too-small`, `nineslice.tile-needs-repeat-sampler`) bump
951
620
  // counters through the owning service (charter P3 machine-readable signals
952
621
  // over a per-frame console.warn flood). Each renderer assembly owns its own
@@ -1016,8 +685,7 @@ async function makeWebGPURenderer(
1016
685
  // feat-20260707 M5 / w33 (D-11 + D-8): project the RhiCaps three-way
1017
686
  // compression triple into the codec-facing `TranscodeCaps` and wire it into
1018
687
  // the registry so the texture / equirect Basis arms can pick a transcode
1019
- // target. One-line projection; the loader stays a pure consumer of the
1020
- // declared ctx input (Pipeline Isolation).
688
+ // target. One-line projection; the loader stays a pure consumer of declared ctx input (Pipeline Isolation).
1021
689
  assets.setTranscodeCaps({
1022
690
  bc: internals.device.caps.textureCompressionBc,
1023
691
  etc2: internals.device.caps.textureCompressionEtc2,
@@ -1039,25 +707,6 @@ async function makeWebGPURenderer(
1039
707
  // count is asserted explicitly. Missing mesh sets are clamp-to-last aliases.
1040
708
  materialShaderUvSetCounts.set('forgeax::default-standard-pbr', 8);
1041
709
  materialShaderUvSetCounts.set(SKIN_MATERIAL_SHADER_ID, 8);
1042
- // M5 wiring (feat-20260517-vite-plugin-image-build-time-cook w14b): hand
1043
- // the RhiDevice to the AssetRegistry so `loadByGuid<TextureAsset>` ->
1044
- // `loadTextureFromEntry` -> `uploadTexture` actually runs the GPU
1045
- // upload step plan-strategy section 3.2 sequence specifies. Without
1046
- // this call uploadTexture step 3 short-circuits with `Result.ok(undefined)`
1047
- // and the render-system materialBindGroup falls through to the 1x1
1048
- // white fallback view (charter P3 violation: silent loss of texture).
1049
- // bug-20260518 D-1: configureGpuDevice gains a 2nd parameter --
1050
- // `pack.createShaderModule`, the top-level async shader-module factory
1051
- // (rhi-webgpu / rhi-wgpu both expose it). The runtime mipmap utility uses
1052
- // this factory inside `generateMipmaps` instead of the (now-deleted)
1053
- // synchronous `device.createShaderModule` member. The parameter is
1054
- // structurally optional at this call-site so the explicit-rhi escape
1055
- // hatch (D-R5) without a top-level factory still routes through the
1056
- // existing `invokeDeviceCreateShaderModule` fallback in
1057
- // buildReadyWebGPU; AssetRegistry.uploadTexture's mipmap branch surfaces
1058
- // the missing-factory case as a structured `rhi-not-available` error
1059
- // (charter P3 explicit failure) rather than throwing here.
1060
- //
1061
710
  const packShaderFactory = adaptMipmapShaderModuleFactory(internals.pack.createShaderModule);
1062
711
  // The RhiDevice surface satisfies `MipmapBlitDevice` (createTexture +
1063
712
  // createTextureView + createCommandEncoder + createBindGroup +
@@ -1128,28 +777,6 @@ async function makeWebGPURenderer(
1128
777
  gpuStore.bindDeviceScope(activeDeviceScope);
1129
778
  };
1130
779
  let readySettled = false;
1131
- // w24 — Renderer.initialization returns Promise<Result<void, RhiError>>: resolve
1132
- // ok(undefined) on success / resolve err(RhiError) on failure. The legacy
1133
- // reject path is converted by a `.then() / .catch()` wrap so the inner
1134
- // factory (`buildReadyWebGPU`) keeps its reject-on-error shape (research
1135
- // F-3 internal contract preserved) while the public surface settles
1136
- // strictly resolve-only (charter proposition 4 explicit failure - AI
1137
- // users branch on `.ok` instead of try/catch).
1138
- // feat-20260523-shader-template-instance-split M9-T03 (D-PipelineBuilder):
1139
- // per-MaterialShader pipeline cache (LDR + HDR variants keyed by
1140
- // `${materialShaderId}:${ldr|hdr}`). Map is owned by the renderer (lifetime
1141
- // = Renderer) so cache hits persist across draw calls but reset between
1142
- // renderers. Map.get / .set are sync; the underlying shader-module compile
1143
- // is async-cached inside `makeShaderDeviceAdapter` (1-frame warmup -- first
1144
- // miss returns Result.err('rhi-not-available'); after the async build
1145
- // resolves the adapter caches the module, the next miss-then-build returns
1146
- // Ok and lands in this Map).
1147
- //
1148
- // M6 fix-up: declared BEFORE `buildReadyWebGPU` so the boot-time SPEC_CONST
1149
- // prewarm step inside that promise can seed URP-variant entries directly
1150
- // (the seeding closure captures this Map by reference; ordering is safe
1151
- // because the prewarm step runs strictly before the first frame's
1152
- // `getMaterialShaderPipeline` lookup that would consume it).
1153
780
  const materialShaderPipelineCache = new Map<string, RenderPipeline>();
1154
781
  const materialShaderPipelineGroup2Contracts = new WeakMap<object, PipelineGroup2Contract>();
1155
782
  const materialShaderManifestEntryCache = new Map<string, MaterialShaderManifestEntry>();
@@ -1415,9 +1042,23 @@ async function makeWebGPURenderer(
1415
1042
  };
1416
1043
  const getOrBuildPreparedMaterialPipelineLayout = (
1417
1044
  materialShaderId: string,
1045
+ clustered = false,
1418
1046
  ): PipelineLayout | null => {
1419
1047
  const cacheState = currentPipelineCacheState();
1420
- const cached = cacheState.preparedMaterialPipelineLayoutCache.get(materialShaderId);
1048
+ const lookup = getShader().findMaterialArtifact(materialShaderId);
1049
+ const source = lookup.ok ? lookup.value.source : '';
1050
+ const hasView =
1051
+ /@group\s*\(\s*0\s*\)\s*@binding\s*\(\s*0\s*\)\s*var\s*<\s*uniform\s*>\s*view(?:X_naga_oil_mod_[A-Z0-9]+)?\b/u.test(
1052
+ source,
1053
+ );
1054
+ const hasSceneDepthAtZero =
1055
+ /@group\s*\(\s*0\s*\)\s*@binding\s*\(\s*0\s*\)[^;]*texture_depth_2d\b/u.test(source);
1056
+ const hasSceneDepthAtOne =
1057
+ /@group\s*\(\s*0\s*\)\s*@binding\s*\(\s*1\s*\)[^;]*texture_depth_2d\b/u.test(source);
1058
+ const sceneInputKind =
1059
+ hasSceneDepthAtOne && hasView ? 'view-depth' : hasSceneDepthAtZero ? 'depth' : 'view';
1060
+ const cacheKey = `${materialShaderId}:${clustered ? 'cluster' : 'mesh'}:${sceneInputKind}`;
1061
+ const cached = cacheState.preparedMaterialPipelineLayoutCache.get(cacheKey);
1421
1062
  if (cached !== undefined) return cached;
1422
1063
  const currentState = currentPipelineState();
1423
1064
  if (currentState === null) return null;
@@ -1426,15 +1067,72 @@ async function makeWebGPURenderer(
1426
1067
  ? undefined
1427
1068
  : getOrBuildPerShaderMaterialLayout(materialShaderId)?.materialBgl) ??
1428
1069
  currentState.materialBindGroupLayout;
1070
+ let sceneInputLayout: BindGroupLayout = currentState.viewBindGroupLayout;
1071
+ if (sceneInputKind !== 'view') {
1072
+ const sceneInputLayoutResult = currentBuildDevice().createBindGroupLayout({
1073
+ label: `prepared-material-${sceneInputKind}-${materialShaderId}`,
1074
+ entries:
1075
+ sceneInputKind === 'view-depth'
1076
+ ? [
1077
+ {
1078
+ binding: 0,
1079
+ visibility: GPU_SHADER_STAGE_VERTEX | GPU_SHADER_STAGE_FRAGMENT,
1080
+ buffer: { type: 'uniform' as const },
1081
+ },
1082
+ {
1083
+ binding: 1,
1084
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
1085
+ texture: {
1086
+ sampleType: 'depth' as const,
1087
+ viewDimension: '2d' as const,
1088
+ multisampled: false,
1089
+ },
1090
+ },
1091
+ ]
1092
+ : [
1093
+ {
1094
+ binding: 0,
1095
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
1096
+ texture: {
1097
+ sampleType: 'depth' as const,
1098
+ viewDimension: '2d' as const,
1099
+ multisampled: false,
1100
+ },
1101
+ },
1102
+ ],
1103
+ });
1104
+ if (!sceneInputLayoutResult.ok) {
1105
+ internals.errorRegistry.fire(sceneInputLayoutResult.error);
1106
+ return null;
1107
+ }
1108
+ sceneInputLayout = sceneInputLayoutResult.value;
1109
+ }
1110
+ const usesMaterialGroup = clustered || /@group\s*\(\s*1\s*\)/u.test(source);
1111
+ const lightingLayout = clustered
1112
+ ? currentBuildDevice().createBindGroupLayout({
1113
+ label: `prepared-material-lighting-${materialShaderId}`,
1114
+ entries: createHdrpBindGroupLayoutDescriptor().entries.filter(
1115
+ (entry) => entry.binding >= 3,
1116
+ ),
1117
+ })
1118
+ : undefined;
1119
+ if (lightingLayout !== undefined && !lightingLayout.ok) {
1120
+ internals.errorRegistry.fire(lightingLayout.error);
1121
+ return null;
1122
+ }
1429
1123
  const result = currentBuildDevice().createPipelineLayout({
1430
1124
  label: `prepared-material-pl-${materialShaderId}`,
1431
- bindGroupLayouts: [currentState.viewBindGroupLayout, materialBgl],
1125
+ bindGroupLayouts: [
1126
+ sceneInputLayout,
1127
+ ...(usesMaterialGroup ? [materialBgl] : []),
1128
+ ...(lightingLayout?.ok ? [lightingLayout.value] : []),
1129
+ ],
1432
1130
  });
1433
1131
  if (!result.ok) {
1434
1132
  internals.errorRegistry.fire(result.error);
1435
1133
  return null;
1436
1134
  }
1437
- cacheState.preparedMaterialPipelineLayoutCache.set(materialShaderId, result.value);
1135
+ cacheState.preparedMaterialPipelineLayoutCache.set(cacheKey, result.value);
1438
1136
  return result.value;
1439
1137
  };
1440
1138
  const getOrBuildPerShaderMaterialLayout = (
@@ -1484,6 +1182,7 @@ async function makeWebGPURenderer(
1484
1182
  const desc = buildBindGroupLayoutDescriptor(spec, {
1485
1183
  kind: 'pbr-material-merged',
1486
1184
  materialParamSchema: paramSchema,
1185
+ caps: { storageBuffer: currentBuildDevice().caps.storageBuffer },
1487
1186
  });
1488
1187
  const bglRes = currentBuildDevice().createBindGroupLayout(desc);
1489
1188
  if (!bglRes.ok) {
@@ -1622,13 +1321,9 @@ async function makeWebGPURenderer(
1622
1321
  internals.pack.createShaderModule,
1623
1322
  internals.errorRegistry,
1624
1323
  requiredMaterialShaders,
1324
+ materialShaderUvSetCounts.get('forgeax::default-standard-pbr') ?? 0,
1625
1325
  requiredFullscreenPostProcesses,
1626
- // feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M2 /
1627
- // T-M2-05 + M3 / T-M3-04: callback set by buildReadyWebGPU once
1628
- // meshSsboController is wired. Both the grow hook AND the read-only
1629
- // state ref land on `internals` so the record stage (M3
1630
- // ensureMeshSsboCapacity) can read slotCount + call grow through
1631
- // RenderSystemInternals.
1326
+ // Publish growth and capacity together for the active or candidate device.
1632
1327
  (hook, state) => {
1633
1328
  if (candidateBuildState !== undefined) {
1634
1329
  candidateBuildState.growMeshSsbo = hook;
@@ -1653,13 +1348,7 @@ async function makeWebGPURenderer(
1653
1348
  }
1654
1349
  materialShaderPipelineGroup2Contracts.set(pso as object, group2Contract);
1655
1350
  },
1656
- // feat-20260621 M-A3 (D-5): register the built-in tonemap on the unified
1657
- // post-process channel once buildReadyWebGPU resolves the tonemap manifest
1658
- // entry's composed WGSL. Fires after the manifest-load await, by which point
1659
- // `renderSystem` (declared synchronously below) is defined. The 16 B params
1660
- // schema mirrors the prior dedicated UBO: [exposure(f32), whitePoint(f32),
1661
- // mode(u32), pad(f32)]; the extract stage bridges Camera.exposure/whitePoint/
1662
- // tonemap into this channel each frame (render-system-extract.ts w13).
1351
+ // Register the output transform after manifest loading, once RenderSystem exists.
1663
1352
  (source: string) => {
1664
1353
  if (candidateBuildState !== undefined || tonemapRegistered) return;
1665
1354
  renderSystem.registerBuiltinPostProcess(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, {
@@ -1699,7 +1388,18 @@ async function makeWebGPURenderer(
1699
1388
  (sources: VolumetricFogShaderSources) => {
1700
1389
  internals.volumetricFogShaders = sources;
1701
1390
  },
1702
- );
1391
+ (sources: SsrShaderSources) => {
1392
+ internals.ssrShaders = sources;
1393
+ },
1394
+ (sources: AtmosphereShaderSources) => {
1395
+ internals.atmosphereShaders = sources;
1396
+ },
1397
+ ).then(async (state) => {
1398
+ // draw() is synchronous. Complete the opt-in capability probe before
1399
+ // publishing readiness, including when rebuilding a lost device.
1400
+ await renderSystem.initializeSsr(device);
1401
+ return state;
1402
+ });
1703
1403
  const ready: Promise<Result<void, RhiError>> = buildPipeline().then(
1704
1404
  (state): Result<void, RhiError> => {
1705
1405
  pipelineState = state;
@@ -1710,14 +1410,8 @@ async function makeWebGPURenderer(
1710
1410
  (e: unknown): Result<void, RhiError> => {
1711
1411
  readySettled = true;
1712
1412
  if (e instanceof RhiError) return err(e);
1713
- const message = e instanceof Error ? e.message : String(e);
1714
- return err(
1715
- new RhiError({
1716
- code: 'webgpu-runtime-error',
1717
- expected: 'Renderer.initialization three-step strict-serial succeeds',
1718
- hint: `pipeline build raised: ${message}`,
1719
- }),
1720
- );
1413
+ if (e instanceof PipelineSpecError) return err(rendererInitializationPipelineError(e));
1414
+ return err(rendererInitializationError(e));
1721
1415
  },
1722
1416
  );
1723
1417
  /**
@@ -1760,33 +1454,17 @@ async function makeWebGPURenderer(
1760
1454
  }
1761
1455
  return standardEntry;
1762
1456
  };
1763
- // Shared builder context for the per-MaterialShader pipeline cache.
1764
- // Built once; reused for both registered shader IDs and fallback
1765
- // paths (D-3). Null when manifest is empty (Camera-only path).
1766
- //
1767
- // feat-20260609-hdrp-cluster-fragment-ggx M4.5 / w37 (D-10 option A):
1768
- // `variantSet` parameter selects between URP `pbrPipelineLayout` (1-slot
1769
- // group(2) BGL) and HDRP `hdrpPbrPipelineLayout` (7-slot group(2) BGL)
1770
- // via `selectPipelineLayoutForVariant`. When undefined (legacy callers
1771
- // / fallback path) the URP layout is used preserving prior behaviour.
1772
1457
  const buildPipelineContext = (
1773
1458
  variantSet?: string,
1774
1459
  materialShaderId?: string,
1775
1460
  group2Contract: PipelineGroup2Contract = 'mesh',
1776
- // feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
1777
- // when layoutKind === 'pbr-skin' the SSOT for the vertex buffer layout
1778
- // is `deriveVertexBufferLayout` (vertex-attribute-layout.ts) — the same
1779
- // function that the WGSL @location(N) declarations / naga reflect tests
1780
- // consume. A caller passing a real `MeshAsset.attributes` produces the
1781
- // exact 6-attribute / 72-byte layout the skin shader expects; passing
1782
- // undefined falls into the synthetic 6-key sentinel below (the skin
1783
- // path is fully determined by layoutKind, so key-presence is the only
1784
- // signal `deriveVertexBufferLayout` reads — values never matter).
1461
+ // Skin layouts derive their vertex ABI from the shared attribute projection.
1785
1462
  meshAttributes?: VertexAttributeMap,
1786
1463
  vertexLayout?: string,
1787
1464
  vertexLayoutProjection?: VertexLayoutProjection,
1788
1465
  shaderModuleMode?: RenderFeatureShaderModuleMode,
1789
1466
  layoutKindOverride?: LayoutKind,
1467
+ particleInputLanes?: number,
1790
1468
  ) => {
1791
1469
  const contractLayoutKind: LayoutKind =
1792
1470
  group2Contract === 'cluster'
@@ -1799,18 +1477,18 @@ async function makeWebGPURenderer(
1799
1477
  const layoutKind = layoutKindOverride ?? contractLayoutKind;
1800
1478
  const usesSharedBootMaterialLayout =
1801
1479
  materialShaderId !== undefined && isEngineOwnedMaterialShader(materialShaderId);
1802
- // Built-in PBR/unlit/skin layouts are assembled at boot. Re-deriving a
1803
- // per-shader layout from the expanded PBR schema would bypass the HDRP
1804
- // group(2) selector and bind the URP mesh BGL to the cluster pipeline.
1480
+ // Built-in layouts are assembled at boot; authored layouts must not bypass
1481
+ // the HDRP group(2) selector or bind the URP mesh BGL to a cluster PSO.
1805
1482
  const perShaderLayout =
1806
- !usesSharedBootMaterialLayout && materialShaderId !== undefined
1483
+ !usesSharedBootMaterialLayout &&
1484
+ layoutKind !== 'pbr-skin' &&
1485
+ layoutKind !== 'gpu-driven-pbr' &&
1486
+ layoutKind !== 'gpu-driven-skin' &&
1487
+ materialShaderId !== undefined
1807
1488
  ? getOrBuildPerShaderMaterialLayout(materialShaderId, undefined, layoutKind)
1808
1489
  : null;
1809
- // feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): an authored
1810
- // shader owns a per-shader pipeline layout derived from its paramSchema,
1811
- // because its compact texture order may differ from standard PBR even
1812
- // when it declares only a few textures. Skin / HDRP keep their dedicated
1813
- // layouts; built-in material IDs remain on the shared boot-time layout.
1490
+ // Authored shaders use their paramSchema; skin/HDRP retain dedicated
1491
+ // layouts and built-in IDs stay on the shared boot-time layout.
1814
1492
  const bindingContract =
1815
1493
  materialShaderId === undefined
1816
1494
  ? 'render-material'
@@ -1827,11 +1505,12 @@ async function makeWebGPURenderer(
1827
1505
  ? getOrBuildViewAndSceneDepthMaterialPipelineLayout()
1828
1506
  : null;
1829
1507
  const preparedMaterialLayout =
1830
- bindingContract === 'render-material' &&
1508
+ (bindingContract === 'render-material' ||
1509
+ bindingContract === 'render-material-with-scene-depth' ||
1510
+ bindingContract === 'render-material-and-scene-depth') &&
1831
1511
  materialShaderId !== undefined &&
1832
- (vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.billboardMaterialInstance ||
1833
- vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance)
1834
- ? getOrBuildPreparedMaterialPipelineLayout(materialShaderId)
1512
+ isPreparedMaterialVertexLayout(vertexLayout)
1513
+ ? getOrBuildPreparedMaterialPipelineLayout(materialShaderId, group2Contract === 'cluster')
1835
1514
  : null;
1836
1515
  const materialShaderLookup =
1837
1516
  materialShaderId === undefined
@@ -1873,64 +1552,15 @@ async function makeWebGPURenderer(
1873
1552
  )
1874
1553
  : undefined))
1875
1554
  : undefined;
1876
- const vertexBuffers: readonly GPUVertexBufferLayout[] =
1877
- vertexInputContract === 'none'
1878
- ? []
1879
- : vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.positionSizeColorInstance
1880
- ? POSITION_SIZE_COLOR_INSTANCE_VERTEX_BUFFERS
1881
- : vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.billboardMaterialInstance
1882
- ? BILLBOARD_MATERIAL_INSTANCE_VERTEX_BUFFERS
1883
- : vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.topologySegmentInstance
1884
- ? TOPOLOGY_SEGMENT_INSTANCE_VERTEX_BUFFERS
1885
- : vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance
1886
- ? MESH_GEOMETRY_MATERIAL_INSTANCE_VERTEX_BUFFERS
1887
- : vertexLayoutProjection !== undefined
1888
- ? toGpuVertexBufferLayouts(
1889
- deriveVertexBufferLayoutFromProjection(
1890
- vertexLayoutProjection,
1891
- resolvedUvSetCount !== undefined
1892
- ? { shaderUvSetCount: resolvedUvSetCount }
1893
- : undefined,
1894
- ),
1895
- )
1896
- : layoutKind === 'pbr-skin'
1897
- ? toGpuVertexBufferLayouts(
1898
- deriveVertexBufferLayout(
1899
- meshAttributes ??
1900
- ({
1901
- position: PBR_SKIN_SENTINEL_ATTR_BUFFER,
1902
- normal: PBR_SKIN_SENTINEL_ATTR_BUFFER,
1903
- uv: PBR_SKIN_SENTINEL_ATTR_BUFFER,
1904
- tangent: PBR_SKIN_SENTINEL_ATTR_BUFFER,
1905
- skinIndex: PBR_SKIN_SENTINEL_ATTR_BUFFER,
1906
- skinWeight: PBR_SKIN_SENTINEL_ATTR_BUFFER,
1907
- } satisfies VertexAttributeMap),
1908
- resolvedUvSetCount !== undefined
1909
- ? { shaderUvSetCount: resolvedUvSetCount }
1910
- : undefined,
1911
- ),
1912
- )
1913
- : meshAttributes !== undefined ||
1914
- (resolvedUvSetCount !== undefined && resolvedUvSetCount > 1)
1915
- ? toGpuVertexBufferLayouts(
1916
- deriveVertexBufferLayout(
1917
- meshAttributes ?? DEFAULT_VERTEX_ATTRS,
1918
- resolvedUvSetCount !== undefined && resolvedUvSetCount > 1
1919
- ? { shaderUvSetCount: resolvedUvSetCount }
1920
- : undefined,
1921
- ),
1922
- )
1923
- : ([
1924
- {
1925
- arrayStride: 12 * 4,
1926
- attributes: [
1927
- { shaderLocation: 0, offset: 0, format: 'float32x3' as const },
1928
- { shaderLocation: 1, offset: 3 * 4, format: 'float32x3' as const },
1929
- { shaderLocation: 2, offset: 6 * 4, format: 'float32x2' as const },
1930
- { shaderLocation: 3, offset: 8 * 4, format: 'float32x4' as const },
1931
- ],
1932
- },
1933
- ] satisfies readonly GPUVertexBufferLayout[]);
1555
+ const vertexBuffers = resolveWebGPUVertexBufferLayouts({
1556
+ vertexInputContract,
1557
+ vertexLayout,
1558
+ vertexLayoutProjection,
1559
+ resolvedUvSetCount,
1560
+ layoutKind,
1561
+ meshAttributes,
1562
+ particleInputLanes,
1563
+ });
1934
1564
  return {
1935
1565
  device: currentBuildDevice(),
1936
1566
  shaderModuleFactory:
@@ -1939,7 +1569,7 @@ async function makeWebGPURenderer(
1939
1569
  : getShaderModuleAdapter(),
1940
1570
  pipelineLayout,
1941
1571
  vertexBuffers,
1942
- ...(layoutKind === 'pbr-skin' ? { layoutKind } : {}),
1572
+ ...(layoutKind === 'pbr-skin' || layoutKind === 'gpu-driven-skin' ? { layoutKind } : {}),
1943
1573
  };
1944
1574
  };
1945
1575
  /**
@@ -1999,11 +1629,20 @@ async function makeWebGPURenderer(
1999
1629
  vertexLayoutProjection?: VertexLayoutProjection,
2000
1630
  shaderModuleMode?: RenderFeatureShaderModuleMode,
2001
1631
  layoutKindOverride?: LayoutKind,
1632
+ vertexEntryPoint?: string,
2002
1633
  additionalColorFormats?: readonly GPUTextureFormat[],
2003
1634
  vertexEntry?: string,
2004
1635
  authoredFragmentEntry?: string,
1636
+ particleInputLanes?: number,
2005
1637
  ): RenderPipeline | null => {
2006
- const group2Contract = resolvePipelineGroup2Contract(entry.source);
1638
+ const declaredGroup2Contract = resolvePipelineGroup2Contract(entry.source);
1639
+ const group2Contract =
1640
+ declaredGroup2Contract === 'mesh' &&
1641
+ (materialShaderId === 'forgeax::vfx-render.particles.mesh' ||
1642
+ materialShaderId === 'forgeax::vfx-render.particles.mesh-inputs') &&
1643
+ (variantSet === '' || variantSet?.includes('CLUSTER_FORWARD_AVAILABLE=true') === true)
1644
+ ? 'cluster'
1645
+ : declaredGroup2Contract;
2007
1646
  const ctx = buildPipelineContext(
2008
1647
  variantSet,
2009
1648
  materialShaderId,
@@ -2013,13 +1652,9 @@ async function makeWebGPURenderer(
2013
1652
  vertexLayoutProjection,
2014
1653
  shaderModuleMode,
2015
1654
  layoutKindOverride,
1655
+ particleInputLanes,
2016
1656
  );
2017
1657
  if (ctx === null) return null;
2018
- // bug-20260612: route the LDR color format through pipelineState so the
2019
- // backend-aware swap-chain format chosen at buildReadyWebGPU flows here.
2020
- // Callers gate on pipelineState !== null before reaching this lambda
2021
- // (search "buildAndCachePipeline" call sites — every one is preceded by
2022
- // the explicit null check).
2023
1658
  const currentState = currentPipelineState();
2024
1659
  if (currentState === null) return null;
2025
1660
  const ldrColorFormat = colorFormatOverride ?? currentState.colorAttachmentFormat;
@@ -2042,12 +1677,13 @@ async function makeWebGPURenderer(
2042
1677
  ? undefined
2043
1678
  : [isHdr ? HDR_COLOR_ATTACHMENT_FORMAT : ldrColorFormat, ...additionalColorFormats],
2044
1679
  depthFormat:
2045
- depthFormatOverride === null ? undefined : (depthFormatOverride ?? DEPTH_TEXTURE_FORMAT),
1680
+ depthFormatOverride === null
1681
+ ? undefined
1682
+ : passKindPolicyTable[passKind]?.shape === 'depth-only'
1683
+ ? (depthFormatOverride ?? 'depth32float')
1684
+ : (depthFormatOverride ?? DEPTH_TEXTURE_FORMAT),
2046
1685
  label,
2047
1686
  // feat-20260604 w16-b: the shader-MODULE cache identity. Stable across
2048
- // topology / renderState / isHdr / indexFormat (all baked into the PSO,
2049
- // not the module), so every pipeline variant of the same shader source
2050
- // reuses one compiled module instead of forcing a fresh async compile
2051
1687
  // per variant. See PipelineBuilderContext.moduleLabel.
2052
1688
  moduleLabel,
2053
1689
  },
@@ -2066,7 +1702,7 @@ async function makeWebGPURenderer(
2066
1702
  : {}),
2067
1703
  }
2068
1704
  : undefined,
2069
- vertexEntry,
1705
+ vertexEntry ?? vertexEntryPoint,
2070
1706
  fragmentEntry,
2071
1707
  undefined, // defines — none
2072
1708
  passKind,
@@ -2079,10 +1715,7 @@ async function makeWebGPURenderer(
2079
1715
  return null;
2080
1716
  }
2081
1717
  currentPipelineCacheState().materialShaderPipelineCache.set(cacheKey, built.value);
2082
- materialShaderPipelineGroup2Contracts.set(
2083
- built.value as object,
2084
- resolvePipelineGroup2Contract(entry.source),
2085
- );
1718
+ materialShaderPipelineGroup2Contracts.set(built.value as object, group2Contract);
2086
1719
  return built.value;
2087
1720
  };
2088
1721
  const getMaterialShaderPipeline = (
@@ -2093,11 +1726,6 @@ async function makeWebGPURenderer(
2093
1726
  indexFormat?: 'uint16' | 'uint32',
2094
1727
  variantSet?: string,
2095
1728
  passKind: PassKind = 'forward',
2096
- // feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
2097
- // forwarded to `buildAndCachePipeline -> buildPipelineContext` so the
2098
- // pbr-skin layout chain reads from the `deriveVertexBufferLayout` SSOT.
2099
- // Optional: existing callers (URP / HDRP / shadow-depth) pass undefined
2100
- // and the hardcoded 4-attribute layout is preserved (AC-04 zero-regression).
2101
1729
  meshAttributes?: VertexAttributeMap,
2102
1730
  // bug-20260615 M2 / m2-1: sampleCount is threaded through to the cache key,
2103
1731
  // buildAndCachePipeline, and ultimately buildPipelineForMaterialShader which
@@ -2132,9 +1760,11 @@ async function makeWebGPURenderer(
2132
1760
  vertexLayoutProjection?: VertexLayoutProjection,
2133
1761
  shaderModuleMode: RenderFeatureShaderModuleMode = 'validated',
2134
1762
  layoutKindOverride?: LayoutKind,
1763
+ vertexEntryPoint?: string,
2135
1764
  additionalColorFormats?: readonly GPUTextureFormat[],
2136
1765
  vertexEntry?: string,
2137
1766
  fragmentEntry?: string,
1767
+ particleInputLanes?: number,
2138
1768
  ): RenderPipeline | null => {
2139
1769
  // WebGL2 is a downlevel backend even when the record stage asks for the
2140
1770
  // native-capability variant string. Resolve the variant axes at this
@@ -2175,20 +1805,14 @@ async function makeWebGPURenderer(
2175
1805
  (pipelineShaderId === 'forgeax::sprite' || pipelineShaderId === 'forgeax::sprite-lit')
2176
1806
  ? undefined
2177
1807
  : resolveCachedMaterialShaderVariantSet(variantSet, manifestEntry);
2178
- // The record stage also composes engine capability axes for user shaders.
2179
- // Keep only manifest-declared axes for non-engine material IDs: a plain
2180
- // custom shader must not collapse that request to '' because '' is the
2181
- // canonical HDRP variant key and would pair an HDRP PSO with the URP mesh
2182
- // bind group. Builtin forgeax:: shaders keep the compatibility request
1808
+ // The record stage composes capability axes; plain custom shaders retain
1809
+ // only manifest-declared axes so '' cannot select an HDRP mesh layout.
1810
+ // Builtin forgeax:: shaders keep the compatibility request
2183
1811
  // when a reduced test/legacy manifest omits variant metadata.
2184
1812
  // A single-source manifest entry must stay on its canonical module key;
2185
1813
  // otherwise a synthesized capability axis races the prewarmed module.
2186
1814
  let effectiveVariantSet = normalizeMaterialShaderVariantSet(resolvedVariantSet, manifestEntry);
2187
- // Runtime-published authored materials are installed from their immutable
2188
- // cooked artifact, not from the build-time material-shader manifest. The
2189
- // published artifact identity already selects the backend-specific bytes;
2190
- // an absent manifest therefore means the source itself is the canonical
2191
- // pipeline module and must not carry a synthesized capability key.
1815
+ // A published artifact is already backend-specific; no manifest means no synthesized key.
2192
1816
  if (manifestEntry === undefined) effectiveVariantSet = undefined;
2193
1817
  if (
2194
1818
  vertexLayoutProjection === undefined &&
@@ -2227,25 +1851,16 @@ async function makeWebGPURenderer(
2227
1851
  effectiveVariantSet =
2228
1852
  filteredVariantParts.length === 0 ? undefined : filteredVariantParts.join('+');
2229
1853
  }
2230
- // feat-20260615-pipeline-spec-ssot M2-T2: cache key derived from PipelineSpec
2231
- // 4-axis SSOT via cacheKeyOf(spec). The spec carries all 4 axes (shader /
2232
- // attachments / geometry / renderState), replacing the legacy 8-segment string
2233
- // construction. vertexLayout is now included in the cache key (was previously
2234
- // threaded but not hashed — research F1 R-VertexLayout-Cache). sampleCount is
2235
- // cast to 1 | 4 for the closed spec axis.
2236
- // M2-fixup follow-up: LDR color format SSOT is `pipelineState.colorAttachmentFormat`
2237
- // (set in buildReadyWebGPU from selectSwapChainFormat). Hardcoding 'bgra8unorm-srgb'
2238
- // here let the cache key drift from the prewarm key on backends where
2239
- // getPreferredCanvasFormat returns rgba8unorm (dawn-node, lavapipe, wgpu-wasm GLES),
2240
- // forcing a redundant second PSO build on first-frame URP record path.
2241
1854
  const colorFormat: GPUTextureFormat = isHdr ? 'rgba16float' : ldrColorFormat;
2242
1855
  const isDepthOnlyPass = passKindPolicyTable[passKind]?.shape === 'depth-only';
1856
+ const resolvedVertexEntry = vertexEntry ?? vertexEntryPoint;
1857
+ const inputVertexBuffers = particleMaterialInputVertexBuffers(vertexLayout, particleInputLanes);
2243
1858
  const spec: PipelineSpec = {
2244
1859
  shader: {
2245
1860
  id: pipelineShaderId,
2246
1861
  passKind,
2247
1862
  variantSet: effectiveVariantSet,
2248
- ...(vertexEntry === undefined ? {} : { vertexEntry }),
1863
+ ...(resolvedVertexEntry === undefined ? {} : { vertexEntry: resolvedVertexEntry }),
2249
1864
  ...(fragmentEntry === undefined ? {} : { fragmentEntry }),
2250
1865
  },
2251
1866
  attachments: {
@@ -2254,7 +1869,7 @@ async function makeWebGPURenderer(
2254
1869
  ? colorFormatsForPassKind(passKind, colorFormat)
2255
1870
  : [colorFormat, ...additionalColorFormats],
2256
1871
  depthFormat: isDepthOnlyPass
2257
- ? 'depth32float'
1872
+ ? (depthFormatOverride ?? 'depth32float')
2258
1873
  : depthFormatOverride === null
2259
1874
  ? undefined
2260
1875
  : (depthFormatOverride ?? 'depth24plus-stencil8'),
@@ -2267,11 +1882,9 @@ async function makeWebGPURenderer(
2267
1882
  meshAttributes ??
2268
1883
  (vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.positionSizeColorInstance
2269
1884
  ? PREPARED_INSTANCE_VERTEX_ATTRS
2270
- : vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.billboardMaterialInstance ||
2271
- vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.topologySegmentInstance ||
2272
- vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance
1885
+ : isPreparedMaterialVertexLayout(vertexLayout)
2273
1886
  ? PREPARED_MATERIAL_INSTANCE_VERTEX_ATTRS
2274
- : DEFAULT_VERTEX_ATTRS),
1887
+ : DEFAULT_VERTEX_ATTRIBUTE_MAP),
2275
1888
  // Thread the shader-declared count whenever it is greater than one.
2276
1889
  // Built-in PBR reserves all eight supported UV inputs; single-UV meshes
2277
1890
  // remain byte-stable because missing sets are clamp-to-last aliases.
@@ -2279,10 +1892,11 @@ async function makeWebGPURenderer(
2279
1892
  ? { shaderUvSetCount: resolvedUvSetCount }
2280
1893
  : {}),
2281
1894
  ...(vertexLayoutProjection === undefined ? {} : { vertexLayoutProjection }),
1895
+ ...(inputVertexBuffers === undefined ? {} : { vertexBuffers: inputVertexBuffers }),
2282
1896
  },
2283
1897
  renderState,
2284
1898
  };
2285
- const cacheKey = cacheKeyOf(spec);
1899
+ const cacheKey = `${cacheKeyOf(spec)}|vertex:${resolvedVertexEntry ?? 'vs_main'}`;
2286
1900
  const cached = currentPipelineCacheState().materialShaderPipelineCache.get(cacheKey);
2287
1901
  if (cached !== undefined) return cached;
2288
1902
  if (pipelineState === null) return null;
@@ -2336,6 +1950,7 @@ async function makeWebGPURenderer(
2336
1950
  vertexLayoutProjection,
2337
1951
  shaderModuleMode,
2338
1952
  layoutKindOverride,
1953
+ vertexEntryPoint,
2339
1954
  additionalColorFormats,
2340
1955
  );
2341
1956
  }
@@ -2392,6 +2007,7 @@ async function makeWebGPURenderer(
2392
2007
  vertexLayoutProjection,
2393
2008
  shaderModuleMode,
2394
2009
  layoutKindOverride,
2010
+ vertexEntryPoint,
2395
2011
  additionalColorFormats,
2396
2012
  );
2397
2013
  }
@@ -2485,9 +2101,11 @@ async function makeWebGPURenderer(
2485
2101
  vertexLayoutProjection,
2486
2102
  shaderModuleMode,
2487
2103
  layoutKindOverride,
2104
+ vertexEntryPoint,
2488
2105
  additionalColorFormats,
2489
2106
  vertexEntry,
2490
2107
  fragmentEntry,
2108
+ particleInputLanes,
2491
2109
  );
2492
2110
  };
2493
2111
  const getMaterialShaderPipelineEntry = (
@@ -2549,7 +2167,7 @@ async function makeWebGPURenderer(
2549
2167
  const buildPostProcessPipeline = (
2550
2168
  entry: PostProcessShaderEntry,
2551
2169
  bgl: BindGroupLayout,
2552
- colorFormat: GPUTextureFormat,
2170
+ colorFormats: readonly GPUTextureFormat[],
2553
2171
  label: string,
2554
2172
  ): RenderPipeline | null => {
2555
2173
  const moduleFactory = getShaderModuleAdapter();
@@ -2597,12 +2215,12 @@ async function makeWebGPURenderer(
2597
2215
  },
2598
2216
  fragment: {
2599
2217
  module: moduleResult.value,
2600
- entryPoint: 'fs_main',
2601
- targets:
2602
- entry.source.includes('@location(1) temporal') &&
2603
- entry.source.includes('TaaResolveOutput')
2604
- ? [{ format: colorFormat }, { format: colorFormat }]
2605
- : [{ format: colorFormat }],
2218
+ // The typed graph reuses the composed tonemap module for separate
2219
+ // linear-LDR and final-encoding passes. Preserve the selected entry
2220
+ // point; forcing fs_main encodes the linear-LDR intermediate a second
2221
+ // time and breaks the Three.js stage contract.
2222
+ entryPoint: entry.fragmentEntryPoint ?? 'fs_main',
2223
+ targets: colorFormats.map((format) => ({ format })),
2606
2224
  },
2607
2225
  primitive: { topology: 'triangle-list', cullMode: 'none', frontFace: 'ccw' },
2608
2226
  depthStencil: undefined,
@@ -2633,10 +2251,7 @@ async function makeWebGPURenderer(
2633
2251
  setFeatureHost: (host) => {
2634
2252
  internals.featureHost = host;
2635
2253
  },
2636
- // Keep the feature-facing factory stable while resolving the adapter at
2637
- // call time. Renderer recovery swaps the device and invalidates the
2638
- // per-device adapter; a boot-time snapshot would feed recovered VFX
2639
- // programs shader modules owned by the lost device.
2254
+ // Resolve the adapter at call time so recovered devices get fresh modules.
2640
2255
  shaderModuleFactory: {
2641
2256
  createShaderModule: (descriptor) => getShaderModuleAdapter().createShaderModule(descriptor),
2642
2257
  },
@@ -2666,6 +2281,28 @@ async function makeWebGPURenderer(
2666
2281
  ...(internals.pack.instrumentation?.resolveSurfaceDevice === undefined
2667
2282
  ? {}
2668
2283
  : { resolveSurfaceDevice: internals.pack.instrumentation.resolveSurfaceDevice }),
2284
+ // The graph records receipt-bound color observations through the same
2285
+ // renderer-owned capture bridge consumed after queue submission. Keep the
2286
+ // identity and demand live on internals so recovery and per-frame graph
2287
+ // rebuilds observe the current device without reconstructing RenderSystem.
2288
+ get deviceGeneration() {
2289
+ return activeDeviceScope.generation;
2290
+ },
2291
+ get observationFrameId() {
2292
+ return internals.observationFrameId;
2293
+ },
2294
+ get observationGraphGeneration() {
2295
+ return internals.observationGraphGeneration;
2296
+ },
2297
+ set observationGraphGeneration(generation: number | undefined) {
2298
+ internals.observationGraphGeneration = generation;
2299
+ },
2300
+ get observationCaptureOwner() {
2301
+ return internals.observationCaptureOwner;
2302
+ },
2303
+ get observationCaptureDomains() {
2304
+ return internals.observationCaptureDomains;
2305
+ },
2669
2306
  invalidateShaderModule: (label: string) => {
2670
2307
  activeShaderState.sharedShaderModuleAdapter?.invalidateModule(label);
2671
2308
  },
@@ -2692,10 +2329,15 @@ async function makeWebGPURenderer(
2692
2329
  getMaterialShaderPipeline,
2693
2330
  getMaterialShaderPipelineEntry,
2694
2331
  getMaterialShaderBindingContract: getCachedMaterialShaderBindingContract,
2332
+ getMaterialShaderArtifact: (materialShaderId, vertexColorAvailable = false) =>
2333
+ resolveStandardPbrProgram(
2334
+ pipelineState?.gpuDrivenPbrPrograms,
2335
+ materialShaderId,
2336
+ vertexColorAvailable,
2337
+ )?.artifact,
2695
2338
  getParamSchema,
2696
2339
  getMaterialBindGroupLayout,
2697
2340
  metrics,
2698
- // feat-20260609 M4 / T-10-a: post-process pipeline factory (CONCERN-1 fix).
2699
2341
  // createRenderSystem wraps this in a per-RenderSystem cache + the public
2700
2342
  // getPostProcessPipeline lookup the dispatcher reads at frame time.
2701
2343
  buildPostProcessPipeline,
@@ -2786,6 +2428,13 @@ async function makeWebGPURenderer(
2786
2428
  get volumetricFogShaders() {
2787
2429
  return internals.volumetricFogShaders;
2788
2430
  },
2431
+ // Readiness-prewarmed SSR sources stay visible through the live internals seam.
2432
+ get ssrShaders() {
2433
+ return internals.ssrShaders;
2434
+ },
2435
+ get atmosphereShaders() {
2436
+ return internals.atmosphereShaders;
2437
+ },
2789
2438
  });
2790
2439
  attachGpuPassTimingSession(internals, gpuPassTimingSession);
2791
2440
  renderSystemForTargetPromotion = renderSystem;
@@ -2809,20 +2458,13 @@ async function makeWebGPURenderer(
2809
2458
  const attachedWorlds = new Set<World>();
2810
2459
  const attachedLeases = new Map<RenderReadLease, World>();
2811
2460
  const leasesByWorld = new Map<World, RenderReadLease>();
2812
- const dynamicGeometry = createDynamicGeometryLifecycle();
2813
- const dynamicGeometryHost = createDynamicGeometryHost({
2814
- lifecycle: dynamicGeometry,
2815
- attachedWorlds,
2816
- getGpuStore: () => gpuStore,
2817
- currentGeneration: () => activeDeviceScope.generation,
2818
- onTopologyChanged: () => renderSystem.invalidateGeometryHistory(),
2819
- isConsumedByRenderFrame: (candidate) =>
2820
- renderSystem.isDynamicGeometryConsumed(
2821
- candidate.world as World,
2822
- candidate.entity ?? -1,
2823
- candidate.meshHandle,
2824
- ),
2825
- });
2461
+ const { lifecycle: dynamicGeometry, host: dynamicGeometryHost } =
2462
+ createRendererDynamicGeometryController({
2463
+ attachedWorlds,
2464
+ getGpuStore: () => gpuStore,
2465
+ currentGeneration: () => activeDeviceScope.generation,
2466
+ renderSystem,
2467
+ });
2826
2468
  const derivedSystemNames = new Map<World, string>();
2827
2469
  const transformReleases = new Map<World, () => void>();
2828
2470
  const attachmentOwner = {};
@@ -2834,8 +2476,184 @@ async function makeWebGPURenderer(
2834
2476
  const issuedReceipts = new WeakSet<FrameReceipt>();
2835
2477
  const receiptTimings = new WeakMap<FrameReceipt, Promise<VolumeTimingObservation>>();
2836
2478
  let latestReceipt: FrameReceipt | undefined;
2479
+ let pendingObservationDomains: readonly FrameObservationDomain[] | undefined;
2480
+ let observationAllocationCount = 0;
2481
+ let observationLiveCount = 0;
2482
+ let observationPeakLiveCount = 0;
2483
+ let observationMapCount = 0;
2484
+ let observationReadbackCount = 0;
2485
+ let observationLiveByteLength = 0;
2486
+ const observationCaptureOwner = (() => {
2487
+ const capturesByFrame = new Map<number, TypedFrameObservationCapture[]>();
2488
+ return {
2489
+ register(capture: TypedFrameObservationCapture): void {
2490
+ const captures = capturesByFrame.get(capture.frameNumber) ?? [];
2491
+ captures.push(capture);
2492
+ capturesByFrame.set(capture.frameNumber, captures);
2493
+ observationAllocationCount += 1;
2494
+ observationLiveCount += 1;
2495
+ observationPeakLiveCount = Math.max(observationPeakLiveCount, observationLiveCount);
2496
+ observationLiveByteLength += capture.bytesPerRow * capture.height;
2497
+ },
2498
+ consume(frameNumber: number): readonly TypedFrameObservationCapture[] {
2499
+ const captures = capturesByFrame.get(frameNumber) ?? [];
2500
+ capturesByFrame.delete(frameNumber);
2501
+ return Object.freeze(captures);
2502
+ },
2503
+ drain(): readonly TypedFrameObservationCapture[] {
2504
+ const captures = [...capturesByFrame.values()].flat();
2505
+ capturesByFrame.clear();
2506
+ return Object.freeze(captures);
2507
+ },
2508
+ };
2509
+ })();
2510
+ Object.assign(internals, { observationCaptureOwner });
2511
+ let expectedObservationGraphGeneration: number | undefined;
2512
+ Object.defineProperty(internals, 'observationGraphGeneration', {
2513
+ configurable: true,
2514
+ get: () => expectedObservationGraphGeneration,
2515
+ set: (generation: number | undefined) => {
2516
+ expectedObservationGraphGeneration = generation;
2517
+ },
2518
+ });
2519
+ type ObservationCaptureRead = () => Promise<
2520
+ RenderResult<
2521
+ readonly {
2522
+ readonly capture: TypedFrameObservationCapture;
2523
+ readonly bytes: Uint8Array;
2524
+ }[],
2525
+ RenderError
2526
+ >
2527
+ >;
2528
+ const receiptObservationCaptures = new Map<FrameReceipt, ObservationCaptureRead>();
2529
+ const receiptObservationBuffers = new Map<
2530
+ FrameReceipt,
2531
+ readonly TypedFrameObservationCapture[]
2532
+ >();
2533
+ const destroyedObservationBuffers = new WeakSet<Buffer>();
2534
+ const observationBufferCleanupFailures = new WeakMap<Buffer, RendererContractFailureError>();
2535
+ const failedObservationCaptures = new Map<Buffer, TypedFrameObservationCapture>();
2536
+ const disposeObservationCaptures = (
2537
+ captures: readonly TypedFrameObservationCapture[],
2538
+ ): RendererContractFailureError | undefined => {
2539
+ const wasDestroyed = new Set(
2540
+ captures
2541
+ .filter((capture) => destroyedObservationBuffers.has(capture.buffer))
2542
+ .map((capture) => capture.buffer),
2543
+ );
2544
+ const result = disposeObservationCaptureSet(
2545
+ captures,
2546
+ destroyedObservationBuffers,
2547
+ observationBufferCleanupFailures,
2548
+ (failure) => internals.errorRegistry.fire(failure),
2549
+ );
2550
+ for (const capture of captures) {
2551
+ if (!wasDestroyed.has(capture.buffer) && destroyedObservationBuffers.has(capture.buffer)) {
2552
+ observationLiveCount = Math.max(0, observationLiveCount - 1);
2553
+ observationLiveByteLength = Math.max(
2554
+ 0,
2555
+ observationLiveByteLength - capture.bytesPerRow * capture.height,
2556
+ );
2557
+ }
2558
+ }
2559
+ return result;
2560
+ };
2561
+ const disposeOwnedObservationCaptures = (
2562
+ captures: readonly TypedFrameObservationCapture[],
2563
+ ): RendererContractFailureError | undefined => {
2564
+ const failure = disposeObservationCaptures(captures);
2565
+ if (failure !== undefined) {
2566
+ for (const capture of captures) {
2567
+ if (!destroyedObservationBuffers.has(capture.buffer)) {
2568
+ failedObservationCaptures.set(capture.buffer, capture);
2569
+ }
2570
+ }
2571
+ }
2572
+ return failure;
2573
+ };
2574
+ const readObservationCapture = async (
2575
+ capture: TypedFrameObservationCapture,
2576
+ ): Promise<RenderResult<Uint8Array, RenderError>> => {
2577
+ try {
2578
+ if (typeof capture.buffer.mapAsync !== 'function') {
2579
+ const cleanupFailure = disposeObservationCaptures([capture]);
2580
+ return err(
2581
+ cleanupFailure ??
2582
+ new RendererContractFailureError(
2583
+ 'observe',
2584
+ `the ${capture.domain} receipt-bound readback backend has no mapAsync capability`,
2585
+ ),
2586
+ );
2587
+ }
2588
+ observationMapCount += 1;
2589
+ const mapped = await capture.buffer.mapAsync(1);
2590
+ if (!mapped.ok) {
2591
+ const cleanupFailure = disposeObservationCaptures([capture]);
2592
+ return err(
2593
+ cleanupFailure ??
2594
+ new RendererContractFailureError(
2595
+ 'observe',
2596
+ `mapping ${capture.domain} receipt-bound readback failed: ${mapped.error.code}`,
2597
+ ),
2598
+ );
2599
+ }
2600
+ const range = mapped.value.getMappedRange();
2601
+ if (!range.ok) {
2602
+ mapped.value.unmap();
2603
+ const cleanupFailure = disposeObservationCaptures([capture]);
2604
+ return err(
2605
+ cleanupFailure ??
2606
+ new RendererContractFailureError(
2607
+ 'observe',
2608
+ `reading ${capture.domain} receipt-bound readback failed: ${range.error.code}`,
2609
+ ),
2610
+ );
2611
+ }
2612
+ const bytes = new Uint8Array(range.value.slice(0));
2613
+ mapped.value.unmap();
2614
+ observationReadbackCount += 1;
2615
+ const cleanupFailure = disposeObservationCaptures([capture]);
2616
+ if (cleanupFailure !== undefined) return err(cleanupFailure);
2617
+ return ok(bytes);
2618
+ } catch (cause) {
2619
+ const cleanupFailure = disposeObservationCaptures([capture]);
2620
+ return err(
2621
+ cleanupFailure ??
2622
+ new RendererContractFailureError(
2623
+ 'observe',
2624
+ `reading ${capture.domain} receipt-bound readback threw: ${
2625
+ cause instanceof Error ? cause.message : String(cause)
2626
+ }`,
2627
+ ),
2628
+ );
2629
+ }
2630
+ };
2631
+
2632
+ const disposeReceiptObservationCaptures = (): RendererContractFailureError | undefined => {
2633
+ let firstFailure: RendererContractFailureError | undefined;
2634
+ for (const [receipt, captures] of receiptObservationBuffers) {
2635
+ const failure = disposeObservationCaptures(captures);
2636
+ firstFailure ??= failure;
2637
+ if (captures.every((capture) => destroyedObservationBuffers.has(capture.buffer))) {
2638
+ receiptObservationBuffers.delete(receipt);
2639
+ }
2640
+ }
2641
+ receiptObservationCaptures.clear();
2642
+ const retryCaptures = [...failedObservationCaptures.values()];
2643
+ const retryFailure = disposeObservationCaptures(retryCaptures);
2644
+ firstFailure ??= retryFailure;
2645
+ for (const capture of retryCaptures) {
2646
+ if (destroyedObservationBuffers.has(capture.buffer)) {
2647
+ failedObservationCaptures.delete(capture.buffer);
2648
+ }
2649
+ }
2650
+ const ownedFailure = disposeOwnedObservationCaptures(observationCaptureOwner.drain());
2651
+ firstFailure ??= ownedFailure;
2652
+ return firstFailure;
2653
+ };
2837
2654
  let surfaceReleased = false;
2838
2655
  const renderer: RendererAssemblyImplementation = {
2656
+ instances: renderSystem.instances,
2839
2657
  attach(world: World): RenderResult<RenderWorldLease, RenderError> {
2840
2658
  const attached = this.attachScene(world);
2841
2659
  if (!attached.ok) {
@@ -2886,6 +2704,7 @@ async function makeWebGPURenderer(
2886
2704
  gpuDriven: renderSystem.gpuDrivenInspection,
2887
2705
  };
2888
2706
  },
2707
+ bounds: (world, entity) => (disposed ? undefined : renderSystem.bounds(world, entity)),
2889
2708
  inspect(): RenderInspection {
2890
2709
  const health = internals.healthRegistry.getLastSnapshot();
2891
2710
  const deviceLost = health.reason === 'device-lost';
@@ -2926,6 +2745,8 @@ async function makeWebGPURenderer(
2926
2745
  ? {}
2927
2746
  : { surfaceProfile: pipelineState.surfaceProfile }),
2928
2747
  rgba16floatRenderable: internals.device.caps.rgba16floatRenderable,
2748
+ autoExposure: renderSystem.autoExposure,
2749
+ standardLut: renderSystem.standardLut,
2929
2750
  ...(internals.context?.presentationProof === undefined
2930
2751
  ? {}
2931
2752
  : { presentationProof: internals.context.presentationProof }),
@@ -2943,16 +2764,18 @@ async function makeWebGPURenderer(
2943
2764
  frameId,
2944
2765
  deviceGeneration: activeDeviceScope.generation,
2945
2766
  }),
2946
- features: Object.freeze(
2947
- (internals.featureHost?.features ?? []).map((feature) => feature.identity),
2948
- ),
2949
- featureDiagnostics: internals.featureHost?.diagnostics() ?? Object.freeze([]),
2767
+ ...projectRendererFeatureInspection(internals.featureHost),
2768
+ featureGraph: renderSystem.featureGraphInspection,
2769
+ materialTextureSources: renderSystem.materialTextureSources,
2950
2770
  frustumStats: Object.freeze({ ...renderSystem.frustumStats }),
2951
2771
  visibilityStats: Object.freeze({ ...renderSystem.visibilityStats }),
2952
2772
  instanceCollections: Object.freeze(
2953
2773
  renderSystem.instanceCollectionsInspection.map((collection) =>
2954
2774
  Object.freeze({
2955
2775
  ...collection,
2776
+ dirtyRanges: Object.freeze(
2777
+ collection.dirtyRanges.map((range) => Object.freeze({ ...range })),
2778
+ ),
2956
2779
  uploadRanges: Object.freeze(
2957
2780
  collection.uploadRanges.map((range) => Object.freeze({ ...range })),
2958
2781
  ),
@@ -2971,6 +2794,7 @@ async function makeWebGPURenderer(
2971
2794
  renderScene: renderSystem.renderScene,
2972
2795
  reflectionProbes: renderSystem.reflectionProbes,
2973
2796
  ssrDependencies: renderSystem.ssrDependencies,
2797
+ ssr: renderSystem.ssr,
2974
2798
  volumetricFog,
2975
2799
  environment: renderSystem.environment,
2976
2800
  temporal: renderSystem.temporal,
@@ -3024,7 +2848,13 @@ async function makeWebGPURenderer(
3024
2848
  lastGeneration: latestReceipt?.deviceGeneration,
3025
2849
  }),
3026
2850
  }),
3027
- ...outputInspection,
2851
+ output: Object.freeze({
2852
+ ...outputInspection,
2853
+ graphPassNames: Object.freeze([...outputInspection.graphPassNames]),
2854
+ ...(outputInspection.standardOutputColor === undefined
2855
+ ? {}
2856
+ : { standardOutputColor: Object.freeze({ ...outputInspection.standardOutputColor }) }),
2857
+ }),
3028
2858
  observation: Object.freeze({
3029
2859
  observationId,
3030
2860
  frameId,
@@ -3039,6 +2869,14 @@ async function makeWebGPURenderer(
3039
2869
  ...(outputInspection.standardOutputColor === undefined
3040
2870
  ? {}
3041
2871
  : { standardOutputColor: Object.freeze({ ...outputInspection.standardOutputColor }) }),
2872
+ resourceStats: Object.freeze({
2873
+ allocationCount: observationAllocationCount,
2874
+ liveCount: observationLiveCount,
2875
+ peakLiveCount: observationPeakLiveCount,
2876
+ mapCount: observationMapCount,
2877
+ readbackCount: observationReadbackCount,
2878
+ liveByteLength: observationLiveByteLength,
2879
+ }),
3042
2880
  }),
3043
2881
  });
3044
2882
  },
@@ -3131,6 +2969,38 @@ async function makeWebGPURenderer(
3131
2969
  observeCurrentFrame(options: FrameObservationOptions) {
3132
2970
  return renderSystem.observeCurrentFrame(options);
3133
2971
  },
2972
+ requestObservation(
2973
+ domains: readonly FrameObservationDomain[],
2974
+ ): RenderResult<void, RenderError> {
2975
+ if (disposed) {
2976
+ return err(
2977
+ new RendererOperationError('renderer-state-invalid', {
2978
+ operation: 'request-observation',
2979
+ state: 'disposed',
2980
+ }),
2981
+ );
2982
+ }
2983
+ if (
2984
+ !Array.isArray(domains) ||
2985
+ domains.length === 0 ||
2986
+ domains.some(
2987
+ (domain) => domain !== 'linear-hdr' && domain !== 'linear-ldr' && domain !== 'final-srgb',
2988
+ ) ||
2989
+ new Set(domains).size !== domains.length
2990
+ ) {
2991
+ return err(
2992
+ new RendererOperationError('frame-input-invalid', {
2993
+ operation: 'request-observation',
2994
+ cause: new RendererContractFailureError(
2995
+ 'request-observation',
2996
+ 'domains must be a non-empty set of linear-hdr, linear-ldr, and final-srgb values',
2997
+ ),
2998
+ }),
2999
+ );
3000
+ }
3001
+ pendingObservationDomains = Object.freeze([...domains]);
3002
+ return ok(undefined);
3003
+ },
3134
3004
  getCurrentGraphTarget(name: string) {
3135
3005
  return renderSystem.getCurrentGraphTarget(name);
3136
3006
  },
@@ -3313,27 +3183,8 @@ async function makeWebGPURenderer(
3313
3183
  : worlds.map((world) => leasesByWorld.get(world)).every((lease) => lease !== undefined)
3314
3184
  ? worlds.map((world) => leasesByWorld.get(world) as RenderReadLease)
3315
3185
  : undefined;
3316
- for (const world of worlds) {
3317
- if (!world.hasResource('PhysicsWorld')) continue;
3318
- const physics = world.getResource<{
3319
- readonly getDerivedAdmission?: () => unknown;
3320
- readonly getDerivedRecoveryState?: () => string;
3321
- }>('PhysicsWorld');
3322
- if (
3323
- physics?.getDerivedAdmission?.() !== undefined ||
3324
- physics?.getDerivedRecoveryState?.() === 'rebuild-required'
3325
- ) {
3326
- return err(
3327
- new RendererOperationError('frame-input-invalid', {
3328
- operation: 'draw',
3329
- cause: new RendererContractFailureError(
3330
- 'draw',
3331
- 'paired geometry admission must finish physics step and writeback in a healthy World before draw',
3332
- ),
3333
- }),
3334
- );
3335
- }
3336
- }
3186
+ const physicsFrameError = derivedPhysicsFrameError(worlds);
3187
+ if (physicsFrameError !== undefined) return err(physicsFrameError);
3337
3188
  if (frameRequest !== undefined && worlds.length !== frameRequest.leases.length) {
3338
3189
  return err(
3339
3190
  new RendererOperationError('world-lease-invalid', {
@@ -3369,6 +3220,9 @@ async function makeWebGPURenderer(
3369
3220
  ? {
3370
3221
  cameraOwner,
3371
3222
  resourceOwner,
3223
+ ...(frameRequest.camera.entityKey === undefined
3224
+ ? {}
3225
+ : { cameraEntityKey: frameRequest.camera.entityKey }),
3372
3226
  ...(frameRequest.profileFrame === undefined
3373
3227
  ? {}
3374
3228
  : { profileFrame: frameRequest.profileFrame }),
@@ -3486,6 +3340,9 @@ async function makeWebGPURenderer(
3486
3340
  gpuPassTimingCapture?: GpuPassTimingCapture | undefined;
3487
3341
  gpuPassTimingSubmittedWork?: Promise<void> | undefined;
3488
3342
  gpuPassTimingBeginReason?: GpuPassTimingReason | undefined;
3343
+ observationFrameId?: number | undefined;
3344
+ observationGraphGeneration?: number | undefined;
3345
+ observationCaptureDomains?: readonly FrameObservationDomain[] | undefined;
3489
3346
  };
3490
3347
  const timingRequested =
3491
3348
  isFrameRequest &&
@@ -3500,6 +3357,9 @@ async function makeWebGPURenderer(
3500
3357
  if (createdTiming.ok) volumeTimingCapture = createdTiming.value;
3501
3358
  }
3502
3359
  if (isFrameRequest) {
3360
+ timingHost.observationFrameId = frameId + 1;
3361
+ timingHost.observationGraphGeneration = undefined;
3362
+ timingHost.observationCaptureDomains = pendingObservationDomains;
3503
3363
  timingHost.gpuPassTimingFrameIdentity = {
3504
3364
  frameId: frameId + 1,
3505
3365
  deviceGeneration: activeDeviceScope.generation,
@@ -3507,9 +3367,13 @@ async function makeWebGPURenderer(
3507
3367
  };
3508
3368
  }
3509
3369
  const submitted = renderSystem.draw(worlds, drawOptions, readLeases, volumeTimingCapture);
3370
+ timingHost.observationFrameId = undefined;
3371
+ timingHost.observationCaptureDomains = undefined;
3510
3372
  timingHost.gpuPassTimingFrameIdentity = undefined;
3511
3373
  if (!submitted) {
3374
+ const cleanupFailure = disposeOwnedObservationCaptures(observationCaptureOwner.drain());
3512
3375
  volumeTimingCapture?.discard();
3376
+ if (cleanupFailure !== undefined) return err(cleanupFailure);
3513
3377
  return err(
3514
3378
  new RendererContractFailureError(
3515
3379
  'draw',
@@ -3524,7 +3388,9 @@ async function makeWebGPURenderer(
3524
3388
  gpuStore.trackMeshSubmission(queueCompletion);
3525
3389
  internals.pack.instrumentation?.onFrameBoundary?.();
3526
3390
  if (!isFrameRequest) {
3391
+ const cleanupFailure = disposeOwnedObservationCaptures(observationCaptureOwner.drain());
3527
3392
  renderTargetHost.onFrameSubmitted();
3393
+ if (cleanupFailure !== undefined) return err(cleanupFailure);
3528
3394
  return ok(undefined);
3529
3395
  }
3530
3396
  const receiptFrameId = ++frameId;
@@ -3599,13 +3465,39 @@ async function makeWebGPURenderer(
3599
3465
  ]).finally(() => {
3600
3466
  frameContinuations.delete(continuation);
3601
3467
  });
3468
+ const captures = observationCaptureOwner.consume(receiptFrameId);
3469
+ pendingObservationDomains = undefined;
3470
+ const receiptGraphGeneration = timingHost.observationGraphGeneration ?? 0;
3602
3471
  const receipt = Object.freeze({
3603
3472
  frameId: receiptFrameId,
3604
3473
  deviceGeneration: receiptGeneration,
3474
+ backendId: internals.device.caps.backendKind,
3475
+ graphGeneration: receiptGraphGeneration,
3605
3476
  completed,
3606
3477
  });
3607
3478
  renderTargetHost.onFrameSubmitted(completed);
3608
3479
  latestReceipt = receipt;
3480
+ if (captures.length > 0) {
3481
+ const captureRead: ObservationCaptureRead = async () => {
3482
+ const completion = await completed;
3483
+ if (!completion.ok) {
3484
+ const cleanupFailure = disposeObservationCaptures(captures);
3485
+ return cleanupFailure === undefined ? completion : err(cleanupFailure);
3486
+ }
3487
+ const read = [];
3488
+ for (const capture of captures) {
3489
+ const bytes = await readObservationCapture(capture);
3490
+ if (!bytes.ok) {
3491
+ const cleanupFailure = disposeObservationCaptures(captures);
3492
+ return cleanupFailure === undefined ? bytes : err(cleanupFailure);
3493
+ }
3494
+ read.push({ capture, bytes: bytes.value });
3495
+ }
3496
+ return ok(read);
3497
+ };
3498
+ receiptObservationBuffers.set(receipt, captures);
3499
+ receiptObservationCaptures.set(receipt, captureRead);
3500
+ }
3609
3501
  const passTimingCapture = timingHost.gpuPassTimingCapture;
3610
3502
  const timingBeginReason = timingHost.gpuPassTimingBeginReason;
3611
3503
  const timingSource =
@@ -3653,6 +3545,11 @@ async function makeWebGPURenderer(
3653
3545
  dynamicGeometryHost.publishDynamicGeometry(receipt, worlds, frameRequest?.fixedStep);
3654
3546
  return ok(receipt);
3655
3547
  } catch (cause) {
3548
+ internals.observationFrameId = undefined;
3549
+ internals.observationCaptureDomains = undefined;
3550
+ internals.gpuPassTimingFrameIdentity = undefined;
3551
+ const cleanupFailure = disposeOwnedObservationCaptures(observationCaptureOwner.drain());
3552
+ if (cleanupFailure !== undefined) return err(cleanupFailure);
3656
3553
  const error =
3657
3554
  cause instanceof Error
3658
3555
  ? { code: 'unknown' as const, message: cause.message, name: cause.name }
@@ -3672,7 +3569,28 @@ async function makeWebGPURenderer(
3672
3569
  receipt: FrameReceipt,
3673
3570
  request: FrameObservationRequest,
3674
3571
  ): Promise<RenderResult<FrameReceiptObservation, RenderError>> {
3675
- if (gpuPassTimingOptions !== undefined) {
3572
+ const includesDomain = (request.include as readonly string[]).some(
3573
+ (include) =>
3574
+ include === 'linear-hdr' || include === 'linear-ldr' || include === 'final-srgb',
3575
+ );
3576
+ let timingObservation: GpuPassTimingObservation | undefined;
3577
+ if (
3578
+ includesDomain &&
3579
+ gpuPassTimingOptions !== undefined &&
3580
+ request.include.includes('timings')
3581
+ ) {
3582
+ const timingResult =
3583
+ timingObservationStore === undefined
3584
+ ? await observeGpuPassTimingDisabled(
3585
+ receipt,
3586
+ request,
3587
+ () => activeDeviceScope.generation,
3588
+ )
3589
+ : await timingObservationStore.observe(receipt, request);
3590
+ if (!timingResult.ok) return timingResult;
3591
+ timingObservation = timingResult.value.timings;
3592
+ }
3593
+ if (gpuPassTimingOptions !== undefined && !includesDomain) {
3676
3594
  return timingObservationStore === undefined
3677
3595
  ? observeGpuPassTimingDisabled(receipt, request, () => activeDeviceScope.generation)
3678
3596
  : timingObservationStore.observe(receipt, request);
@@ -3680,7 +3598,8 @@ async function makeWebGPURenderer(
3680
3598
  if (
3681
3599
  !currentReceipts.has(receipt) ||
3682
3600
  !issuedReceipts.has(receipt) ||
3683
- receipt.deviceGeneration !== activeDeviceScope.generation
3601
+ receipt.deviceGeneration !== activeDeviceScope.generation ||
3602
+ receipt.backendId !== internals.device.caps.backendKind
3684
3603
  ) {
3685
3604
  const currentGeneration = activeDeviceScope.generation;
3686
3605
  return err(
@@ -3703,6 +3622,97 @@ async function makeWebGPURenderer(
3703
3622
  }),
3704
3623
  );
3705
3624
  }
3625
+ const requestedDomains = (request.include as readonly string[]).filter(
3626
+ (include): include is TypedFrameObservationCapture['domain'] =>
3627
+ include === 'linear-hdr' || include === 'linear-ldr' || include === 'final-srgb',
3628
+ );
3629
+ let observations: FrameReceiptObservation['observations'];
3630
+ if (requestedDomains.length > 0) {
3631
+ if (new Set(requestedDomains).size !== requestedDomains.length) {
3632
+ return err(
3633
+ new RendererContractFailureError(
3634
+ 'observe',
3635
+ 'each receipt-bound color domain may be requested only once',
3636
+ ),
3637
+ );
3638
+ }
3639
+ const readCaptures = receiptObservationCaptures.get(receipt);
3640
+ receiptObservationCaptures.delete(receipt);
3641
+ if (readCaptures === undefined) {
3642
+ return err(
3643
+ new RendererContractFailureError(
3644
+ 'observe',
3645
+ 'receipt-bound color-domain observations were not explicitly armed before draw, are stale, duplicated, or unavailable',
3646
+ ),
3647
+ );
3648
+ }
3649
+ const capturesResult = await readCaptures();
3650
+ if (!capturesResult.ok) return capturesResult;
3651
+ const captures = capturesResult.value;
3652
+ const graphGenerations = new Set(captures.map(({ capture }) => capture.graphGeneration));
3653
+ if (
3654
+ captures.some(
3655
+ ({ capture }) =>
3656
+ capture.frameNumber !== receipt.frameId ||
3657
+ capture.deviceGeneration !== receipt.deviceGeneration ||
3658
+ capture.backendId !== internals.device.caps.backendKind ||
3659
+ (receipt.graphGeneration !== undefined &&
3660
+ capture.graphGeneration !== receipt.graphGeneration),
3661
+ ) ||
3662
+ graphGenerations.size !== 1
3663
+ ) {
3664
+ return err(
3665
+ new RendererContractFailureError(
3666
+ 'observe',
3667
+ 'receipt-bound color-domain captures crossed frame, device, backend, or graph identity',
3668
+ ),
3669
+ );
3670
+ }
3671
+ const byDomain = new Map(
3672
+ captures.map(({ capture, bytes }) => [capture.domain, { capture, bytes }]),
3673
+ );
3674
+ if (
3675
+ byDomain.size !== captures.length ||
3676
+ requestedDomains.some((domain) => !byDomain.has(domain))
3677
+ ) {
3678
+ return err(
3679
+ new RendererContractFailureError(
3680
+ 'observe',
3681
+ 'receipt-bound color-domain captures must contain one source per requested domain',
3682
+ ),
3683
+ );
3684
+ }
3685
+ const read: NonNullable<FrameReceiptObservation['observations']>[number][] = [];
3686
+ for (const domain of requestedDomains) {
3687
+ const resolved = byDomain.get(domain);
3688
+ if (resolved === undefined) continue;
3689
+ const { capture, bytes } = resolved;
3690
+ read.push({
3691
+ domain,
3692
+ bytes,
3693
+ metadata: {
3694
+ frameId: receipt.frameId,
3695
+ deviceGeneration: receipt.deviceGeneration,
3696
+ backendId: receipt.backendId,
3697
+ format: capture.format,
3698
+ graphGeneration: capture.graphGeneration,
3699
+ textureIdentity: getTextureIdentity(capture.texture),
3700
+ readbackIdentity: getTextureIdentity(
3701
+ capture.buffer as unknown as import('@forgeax/engine-rhi').Texture,
3702
+ ),
3703
+ width: capture.width,
3704
+ height: capture.height,
3705
+ bytesPerRow: capture.bytesPerRow,
3706
+ footprint: { resourceCount: 1, bindGroupCount: 0 },
3707
+ },
3708
+ });
3709
+ }
3710
+ observations = Object.freeze(read);
3711
+ const receiptCaptures = receiptObservationBuffers.get(receipt);
3712
+ if (receiptCaptures?.every((capture) => destroyedObservationBuffers.has(capture.buffer))) {
3713
+ receiptObservationBuffers.delete(receipt);
3714
+ }
3715
+ }
3706
3716
  await renderSystem.observeLodOcclusion();
3707
3717
  const targetReadbacks =
3708
3718
  request.targetReadbacks === undefined || request.targetReadbacks.length === 0
@@ -3724,7 +3734,9 @@ async function makeWebGPURenderer(
3724
3734
  ...(targetReadbacks === undefined
3725
3735
  ? {}
3726
3736
  : { targetReadbacks: Object.freeze(targetReadbacks.value) }),
3737
+ ...(timingObservation === undefined ? {} : { timings: timingObservation }),
3727
3738
  ...(volumeTimings === undefined ? {} : { volumeTimings }),
3739
+ ...(observations === undefined ? {} : { observations }),
3728
3740
  }),
3729
3741
  );
3730
3742
  },
@@ -3799,15 +3811,17 @@ async function makeWebGPURenderer(
3799
3811
  dispose(): RenderResult<void, RenderError> {
3800
3812
  if (disposed) return ok(undefined);
3801
3813
  disposed = true;
3802
- for (const world of attachedWorlds) {
3803
- dynamicGeometryHost.invalidateDynamicGeometryWorld(world);
3804
- }
3814
+ for (const world of attachedWorlds) dynamicGeometryHost.invalidateDynamicGeometryWorld(world);
3805
3815
  dynamicGeometry.dispose();
3806
3816
  for (const continuation of frameContinuations) {
3807
3817
  continuation.terminate({ code: 'disposed' });
3808
3818
  }
3809
3819
  currentReceipts.clear();
3810
3820
  const cleanupFailures: RendererOperationCause[] = [];
3821
+ const observationCleanupFailure = disposeReceiptObservationCaptures();
3822
+ if (observationCleanupFailure !== undefined) {
3823
+ cleanupFailures.push(observationCleanupFailure);
3824
+ }
3811
3825
  for (const world of attachedWorlds) {
3812
3826
  try {
3813
3827
  const derivedSystemName = derivedSystemNames.get(world);
@@ -3956,6 +3970,10 @@ async function makeWebGPURenderer(
3956
3970
  return internals.healthRegistry.getLastSnapshot();
3957
3971
  },
3958
3972
  recover(): Promise<Result<void, RecoverFailure>> {
3973
+ const observationCleanupFailure = disposeReceiptObservationCaptures();
3974
+ if (observationCleanupFailure !== undefined) {
3975
+ return Promise.resolve(err(new RecoverError('recover-device-unavailable')));
3976
+ }
3959
3977
  return recoveryFlight.run();
3960
3978
  },
3961
3979
  };
@@ -3973,7 +3991,6 @@ async function makeWebGPURenderer(
3973
3991
  candidatePostProcessResources: RecoveryPostProcessResources,
3974
3992
  recoveryRootBundle: RecoveryRootBundle,
3975
3993
  recoveryGraphCandidate: RecoveryGraphCandidate | undefined,
3976
- recoveryColdWorkGuard: RecoveryColdWorkGuard,
3977
3994
  ): void => {
3978
3995
  if (!candidate.scope.isAlive()) {
3979
3996
  throw new Error('Renderer generation candidate is not publishable.');
@@ -4020,9 +4037,6 @@ async function makeWebGPURenderer(
4020
4037
  // fallback pipeline or residency entry. Candidate preparation populated
4021
4038
  // these caches already; a miss here is a hard recovery failure, not an
4022
4039
  // opportunity to hide cold work inside the first visible frame.
4023
- candidate.producerBindings.gpuStore.setRecoveryColdWorkGuard(recoveryColdWorkGuard);
4024
- internals.recoveryColdWorkGuard = recoveryColdWorkGuard;
4025
- if (recoveryGraphCandidate !== undefined) recoveryColdWorkGuard.arm();
4026
4040
  renderSystem.publishRecoveryPostProcessResources(candidatePostProcessResources);
4027
4041
  if (recoveryGraphCandidate !== undefined) {
4028
4042
  renderSystem.publishRecoveryGraphCandidate(recoveryGraphCandidate);