@forgeax/engine-render 0.1.28 → 0.1.30

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 (768) hide show
  1. package/README.md +856 -126
  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/device-feature-admission.d.ts +4 -3
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -1
  6. package/dist/assembly/dynamic-geometry-host.d.ts +27 -0
  7. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  8. package/dist/assembly/dynamic-geometry-runtime.d.ts +20 -0
  9. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -0
  10. package/dist/assembly/factory.d.ts +3 -1
  11. package/dist/assembly/factory.d.ts.map +1 -1
  12. package/dist/assembly/gpu-driven-owner.d.ts +10 -0
  13. package/dist/assembly/gpu-driven-owner.d.ts.map +1 -0
  14. package/dist/assembly/host-contract.d.ts +14 -0
  15. package/dist/assembly/host-contract.d.ts.map +1 -1
  16. package/dist/assembly/material/assembly.d.ts +31 -1
  17. package/dist/assembly/material/assembly.d.ts.map +1 -1
  18. package/dist/assembly/material/pipeline-helpers.d.ts +24 -0
  19. package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -0
  20. package/dist/assembly/material-shader-policy.d.ts +3 -1
  21. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  22. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
  23. package/dist/assembly/recovery/failure-location.d.ts +12 -0
  24. package/dist/assembly/recovery/failure-location.d.ts.map +1 -0
  25. package/dist/assembly/recovery/recovery-attempt.d.ts +1 -6
  26. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -1
  27. package/dist/assembly/recovery/renderer-recover.d.ts +7 -4
  28. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  29. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  30. package/dist/assembly/renderer-feature-inspection.d.ts +5 -0
  31. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -0
  32. package/dist/assembly/renderer-frame-transaction.d.ts +19 -0
  33. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  34. package/dist/assembly/renderer-helpers.d.ts +3 -0
  35. package/dist/assembly/renderer-helpers.d.ts.map +1 -1
  36. package/dist/assembly/renderer-inspection.d.ts +4 -0
  37. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  38. package/dist/assembly/shader-prewarm-policy.d.ts +7 -1
  39. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -1
  40. package/dist/assembly/skin-palette-owner.d.ts +8 -0
  41. package/dist/assembly/skin-palette-owner.d.ts.map +1 -0
  42. package/dist/assembly/ssr-shader-prewarm.d.ts +6 -0
  43. package/dist/assembly/ssr-shader-prewarm.d.ts.map +1 -0
  44. package/dist/assembly/webgpu-device-loss.d.ts +15 -0
  45. package/dist/assembly/webgpu-device-loss.d.ts.map +1 -0
  46. package/dist/assembly/webgpu-pbr-ready.d.ts +38 -0
  47. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -0
  48. package/dist/assembly/webgpu-ready-mipmap.d.ts +11 -0
  49. package/dist/assembly/webgpu-ready-mipmap.d.ts.map +1 -0
  50. package/dist/assembly/webgpu-ready.d.ts +7 -3
  51. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  52. package/dist/assembly/webgpu-renderer-bootstrap.d.ts +10 -0
  53. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -0
  54. package/dist/assembly/webgpu-renderer-contract.d.ts +62 -0
  55. package/dist/assembly/webgpu-renderer-contract.d.ts.map +1 -0
  56. package/dist/assembly/webgpu-renderer-guards.d.ts +9 -0
  57. package/dist/assembly/webgpu-renderer-guards.d.ts.map +1 -0
  58. package/dist/assembly/webgpu-renderer-observation.d.ts +6 -0
  59. package/dist/assembly/webgpu-renderer-observation.d.ts.map +1 -0
  60. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts +12 -0
  61. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts.map +1 -0
  62. package/dist/assembly/webgpu-renderer.d.ts +7 -141
  63. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  64. package/dist/assembly/webgpu-vertex-layouts.d.ts +28 -84
  65. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -1
  66. package/dist/authoring.mjs +2 -2
  67. package/dist/capture/scene-projection.d.ts +41 -0
  68. package/dist/capture/scene-projection.d.ts.map +1 -0
  69. package/dist/{chunk-J52NMQKR.mjs → chunk-7MQ3XBSP.mjs} +1098 -844
  70. package/dist/chunk-7MQ3XBSP.mjs.map +1 -0
  71. package/dist/{chunk-GF523LHF.mjs → chunk-BLXGONR2.mjs} +30 -12
  72. package/dist/chunk-BLXGONR2.mjs.map +1 -0
  73. package/dist/{chunk-VSL23LZE.mjs → chunk-C3DDSXYK.mjs} +1188 -463
  74. package/dist/chunk-C3DDSXYK.mjs.map +1 -0
  75. package/dist/{chunk-IQMKJLNU.mjs → chunk-FUUO4UND.mjs} +443 -30
  76. package/dist/chunk-FUUO4UND.mjs.map +1 -0
  77. package/dist/{chunk-J6KACNOH.mjs → chunk-JRBDO2KB.mjs} +617 -7
  78. package/dist/chunk-JRBDO2KB.mjs.map +1 -0
  79. package/dist/{chunk-GE3SDD72.mjs → chunk-MDQJPY3T.mjs} +20 -40
  80. package/dist/chunk-MDQJPY3T.mjs.map +1 -0
  81. package/dist/{chunk-MYJ5RPJO.mjs → chunk-NO4WZCAH.mjs} +3 -3
  82. package/dist/chunk-NO4WZCAH.mjs.map +1 -0
  83. package/dist/{chunk-RBDBZ5R7.mjs → chunk-PA5DRJNI.mjs} +2 -2
  84. package/dist/chunk-PA5DRJNI.mjs.map +1 -0
  85. package/dist/{chunk-MBU4ZVE7.mjs → chunk-PSEQ6QPH.mjs} +4655 -892
  86. package/dist/chunk-PSEQ6QPH.mjs.map +1 -0
  87. package/dist/{chunk-JAASCMXA.mjs → chunk-S5UE2T5P.mjs} +289 -41
  88. package/dist/chunk-S5UE2T5P.mjs.map +1 -0
  89. package/dist/components/camera.d.ts +62 -2
  90. package/dist/components/camera.d.ts.map +1 -1
  91. package/dist/components/directional-light.d.ts +2 -2
  92. package/dist/components/dynamic-resolution.d.ts +15 -0
  93. package/dist/components/dynamic-resolution.d.ts.map +1 -0
  94. package/dist/components/index.d.ts +2 -0
  95. package/dist/components/index.d.ts.map +1 -1
  96. package/dist/components/instances.d.ts +6 -20
  97. package/dist/components/instances.d.ts.map +1 -1
  98. package/dist/components/light-helpers.d.ts.map +1 -1
  99. package/dist/components/reflection-probe.d.ts +1 -0
  100. package/dist/components/reflection-probe.d.ts.map +1 -1
  101. package/dist/components/scene-instance.d.ts +2 -1
  102. package/dist/components/scene-instance.d.ts.map +1 -1
  103. package/dist/components/screen-space-reflection.d.ts +14 -0
  104. package/dist/components/screen-space-reflection.d.ts.map +1 -0
  105. package/dist/components/skylight.d.ts +4 -1
  106. package/dist/components/skylight.d.ts.map +1 -1
  107. package/dist/construct-renderer.mjs +14809 -7570
  108. package/dist/construct-renderer.mjs.map +1 -1
  109. package/dist/device/device-scope.d.ts +2 -0
  110. package/dist/device/device-scope.d.ts.map +1 -1
  111. package/dist/device/gpu-residency.d.ts +3 -14
  112. package/dist/device/gpu-residency.d.ts.map +1 -1
  113. package/dist/dynamic-geometry.d.ts.map +1 -1
  114. package/dist/environment/background.d.ts +7 -0
  115. package/dist/environment/background.d.ts.map +1 -0
  116. package/dist/environment/ibl.d.ts +11 -0
  117. package/dist/environment/ibl.d.ts.map +1 -0
  118. package/dist/environment/storage.d.ts +14 -0
  119. package/dist/environment/storage.d.ts.map +1 -0
  120. package/dist/errors/gpu-driven.d.ts +6 -3
  121. package/dist/errors/gpu-driven.d.ts.map +1 -1
  122. package/dist/errors/render.d.ts +41 -4
  123. package/dist/errors/render.d.ts.map +1 -1
  124. package/dist/extract/camera.d.ts.map +1 -1
  125. package/dist/extract/contracts.d.ts +7 -0
  126. package/dist/extract/contracts.d.ts.map +1 -1
  127. package/dist/extract/gpu-driven.d.ts +50 -3
  128. package/dist/extract/gpu-driven.d.ts.map +1 -1
  129. package/dist/features/host.d.ts +23 -1
  130. package/dist/features/host.d.ts.map +1 -1
  131. package/dist/features/noise-texture.d.ts +17 -0
  132. package/dist/features/noise-texture.d.ts.map +1 -0
  133. package/dist/features/plan.d.ts +83 -2
  134. package/dist/features/plan.d.ts.map +1 -1
  135. package/dist/features/prepared-gpu-work.d.ts +37 -4
  136. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  137. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  138. package/dist/features/prepared-graphics.d.ts +5 -0
  139. package/dist/features/prepared-graphics.d.ts.map +1 -1
  140. package/dist/features/render-graph-compute.d.ts +5 -3
  141. package/dist/features/render-graph-compute.d.ts.map +1 -1
  142. package/dist/features/render-graph-contribution.d.ts +11 -3
  143. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  144. package/dist/features/render-graph-raster.d.ts +19 -3
  145. package/dist/features/render-graph-raster.d.ts.map +1 -1
  146. package/dist/features/types.d.ts +11 -0
  147. package/dist/features/types.d.ts.map +1 -1
  148. package/dist/fullscreen-post-process-pass.d.ts +2 -0
  149. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  150. package/dist/gpu-driven/batch-topology.d.ts +34 -1
  151. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  152. package/dist/gpu-driven/material-artifacts.d.ts +24 -0
  153. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -0
  154. package/dist/gpu-driven/material-bindings.d.ts +7 -0
  155. package/dist/gpu-driven/material-bindings.d.ts.map +1 -0
  156. package/dist/gpu-driven/pbr-program.d.ts +10 -0
  157. package/dist/gpu-driven/pbr-program.d.ts.map +1 -0
  158. package/dist/gpu-driven/prepared-draw.d.ts +2 -0
  159. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  160. package/dist/gpu-driven/production-raster.d.ts +169 -27
  161. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  162. package/dist/gpu-driven/shadow-views.d.ts +126 -0
  163. package/dist/gpu-driven/shadow-views.d.ts.map +1 -0
  164. package/dist/gpu-driven/view-gpu.d.ts +18 -2
  165. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  166. package/dist/gpu-resource.d.ts +5 -2
  167. package/dist/gpu-resource.d.ts.map +1 -1
  168. package/dist/gpu-scene-schema.d.ts +2 -8
  169. package/dist/gpu-scene-schema.d.ts.map +1 -1
  170. package/dist/gpu-scene.d.ts +14 -0
  171. package/dist/gpu-scene.d.ts.map +1 -1
  172. package/dist/gpu-texture-usage.d.ts +1 -0
  173. package/dist/gpu-texture-usage.d.ts.map +1 -1
  174. package/dist/hdrp-buffers.d.ts +9 -0
  175. package/dist/hdrp-buffers.d.ts.map +1 -1
  176. package/dist/ibl/IblPipelineCache.d.ts +2 -0
  177. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  178. package/dist/ibl/frame-resources.d.ts +8 -0
  179. package/dist/ibl/frame-resources.d.ts.map +1 -0
  180. package/dist/ibl/skylight-bind-group.d.ts +3 -1
  181. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  182. package/dist/index.d.ts +87 -75
  183. package/dist/index.d.ts.map +1 -1
  184. package/dist/index.mjs +10 -8
  185. package/dist/index.mjs.map +1 -1
  186. package/dist/inspection-types.d.ts +180 -1
  187. package/dist/inspection-types.d.ts.map +1 -1
  188. package/dist/instances-derived-bounds.d.ts +11 -0
  189. package/dist/instances-derived-bounds.d.ts.map +1 -1
  190. package/dist/instances.d.ts +89 -44
  191. package/dist/instances.d.ts.map +1 -1
  192. package/dist/internal.d.ts +3 -0
  193. package/dist/internal.d.ts.map +1 -1
  194. package/dist/internal.mjs +117 -9
  195. package/dist/internal.mjs.map +1 -1
  196. package/dist/lifecycle.d.ts +2 -1
  197. package/dist/lifecycle.d.ts.map +1 -1
  198. package/dist/material-row.d.ts +15 -0
  199. package/dist/material-row.d.ts.map +1 -0
  200. package/dist/materials.d.ts +15 -0
  201. package/dist/materials.d.ts.map +1 -1
  202. package/dist/pbr-pipeline.d.ts +23 -5
  203. package/dist/pbr-pipeline.d.ts.map +1 -1
  204. package/dist/pipeline/render-extent.d.ts +51 -0
  205. package/dist/pipeline/render-extent.d.ts.map +1 -0
  206. package/dist/pipeline/standard-build-context.d.ts +7 -0
  207. package/dist/pipeline/standard-build-context.d.ts.map +1 -0
  208. package/dist/pipeline/standard-forward-lane.d.ts +3 -2
  209. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  210. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts +12 -0
  211. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts.map +1 -0
  212. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts +70 -0
  213. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts.map +1 -0
  214. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts +75 -0
  215. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts.map +1 -0
  216. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts +111 -0
  217. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts.map +1 -0
  218. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts +16 -0
  219. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts.map +1 -0
  220. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts +14 -0
  221. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts.map +1 -0
  222. package/dist/pipeline/standard-output/auto-exposure/state.d.ts +46 -0
  223. package/dist/pipeline/standard-output/auto-exposure/state.d.ts.map +1 -0
  224. package/dist/pipeline/standard-output/color-transform.d.ts +27 -0
  225. package/dist/pipeline/standard-output/color-transform.d.ts.map +1 -0
  226. package/dist/pipeline/standard-output/graph.d.ts +15 -0
  227. package/dist/pipeline/standard-output/graph.d.ts.map +1 -0
  228. package/dist/pipeline/standard-output/lut-admission.d.ts +29 -0
  229. package/dist/pipeline/standard-output/lut-admission.d.ts.map +1 -0
  230. package/dist/pipeline/standard-output/lut-gpu.d.ts +48 -0
  231. package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -0
  232. package/dist/pipeline/standard-output/lut-state.d.ts +68 -0
  233. package/dist/pipeline/standard-output/lut-state.d.ts.map +1 -0
  234. package/dist/pipeline/standard-output/resources.d.ts +10 -0
  235. package/dist/pipeline/standard-output/resources.d.ts.map +1 -0
  236. package/dist/pipeline/standard-output/types.d.ts +39 -0
  237. package/dist/pipeline/standard-output/types.d.ts.map +1 -0
  238. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  239. package/dist/pipeline/standard-post.d.ts +2 -2
  240. package/dist/pipeline/standard-post.d.ts.map +1 -1
  241. package/dist/pipeline/standard-profile.d.ts +2 -1
  242. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  243. package/dist/pipeline-builder.d.ts +5 -2
  244. package/dist/pipeline-builder.d.ts.map +1 -1
  245. package/dist/pipeline-spec-types.d.ts +4 -0
  246. package/dist/pipeline-spec-types.d.ts.map +1 -1
  247. package/dist/pipeline-spec.d.ts +1 -9
  248. package/dist/pipeline-spec.d.ts.map +1 -1
  249. package/dist/plugin.d.ts.map +1 -1
  250. package/dist/points-lines/standard-owner.d.ts +1 -1
  251. package/dist/points-lines/standard-owner.d.ts.map +1 -1
  252. package/dist/post-process-errors.d.ts +9 -2
  253. package/dist/post-process-errors.d.ts.map +1 -1
  254. package/dist/prepare/extended-lighting/resources.d.ts +3 -3
  255. package/dist/prepare/prepared-graphics-resolver.d.ts +6 -1
  256. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  257. package/dist/record/dynamic-geometry-consumption.d.ts +15 -0
  258. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -0
  259. package/dist/record/frame-lighting.d.ts +3 -3
  260. package/dist/record/frame-lighting.d.ts.map +1 -1
  261. package/dist/record/frame-snapshot.d.ts +109 -13
  262. package/dist/record/frame-snapshot.d.ts.map +1 -1
  263. package/dist/record/frame-targets.d.ts.map +1 -1
  264. package/dist/record/frame.d.ts +41 -5
  265. package/dist/record/frame.d.ts.map +1 -1
  266. package/dist/record/main-pass-geometry.d.ts +13 -9
  267. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  268. package/dist/record/main-pass-material.d.ts +43 -3
  269. package/dist/record/main-pass-material.d.ts.map +1 -1
  270. package/dist/record/main-pass-sprite-draws.d.ts +2 -10
  271. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  272. package/dist/record/main-pass.d.ts +14 -2
  273. package/dist/record/main-pass.d.ts.map +1 -1
  274. package/dist/record/mesh-ssbo.d.ts +15 -6
  275. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  276. package/dist/record/prepared-material-bindings.d.ts +19 -0
  277. package/dist/record/prepared-material-bindings.d.ts.map +1 -0
  278. package/dist/record/recovery-pipeline.d.ts +1 -0
  279. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  280. package/dist/record/render-context.d.ts +101 -22
  281. package/dist/record/render-context.d.ts.map +1 -1
  282. package/dist/record/shadow-pass.d.ts +62 -7
  283. package/dist/record/shadow-pass.d.ts.map +1 -1
  284. package/dist/record/skybox-post-pass.d.ts +1 -1
  285. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  286. package/dist/record/typed-frame-graph.d.ts +39 -8
  287. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  288. package/dist/record/view-ubo.d.ts +1 -0
  289. package/dist/record/view-ubo.d.ts.map +1 -1
  290. package/dist/recovery/render-system-candidate.d.ts +10 -2
  291. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  292. package/dist/recovery/render-system-roots.d.ts +11 -0
  293. package/dist/recovery/render-system-roots.d.ts.map +1 -0
  294. package/dist/recovery/types.d.ts +76 -0
  295. package/dist/recovery/types.d.ts.map +1 -0
  296. package/dist/reflection/background.d.ts +14 -0
  297. package/dist/reflection/background.d.ts.map +1 -0
  298. package/dist/reflection/filter.d.ts.map +1 -1
  299. package/dist/reflection/projection.d.ts +1 -0
  300. package/dist/reflection/projection.d.ts.map +1 -1
  301. package/dist/reflection/record-owner.d.ts +4 -4
  302. package/dist/reflection/record-owner.d.ts.map +1 -1
  303. package/dist/render-contract.d.ts +211 -53
  304. package/dist/render-contract.d.ts.map +1 -1
  305. package/dist/render-graph-primitives.d.ts +1 -0
  306. package/dist/render-graph-primitives.d.ts.map +1 -1
  307. package/dist/render-pipeline.d.ts +81 -6
  308. package/dist/render-pipeline.d.ts.map +1 -1
  309. package/dist/render-system-extract-tail.d.ts +15 -6
  310. package/dist/render-system-extract-tail.d.ts.map +1 -1
  311. package/dist/render-system-extract.d.ts +128 -41
  312. package/dist/render-system-extract.d.ts.map +1 -1
  313. package/dist/render-system-projections.d.ts +22 -0
  314. package/dist/render-system-projections.d.ts.map +1 -0
  315. package/dist/render-system.d.ts +27 -77
  316. package/dist/render-system.d.ts.map +1 -1
  317. package/dist/scene/render-scene-types.d.ts +14 -7
  318. package/dist/scene/render-scene-types.d.ts.map +1 -1
  319. package/dist/scene/render-scene.d.ts +31 -11
  320. package/dist/scene/render-scene.d.ts.map +1 -1
  321. package/dist/scene/shadow-visibility.d.ts +5 -0
  322. package/dist/scene/shadow-visibility.d.ts.map +1 -0
  323. package/dist/ssao-config.d.ts +8 -6
  324. package/dist/ssao-config.d.ts.map +1 -1
  325. package/dist/ssr/admission.d.ts +70 -1
  326. package/dist/ssr/admission.d.ts.map +1 -1
  327. package/dist/ssr/compose.d.ts +13 -0
  328. package/dist/ssr/compose.d.ts.map +1 -0
  329. package/dist/ssr/errors.d.ts +35 -0
  330. package/dist/ssr/errors.d.ts.map +1 -1
  331. package/dist/ssr/graph.d.ts +60 -0
  332. package/dist/ssr/graph.d.ts.map +1 -0
  333. package/dist/ssr/history.d.ts +97 -0
  334. package/dist/ssr/history.d.ts.map +1 -0
  335. package/dist/ssr/hiz.d.ts +37 -0
  336. package/dist/ssr/hiz.d.ts.map +1 -0
  337. package/dist/ssr/inspection.d.ts +42 -0
  338. package/dist/ssr/inspection.d.ts.map +1 -0
  339. package/dist/ssr/resources.d.ts +58 -0
  340. package/dist/ssr/resources.d.ts.map +1 -0
  341. package/dist/ssr/temporal.d.ts +38 -0
  342. package/dist/ssr/temporal.d.ts.map +1 -0
  343. package/dist/systems/skin-palette-allocator.d.ts +32 -10
  344. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  345. package/dist/systems/skin-palette-types.d.ts +15 -0
  346. package/dist/systems/skin-palette-types.d.ts.map +1 -1
  347. package/dist/systems/transparent-dispatch.d.ts +9 -0
  348. package/dist/systems/transparent-dispatch.d.ts.map +1 -0
  349. package/dist/targets/material-source.d.ts +7 -0
  350. package/dist/targets/material-source.d.ts.map +1 -1
  351. package/dist/temporal/frame-coordinator.d.ts +7 -0
  352. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  353. package/dist/temporal/gpu.d.ts +8 -0
  354. package/dist/temporal/gpu.d.ts.map +1 -1
  355. package/dist/temporal/history.d.ts.map +1 -1
  356. package/dist/temporal/index.mjs +6 -6
  357. package/dist/temporal/inspection.d.ts.map +1 -1
  358. package/dist/temporal/standard-scene-data.d.ts +4 -2
  359. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  360. package/dist/temporal/target-coverage-attachment.d.ts +15 -0
  361. package/dist/temporal/target-coverage-attachment.d.ts.map +1 -0
  362. package/dist/temporal/temporal-view.d.ts +6 -0
  363. package/dist/temporal/temporal-view.d.ts.map +1 -1
  364. package/dist/temporal/view.d.ts +6 -0
  365. package/dist/temporal/view.d.ts.map +1 -1
  366. package/dist/typed-render-graph-primitives.d.ts +41 -1
  367. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  368. package/dist/typed-shadow-passes.d.ts +3 -1
  369. package/dist/typed-shadow-passes.d.ts.map +1 -1
  370. package/dist/volume/component.d.ts +1 -0
  371. package/dist/volume/component.d.ts.map +1 -1
  372. package/dist/volume/temporal.d.ts +6 -0
  373. package/dist/volume/temporal.d.ts.map +1 -1
  374. package/package.json +21 -20
  375. package/src/__tests__/auto-exposure-gpu-evidence.ts +291 -0
  376. package/src/__tests__/auto-exposure-inspection.unit.test.ts +80 -0
  377. package/src/__tests__/auto-exposure-transaction.integration.test.ts +99 -0
  378. package/src/__tests__/auto-exposure.browser.test.ts +33 -0
  379. package/src/__tests__/auto-exposure.dawn.test.ts +33 -0
  380. package/src/__tests__/capture-scene-projection.unit.test.ts +24 -0
  381. package/src/__tests__/detached-inspection.integration.test.ts +5 -0
  382. package/src/__tests__/device-feature-admission.unit.test.ts +15 -24
  383. package/src/__tests__/device-loss-fanout.unit.test.ts +43 -0
  384. package/src/__tests__/device-scope-lifecycle.unit.test.ts +18 -0
  385. package/src/__tests__/device-scope-stale-matrix.unit.test.ts +18 -0
  386. package/src/__tests__/docs-gate-green.unit.test.ts +3 -3
  387. package/src/__tests__/dynamic-geometry-batch.unit.test.ts +157 -0
  388. package/src/__tests__/dynamic-geometry.unit.test.ts +36 -0
  389. package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +81 -0
  390. package/src/__tests__/factory-contract.integration.test.ts +231 -9
  391. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +19 -0
  392. package/src/__tests__/feature-depth-input.dawn.test.ts +121 -0
  393. package/src/__tests__/feature-noise.unit.test.ts +38 -0
  394. package/src/__tests__/fixtures/taa-resolve-pinned-baseline.wgsl +301 -0
  395. package/src/__tests__/frame-camera-selection.unit.test.ts +131 -0
  396. package/src/__tests__/frame-observation.contract.test.ts +21 -0
  397. package/src/__tests__/frame-plan-contract.unit.test.ts +24 -0
  398. package/src/__tests__/frame-targets.unit.test.ts +24 -0
  399. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +3 -0
  400. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +79 -0
  401. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +307 -7
  402. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +122 -142
  403. package/src/__tests__/gpu-driven-indirect-raster.browser.test.ts +10 -1
  404. package/src/__tests__/gpu-driven-lod-cache.unit.test.ts +261 -0
  405. package/src/__tests__/gpu-driven-mesh-world-identity.unit.test.ts +346 -0
  406. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +1422 -0
  407. package/src/__tests__/gpu-driven-pbr.integration.test.ts +21 -0
  408. package/src/__tests__/gpu-driven-production-fixture.ts +96 -0
  409. package/src/__tests__/gpu-driven-production.integration.test.ts +1076 -106
  410. package/src/__tests__/gpu-driven-scaling.unit.test.ts +1 -3
  411. package/src/__tests__/gpu-driven-shadow-views.browser.test.ts +63 -0
  412. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +294 -0
  413. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +97 -0
  414. package/src/__tests__/gpu-driven-shadow-views.unit.test.ts +168 -0
  415. package/src/__tests__/gpu-driven-skin-contract.test-d.ts +11 -0
  416. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +72 -20
  417. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +30 -9
  418. package/src/__tests__/gpu-driven-view.unit.test.ts +160 -52
  419. package/src/__tests__/gpu-pass-timing-browser-runner.ts +15 -0
  420. package/src/__tests__/gpu-pass-timing.browser.test.ts +20 -2
  421. package/src/__tests__/gpu-resource-scope.unit.test.ts +71 -0
  422. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +42 -0
  423. package/src/__tests__/gpu-scene-render-graph-gpu.ts +19 -5
  424. package/src/__tests__/gpu-scene.dawn.test.ts +40 -13
  425. package/src/__tests__/gpu-scene.unit.test.ts +613 -33
  426. package/src/__tests__/instances-culling.integration.test.ts +36 -10
  427. package/src/__tests__/instances-derived-bounds.unit.test.ts +54 -0
  428. package/src/__tests__/instances-store.unit.test.ts +242 -39
  429. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +6 -0
  430. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +13 -16
  431. package/src/__tests__/lut-lkg-transaction.integration.test.ts +122 -0
  432. package/src/__tests__/material-contract-inventory.unit.test.ts +5 -6
  433. package/src/__tests__/material-dispatch-shader-id.unit.test.ts +51 -0
  434. package/src/__tests__/material-lane-selection.unit.test.ts +60 -0
  435. package/src/__tests__/material-numeric-upload.unit.test.ts +53 -0
  436. package/src/__tests__/material-texture-handles.unit.test.ts +18 -0
  437. package/src/__tests__/material-texture-source.unit.test.ts +87 -0
  438. package/src/__tests__/materials-authoring-validation.unit.test.ts +41 -0
  439. package/src/__tests__/materials-standard-contract.unit.test.ts +57 -3
  440. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +4 -0
  441. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +8 -0
  442. package/src/__tests__/morph-neutral-admission.unit.test.ts +19 -0
  443. package/src/__tests__/normal-fallback.dawn.test.ts +122 -0
  444. package/src/__tests__/particle-material-input-layout.unit.test.ts +104 -0
  445. package/src/__tests__/physical-clearcoat.integration.test.ts +42 -3
  446. package/src/__tests__/pipeline-texture-specialization.unit.test.ts +138 -0
  447. package/src/__tests__/point-shadow-inspection.unit.test.ts +2 -0
  448. package/src/__tests__/post-process-error-policy-owner.unit.test.ts +6 -5
  449. package/src/__tests__/prepared-gpu-driven-pbr-contract.test-d.ts +31 -0
  450. package/src/__tests__/prepared-gpu-driven-pbr-contract.unit.test.ts +39 -0
  451. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +251 -9
  452. package/src/__tests__/prepared-graphics-capability.unit.test.ts +21 -0
  453. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +6 -2
  454. package/src/__tests__/profiler-phase-catalog.test.ts +5 -0
  455. package/src/__tests__/public-surface.test-d.ts +3 -0
  456. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +30 -12
  457. package/src/__tests__/reflection-fallback-async-publication.unit.test.ts +171 -0
  458. package/src/__tests__/reflection-probe-authoring.unit.test.ts +12 -10
  459. package/src/__tests__/reflection-probe-box.dawn.test.ts +71 -0
  460. package/src/__tests__/reflection-probe-filter.unit.test.ts +9 -0
  461. package/src/__tests__/reflection-probe-orientation.dawn.test.ts +294 -0
  462. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +5 -14
  463. package/src/__tests__/reflection-probe-updates.unit.test.ts +129 -0
  464. package/src/__tests__/render-error-code-owner.test-d.ts +7 -0
  465. package/src/__tests__/render-error-detail-ssot.test-d.ts +27 -1
  466. package/src/__tests__/render-error-exhaustive.test-d.ts +15 -0
  467. package/src/__tests__/render-extent.unit.test.ts +63 -0
  468. package/src/__tests__/render-feature-gpu-work.integration.test.ts +285 -2
  469. package/src/__tests__/render-feature-plan.unit.test.ts +245 -0
  470. package/src/__tests__/render-feature-shadow-order.unit.test.ts +228 -0
  471. package/src/__tests__/render-feature-zero-work.unit.test.ts +35 -1
  472. package/src/__tests__/render-graph-fallback.integration.test.ts +2 -2
  473. package/src/__tests__/render-owner-cohesion.unit.test.ts +55 -0
  474. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +426 -0
  475. package/src/__tests__/render-scene-projection.unit.test.ts +915 -23
  476. package/src/__tests__/render-scene-resource-updates.integration.test.ts +214 -0
  477. package/src/__tests__/render-scene-skin-lifecycle.integration.test.ts +173 -0
  478. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +133 -0
  479. package/src/__tests__/render-target-material-source.test-d.ts +27 -3
  480. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +233 -9
  481. package/src/__tests__/renderer-frame-transaction.integration.test.ts +923 -5
  482. package/src/__tests__/shader-manifest-fixture.ts +45 -0
  483. package/src/__tests__/skin-motion-regression.unit.test.ts +0 -33
  484. package/src/__tests__/skin-palette-persistent.integration.test.ts +28 -0
  485. package/src/__tests__/skin-palette-persistent.unit.test.ts +247 -0
  486. package/src/__tests__/skinned-shadow-caster.test.ts +125 -14
  487. package/src/__tests__/specular-environment-scale.dawn.test.ts +158 -0
  488. package/src/__tests__/ssao-config.unit.test.ts +7 -0
  489. package/src/__tests__/ssao-surface.dawn.test.ts +252 -0
  490. package/src/__tests__/ssr-admission-matrix.integration.test.ts +7 -1
  491. package/src/__tests__/ssr-confidence-compose.unit.test.ts +84 -0
  492. package/src/__tests__/ssr-coordinate-oracle.unit.test.ts +28 -0
  493. package/src/__tests__/ssr-cube-silhouette.dawn.test.ts +270 -0
  494. package/src/__tests__/ssr-format-probe-gating.integration.test.ts +82 -0
  495. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +99 -0
  496. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +116 -0
  497. package/src/__tests__/ssr-gpu-dispatch.ts +951 -0
  498. package/src/__tests__/ssr-graph-topology.unit.test.ts +104 -0
  499. package/src/__tests__/ssr-history-filter.dawn.test.ts +198 -0
  500. package/src/__tests__/ssr-hit-color.dawn.test.ts +273 -0
  501. package/src/__tests__/ssr-hiz-odd-reduction.dawn.test.ts +569 -0
  502. package/src/__tests__/ssr-hiz-rescue.dawn.test.ts +279 -0
  503. package/src/__tests__/ssr-hiz.browser.test.ts +150 -0
  504. package/src/__tests__/ssr-hiz.dawn.test.ts +150 -0
  505. package/src/__tests__/ssr-hiz.unit.test.ts +56 -0
  506. package/src/__tests__/ssr-inspection.unit.test.ts +105 -0
  507. package/src/__tests__/ssr-memory-budget.unit.test.ts +45 -0
  508. package/src/__tests__/ssr-performance-sequence-aggregation.unit.test.ts +15 -0
  509. package/src/__tests__/ssr-public-surface.test-d.ts +37 -0
  510. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +387 -0
  511. package/src/__tests__/ssr-reflection-mip-reducer.dawn.test.ts +309 -0
  512. package/src/__tests__/ssr-reprojection-probe.ts +90 -0
  513. package/src/__tests__/ssr-roughness-mip.dawn.test.ts +163 -0
  514. package/src/__tests__/ssr-shader-prewarm.unit.test.ts +59 -0
  515. package/src/__tests__/ssr-spatial.integration.test.ts +141 -0
  516. package/src/__tests__/ssr-stable-lattice.dawn.test.ts +475 -0
  517. package/src/__tests__/ssr-temporal-history.unit.test.ts +202 -0
  518. package/src/__tests__/ssr-texel-crossing.dawn.test.ts +448 -0
  519. package/src/__tests__/ssr-zero-work.integration.test.ts +87 -0
  520. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +84 -12
  521. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +29 -0
  522. package/src/__tests__/standard-output-chain-contract.unit.test.ts +99 -0
  523. package/src/__tests__/standard-output-chain.integration.test.ts +73 -0
  524. package/src/__tests__/standard-output-gpu-evidence.ts +205 -0
  525. package/src/__tests__/standard-output.browser.test.ts +15 -0
  526. package/src/__tests__/standard-output.dawn.test.ts +15 -0
  527. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +275 -0
  528. package/src/__tests__/standard-pipeline.integration.test.ts +320 -3
  529. package/src/__tests__/standard-profile-oracle.unit.test.ts +49 -0
  530. package/src/__tests__/standard-surface-lighting.unit.test.ts +41 -0
  531. package/src/__tests__/standard-texture-specialization.dawn.test.ts +141 -0
  532. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +6 -3
  533. package/src/__tests__/taa-coverage-edge.dawn.test.ts +199 -0
  534. package/src/__tests__/taa-history-clipping.dawn.test.ts +328 -0
  535. package/src/__tests__/taa-neighborhood-equivalence.dawn.test.ts +782 -0
  536. package/src/__tests__/taa-stability-feedback.browser.test.ts +23 -0
  537. package/src/__tests__/taa-stability-feedback.dawn.test.ts +68 -0
  538. package/src/__tests__/taa-stability-feedback.ts +342 -0
  539. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +49 -7
  540. package/src/__tests__/temporal-reset-matrix.unit.test.ts +9 -0
  541. package/src/__tests__/temporal-transaction.integration.test.ts +28 -0
  542. package/src/__tests__/temporal-view.unit.test.ts +17 -0
  543. package/src/__tests__/texture-usage-surface.unit.test.ts +4 -2
  544. package/src/__tests__/transmission-prewarm.unit.test.ts +57 -0
  545. package/src/__tests__/typed-pipeline-topology.unit.test.ts +109 -4
  546. package/src/__tests__/typed-scene-pass-material-cache.integration.test.ts +126 -0
  547. package/src/__tests__/vertex-layouts-ssot.unit.test.ts +47 -0
  548. package/src/__tests__/visibility-extract.perf.test.ts +20 -8
  549. package/src/__tests__/visibility-instances.integration.test.ts +19 -7
  550. package/src/__tests__/visibility-producer-matrix.integration.test.ts +20 -9
  551. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +62 -13
  552. package/src/__tests__/volumetric-fog-topology.unit.test.ts +7 -7
  553. package/src/assembly/atmosphere-shader-prewarm.ts +39 -0
  554. package/src/assembly/device-feature-admission.ts +9 -4
  555. package/src/assembly/dynamic-geometry-host.ts +284 -163
  556. package/src/assembly/dynamic-geometry-runtime.ts +71 -0
  557. package/src/assembly/factory.ts +4 -0
  558. package/src/assembly/gpu-driven-owner.ts +29 -0
  559. package/src/assembly/host-contract.ts +19 -0
  560. package/src/assembly/material/assembly.ts +383 -1
  561. package/src/assembly/material/pipeline-helpers.ts +100 -0
  562. package/src/assembly/material-shader-policy.ts +110 -46
  563. package/src/assembly/recovery/device-loss-fanout.ts +5 -0
  564. package/src/assembly/recovery/failure-location.ts +62 -0
  565. package/src/assembly/recovery/recovery-attempt.ts +2 -29
  566. package/src/assembly/recovery/renderer-recover.ts +16 -17
  567. package/src/assembly/renderer-facade.ts +34 -1
  568. package/src/assembly/renderer-feature-inspection.ts +13 -0
  569. package/src/assembly/renderer-frame-transaction.ts +63 -0
  570. package/src/assembly/renderer-helpers.ts +26 -0
  571. package/src/assembly/renderer-inspection.ts +6 -0
  572. package/src/assembly/shader-prewarm-policy.ts +46 -1
  573. package/src/assembly/skin-palette-owner.ts +21 -0
  574. package/src/assembly/ssr-shader-prewarm.ts +41 -0
  575. package/src/assembly/webgpu-device-loss.ts +75 -0
  576. package/src/assembly/webgpu-pbr-ready.ts +232 -0
  577. package/src/assembly/webgpu-ready-mipmap.ts +26 -0
  578. package/src/assembly/webgpu-ready.ts +161 -131
  579. package/src/assembly/webgpu-renderer-bootstrap.ts +85 -0
  580. package/src/assembly/webgpu-renderer-contract.ts +69 -0
  581. package/src/assembly/webgpu-renderer-guards.ts +32 -0
  582. package/src/assembly/webgpu-renderer-observation.ts +43 -0
  583. package/src/assembly/webgpu-renderer-recovery-failure.ts +62 -0
  584. package/src/assembly/webgpu-renderer.ts +744 -730
  585. package/src/assembly/webgpu-vertex-layouts.ts +233 -87
  586. package/src/capture/scene-projection.ts +37 -0
  587. package/src/components/__tests__/camera-exposure.contract.unit.test.ts +153 -0
  588. package/src/components/__tests__/dynamic-resolution-schema.unit.test.ts +65 -0
  589. package/src/components/__tests__/light-radius.integration.test.ts +59 -0
  590. package/src/components/camera.ts +287 -7
  591. package/src/components/directional-light.ts +3 -3
  592. package/src/components/dynamic-resolution.ts +67 -0
  593. package/src/components/index.ts +2 -0
  594. package/src/components/instances.ts +45 -18
  595. package/src/components/light-helpers.ts +6 -1
  596. package/src/components/reflection-probe.ts +1 -0
  597. package/src/components/scene-instance.ts +2 -1
  598. package/src/components/screen-space-reflection.ts +21 -0
  599. package/src/components/skylight.ts +4 -1
  600. package/src/components/sprite-animation.ts +2 -2
  601. package/src/components/sprite-instances.ts +1 -1
  602. package/src/device/device-scope.ts +13 -7
  603. package/src/device/gpu-residency.ts +45 -42
  604. package/src/dynamic-geometry.ts +13 -8
  605. package/src/environment/background.ts +281 -0
  606. package/src/environment/ibl.ts +211 -0
  607. package/src/environment/storage.ts +63 -0
  608. package/src/errors/gpu-driven.ts +49 -8
  609. package/src/errors/render.ts +91 -3
  610. package/src/extract/camera.ts +37 -1
  611. package/src/extract/contracts.ts +10 -0
  612. package/src/extract/gpu-driven.ts +240 -10
  613. package/src/features/host.ts +237 -19
  614. package/src/features/noise-texture.ts +91 -0
  615. package/src/features/plan.ts +553 -24
  616. package/src/features/prepared-gpu-work.ts +631 -49
  617. package/src/features/prepared-graphics-store.ts +31 -0
  618. package/src/features/prepared-graphics.ts +5 -0
  619. package/src/features/render-graph-compute.ts +41 -8
  620. package/src/features/render-graph-contribution.ts +141 -3
  621. package/src/features/render-graph-raster.ts +143 -30
  622. package/src/features/types.ts +11 -0
  623. package/src/fullscreen-post-process-pass.ts +3 -0
  624. package/src/gpu-driven/batch-topology.ts +452 -108
  625. package/src/gpu-driven/material-artifacts.ts +179 -0
  626. package/src/gpu-driven/material-bindings.ts +41 -0
  627. package/src/gpu-driven/pbr-program.ts +31 -0
  628. package/src/gpu-driven/prepared-draw.ts +42 -6
  629. package/src/gpu-driven/production-raster.ts +2806 -716
  630. package/src/gpu-driven/shadow-views.ts +539 -0
  631. package/src/gpu-driven/view-gpu.ts +188 -41
  632. package/src/gpu-resource.ts +21 -2
  633. package/src/gpu-scene-schema.ts +61 -11
  634. package/src/gpu-scene.ts +197 -99
  635. package/src/gpu-texture-usage.ts +1 -0
  636. package/src/hdrp-buffers.ts +141 -36
  637. package/src/ibl/IblPipelineCache.ts +5 -0
  638. package/src/ibl/frame-resources.ts +38 -0
  639. package/src/ibl/skylight-bind-group.ts +32 -21
  640. package/src/index.ts +194 -75
  641. package/src/inspection-types.ts +207 -1
  642. package/src/instances-derived-bounds.ts +258 -34
  643. package/src/instances.ts +459 -150
  644. package/src/internal.ts +22 -0
  645. package/src/lifecycle.ts +2 -0
  646. package/src/material-row.ts +194 -0
  647. package/src/materials.ts +44 -8
  648. package/src/pbr-pipeline.ts +172 -19
  649. package/src/pipeline/render-extent.ts +111 -0
  650. package/src/pipeline/standard-build-context.ts +7 -0
  651. package/src/pipeline/standard-forward-lane.ts +74 -29
  652. package/src/pipeline/standard-output/__tests__/lut-admission.unit.test.ts +103 -0
  653. package/src/pipeline/standard-output/auto-exposure/__tests__/capability.unit.test.ts +40 -0
  654. package/src/pipeline/standard-output/auto-exposure/__tests__/gpu.contract.unit.test.ts +125 -0
  655. package/src/pipeline/standard-output/auto-exposure/__tests__/graph.contract.unit.test.ts +94 -0
  656. package/src/pipeline/standard-output/auto-exposure/__tests__/oracle.test.ts +108 -0
  657. package/src/pipeline/standard-output/auto-exposure/__tests__/preset.test.ts +26 -0
  658. package/src/pipeline/standard-output/auto-exposure/capability.ts +38 -0
  659. package/src/pipeline/standard-output/auto-exposure/gpu.ts +567 -0
  660. package/src/pipeline/standard-output/auto-exposure/graph.ts +221 -0
  661. package/src/pipeline/standard-output/auto-exposure/inspection.ts +231 -0
  662. package/src/pipeline/standard-output/auto-exposure/oracle.ts +291 -0
  663. package/src/pipeline/standard-output/auto-exposure/preset.ts +28 -0
  664. package/src/pipeline/standard-output/auto-exposure/state.ts +156 -0
  665. package/src/pipeline/standard-output/color-transform.ts +124 -0
  666. package/src/pipeline/standard-output/graph.ts +139 -0
  667. package/src/pipeline/standard-output/lut-admission.ts +121 -0
  668. package/src/pipeline/standard-output/lut-gpu.ts +399 -0
  669. package/src/pipeline/standard-output/lut-state.ts +176 -0
  670. package/src/pipeline/standard-output/resources.ts +121 -0
  671. package/src/pipeline/standard-output/types.ts +58 -0
  672. package/src/pipeline/standard-pipeline.ts +232 -70
  673. package/src/pipeline/standard-post.ts +190 -27
  674. package/src/pipeline/standard-profile.ts +2 -1
  675. package/src/pipeline-builder.ts +28 -9
  676. package/src/pipeline-spec-types.ts +4 -0
  677. package/src/pipeline-spec.ts +37 -29
  678. package/src/plugin.ts +4 -0
  679. package/src/points-lines/standard-owner.ts +6 -14
  680. package/src/post-process-errors.ts +26 -18
  681. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +2 -2
  682. package/src/prepare/extended-lighting/resources.ts +3 -3
  683. package/src/prepare/prepared-graphics-resolver.ts +9 -2
  684. package/src/record/__tests__/main-pass-geometry.unit.test.ts +68 -0
  685. package/src/record/__tests__/material-bg-cache.unit.test.ts +45 -0
  686. package/src/record/__tests__/material-slot-plan.unit.test.ts +57 -0
  687. package/src/record/__tests__/mesh-ssbo.unit.test.ts +40 -1
  688. package/src/record/__tests__/recovery-pipeline.unit.test.ts +20 -1
  689. package/src/record/__tests__/sprite-material-abi.unit.test.ts +41 -6
  690. package/src/record/__tests__/temporal-payload.unit.test.ts +2 -0
  691. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +45 -0
  692. package/src/record/__tests__/view-ubo-layout.unit.test.ts +5 -0
  693. package/src/record/dynamic-geometry-consumption.ts +59 -0
  694. package/src/record/frame-lighting.ts +15 -12
  695. package/src/record/frame-snapshot.ts +146 -34
  696. package/src/record/frame-targets.ts +9 -0
  697. package/src/record/frame.ts +838 -170
  698. package/src/record/main-pass-geometry.ts +209 -103
  699. package/src/record/main-pass-material.ts +157 -40
  700. package/src/record/main-pass-sprite-draws.ts +16 -107
  701. package/src/record/main-pass.ts +141 -12
  702. package/src/record/material-uniforms.ts +1 -1
  703. package/src/record/mesh-ssbo.ts +59 -15
  704. package/src/record/prepared-material-bindings.ts +238 -0
  705. package/src/record/recovery-pipeline.ts +69 -90
  706. package/src/record/render-context.ts +123 -24
  707. package/src/record/shadow-pass.ts +1389 -230
  708. package/src/record/skybox-post-pass.ts +14 -3
  709. package/src/record/typed-frame-graph.ts +1064 -47
  710. package/src/record/view-ubo.ts +12 -1
  711. package/src/recovery/render-system-candidate.ts +135 -68
  712. package/src/recovery/render-system-roots.ts +113 -0
  713. package/src/recovery/types.ts +77 -0
  714. package/src/reflection/background.ts +151 -0
  715. package/src/reflection/filter.ts +3 -0
  716. package/src/reflection/projection.ts +16 -6
  717. package/src/reflection/record-owner.ts +308 -140
  718. package/src/render-contract.ts +237 -49
  719. package/src/render-graph-primitives.ts +34 -5
  720. package/src/render-pipeline.ts +90 -2
  721. package/src/render-system-extract-tail.ts +431 -162
  722. package/src/render-system-extract.ts +478 -119
  723. package/src/render-system-projections.ts +241 -0
  724. package/src/render-system.ts +715 -828
  725. package/src/scene/render-scene-types.ts +13 -4
  726. package/src/scene/render-scene.ts +577 -355
  727. package/src/scene/shadow-visibility.ts +21 -0
  728. package/src/ssao-config.ts +25 -8
  729. package/src/ssr/admission.ts +380 -14
  730. package/src/ssr/compose.ts +144 -0
  731. package/src/ssr/errors.ts +48 -0
  732. package/src/ssr/graph.ts +797 -0
  733. package/src/ssr/history.ts +436 -0
  734. package/src/ssr/hiz.ts +124 -0
  735. package/src/ssr/inspection.ts +124 -0
  736. package/src/ssr/resources.ts +202 -0
  737. package/src/ssr/temporal.ts +164 -0
  738. package/src/systems/skin-palette-allocator.ts +495 -148
  739. package/src/systems/skin-palette-types.ts +17 -0
  740. package/src/systems/transparent-dispatch.ts +90 -0
  741. package/src/targets/material-source.ts +34 -0
  742. package/src/temporal/coverage.ts +1 -1
  743. package/src/temporal/frame-coordinator.ts +23 -2
  744. package/src/temporal/gpu.ts +79 -12
  745. package/src/temporal/history.ts +3 -1
  746. package/src/temporal/inspection.ts +2 -1
  747. package/src/temporal/standard-scene-data.ts +10 -2
  748. package/src/temporal/target-coverage-attachment.ts +72 -0
  749. package/src/temporal/temporal-view.ts +11 -2
  750. package/src/temporal/view.ts +12 -2
  751. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +219 -139
  752. package/src/typed-render-graph-primitives.ts +304 -74
  753. package/src/typed-shadow-passes.ts +122 -12
  754. package/src/volume/component.ts +1 -0
  755. package/src/volume/passes.ts +3 -3
  756. package/src/volume/temporal.ts +10 -0
  757. package/dist/chunk-GE3SDD72.mjs.map +0 -1
  758. package/dist/chunk-GF523LHF.mjs.map +0 -1
  759. package/dist/chunk-IQMKJLNU.mjs.map +0 -1
  760. package/dist/chunk-J52NMQKR.mjs.map +0 -1
  761. package/dist/chunk-J6KACNOH.mjs.map +0 -1
  762. package/dist/chunk-JAASCMXA.mjs.map +0 -1
  763. package/dist/chunk-MBU4ZVE7.mjs.map +0 -1
  764. package/dist/chunk-MYJ5RPJO.mjs.map +0 -1
  765. package/dist/chunk-RBDBZ5R7.mjs.map +0 -1
  766. package/dist/chunk-VSL23LZE.mjs.map +0 -1
  767. package/src/__tests__/instances-world-ownership.integration.test.ts +0 -62
  768. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +0 -36
@@ -1,24 +1,5 @@
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`.
20
-
21
- import type { MipmapShaderModuleFactory } from '@forgeax/engine-assets-runtime';
1
+ // @forgeax/engine-render — WebGPU renderer lifecycle/frame interpreter.
2
+ // Runtime selects the backend; structured RHI errors and browser isolation stay intact.
22
3
  import {
23
4
  AssetRegistry,
24
5
  adaptDynamicTextureDevice,
@@ -28,12 +9,12 @@ import { audioLoader } from '@forgeax/engine-audio-webaudio';
28
9
  import { Update, type World } from '@forgeax/engine-ecs';
29
10
  import { createRenderReadLease, type RenderReadLease } from '@forgeax/engine-ecs/projection';
30
11
  import {
31
- deriveVertexBufferLayout,
32
- deriveVertexBufferLayoutFromProjection,
12
+ DEFAULT_VERTEX_ATTRIBUTE_MAP,
33
13
  type VertexLayoutProjection,
34
14
  } from '@forgeax/engine-geometry';
35
15
  import type {
36
16
  BindGroupLayout,
17
+ Buffer,
37
18
  PipelineLayout,
38
19
  RenderPipeline,
39
20
  Result,
@@ -50,19 +31,17 @@ import {
50
31
  standardPhysicalTextureFields,
51
32
  } from '@forgeax/engine-shader';
52
33
  import type {
53
- ImportTransport,
54
34
  MaterialRenderState,
55
35
  ParamSchemaEntry,
56
36
  PassKind,
57
37
  PrimitiveTopology,
58
38
  VertexAttributeMap,
59
39
  } from '@forgeax/engine-types';
60
- import { createDynamicGeometryLifecycle } from '../dynamic-geometry';
40
+ import { transmissionBackdropAvailable } from './device-feature-admission';
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 {
@@ -84,13 +63,9 @@ import {
84
63
  import { glyphTextLayoutSystem } from '../glyph-text-layout-system';
85
64
  import { GPU_SHADER_STAGE_FRAGMENT, GPU_SHADER_STAGE_VERTEX } from '../gpu-stage';
86
65
  import type { BloomInspection } from '../inspection-types';
66
+ import { DeviceScope } from '../lifecycle';
87
67
  import {
88
- DeviceScope,
89
- HealthListenerRegistry,
90
- LostListenerRegistry,
91
- RhiErrorListenerRegistry,
92
- } from '../lifecycle';
93
- import {
68
+ createHdrpBindGroupLayoutDescriptor,
94
69
  isCanonicalStandardPbrMaterialShader,
95
70
  isStandardPbrMaterialShader,
96
71
  type PipelineGroup2Contract,
@@ -105,6 +80,7 @@ import {
105
80
  cacheKeyOf,
106
81
  colorFormatsForPassKind,
107
82
  type PipelineSpec,
83
+ PipelineSpecError,
108
84
  passKindPolicyTable,
109
85
  } from '../pipeline-spec';
110
86
  import type { ExtendedLightingResourceCandidate } from '../prepare/extended-lighting/resources';
@@ -122,6 +98,7 @@ import {
122
98
  promoteExtendedLightingCandidate,
123
99
  } from '../prepare/extended-lighting/state';
124
100
  import type { FrameObservationOptions } from '../record/frame';
101
+ import { getTextureIdentity } from '../record/frame-snapshot';
125
102
  import type { GpuPassTimingReason } from '../record/gpu-pass-timing/errors.js';
126
103
  import {
127
104
  createGpuPassTimingSession,
@@ -131,14 +108,14 @@ import {
131
108
  type GpuPassTimingSession,
132
109
  } from '../record/gpu-pass-timing/index.js';
133
110
  import { GpuTimingCapture, type VolumeTimingObservation } from '../record/gpu-timing';
134
- import type { RecoveryColdWorkGuard } from '../record/render-context';
135
111
  import type {
112
+ AtmosphereShaderSources,
136
113
  DrawOwnerOptions,
114
+ FrameObservationDomain,
137
115
  FrameObservationRequest,
138
116
  FrameReceipt,
139
117
  FrameReceiptObservation,
140
118
  HealthSnapshot,
141
- RenderDebugOverlay,
142
119
  RendererErrorListener,
143
120
  RendererLostListener,
144
121
  RendererOptions,
@@ -147,6 +124,7 @@ import type {
147
124
  RenderProfile,
148
125
  RenderResult,
149
126
  RenderWorldLease,
127
+ SsrShaderSources,
150
128
  VolumetricFogShaderSources,
151
129
  } from '../render-contract';
152
130
  import { FXAA_POST_PROCESS_ID, STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from '../render-contract';
@@ -163,11 +141,18 @@ import { createGpuPassTimingObservationStore, observeGpuPassTimingDisabled } fro
163
141
  import { postSpawnResolveJoints } from '../scene-instances/post-spawn-resolve-joints';
164
142
  import type { RenderTarget } from '../targets/contracts';
165
143
  import { tilemapChunkExtractSystem } from '../tilemap-chunk-extract-system';
144
+ import type { TypedFrameObservationCapture } from '../typed-render-graph-primitives';
166
145
  import type { RhiBackendPack } from './backend-contract';
167
146
  import type { BundlerOptions } from './bundler-contract';
168
- import { deviceOptionsForAdapter } from './device-feature-admission';
169
- import { createDynamicGeometryHost } from './dynamic-geometry-host';
147
+ import { createRendererDynamicGeometryController } from './dynamic-geometry-host';
170
148
  import type { RendererAssemblyImplementation } from './host-contract';
149
+ import {
150
+ isEngineOwnedMaterialShader,
151
+ resolveMaterialPipelineBindGroups,
152
+ resolveMaterialPipelineShaderId,
153
+ resolveMaterialPipelineVertexEntry,
154
+ resolveRendererMaterialShaderArtifact,
155
+ } from './material/pipeline-helpers';
171
156
  import {
172
157
  allowsUnlitPreparedFallback,
173
158
  invokeDeviceCreateShaderModule,
@@ -188,7 +173,6 @@ import {
188
173
  shouldDeferMissingPreparedMaterialShader,
189
174
  } from './material-shader-policy';
190
175
  import type { MeshSsboGrowResult, MeshSsboState } from './mesh-ssbo-grow';
191
- import { attachDeviceLostFanout } from './recovery/device-loss-fanout';
192
176
  import {
193
177
  type GenerationAggregate,
194
178
  type GenerationPublication,
@@ -201,32 +185,48 @@ import {
201
185
  structuredRendererCause,
202
186
  validateRenderProfile,
203
187
  } from './renderer-facade';
188
+ import { projectRendererFeatureInspection } from './renderer-feature-inspection';
204
189
  import {
205
190
  type ContinuationTerminator,
206
191
  createContinuationTerminator,
207
192
  } from './renderer-frame-transaction';
208
- import { ensureContextConfigured, wrapDisposeError } from './renderer-helpers';
193
+ import {
194
+ ensureContextConfigured,
195
+ rendererInitializationError,
196
+ rendererInitializationPipelineError,
197
+ wrapDisposeError,
198
+ } from './renderer-helpers';
209
199
  import { projectRendererOutputInspection } from './renderer-inspection';
210
200
  import {
211
201
  createRecoveryContinuation,
212
202
  createRecoveryDeadline,
213
203
  createSingleFlight,
214
- type RecoveryGuidance,
215
204
  type RecoveryPhase,
216
205
  } from './renderer-lifecycle';
206
+ import { derivedPhysicsFrameError } from './webgpu-renderer-guards';
207
+ import {
208
+ createRecoveryFailureLocation,
209
+ type RecoveryFailureLocation,
210
+ } from './webgpu-renderer-recovery-failure';
211
+
212
+ export type { RecoveryFailureLocation } from './webgpu-renderer-recovery-failure';
213
+
217
214
  import { STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES } from './shader-prewarm-policy';
218
215
  import { buildReadyWebGPU } from './webgpu-ready';
219
216
  import { DEPTH_TEXTURE_FORMAT, HDR_COLOR_ATTACHMENT_FORMAT } from './webgpu-ready-contract';
217
+ import { adaptMipmapShaderModuleFactory } from './webgpu-ready-mipmap';
218
+ import {
219
+ tryCreateWebGPURenderer as tryCreateWebGPURendererBootstrap,
220
+ type WebGPURendererInternals,
221
+ } from './webgpu-renderer-bootstrap';
222
+ import type { WebGPUOutcome } from './webgpu-renderer-contract';
223
+ import { disposeObservationCaptureSet } from './webgpu-renderer-observation';
220
224
  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,
225
+ isPreparedMaterialVertexLayout,
226
226
  PREPARED_INSTANCE_VERTEX_ATTRS,
227
227
  PREPARED_MATERIAL_INSTANCE_VERTEX_ATTRS,
228
- TOPOLOGY_SEGMENT_INSTANCE_VERTEX_BUFFERS,
229
- toGpuVertexBufferLayouts,
228
+ particleMaterialInputVertexBuffers,
229
+ resolveWebGPUVertexBufferLayouts,
230
230
  } from './webgpu-vertex-layouts';
231
231
 
232
232
  export type { BundlerOptions } from './bundler-contract';
@@ -249,366 +249,45 @@ export {
249
249
  selectPipelineLayoutForVariant,
250
250
  shouldDeferMissingPreparedMaterialShader,
251
251
  } 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';
261
252
  export {
262
253
  createMeshSsboGrowController,
263
254
  deriveStorageBufferCeiling,
264
255
  INITIAL_MESH_SSBO_SLOT_COUNT,
256
+ type MeshSsboBufferWrapper,
257
+ type MeshSsboGrowController,
258
+ type MeshSsboGrowControllerInit,
259
+ type MeshSsboGrowDevice,
260
+ type MeshSsboGrowErrorRegistry,
261
+ type MeshSsboGrowResult,
262
+ type MeshSsboState,
265
263
  requireMeshSsboBuffer,
266
264
  } from './mesh-ssbo-grow';
267
265
  export { exposeRenderer } from './renderer-facade';
268
266
  export {
267
+ selectGpuDrivenSceneIndexVariant,
269
268
  selectHdrpPbrPrewarmVariants,
270
269
  selectProbePrewarmVariants,
271
270
  selectSkinPrewarmVariants,
272
271
  selectStandardPbrTransmissionPrewarmVariants,
273
272
  } from './shader-prewarm-policy';
273
+ export type { WebGPUOutcome, WebGPURendererInternals } from './webgpu-renderer-contract';
274
+ export { disposeObservationCaptureSet } from './webgpu-renderer-observation';
274
275
 
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
- */
276
+ /** Bundler injection and the WebGPU renderer construction entry points. */
362
277
  let nextDerivedSystemId = 0;
363
-
364
278
  export async function tryCreateWebGPURenderer(
365
279
  canvas: HTMLCanvasElement | OffscreenCanvas,
366
280
  options: RendererOptions | undefined,
367
281
  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
282
  bundler: BundlerOptions | undefined,
373
283
  ): 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
- };
478
- }
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;
284
+ return tryCreateWebGPURendererBootstrap(canvas, options, pack, bundler, makeWebGPURenderer);
555
285
  }
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
286
  async function makeWebGPURenderer(
609
287
  internals: WebGPURendererInternals,
610
288
  ): Promise<RendererAssemblyImplementation> {
611
289
  let disposed = false;
290
+ const observeTiming = observeGpuPassTimingDisabled;
612
291
  const frameContinuations = new Set<ContinuationTerminator>();
613
292
  type RecoveryInspection = RenderInspection['recovery'];
614
293
  let recoveryAttempt = 0;
@@ -893,27 +572,16 @@ async function makeWebGPURenderer(
893
572
  // looks up MeshFilter.assetHandle here per frame; buildReadyWebGPU step 3
894
573
  // uploads the registry's builtin mesh geometry to GPU buffers so the
895
574
  // RenderSystem can route MeshFilter.assetHandle to a real (vbo, ibo) pair.
896
- //
897
575
  // feat-20260527 M1 / w1: ShaderCatalog is now eager-constructed and
898
576
  // constructor-injected into AssetRegistry (plan-strategy D-1). The
899
577
  // ShaderCatalogDevice adapter (shared between the catalog and per-MaterialShader
900
578
  // pipeline cache) is already available at this point.
901
579
  const shaderCatalog = getShader();
902
-
903
- // feat-20260528-material-shader-registration-unification M3 / w14:
904
- // placeholder hardcoded installMaterialArtifact calls deleted.
905
- //
906
580
  // bug-20260601-hello-tonemap-material-register M1 (plan-strategy D-1):
907
581
  // All material-shader entries are now registered from the manifest
908
582
  // BEFORE the renderer is returned, so `register<MaterialAsset>` referencing
909
583
  // an engine shader succeeds without waiting for `renderer.initialization`.
910
-
911
- // feat-20260623-asset-payload-generic-open-registry M3 / w10: host apps
912
- // that need custom loaders register them on `assets.loaders.register(...)`.
913
- // feat-20260604-hdr-equirect-cube-importer-loader M4 / w16 (D-3 / AC-05):
914
- // the host-injected ImportTransport (or undefined for the shipped form) is
915
- // threaded into the AssetRegistry ctor -- the construction-time-
916
- // only single injection point (no setter, no illegal intermediate state).
584
+ // Host-injected ImportTransport remains a constructor-only dependency.
917
585
  // feat-20260705-runtime-tier2-decomposition M1 / w10 (D-1 / D-2): the sole
918
586
  // production assembly point injects (a) the post-spawn hook
919
587
  // `postSpawnResolveJoints` (auto-wire Skin.joints; D-1) and the audio
@@ -945,8 +613,7 @@ async function makeWebGPURenderer(
945
613
  renderSystemForTargetPromotion?.isCubeCapturePending(target) !== true,
946
614
  });
947
615
  // 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
616
+ // EngineMetrics counter reaches the record stage via `RenderSystemRuntime.metrics`.
950
617
  // (`nineslice.scale-too-small`, `nineslice.tile-needs-repeat-sampler`) bump
951
618
  // counters through the owning service (charter P3 machine-readable signals
952
619
  // over a per-frame console.warn flood). Each renderer assembly owns its own
@@ -1016,14 +683,12 @@ async function makeWebGPURenderer(
1016
683
  // feat-20260707 M5 / w33 (D-11 + D-8): project the RhiCaps three-way
1017
684
  // compression triple into the codec-facing `TranscodeCaps` and wire it into
1018
685
  // 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).
686
+ // target. One-line projection; the loader stays a pure consumer of declared ctx input (Pipeline Isolation).
1021
687
  assets.setTranscodeCaps({
1022
688
  bc: internals.device.caps.textureCompressionBc,
1023
689
  etc2: internals.device.caps.textureCompressionEtc2,
1024
690
  astc: internals.device.caps.textureCompressionAstc,
1025
691
  });
1026
-
1027
692
  // M1 (bug-20260601-hello-tonemap-material-register D-1/D-2): prepare
1028
693
  // engine-shipped material shaders (cap gate + manifest load + registration)
1029
694
  // so they are available in ShaderCatalog before `register<MaterialAsset>`.
@@ -1039,25 +704,6 @@ async function makeWebGPURenderer(
1039
704
  // count is asserted explicitly. Missing mesh sets are clamp-to-last aliases.
1040
705
  materialShaderUvSetCounts.set('forgeax::default-standard-pbr', 8);
1041
706
  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
707
  const packShaderFactory = adaptMipmapShaderModuleFactory(internals.pack.createShaderModule);
1062
708
  // The RhiDevice surface satisfies `MipmapBlitDevice` (createTexture +
1063
709
  // createTextureView + createCommandEncoder + createBindGroup +
@@ -1128,28 +774,6 @@ async function makeWebGPURenderer(
1128
774
  gpuStore.bindDeviceScope(activeDeviceScope);
1129
775
  };
1130
776
  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
777
  const materialShaderPipelineCache = new Map<string, RenderPipeline>();
1154
778
  const materialShaderPipelineGroup2Contracts = new WeakMap<object, PipelineGroup2Contract>();
1155
779
  const materialShaderManifestEntryCache = new Map<string, MaterialShaderManifestEntry>();
@@ -1172,14 +796,7 @@ async function makeWebGPURenderer(
1172
796
  { materialBgl: BindGroupLayout; pipelineLayout: PipelineLayout }
1173
797
  >();
1174
798
  const preparedMaterialPipelineLayoutCache = new Map<string, PipelineLayout>();
1175
- // feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): per-shader material
1176
- // BGL + pipeline layout cache. Every authored material shader owns the
1177
- // user-region binding order emitted by its paramSchema, plus the shared
1178
- // engine-injection suffix. A custom shader with four-or-fewer textures can
1179
- // still differ from the standard PBR order (for example, baseColorTexture
1180
- // followed directly by normalTexture), so it cannot safely reuse the
1181
- // standard material BGL. Built lazily on first request and cached by
1182
- // shaderId; built-in shader IDs continue to use the boot-time shared layout.
799
+ // Cache authored material layouts by their exact mesh and lighting contract.
1183
800
  type PerShaderMaterialLayout = {
1184
801
  materialBgl: BindGroupLayout;
1185
802
  pipelineLayout: PipelineLayout;
@@ -1191,16 +808,6 @@ async function makeWebGPURenderer(
1191
808
  readonly layout: PerShaderMaterialLayout | null;
1192
809
  };
1193
810
  const perShaderMaterialLayoutCache = new Map<string, PerShaderMaterialLayoutCacheEntry>();
1194
- const isEngineOwnedMaterialShader = (materialShaderId: string): boolean =>
1195
- materialShaderId === 'forgeax::default-unlit' ||
1196
- materialShaderId === 'forgeax::default-shadow-caster' ||
1197
- materialShaderId === 'forgeax::sprite' ||
1198
- materialShaderId === 'forgeax::sprite-lit' ||
1199
- materialShaderId === 'forgeax::default-sprite' ||
1200
- materialShaderId === 'forgeax::msdf-text' ||
1201
- materialShaderId === 'forgeax::default-standard-pbr' ||
1202
- materialShaderId === 'forgeax::pbr-skin' ||
1203
- materialShaderId === 'forgeax::default-standard-pbr-skin';
1204
811
  // Material binding contracts are derived from WGSL source, but the lookup
1205
812
  // is also used by the per-submesh bind-group path. Cache the derived value
1206
813
  // per renderer so a frame does not re-run the comment stripping and regex
@@ -1415,9 +1022,23 @@ async function makeWebGPURenderer(
1415
1022
  };
1416
1023
  const getOrBuildPreparedMaterialPipelineLayout = (
1417
1024
  materialShaderId: string,
1025
+ clustered = false,
1418
1026
  ): PipelineLayout | null => {
1419
1027
  const cacheState = currentPipelineCacheState();
1420
- const cached = cacheState.preparedMaterialPipelineLayoutCache.get(materialShaderId);
1028
+ const lookup = getShader().findMaterialArtifact(materialShaderId);
1029
+ const source = lookup.ok ? lookup.value.source : '';
1030
+ const hasView =
1031
+ /@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(
1032
+ source,
1033
+ );
1034
+ const hasSceneDepthAtZero =
1035
+ /@group\s*\(\s*0\s*\)\s*@binding\s*\(\s*0\s*\)[^;]*texture_depth_2d\b/u.test(source);
1036
+ const hasSceneDepthAtOne =
1037
+ /@group\s*\(\s*0\s*\)\s*@binding\s*\(\s*1\s*\)[^;]*texture_depth_2d\b/u.test(source);
1038
+ const sceneInputKind =
1039
+ hasSceneDepthAtOne && hasView ? 'view-depth' : hasSceneDepthAtZero ? 'depth' : 'view';
1040
+ const cacheKey = `${materialShaderId}:${clustered ? 'cluster' : 'mesh'}:${sceneInputKind}`;
1041
+ const cached = cacheState.preparedMaterialPipelineLayoutCache.get(cacheKey);
1421
1042
  if (cached !== undefined) return cached;
1422
1043
  const currentState = currentPipelineState();
1423
1044
  if (currentState === null) return null;
@@ -1426,31 +1047,90 @@ async function makeWebGPURenderer(
1426
1047
  ? undefined
1427
1048
  : getOrBuildPerShaderMaterialLayout(materialShaderId)?.materialBgl) ??
1428
1049
  currentState.materialBindGroupLayout;
1050
+ let sceneInputLayout: BindGroupLayout = currentState.viewBindGroupLayout;
1051
+ if (sceneInputKind !== 'view') {
1052
+ const sceneInputLayoutResult = currentBuildDevice().createBindGroupLayout({
1053
+ label: `prepared-material-${sceneInputKind}-${materialShaderId}`,
1054
+ entries:
1055
+ sceneInputKind === 'view-depth'
1056
+ ? [
1057
+ {
1058
+ binding: 0,
1059
+ visibility: GPU_SHADER_STAGE_VERTEX | GPU_SHADER_STAGE_FRAGMENT,
1060
+ buffer: { type: 'uniform' as const },
1061
+ },
1062
+ {
1063
+ binding: 1,
1064
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
1065
+ texture: {
1066
+ sampleType: 'depth' as const,
1067
+ viewDimension: '2d' as const,
1068
+ multisampled: false,
1069
+ },
1070
+ },
1071
+ ]
1072
+ : [
1073
+ {
1074
+ binding: 0,
1075
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
1076
+ texture: {
1077
+ sampleType: 'depth' as const,
1078
+ viewDimension: '2d' as const,
1079
+ multisampled: false,
1080
+ },
1081
+ },
1082
+ ],
1083
+ });
1084
+ if (!sceneInputLayoutResult.ok) {
1085
+ internals.errorRegistry.fire(sceneInputLayoutResult.error);
1086
+ return null;
1087
+ }
1088
+ sceneInputLayout = sceneInputLayoutResult.value;
1089
+ }
1090
+ const usesMaterialGroup = clustered || /@group\s*\(\s*1\s*\)/u.test(source);
1091
+ const lightingLayout = clustered
1092
+ ? currentBuildDevice().createBindGroupLayout({
1093
+ label: `prepared-material-lighting-${materialShaderId}`,
1094
+ entries: createHdrpBindGroupLayoutDescriptor().entries.filter(
1095
+ (entry) => entry.binding >= 3,
1096
+ ),
1097
+ })
1098
+ : undefined;
1099
+ if (lightingLayout !== undefined && !lightingLayout.ok) {
1100
+ internals.errorRegistry.fire(lightingLayout.error);
1101
+ return null;
1102
+ }
1429
1103
  const result = currentBuildDevice().createPipelineLayout({
1430
1104
  label: `prepared-material-pl-${materialShaderId}`,
1431
- bindGroupLayouts: [currentState.viewBindGroupLayout, materialBgl],
1105
+ bindGroupLayouts: [
1106
+ sceneInputLayout,
1107
+ ...(usesMaterialGroup ? [materialBgl] : []),
1108
+ ...(lightingLayout?.ok ? [lightingLayout.value] : []),
1109
+ ],
1432
1110
  });
1433
1111
  if (!result.ok) {
1434
1112
  internals.errorRegistry.fire(result.error);
1435
1113
  return null;
1436
1114
  }
1437
- cacheState.preparedMaterialPipelineLayoutCache.set(materialShaderId, result.value);
1115
+ cacheState.preparedMaterialPipelineLayoutCache.set(cacheKey, result.value);
1438
1116
  return result.value;
1439
1117
  };
1440
1118
  const getOrBuildPerShaderMaterialLayout = (
1441
1119
  materialShaderId: string,
1442
1120
  explicitParamSchema?: readonly ParamSchemaEntry[],
1443
1121
  layoutKind: LayoutKind = 'pbr',
1122
+ clustered = false,
1444
1123
  ): PerShaderMaterialLayout | null => {
1445
1124
  const cacheState = currentPipelineCacheState();
1446
1125
  const currentState = currentPipelineState();
1447
1126
  if (currentState === null) return null;
1448
- if (isEngineOwnedMaterialShader(materialShaderId)) return null;
1127
+ if (isEngineOwnedMaterialShader(materialShaderId) && !clustered) return null;
1449
1128
  const lookup = getShader().findMaterialArtifact(materialShaderId);
1450
1129
  const source = lookup.ok ? lookup.value.source : '';
1451
1130
  const paramSchema = lookup.ok ? lookup.value.paramSchema : explicitParamSchema;
1452
1131
  if (paramSchema === undefined) return null;
1453
- const cached = cacheState.perShaderMaterialLayoutCache.get(materialShaderId);
1132
+ const cacheKey = `${materialShaderId}:${layoutKind}:${clustered}`;
1133
+ const cached = cacheState.perShaderMaterialLayoutCache.get(cacheKey);
1454
1134
  if (
1455
1135
  cached?.source === source &&
1456
1136
  cached.paramSchema === paramSchema &&
@@ -1466,8 +1146,8 @@ async function makeWebGPURenderer(
1466
1146
  isStandardPbrMaterialShader(materialShaderId) ||
1467
1147
  standardPhysicalTextureFields(paramSchema).length > 0;
1468
1148
  const isStandardMapLayout = requiresStandardMapLayout;
1469
- if (!isStandardMapLayout && isSharedMaterialUserRegionCompatible(paramSchema)) {
1470
- cacheState.perShaderMaterialLayoutCache.set(materialShaderId, {
1149
+ if (!clustered && !isStandardMapLayout && isSharedMaterialUserRegionCompatible(paramSchema)) {
1150
+ cacheState.perShaderMaterialLayoutCache.set(cacheKey, {
1471
1151
  source,
1472
1152
  paramSchema,
1473
1153
  layoutKind,
@@ -1484,26 +1164,27 @@ async function makeWebGPURenderer(
1484
1164
  const desc = buildBindGroupLayoutDescriptor(spec, {
1485
1165
  kind: 'pbr-material-merged',
1486
1166
  materialParamSchema: paramSchema,
1167
+ caps: {
1168
+ storageBuffer: currentBuildDevice().caps.storageBuffer,
1169
+ transmissionBackdrop: transmissionBackdropAvailable(
1170
+ currentBuildDevice().limits.maxSampledTexturesPerShaderStage,
1171
+ ),
1172
+ },
1487
1173
  });
1488
1174
  const bglRes = currentBuildDevice().createBindGroupLayout(desc);
1489
1175
  if (!bglRes.ok) {
1490
1176
  internals.errorRegistry.fire(bglRes.error);
1491
1177
  return null;
1492
1178
  }
1493
- const meshLayout =
1494
- layoutKind === 'pbr-skin'
1495
- ? currentState.pbrSkinMeshBindGroupLayout
1496
- : layoutKind === 'hdrp-skin'
1497
- ? currentState.hdrpSkinMeshBindGroupLayout
1498
- : currentState.meshBindGroupLayout;
1499
- if (meshLayout === null) return null;
1179
+ const bindGroups = resolveMaterialPipelineBindGroups(layoutKind, currentState, clustered);
1180
+ if (bindGroups === null) return null;
1500
1181
  const plRes = currentBuildDevice().createPipelineLayout({
1501
1182
  label: `pbr-pl-${materialShaderId}`,
1502
1183
  bindGroupLayouts: [
1503
1184
  currentState.viewBindGroupLayout,
1504
1185
  bglRes.value,
1505
- meshLayout,
1506
- currentState.instancesBindGroupLayout,
1186
+ bindGroups.meshLayout,
1187
+ bindGroups.instancesLayout,
1507
1188
  ],
1508
1189
  });
1509
1190
  if (!plRes.ok) {
@@ -1511,7 +1192,7 @@ async function makeWebGPURenderer(
1511
1192
  return null;
1512
1193
  }
1513
1194
  const built = { materialBgl: bglRes.value, pipelineLayout: plRes.value };
1514
- cacheState.perShaderMaterialLayoutCache.set(materialShaderId, {
1195
+ cacheState.perShaderMaterialLayoutCache.set(cacheKey, {
1515
1196
  source,
1516
1197
  paramSchema,
1517
1198
  layoutKind,
@@ -1621,14 +1302,16 @@ async function makeWebGPURenderer(
1621
1302
  residencyStore,
1622
1303
  internals.pack.createShaderModule,
1623
1304
  internals.errorRegistry,
1624
- requiredMaterialShaders,
1305
+ candidateBuildState === undefined
1306
+ ? requiredMaterialShaders
1307
+ : [
1308
+ ...new Set([...requiredMaterialShaders, ...getShader().materialShaderIdentifiers()]),
1309
+ ].filter(
1310
+ (id) => requiredMaterialShaders.includes(id) || !isEngineOwnedMaterialShader(id),
1311
+ ),
1312
+ materialShaderUvSetCounts.get('forgeax::default-standard-pbr') ?? 0,
1625
1313
  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.
1314
+ // Publish growth and capacity together for the active or candidate device.
1632
1315
  (hook, state) => {
1633
1316
  if (candidateBuildState !== undefined) {
1634
1317
  candidateBuildState.growMeshSsbo = hook;
@@ -1638,14 +1321,12 @@ async function makeWebGPURenderer(
1638
1321
  internals.meshSsboState = state;
1639
1322
  }
1640
1323
  },
1641
- // feat-20260609 R3-fixup: seed the lazy adapter cache with the
1642
- // eagerly-compiled shadow_caster module.
1324
+ // Both module adapters consume the generation's eager shader cache.
1643
1325
  (label, module) => {
1644
1326
  getShaderModuleAdapter().seedModule(label, module);
1327
+ getImmediateShaderModuleAdapter().seedModule(label, module);
1645
1328
  },
1646
- // M6 fix-up: seed the active or candidate material pipeline cache from
1647
- // the boot-time SPEC_CONST prewarm. Recovery builds must never put a
1648
- // candidate-device PSO into the retired generation's cache.
1329
+ // Prewarm only the active build generation's material pipeline cache.
1649
1330
  (key, pso, group2Contract) => {
1650
1331
  const pipelineCache = currentPipelineCacheState().materialShaderPipelineCache;
1651
1332
  if (!pipelineCache.has(key)) {
@@ -1653,13 +1334,7 @@ async function makeWebGPURenderer(
1653
1334
  }
1654
1335
  materialShaderPipelineGroup2Contracts.set(pso as object, group2Contract);
1655
1336
  },
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).
1337
+ // Register the output transform after manifest loading, once RenderSystem exists.
1663
1338
  (source: string) => {
1664
1339
  if (candidateBuildState !== undefined || tonemapRegistered) return;
1665
1340
  renderSystem.registerBuiltinPostProcess(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, {
@@ -1699,7 +1374,18 @@ async function makeWebGPURenderer(
1699
1374
  (sources: VolumetricFogShaderSources) => {
1700
1375
  internals.volumetricFogShaders = sources;
1701
1376
  },
1702
- );
1377
+ (sources: SsrShaderSources) => {
1378
+ internals.ssrShaders = sources;
1379
+ },
1380
+ (sources: AtmosphereShaderSources) => {
1381
+ internals.atmosphereShaders = sources;
1382
+ },
1383
+ ).then(async (state) => {
1384
+ // draw() is synchronous. Complete the opt-in capability probe before
1385
+ // publishing readiness, including when rebuilding a lost device.
1386
+ await renderSystem.initializeSsr(device);
1387
+ return state;
1388
+ });
1703
1389
  const ready: Promise<Result<void, RhiError>> = buildPipeline().then(
1704
1390
  (state): Result<void, RhiError> => {
1705
1391
  pipelineState = state;
@@ -1710,14 +1396,8 @@ async function makeWebGPURenderer(
1710
1396
  (e: unknown): Result<void, RhiError> => {
1711
1397
  readySettled = true;
1712
1398
  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
- );
1399
+ if (e instanceof PipelineSpecError) return err(rendererInitializationPipelineError(e));
1400
+ return err(rendererInitializationError(e));
1721
1401
  },
1722
1402
  );
1723
1403
  /**
@@ -1760,33 +1440,17 @@ async function makeWebGPURenderer(
1760
1440
  }
1761
1441
  return standardEntry;
1762
1442
  };
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
1443
  const buildPipelineContext = (
1773
1444
  variantSet?: string,
1774
1445
  materialShaderId?: string,
1775
1446
  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).
1447
+ // Skin layouts derive their vertex ABI from the shared attribute projection.
1785
1448
  meshAttributes?: VertexAttributeMap,
1786
1449
  vertexLayout?: string,
1787
1450
  vertexLayoutProjection?: VertexLayoutProjection,
1788
1451
  shaderModuleMode?: RenderFeatureShaderModuleMode,
1789
1452
  layoutKindOverride?: LayoutKind,
1453
+ particleInputLanes?: number,
1790
1454
  ) => {
1791
1455
  const contractLayoutKind: LayoutKind =
1792
1456
  group2Contract === 'cluster'
@@ -1797,20 +1461,21 @@ async function makeWebGPURenderer(
1797
1461
  ? 'pbr-skin'
1798
1462
  : 'pbr';
1799
1463
  const layoutKind = layoutKindOverride ?? contractLayoutKind;
1464
+ const gpuClustered =
1465
+ (layoutKind === 'gpu-driven-pbr' || layoutKind === 'gpu-driven-skin') &&
1466
+ (group2Contract === 'cluster' || group2Contract === 'skin-cluster');
1800
1467
  const usesSharedBootMaterialLayout =
1801
- 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.
1468
+ !gpuClustered &&
1469
+ materialShaderId !== undefined &&
1470
+ isEngineOwnedMaterialShader(materialShaderId);
1471
+ // Built-in layouts are assembled at boot; authored layouts must not bypass
1472
+ // the HDRP group(2) selector or bind the URP mesh BGL to a cluster PSO.
1805
1473
  const perShaderLayout =
1806
1474
  !usesSharedBootMaterialLayout && materialShaderId !== undefined
1807
- ? getOrBuildPerShaderMaterialLayout(materialShaderId, undefined, layoutKind)
1475
+ ? getOrBuildPerShaderMaterialLayout(materialShaderId, undefined, layoutKind, gpuClustered)
1808
1476
  : 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.
1477
+ // Authored shaders retain their own material schema and selected mesh
1478
+ // layout, including skin. Built-in IDs keep their boot-time layout.
1814
1479
  const bindingContract =
1815
1480
  materialShaderId === undefined
1816
1481
  ? 'render-material'
@@ -1827,11 +1492,12 @@ async function makeWebGPURenderer(
1827
1492
  ? getOrBuildViewAndSceneDepthMaterialPipelineLayout()
1828
1493
  : null;
1829
1494
  const preparedMaterialLayout =
1830
- bindingContract === 'render-material' &&
1495
+ (bindingContract === 'render-material' ||
1496
+ bindingContract === 'render-material-with-scene-depth' ||
1497
+ bindingContract === 'render-material-and-scene-depth') &&
1831
1498
  materialShaderId !== undefined &&
1832
- (vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.billboardMaterialInstance ||
1833
- vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.meshGeometryMaterialInstance)
1834
- ? getOrBuildPreparedMaterialPipelineLayout(materialShaderId)
1499
+ isPreparedMaterialVertexLayout(vertexLayout)
1500
+ ? getOrBuildPreparedMaterialPipelineLayout(materialShaderId, group2Contract === 'cluster')
1835
1501
  : null;
1836
1502
  const materialShaderLookup =
1837
1503
  materialShaderId === undefined
@@ -1873,64 +1539,15 @@ async function makeWebGPURenderer(
1873
1539
  )
1874
1540
  : undefined))
1875
1541
  : 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[]);
1542
+ const vertexBuffers = resolveWebGPUVertexBufferLayouts({
1543
+ vertexInputContract,
1544
+ vertexLayout,
1545
+ vertexLayoutProjection,
1546
+ resolvedUvSetCount,
1547
+ layoutKind,
1548
+ meshAttributes,
1549
+ particleInputLanes,
1550
+ });
1934
1551
  return {
1935
1552
  device: currentBuildDevice(),
1936
1553
  shaderModuleFactory:
@@ -1939,7 +1556,7 @@ async function makeWebGPURenderer(
1939
1556
  : getShaderModuleAdapter(),
1940
1557
  pipelineLayout,
1941
1558
  vertexBuffers,
1942
- ...(layoutKind === 'pbr-skin' ? { layoutKind } : {}),
1559
+ ...(layoutKind === 'pbr-skin' || layoutKind === 'gpu-driven-skin' ? { layoutKind } : {}),
1943
1560
  };
1944
1561
  };
1945
1562
  /**
@@ -1999,11 +1616,26 @@ async function makeWebGPURenderer(
1999
1616
  vertexLayoutProjection?: VertexLayoutProjection,
2000
1617
  shaderModuleMode?: RenderFeatureShaderModuleMode,
2001
1618
  layoutKindOverride?: LayoutKind,
1619
+ vertexEntryPoint?: string,
2002
1620
  additionalColorFormats?: readonly GPUTextureFormat[],
2003
1621
  vertexEntry?: string,
2004
1622
  authoredFragmentEntry?: string,
1623
+ particleInputLanes?: number,
1624
+ coverageOnly = false,
2005
1625
  ): RenderPipeline | null => {
2006
- const group2Contract = resolvePipelineGroup2Contract(entry.source);
1626
+ const declaredGroup2Contract = resolvePipelineGroup2Contract(entry.source);
1627
+ const group2Contract =
1628
+ declaredGroup2Contract === 'mesh' &&
1629
+ (materialShaderId === 'forgeax::vfx-render.particles.mesh' ||
1630
+ materialShaderId === 'forgeax::vfx-render.particles.mesh-inputs') &&
1631
+ (variantSet === '' || variantSet?.includes('CLUSTER_FORWARD_AVAILABLE=true') === true)
1632
+ ? 'cluster'
1633
+ : declaredGroup2Contract;
1634
+ const resolvedVertexEntry = resolveMaterialPipelineVertexEntry(
1635
+ layoutKindOverride,
1636
+ vertexEntryPoint,
1637
+ vertexEntry,
1638
+ );
2007
1639
  const ctx = buildPipelineContext(
2008
1640
  variantSet,
2009
1641
  materialShaderId,
@@ -2013,13 +1645,9 @@ async function makeWebGPURenderer(
2013
1645
  vertexLayoutProjection,
2014
1646
  shaderModuleMode,
2015
1647
  layoutKindOverride,
1648
+ particleInputLanes,
2016
1649
  );
2017
1650
  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
1651
  const currentState = currentPipelineState();
2024
1652
  if (currentState === null) return null;
2025
1653
  const ldrColorFormat = colorFormatOverride ?? currentState.colorAttachmentFormat;
@@ -2042,12 +1670,14 @@ async function makeWebGPURenderer(
2042
1670
  ? undefined
2043
1671
  : [isHdr ? HDR_COLOR_ATTACHMENT_FORMAT : ldrColorFormat, ...additionalColorFormats],
2044
1672
  depthFormat:
2045
- depthFormatOverride === null ? undefined : (depthFormatOverride ?? DEPTH_TEXTURE_FORMAT),
1673
+ depthFormatOverride === null
1674
+ ? undefined
1675
+ : passKindPolicyTable[passKind]?.shape === 'depth-only'
1676
+ ? (depthFormatOverride ?? 'depth32float')
1677
+ : (depthFormatOverride ?? DEPTH_TEXTURE_FORMAT),
1678
+ coverageOnly,
2046
1679
  label,
2047
1680
  // 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
1681
  // per variant. See PipelineBuilderContext.moduleLabel.
2052
1682
  moduleLabel,
2053
1683
  },
@@ -2066,7 +1696,7 @@ async function makeWebGPURenderer(
2066
1696
  : {}),
2067
1697
  }
2068
1698
  : undefined,
2069
- vertexEntry,
1699
+ resolvedVertexEntry,
2070
1700
  fragmentEntry,
2071
1701
  undefined, // defines — none
2072
1702
  passKind,
@@ -2079,10 +1709,7 @@ async function makeWebGPURenderer(
2079
1709
  return null;
2080
1710
  }
2081
1711
  currentPipelineCacheState().materialShaderPipelineCache.set(cacheKey, built.value);
2082
- materialShaderPipelineGroup2Contracts.set(
2083
- built.value as object,
2084
- resolvePipelineGroup2Contract(entry.source),
2085
- );
1712
+ materialShaderPipelineGroup2Contracts.set(built.value as object, group2Contract);
2086
1713
  return built.value;
2087
1714
  };
2088
1715
  const getMaterialShaderPipeline = (
@@ -2093,11 +1720,6 @@ async function makeWebGPURenderer(
2093
1720
  indexFormat?: 'uint16' | 'uint32',
2094
1721
  variantSet?: string,
2095
1722
  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
1723
  meshAttributes?: VertexAttributeMap,
2102
1724
  // bug-20260615 M2 / m2-1: sampleCount is threaded through to the cache key,
2103
1725
  // buildAndCachePipeline, and ultimately buildPipelineForMaterialShader which
@@ -2132,10 +1754,14 @@ async function makeWebGPURenderer(
2132
1754
  vertexLayoutProjection?: VertexLayoutProjection,
2133
1755
  shaderModuleMode: RenderFeatureShaderModuleMode = 'validated',
2134
1756
  layoutKindOverride?: LayoutKind,
1757
+ vertexEntryPoint?: string,
2135
1758
  additionalColorFormats?: readonly GPUTextureFormat[],
2136
1759
  vertexEntry?: string,
2137
1760
  fragmentEntry?: string,
1761
+ particleInputLanes?: number,
2138
1762
  ): RenderPipeline | null => {
1763
+ const coverageOnly =
1764
+ passKind === 'temporal' && variantSet?.split('+').includes('COVERAGE_ONLY=true') === true;
2139
1765
  // WebGL2 is a downlevel backend even when the record stage asks for the
2140
1766
  // native-capability variant string. Resolve the variant axes at this
2141
1767
  // shared PSO seam so the shader source, BGL layout, and cache key agree on
@@ -2148,10 +1774,11 @@ async function makeWebGPURenderer(
2148
1774
  (currentState !== null ? currentState.colorAttachmentFormat : 'bgra8unorm-srgb');
2149
1775
  // feat-20260629 M4: auto-fill shaderUvSetCount from naga reflection
2150
1776
  // (stored in materialShaderUvSetCounts during prepareMaterialShaders).
1777
+ const materialShaderIdForPass = resolveMaterialPipelineShaderId(materialShaderId, passKind);
2151
1778
  const pipelineShaderId = resolveMaterialShaderBackendArtifactKey(
2152
- materialShaderId,
1779
+ materialShaderIdForPass,
2153
1780
  internals.device.caps.backendKind,
2154
- assets.getMaterialArtifact(materialShaderId),
1781
+ assets.getMaterialArtifact(materialShaderIdForPass),
2155
1782
  );
2156
1783
  const materialShaderLookup = getShader().findMaterialArtifact(pipelineShaderId);
2157
1784
  const materialShaderSource = materialShaderLookup.ok
@@ -2175,20 +1802,14 @@ async function makeWebGPURenderer(
2175
1802
  (pipelineShaderId === 'forgeax::sprite' || pipelineShaderId === 'forgeax::sprite-lit')
2176
1803
  ? undefined
2177
1804
  : 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
1805
+ // The record stage composes capability axes; plain custom shaders retain
1806
+ // only manifest-declared axes so '' cannot select an HDRP mesh layout.
1807
+ // Builtin forgeax:: shaders keep the compatibility request
2183
1808
  // when a reduced test/legacy manifest omits variant metadata.
2184
1809
  // A single-source manifest entry must stay on its canonical module key;
2185
1810
  // otherwise a synthesized capability axis races the prewarmed module.
2186
1811
  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.
1812
+ // A published artifact is already backend-specific; no manifest means no synthesized key.
2192
1813
  if (manifestEntry === undefined) effectiveVariantSet = undefined;
2193
1814
  if (
2194
1815
  vertexLayoutProjection === undefined &&
@@ -2227,25 +1848,16 @@ async function makeWebGPURenderer(
2227
1848
  effectiveVariantSet =
2228
1849
  filteredVariantParts.length === 0 ? undefined : filteredVariantParts.join('+');
2229
1850
  }
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
1851
  const colorFormat: GPUTextureFormat = isHdr ? 'rgba16float' : ldrColorFormat;
2242
1852
  const isDepthOnlyPass = passKindPolicyTable[passKind]?.shape === 'depth-only';
1853
+ const resolvedVertexEntry = vertexEntry ?? vertexEntryPoint;
1854
+ const inputVertexBuffers = particleMaterialInputVertexBuffers(vertexLayout, particleInputLanes);
2243
1855
  const spec: PipelineSpec = {
2244
1856
  shader: {
2245
1857
  id: pipelineShaderId,
2246
1858
  passKind,
2247
1859
  variantSet: effectiveVariantSet,
2248
- ...(vertexEntry === undefined ? {} : { vertexEntry }),
1860
+ ...(resolvedVertexEntry === undefined ? {} : { vertexEntry: resolvedVertexEntry }),
2249
1861
  ...(fragmentEntry === undefined ? {} : { fragmentEntry }),
2250
1862
  },
2251
1863
  attachments: {
@@ -2254,7 +1866,7 @@ async function makeWebGPURenderer(
2254
1866
  ? colorFormatsForPassKind(passKind, colorFormat)
2255
1867
  : [colorFormat, ...additionalColorFormats],
2256
1868
  depthFormat: isDepthOnlyPass
2257
- ? 'depth32float'
1869
+ ? (depthFormatOverride ?? 'depth32float')
2258
1870
  : depthFormatOverride === null
2259
1871
  ? undefined
2260
1872
  : (depthFormatOverride ?? 'depth24plus-stencil8'),
@@ -2267,11 +1879,9 @@ async function makeWebGPURenderer(
2267
1879
  meshAttributes ??
2268
1880
  (vertexLayout === RENDER_FEATURE_VERTEX_LAYOUTS.positionSizeColorInstance
2269
1881
  ? 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
1882
+ : isPreparedMaterialVertexLayout(vertexLayout)
2273
1883
  ? PREPARED_MATERIAL_INSTANCE_VERTEX_ATTRS
2274
- : DEFAULT_VERTEX_ATTRS),
1884
+ : DEFAULT_VERTEX_ATTRIBUTE_MAP),
2275
1885
  // Thread the shader-declared count whenever it is greater than one.
2276
1886
  // Built-in PBR reserves all eight supported UV inputs; single-UV meshes
2277
1887
  // remain byte-stable because missing sets are clamp-to-last aliases.
@@ -2279,10 +1889,11 @@ async function makeWebGPURenderer(
2279
1889
  ? { shaderUvSetCount: resolvedUvSetCount }
2280
1890
  : {}),
2281
1891
  ...(vertexLayoutProjection === undefined ? {} : { vertexLayoutProjection }),
1892
+ ...(inputVertexBuffers === undefined ? {} : { vertexBuffers: inputVertexBuffers }),
2282
1893
  },
2283
1894
  renderState,
2284
1895
  };
2285
- const cacheKey = cacheKeyOf(spec);
1896
+ const cacheKey = `${cacheKeyOf(spec)}|vertex:${resolvedVertexEntry ?? 'vs_main'}`;
2286
1897
  const cached = currentPipelineCacheState().materialShaderPipelineCache.get(cacheKey);
2287
1898
  if (cached !== undefined) return cached;
2288
1899
  if (pipelineState === null) return null;
@@ -2336,7 +1947,12 @@ async function makeWebGPURenderer(
2336
1947
  vertexLayoutProjection,
2337
1948
  shaderModuleMode,
2338
1949
  layoutKindOverride,
1950
+ vertexEntryPoint,
2339
1951
  additionalColorFormats,
1952
+ undefined,
1953
+ undefined,
1954
+ undefined,
1955
+ coverageOnly,
2340
1956
  );
2341
1957
  }
2342
1958
  const pbrEntry = findStandardPbrEntry(effectiveVariantSet);
@@ -2392,7 +2008,12 @@ async function makeWebGPURenderer(
2392
2008
  vertexLayoutProjection,
2393
2009
  shaderModuleMode,
2394
2010
  layoutKindOverride,
2011
+ vertexEntryPoint,
2395
2012
  additionalColorFormats,
2013
+ undefined,
2014
+ undefined,
2015
+ undefined,
2016
+ coverageOnly,
2396
2017
  );
2397
2018
  }
2398
2019
  // feat-20260609 M4 / w31: resolve variant WGSL from manifest when
@@ -2485,9 +2106,12 @@ async function makeWebGPURenderer(
2485
2106
  vertexLayoutProjection,
2486
2107
  shaderModuleMode,
2487
2108
  layoutKindOverride,
2109
+ vertexEntryPoint,
2488
2110
  additionalColorFormats,
2489
2111
  vertexEntry,
2490
2112
  fragmentEntry,
2113
+ particleInputLanes,
2114
+ coverageOnly,
2491
2115
  );
2492
2116
  };
2493
2117
  const getMaterialShaderPipelineEntry = (
@@ -2549,7 +2173,7 @@ async function makeWebGPURenderer(
2549
2173
  const buildPostProcessPipeline = (
2550
2174
  entry: PostProcessShaderEntry,
2551
2175
  bgl: BindGroupLayout,
2552
- colorFormat: GPUTextureFormat,
2176
+ colorFormats: readonly GPUTextureFormat[],
2553
2177
  label: string,
2554
2178
  ): RenderPipeline | null => {
2555
2179
  const moduleFactory = getShaderModuleAdapter();
@@ -2597,12 +2221,12 @@ async function makeWebGPURenderer(
2597
2221
  },
2598
2222
  fragment: {
2599
2223
  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 }],
2224
+ // The typed graph reuses the composed tonemap module for separate
2225
+ // linear-LDR and final-encoding passes. Preserve the selected entry
2226
+ // point; forcing fs_main encodes the linear-LDR intermediate a second
2227
+ // time and breaks the Three.js stage contract.
2228
+ entryPoint: entry.fragmentEntryPoint ?? 'fs_main',
2229
+ targets: colorFormats.map((format) => ({ format })),
2606
2230
  },
2607
2231
  primitive: { topology: 'triangle-list', cullMode: 'none', frontFace: 'ccw' },
2608
2232
  depthStencil: undefined,
@@ -2623,6 +2247,7 @@ async function makeWebGPURenderer(
2623
2247
  return internals.bundler?.build;
2624
2248
  },
2625
2249
  ssrIdentity: internals.options?.ssrIdentity,
2250
+ captureReflectionFallbackReadback: internals.options?.captureReflectionFallbackReadback,
2626
2251
  get standardProfile() {
2627
2252
  return activeProfile;
2628
2253
  },
@@ -2633,10 +2258,7 @@ async function makeWebGPURenderer(
2633
2258
  setFeatureHost: (host) => {
2634
2259
  internals.featureHost = host;
2635
2260
  },
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.
2261
+ // Resolve the adapter at call time so recovered devices get fresh modules.
2640
2262
  shaderModuleFactory: {
2641
2263
  createShaderModule: (descriptor) => getShaderModuleAdapter().createShaderModule(descriptor),
2642
2264
  },
@@ -2645,11 +2267,8 @@ async function makeWebGPURenderer(
2645
2267
  getImmediateShaderModuleAdapter().createShaderModule(descriptor),
2646
2268
  },
2647
2269
  profiler: internals.options?.profiler,
2648
- // feat-20260622-s5 M3 / w17: device + context read live off `internals` via
2649
- // getters so the recover() rebuild (which swaps internals.device /
2650
- // internals.context for a freshly-acquired pair) is observed by the record
2651
- // stage without reconstructing the RenderSystem (RenderSystemRuntime.device
2652
- // / RenderSystemInternals.context are read at frame time, not cached).
2270
+ // Keep device/context getters live so recovery swaps are observed by the
2271
+ // record stage without reconstructing the RenderSystem.
2653
2272
  get device() {
2654
2273
  return internals.device;
2655
2274
  },
@@ -2666,6 +2285,28 @@ async function makeWebGPURenderer(
2666
2285
  ...(internals.pack.instrumentation?.resolveSurfaceDevice === undefined
2667
2286
  ? {}
2668
2287
  : { resolveSurfaceDevice: internals.pack.instrumentation.resolveSurfaceDevice }),
2288
+ // The graph records receipt-bound color observations through the same
2289
+ // renderer-owned capture bridge consumed after queue submission. Keep the
2290
+ // identity and demand live on internals so recovery and per-frame graph
2291
+ // rebuilds observe the current device without reconstructing RenderSystem.
2292
+ get deviceGeneration() {
2293
+ return activeDeviceScope.generation;
2294
+ },
2295
+ get observationFrameId() {
2296
+ return internals.observationFrameId;
2297
+ },
2298
+ get observationGraphGeneration() {
2299
+ return internals.observationGraphGeneration;
2300
+ },
2301
+ set observationGraphGeneration(generation: number | undefined) {
2302
+ internals.observationGraphGeneration = generation;
2303
+ },
2304
+ get observationCaptureOwner() {
2305
+ return internals.observationCaptureOwner;
2306
+ },
2307
+ get observationCaptureDomains() {
2308
+ return internals.observationCaptureDomains;
2309
+ },
2669
2310
  invalidateShaderModule: (label: string) => {
2670
2311
  activeShaderState.sharedShaderModuleAdapter?.invalidateModule(label);
2671
2312
  },
@@ -2692,10 +2333,16 @@ async function makeWebGPURenderer(
2692
2333
  getMaterialShaderPipeline,
2693
2334
  getMaterialShaderPipelineEntry,
2694
2335
  getMaterialShaderBindingContract: getCachedMaterialShaderBindingContract,
2336
+ getMaterialShaderArtifact: (materialShaderId, options = {}) =>
2337
+ resolveRendererMaterialShaderArtifact(
2338
+ materialShaderId,
2339
+ getShader(),
2340
+ pipelineState?.gpuDrivenPbrPrograms,
2341
+ options,
2342
+ ),
2695
2343
  getParamSchema,
2696
2344
  getMaterialBindGroupLayout,
2697
2345
  metrics,
2698
- // feat-20260609 M4 / T-10-a: post-process pipeline factory (CONCERN-1 fix).
2699
2346
  // createRenderSystem wraps this in a per-RenderSystem cache + the public
2700
2347
  // getPostProcessPipeline lookup the dispatcher reads at frame time.
2701
2348
  buildPostProcessPipeline,
@@ -2786,6 +2433,13 @@ async function makeWebGPURenderer(
2786
2433
  get volumetricFogShaders() {
2787
2434
  return internals.volumetricFogShaders;
2788
2435
  },
2436
+ // Readiness-prewarmed SSR sources stay visible through the live internals seam.
2437
+ get ssrShaders() {
2438
+ return internals.ssrShaders;
2439
+ },
2440
+ get atmosphereShaders() {
2441
+ return internals.atmosphereShaders;
2442
+ },
2789
2443
  });
2790
2444
  attachGpuPassTimingSession(internals, gpuPassTimingSession);
2791
2445
  renderSystemForTargetPromotion = renderSystem;
@@ -2809,33 +2463,199 @@ async function makeWebGPURenderer(
2809
2463
  const attachedWorlds = new Set<World>();
2810
2464
  const attachedLeases = new Map<RenderReadLease, World>();
2811
2465
  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
- });
2466
+ const { lifecycle: dynamicGeometry, host: dynamicGeometryHost } =
2467
+ createRendererDynamicGeometryController({
2468
+ attachedWorlds,
2469
+ getGpuStore: () => gpuStore,
2470
+ currentGeneration: () => activeDeviceScope.generation,
2471
+ renderSystem,
2472
+ });
2826
2473
  const derivedSystemNames = new Map<World, string>();
2827
2474
  const transformReleases = new Map<World, () => void>();
2828
2475
  const attachmentOwner = {};
2829
2476
  let frameId = 0;
2830
- // Receipts remain observable until their device generation is retired. A
2831
- // registry avoids invalidating a still-valid frame merely because another
2832
- // submission completed in the meantime.
2833
- const currentReceipts = new Set<FrameReceipt>();
2477
+ // Caller-owned receipts remain observable; Renderer does not retain every submitted receipt.
2834
2478
  const issuedReceipts = new WeakSet<FrameReceipt>();
2835
2479
  const receiptTimings = new WeakMap<FrameReceipt, Promise<VolumeTimingObservation>>();
2836
2480
  let latestReceipt: FrameReceipt | undefined;
2481
+ let pendingObservationDomains: readonly FrameObservationDomain[] | undefined;
2482
+ let observationAllocationCount = 0;
2483
+ let observationLiveCount = 0;
2484
+ let observationPeakLiveCount = 0;
2485
+ let observationMapCount = 0;
2486
+ let observationReadbackCount = 0;
2487
+ let observationLiveByteLength = 0;
2488
+ const observationCaptureOwner = (() => {
2489
+ const capturesByFrame = new Map<number, TypedFrameObservationCapture[]>();
2490
+ return {
2491
+ register(capture: TypedFrameObservationCapture): void {
2492
+ const captures = capturesByFrame.get(capture.frameNumber) ?? [];
2493
+ captures.push(capture);
2494
+ capturesByFrame.set(capture.frameNumber, captures);
2495
+ observationAllocationCount += 1;
2496
+ observationLiveCount += 1;
2497
+ observationPeakLiveCount = Math.max(observationPeakLiveCount, observationLiveCount);
2498
+ observationLiveByteLength += capture.bytesPerRow * capture.height;
2499
+ },
2500
+ consume(frameNumber: number): readonly TypedFrameObservationCapture[] {
2501
+ const captures = capturesByFrame.get(frameNumber) ?? [];
2502
+ capturesByFrame.delete(frameNumber);
2503
+ return Object.freeze(captures);
2504
+ },
2505
+ drain(): readonly TypedFrameObservationCapture[] {
2506
+ const captures = [...capturesByFrame.values()].flat();
2507
+ capturesByFrame.clear();
2508
+ return Object.freeze(captures);
2509
+ },
2510
+ };
2511
+ })();
2512
+ Object.assign(internals, { observationCaptureOwner });
2513
+ let expectedObservationGraphGeneration: number | undefined;
2514
+ Object.defineProperty(internals, 'observationGraphGeneration', {
2515
+ configurable: true,
2516
+ get: () => expectedObservationGraphGeneration,
2517
+ set: (generation: number | undefined) => {
2518
+ expectedObservationGraphGeneration = generation;
2519
+ },
2520
+ });
2521
+ type ObservationCaptureRead = () => Promise<
2522
+ RenderResult<
2523
+ readonly {
2524
+ readonly capture: TypedFrameObservationCapture;
2525
+ readonly bytes: Uint8Array;
2526
+ }[],
2527
+ RenderError
2528
+ >
2529
+ >;
2530
+ const receiptObservationCaptures = new Map<FrameReceipt, ObservationCaptureRead>();
2531
+ const receiptObservationBuffers = new Map<
2532
+ FrameReceipt,
2533
+ readonly TypedFrameObservationCapture[]
2534
+ >();
2535
+ const destroyedObservationBuffers = new WeakSet<Buffer>();
2536
+ const observationBufferCleanupFailures = new WeakMap<Buffer, RendererContractFailureError>();
2537
+ const failedObservationCaptures = new Map<Buffer, TypedFrameObservationCapture>();
2538
+ const disposeObservationCaptures = (
2539
+ captures: readonly TypedFrameObservationCapture[],
2540
+ ): RendererContractFailureError | undefined => {
2541
+ const wasDestroyed = new Set(
2542
+ captures
2543
+ .filter((capture) => destroyedObservationBuffers.has(capture.buffer))
2544
+ .map((capture) => capture.buffer),
2545
+ );
2546
+ const result = disposeObservationCaptureSet(
2547
+ captures,
2548
+ destroyedObservationBuffers,
2549
+ observationBufferCleanupFailures,
2550
+ (failure) => internals.errorRegistry.fire(failure),
2551
+ );
2552
+ for (const capture of captures) {
2553
+ if (!wasDestroyed.has(capture.buffer) && destroyedObservationBuffers.has(capture.buffer)) {
2554
+ observationLiveCount = Math.max(0, observationLiveCount - 1);
2555
+ observationLiveByteLength = Math.max(
2556
+ 0,
2557
+ observationLiveByteLength - capture.bytesPerRow * capture.height,
2558
+ );
2559
+ }
2560
+ }
2561
+ return result;
2562
+ };
2563
+ const disposeOwnedObservationCaptures = (
2564
+ captures: readonly TypedFrameObservationCapture[],
2565
+ ): RendererContractFailureError | undefined => {
2566
+ const failure = disposeObservationCaptures(captures);
2567
+ if (failure !== undefined) {
2568
+ for (const capture of captures) {
2569
+ if (!destroyedObservationBuffers.has(capture.buffer)) {
2570
+ failedObservationCaptures.set(capture.buffer, capture);
2571
+ }
2572
+ }
2573
+ }
2574
+ return failure;
2575
+ };
2576
+ const readObservationCapture = async (
2577
+ capture: TypedFrameObservationCapture,
2578
+ ): Promise<RenderResult<Uint8Array, RenderError>> => {
2579
+ try {
2580
+ if (typeof capture.buffer.mapAsync !== 'function') {
2581
+ const cleanupFailure = disposeObservationCaptures([capture]);
2582
+ return err(
2583
+ cleanupFailure ??
2584
+ new RendererContractFailureError(
2585
+ 'observe',
2586
+ `the ${capture.domain} receipt-bound readback backend has no mapAsync capability`,
2587
+ ),
2588
+ );
2589
+ }
2590
+ observationMapCount += 1;
2591
+ const mapped = await capture.buffer.mapAsync(1);
2592
+ if (!mapped.ok) {
2593
+ const cleanupFailure = disposeObservationCaptures([capture]);
2594
+ return err(
2595
+ cleanupFailure ??
2596
+ new RendererContractFailureError(
2597
+ 'observe',
2598
+ `mapping ${capture.domain} receipt-bound readback failed: ${mapped.error.code}`,
2599
+ ),
2600
+ );
2601
+ }
2602
+ const range = mapped.value.getMappedRange();
2603
+ if (!range.ok) {
2604
+ mapped.value.unmap();
2605
+ const cleanupFailure = disposeObservationCaptures([capture]);
2606
+ return err(
2607
+ cleanupFailure ??
2608
+ new RendererContractFailureError(
2609
+ 'observe',
2610
+ `reading ${capture.domain} receipt-bound readback failed: ${range.error.code}`,
2611
+ ),
2612
+ );
2613
+ }
2614
+ const bytes = new Uint8Array(range.value.slice(0));
2615
+ mapped.value.unmap();
2616
+ observationReadbackCount += 1;
2617
+ const cleanupFailure = disposeObservationCaptures([capture]);
2618
+ if (cleanupFailure !== undefined) return err(cleanupFailure);
2619
+ return ok(bytes);
2620
+ } catch (cause) {
2621
+ const cleanupFailure = disposeObservationCaptures([capture]);
2622
+ return err(
2623
+ cleanupFailure ??
2624
+ new RendererContractFailureError(
2625
+ 'observe',
2626
+ `reading ${capture.domain} receipt-bound readback threw: ${
2627
+ cause instanceof Error ? cause.message : String(cause)
2628
+ }`,
2629
+ ),
2630
+ );
2631
+ }
2632
+ };
2633
+
2634
+ const disposeReceiptObservationCaptures = (): RendererContractFailureError | undefined => {
2635
+ let firstFailure: RendererContractFailureError | undefined;
2636
+ for (const [receipt, captures] of receiptObservationBuffers) {
2637
+ const failure = disposeObservationCaptures(captures);
2638
+ firstFailure ??= failure;
2639
+ if (captures.every((capture) => destroyedObservationBuffers.has(capture.buffer))) {
2640
+ receiptObservationBuffers.delete(receipt);
2641
+ }
2642
+ }
2643
+ receiptObservationCaptures.clear();
2644
+ const retryCaptures = [...failedObservationCaptures.values()];
2645
+ const retryFailure = disposeObservationCaptures(retryCaptures);
2646
+ firstFailure ??= retryFailure;
2647
+ for (const capture of retryCaptures) {
2648
+ if (destroyedObservationBuffers.has(capture.buffer)) {
2649
+ failedObservationCaptures.delete(capture.buffer);
2650
+ }
2651
+ }
2652
+ const ownedFailure = disposeOwnedObservationCaptures(observationCaptureOwner.drain());
2653
+ firstFailure ??= ownedFailure;
2654
+ return firstFailure;
2655
+ };
2837
2656
  let surfaceReleased = false;
2838
2657
  const renderer: RendererAssemblyImplementation = {
2658
+ instances: renderSystem.instances,
2839
2659
  attach(world: World): RenderResult<RenderWorldLease, RenderError> {
2840
2660
  const attached = this.attachScene(world);
2841
2661
  if (!attached.ok) {
@@ -2886,6 +2706,7 @@ async function makeWebGPURenderer(
2886
2706
  gpuDriven: renderSystem.gpuDrivenInspection,
2887
2707
  };
2888
2708
  },
2709
+ bounds: (world, entity) => (disposed ? undefined : renderSystem.bounds(world, entity)),
2889
2710
  inspect(): RenderInspection {
2890
2711
  const health = internals.healthRegistry.getLastSnapshot();
2891
2712
  const deviceLost = health.reason === 'device-lost';
@@ -2926,6 +2747,8 @@ async function makeWebGPURenderer(
2926
2747
  ? {}
2927
2748
  : { surfaceProfile: pipelineState.surfaceProfile }),
2928
2749
  rgba16floatRenderable: internals.device.caps.rgba16floatRenderable,
2750
+ autoExposure: renderSystem.autoExposure,
2751
+ standardLut: renderSystem.standardLut,
2929
2752
  ...(internals.context?.presentationProof === undefined
2930
2753
  ? {}
2931
2754
  : { presentationProof: internals.context.presentationProof }),
@@ -2943,16 +2766,18 @@ async function makeWebGPURenderer(
2943
2766
  frameId,
2944
2767
  deviceGeneration: activeDeviceScope.generation,
2945
2768
  }),
2946
- features: Object.freeze(
2947
- (internals.featureHost?.features ?? []).map((feature) => feature.identity),
2948
- ),
2949
- featureDiagnostics: internals.featureHost?.diagnostics() ?? Object.freeze([]),
2769
+ ...projectRendererFeatureInspection(internals.featureHost),
2770
+ featureGraph: renderSystem.featureGraphInspection,
2771
+ materialTextureSources: renderSystem.materialTextureSources,
2950
2772
  frustumStats: Object.freeze({ ...renderSystem.frustumStats }),
2951
2773
  visibilityStats: Object.freeze({ ...renderSystem.visibilityStats }),
2952
2774
  instanceCollections: Object.freeze(
2953
2775
  renderSystem.instanceCollectionsInspection.map((collection) =>
2954
2776
  Object.freeze({
2955
2777
  ...collection,
2778
+ dirtyRanges: Object.freeze(
2779
+ collection.dirtyRanges.map((range) => Object.freeze({ ...range })),
2780
+ ),
2956
2781
  uploadRanges: Object.freeze(
2957
2782
  collection.uploadRanges.map((range) => Object.freeze({ ...range })),
2958
2783
  ),
@@ -2969,8 +2794,10 @@ async function makeWebGPURenderer(
2969
2794
  ),
2970
2795
  dynamicGeometry: dynamicGeometry.inspect(),
2971
2796
  renderScene: renderSystem.renderScene,
2797
+ ...(renderSystem.iblBinding === undefined ? {} : { iblBinding: renderSystem.iblBinding }),
2972
2798
  reflectionProbes: renderSystem.reflectionProbes,
2973
2799
  ssrDependencies: renderSystem.ssrDependencies,
2800
+ ssr: renderSystem.ssr,
2974
2801
  volumetricFog,
2975
2802
  environment: renderSystem.environment,
2976
2803
  temporal: renderSystem.temporal,
@@ -3024,7 +2851,13 @@ async function makeWebGPURenderer(
3024
2851
  lastGeneration: latestReceipt?.deviceGeneration,
3025
2852
  }),
3026
2853
  }),
3027
- ...outputInspection,
2854
+ output: Object.freeze({
2855
+ ...outputInspection,
2856
+ graphPassNames: Object.freeze([...outputInspection.graphPassNames]),
2857
+ ...(outputInspection.standardOutputColor === undefined
2858
+ ? {}
2859
+ : { standardOutputColor: Object.freeze({ ...outputInspection.standardOutputColor }) }),
2860
+ }),
3028
2861
  observation: Object.freeze({
3029
2862
  observationId,
3030
2863
  frameId,
@@ -3039,6 +2872,14 @@ async function makeWebGPURenderer(
3039
2872
  ...(outputInspection.standardOutputColor === undefined
3040
2873
  ? {}
3041
2874
  : { standardOutputColor: Object.freeze({ ...outputInspection.standardOutputColor }) }),
2875
+ resourceStats: Object.freeze({
2876
+ allocationCount: observationAllocationCount,
2877
+ liveCount: observationLiveCount,
2878
+ peakLiveCount: observationPeakLiveCount,
2879
+ mapCount: observationMapCount,
2880
+ readbackCount: observationReadbackCount,
2881
+ liveByteLength: observationLiveByteLength,
2882
+ }),
3042
2883
  }),
3043
2884
  });
3044
2885
  },
@@ -3131,6 +2972,38 @@ async function makeWebGPURenderer(
3131
2972
  observeCurrentFrame(options: FrameObservationOptions) {
3132
2973
  return renderSystem.observeCurrentFrame(options);
3133
2974
  },
2975
+ requestObservation(
2976
+ domains: readonly FrameObservationDomain[],
2977
+ ): RenderResult<void, RenderError> {
2978
+ if (disposed) {
2979
+ return err(
2980
+ new RendererOperationError('renderer-state-invalid', {
2981
+ operation: 'request-observation',
2982
+ state: 'disposed',
2983
+ }),
2984
+ );
2985
+ }
2986
+ if (
2987
+ !Array.isArray(domains) ||
2988
+ domains.length === 0 ||
2989
+ domains.some(
2990
+ (domain) => domain !== 'linear-hdr' && domain !== 'linear-ldr' && domain !== 'final-srgb',
2991
+ ) ||
2992
+ new Set(domains).size !== domains.length
2993
+ ) {
2994
+ return err(
2995
+ new RendererOperationError('frame-input-invalid', {
2996
+ operation: 'request-observation',
2997
+ cause: new RendererContractFailureError(
2998
+ 'request-observation',
2999
+ 'domains must be a non-empty set of linear-hdr, linear-ldr, and final-srgb values',
3000
+ ),
3001
+ }),
3002
+ );
3003
+ }
3004
+ pendingObservationDomains = Object.freeze([...domains]);
3005
+ return ok(undefined);
3006
+ },
3134
3007
  getCurrentGraphTarget(name: string) {
3135
3008
  return renderSystem.getCurrentGraphTarget(name);
3136
3009
  },
@@ -3313,27 +3186,8 @@ async function makeWebGPURenderer(
3313
3186
  : worlds.map((world) => leasesByWorld.get(world)).every((lease) => lease !== undefined)
3314
3187
  ? worlds.map((world) => leasesByWorld.get(world) as RenderReadLease)
3315
3188
  : 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
- }
3189
+ const physicsFrameError = derivedPhysicsFrameError(worlds);
3190
+ if (physicsFrameError !== undefined) return err(physicsFrameError);
3337
3191
  if (frameRequest !== undefined && worlds.length !== frameRequest.leases.length) {
3338
3192
  return err(
3339
3193
  new RendererOperationError('world-lease-invalid', {
@@ -3369,6 +3223,9 @@ async function makeWebGPURenderer(
3369
3223
  ? {
3370
3224
  cameraOwner,
3371
3225
  resourceOwner,
3226
+ ...(frameRequest.camera.entityKey === undefined
3227
+ ? {}
3228
+ : { cameraEntityKey: frameRequest.camera.entityKey }),
3372
3229
  ...(frameRequest.profileFrame === undefined
3373
3230
  ? {}
3374
3231
  : { profileFrame: frameRequest.profileFrame }),
@@ -3486,6 +3343,9 @@ async function makeWebGPURenderer(
3486
3343
  gpuPassTimingCapture?: GpuPassTimingCapture | undefined;
3487
3344
  gpuPassTimingSubmittedWork?: Promise<void> | undefined;
3488
3345
  gpuPassTimingBeginReason?: GpuPassTimingReason | undefined;
3346
+ observationFrameId?: number | undefined;
3347
+ observationGraphGeneration?: number | undefined;
3348
+ observationCaptureDomains?: readonly FrameObservationDomain[] | undefined;
3489
3349
  };
3490
3350
  const timingRequested =
3491
3351
  isFrameRequest &&
@@ -3500,6 +3360,9 @@ async function makeWebGPURenderer(
3500
3360
  if (createdTiming.ok) volumeTimingCapture = createdTiming.value;
3501
3361
  }
3502
3362
  if (isFrameRequest) {
3363
+ timingHost.observationFrameId = frameId + 1;
3364
+ timingHost.observationGraphGeneration = undefined;
3365
+ timingHost.observationCaptureDomains = pendingObservationDomains;
3503
3366
  timingHost.gpuPassTimingFrameIdentity = {
3504
3367
  frameId: frameId + 1,
3505
3368
  deviceGeneration: activeDeviceScope.generation,
@@ -3507,9 +3370,13 @@ async function makeWebGPURenderer(
3507
3370
  };
3508
3371
  }
3509
3372
  const submitted = renderSystem.draw(worlds, drawOptions, readLeases, volumeTimingCapture);
3373
+ timingHost.observationFrameId = undefined;
3374
+ timingHost.observationCaptureDomains = undefined;
3510
3375
  timingHost.gpuPassTimingFrameIdentity = undefined;
3511
3376
  if (!submitted) {
3377
+ const cleanupFailure = disposeOwnedObservationCaptures(observationCaptureOwner.drain());
3512
3378
  volumeTimingCapture?.discard();
3379
+ if (cleanupFailure !== undefined) return err(cleanupFailure);
3513
3380
  return err(
3514
3381
  new RendererContractFailureError(
3515
3382
  'draw',
@@ -3524,7 +3391,9 @@ async function makeWebGPURenderer(
3524
3391
  gpuStore.trackMeshSubmission(queueCompletion);
3525
3392
  internals.pack.instrumentation?.onFrameBoundary?.();
3526
3393
  if (!isFrameRequest) {
3394
+ const cleanupFailure = disposeOwnedObservationCaptures(observationCaptureOwner.drain());
3527
3395
  renderTargetHost.onFrameSubmitted();
3396
+ if (cleanupFailure !== undefined) return err(cleanupFailure);
3528
3397
  return ok(undefined);
3529
3398
  }
3530
3399
  const receiptFrameId = ++frameId;
@@ -3599,13 +3468,39 @@ async function makeWebGPURenderer(
3599
3468
  ]).finally(() => {
3600
3469
  frameContinuations.delete(continuation);
3601
3470
  });
3471
+ const captures = observationCaptureOwner.consume(receiptFrameId);
3472
+ pendingObservationDomains = undefined;
3473
+ const receiptGraphGeneration = timingHost.observationGraphGeneration ?? 0;
3602
3474
  const receipt = Object.freeze({
3603
3475
  frameId: receiptFrameId,
3604
3476
  deviceGeneration: receiptGeneration,
3477
+ backendId: internals.device.caps.backendKind,
3478
+ graphGeneration: receiptGraphGeneration,
3605
3479
  completed,
3606
3480
  });
3607
3481
  renderTargetHost.onFrameSubmitted(completed);
3608
3482
  latestReceipt = receipt;
3483
+ if (captures.length > 0) {
3484
+ const captureRead: ObservationCaptureRead = async () => {
3485
+ const completion = await completed;
3486
+ if (!completion.ok) {
3487
+ const cleanupFailure = disposeObservationCaptures(captures);
3488
+ return cleanupFailure === undefined ? completion : err(cleanupFailure);
3489
+ }
3490
+ const read = [];
3491
+ for (const capture of captures) {
3492
+ const bytes = await readObservationCapture(capture);
3493
+ if (!bytes.ok) {
3494
+ const cleanupFailure = disposeObservationCaptures(captures);
3495
+ return cleanupFailure === undefined ? bytes : err(cleanupFailure);
3496
+ }
3497
+ read.push({ capture, bytes: bytes.value });
3498
+ }
3499
+ return ok(read);
3500
+ };
3501
+ receiptObservationBuffers.set(receipt, captures);
3502
+ receiptObservationCaptures.set(receipt, captureRead);
3503
+ }
3609
3504
  const passTimingCapture = timingHost.gpuPassTimingCapture;
3610
3505
  const timingBeginReason = timingHost.gpuPassTimingBeginReason;
3611
3506
  const timingSource =
@@ -3638,7 +3533,6 @@ async function makeWebGPURenderer(
3638
3533
  return { status: 'complete', frame };
3639
3534
  };
3640
3535
  timingObservationStore?.register(receipt, timingSource);
3641
- currentReceipts.add(receipt);
3642
3536
  if (timingRequested && gpuPassTimingOptions === undefined) {
3643
3537
  receiptTimings.set(
3644
3538
  receipt,
@@ -3653,6 +3547,11 @@ async function makeWebGPURenderer(
3653
3547
  dynamicGeometryHost.publishDynamicGeometry(receipt, worlds, frameRequest?.fixedStep);
3654
3548
  return ok(receipt);
3655
3549
  } catch (cause) {
3550
+ internals.observationFrameId = undefined;
3551
+ internals.observationCaptureDomains = undefined;
3552
+ internals.gpuPassTimingFrameIdentity = undefined;
3553
+ const cleanupFailure = disposeOwnedObservationCaptures(observationCaptureOwner.drain());
3554
+ if (cleanupFailure !== undefined) return err(cleanupFailure);
3656
3555
  const error =
3657
3556
  cause instanceof Error
3658
3557
  ? { code: 'unknown' as const, message: cause.message, name: cause.name }
@@ -3672,15 +3571,37 @@ async function makeWebGPURenderer(
3672
3571
  receipt: FrameReceipt,
3673
3572
  request: FrameObservationRequest,
3674
3573
  ): Promise<RenderResult<FrameReceiptObservation, RenderError>> {
3675
- if (gpuPassTimingOptions !== undefined) {
3574
+ const includesDomain = (request.include as readonly string[]).some(
3575
+ (include) =>
3576
+ include === 'linear-hdr' || include === 'linear-ldr' || include === 'final-srgb',
3577
+ );
3578
+ let timingObservation: GpuPassTimingObservation | undefined;
3579
+ if (
3580
+ includesDomain &&
3581
+ gpuPassTimingOptions !== undefined &&
3582
+ request.include.includes('timings')
3583
+ ) {
3584
+ const timingResult =
3585
+ timingObservationStore === undefined
3586
+ ? await observeGpuPassTimingDisabled(
3587
+ receipt,
3588
+ request,
3589
+ () => activeDeviceScope.generation,
3590
+ )
3591
+ : await timingObservationStore.observe(receipt, request);
3592
+ if (!timingResult.ok) return timingResult;
3593
+ timingObservation = timingResult.value.timings;
3594
+ }
3595
+ if (gpuPassTimingOptions !== undefined && !includesDomain) {
3676
3596
  return timingObservationStore === undefined
3677
- ? observeGpuPassTimingDisabled(receipt, request, () => activeDeviceScope.generation)
3597
+ ? observeTiming(receipt, request, () => activeDeviceScope.generation)
3678
3598
  : timingObservationStore.observe(receipt, request);
3679
3599
  }
3680
3600
  if (
3681
- !currentReceipts.has(receipt) ||
3601
+ disposed ||
3682
3602
  !issuedReceipts.has(receipt) ||
3683
- receipt.deviceGeneration !== activeDeviceScope.generation
3603
+ receipt.deviceGeneration !== activeDeviceScope.generation ||
3604
+ receipt.backendId !== internals.device.caps.backendKind
3684
3605
  ) {
3685
3606
  const currentGeneration = activeDeviceScope.generation;
3686
3607
  return err(
@@ -3703,6 +3624,97 @@ async function makeWebGPURenderer(
3703
3624
  }),
3704
3625
  );
3705
3626
  }
3627
+ const requestedDomains = (request.include as readonly string[]).filter(
3628
+ (include): include is TypedFrameObservationCapture['domain'] =>
3629
+ include === 'linear-hdr' || include === 'linear-ldr' || include === 'final-srgb',
3630
+ );
3631
+ let observations: FrameReceiptObservation['observations'];
3632
+ if (requestedDomains.length > 0) {
3633
+ if (new Set(requestedDomains).size !== requestedDomains.length) {
3634
+ return err(
3635
+ new RendererContractFailureError(
3636
+ 'observe',
3637
+ 'each receipt-bound color domain may be requested only once',
3638
+ ),
3639
+ );
3640
+ }
3641
+ const readCaptures = receiptObservationCaptures.get(receipt);
3642
+ receiptObservationCaptures.delete(receipt);
3643
+ if (readCaptures === undefined) {
3644
+ return err(
3645
+ new RendererContractFailureError(
3646
+ 'observe',
3647
+ 'receipt-bound color-domain observations were not explicitly armed before draw, are stale, duplicated, or unavailable',
3648
+ ),
3649
+ );
3650
+ }
3651
+ const capturesResult = await readCaptures();
3652
+ if (!capturesResult.ok) return capturesResult;
3653
+ const captures = capturesResult.value;
3654
+ const graphGenerations = new Set(captures.map(({ capture }) => capture.graphGeneration));
3655
+ if (
3656
+ captures.some(
3657
+ ({ capture }) =>
3658
+ capture.frameNumber !== receipt.frameId ||
3659
+ capture.deviceGeneration !== receipt.deviceGeneration ||
3660
+ capture.backendId !== internals.device.caps.backendKind ||
3661
+ (receipt.graphGeneration !== undefined &&
3662
+ capture.graphGeneration !== receipt.graphGeneration),
3663
+ ) ||
3664
+ graphGenerations.size !== 1
3665
+ ) {
3666
+ return err(
3667
+ new RendererContractFailureError(
3668
+ 'observe',
3669
+ 'receipt-bound color-domain captures crossed frame, device, backend, or graph identity',
3670
+ ),
3671
+ );
3672
+ }
3673
+ const byDomain = new Map(
3674
+ captures.map(({ capture, bytes }) => [capture.domain, { capture, bytes }]),
3675
+ );
3676
+ if (
3677
+ byDomain.size !== captures.length ||
3678
+ requestedDomains.some((domain) => !byDomain.has(domain))
3679
+ ) {
3680
+ return err(
3681
+ new RendererContractFailureError(
3682
+ 'observe',
3683
+ 'receipt-bound color-domain captures must contain one source per requested domain',
3684
+ ),
3685
+ );
3686
+ }
3687
+ const read: NonNullable<FrameReceiptObservation['observations']>[number][] = [];
3688
+ for (const domain of requestedDomains) {
3689
+ const resolved = byDomain.get(domain);
3690
+ if (resolved === undefined) continue;
3691
+ const { capture, bytes } = resolved;
3692
+ read.push({
3693
+ domain,
3694
+ bytes,
3695
+ metadata: {
3696
+ frameId: receipt.frameId,
3697
+ deviceGeneration: receipt.deviceGeneration,
3698
+ backendId: receipt.backendId,
3699
+ format: capture.format,
3700
+ graphGeneration: capture.graphGeneration,
3701
+ textureIdentity: getTextureIdentity(capture.texture),
3702
+ readbackIdentity: getTextureIdentity(
3703
+ capture.buffer as unknown as import('@forgeax/engine-rhi').Texture,
3704
+ ),
3705
+ width: capture.width,
3706
+ height: capture.height,
3707
+ bytesPerRow: capture.bytesPerRow,
3708
+ footprint: { resourceCount: 1, bindGroupCount: 0 },
3709
+ },
3710
+ });
3711
+ }
3712
+ observations = Object.freeze(read);
3713
+ const receiptCaptures = receiptObservationBuffers.get(receipt);
3714
+ if (receiptCaptures?.every((capture) => destroyedObservationBuffers.has(capture.buffer))) {
3715
+ receiptObservationBuffers.delete(receipt);
3716
+ }
3717
+ }
3706
3718
  await renderSystem.observeLodOcclusion();
3707
3719
  const targetReadbacks =
3708
3720
  request.targetReadbacks === undefined || request.targetReadbacks.length === 0
@@ -3716,15 +3728,17 @@ async function makeWebGPURenderer(
3716
3728
  reason: 'timestamp capture was not requested for this frame',
3717
3729
  }))
3718
3730
  : undefined;
3731
+ const observation = await observeTiming(receipt, request, () => activeDeviceScope.generation);
3732
+ if (!observation.ok) return observation;
3719
3733
  return ok(
3720
3734
  Object.freeze({
3721
- frameId: receipt.frameId,
3722
- deviceGeneration: receipt.deviceGeneration,
3723
- include: Object.freeze([...request.include]),
3735
+ ...observation.value,
3724
3736
  ...(targetReadbacks === undefined
3725
3737
  ? {}
3726
3738
  : { targetReadbacks: Object.freeze(targetReadbacks.value) }),
3739
+ ...(timingObservation === undefined ? {} : { timings: timingObservation }),
3727
3740
  ...(volumeTimings === undefined ? {} : { volumeTimings }),
3741
+ ...(observations === undefined ? {} : { observations }),
3728
3742
  }),
3729
3743
  );
3730
3744
  },
@@ -3783,31 +3797,24 @@ async function makeWebGPURenderer(
3783
3797
  * `disposed` latch flips up-front so a re-entrant dispose (or a draw
3784
3798
  * that races with the cascade) short-circuits.
3785
3799
  *
3786
- * NOT calling `device.destroy()` on the raw GPUDevice (w25 lesson from
3787
- * feat-20260517-vite-plugin-image-build-time-cook CI-fix v4): w23
3788
- * attempted to evict stale devices from the chromium WebGPU adapter
3789
- * pool via `_internal_getRawDevice`, but the explicit destroy
3790
- * accelerated pool poisoning -- after the first `dispose()` the
3791
- * chromium adapter pool started recycling the just-destroyed device
3792
- * into the next `requestDevice` so the NEXT test's `Engine.create`
3793
- * resolved with `ready.error.code: 'device-lost'`. Cross-test isolation
3794
- * moved to the vitest infra layer (`browser.isolate: true`) instead of
3795
- * test code self-managing device lifecycle.
3800
+ * Raw-device destruction is owned by backend lifetime, not this facade.
3801
+ * Browser test isolation handles adapter-pool reuse between tests.
3796
3802
  *
3797
3803
  * Stays in sync with the `Renderer.dispose` row of the README "API index".
3798
3804
  */
3799
3805
  dispose(): RenderResult<void, RenderError> {
3800
3806
  if (disposed) return ok(undefined);
3801
3807
  disposed = true;
3802
- for (const world of attachedWorlds) {
3803
- dynamicGeometryHost.invalidateDynamicGeometryWorld(world);
3804
- }
3808
+ for (const world of attachedWorlds) dynamicGeometryHost.invalidateDynamicGeometryWorld(world);
3805
3809
  dynamicGeometry.dispose();
3806
3810
  for (const continuation of frameContinuations) {
3807
3811
  continuation.terminate({ code: 'disposed' });
3808
3812
  }
3809
- currentReceipts.clear();
3810
3813
  const cleanupFailures: RendererOperationCause[] = [];
3814
+ const observationCleanupFailure = disposeReceiptObservationCaptures();
3815
+ if (observationCleanupFailure !== undefined) {
3816
+ cleanupFailures.push(observationCleanupFailure);
3817
+ }
3811
3818
  for (const world of attachedWorlds) {
3812
3819
  try {
3813
3820
  const derivedSystemName = derivedSystemNames.get(world);
@@ -3956,6 +3963,10 @@ async function makeWebGPURenderer(
3956
3963
  return internals.healthRegistry.getLastSnapshot();
3957
3964
  },
3958
3965
  recover(): Promise<Result<void, RecoverFailure>> {
3966
+ const observationCleanupFailure = disposeReceiptObservationCaptures();
3967
+ if (observationCleanupFailure !== undefined) {
3968
+ return Promise.resolve(err(new RecoverError('recover-device-unavailable')));
3969
+ }
3959
3970
  return recoveryFlight.run();
3960
3971
  },
3961
3972
  };
@@ -3973,7 +3984,6 @@ async function makeWebGPURenderer(
3973
3984
  candidatePostProcessResources: RecoveryPostProcessResources,
3974
3985
  recoveryRootBundle: RecoveryRootBundle,
3975
3986
  recoveryGraphCandidate: RecoveryGraphCandidate | undefined,
3976
- recoveryColdWorkGuard: RecoveryColdWorkGuard,
3977
3987
  ): void => {
3978
3988
  if (!candidate.scope.isAlive()) {
3979
3989
  throw new Error('Renderer generation candidate is not publishable.');
@@ -4020,9 +4030,6 @@ async function makeWebGPURenderer(
4020
4030
  // fallback pipeline or residency entry. Candidate preparation populated
4021
4031
  // these caches already; a miss here is a hard recovery failure, not an
4022
4032
  // 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
4033
  renderSystem.publishRecoveryPostProcessResources(candidatePostProcessResources);
4027
4034
  if (recoveryGraphCandidate !== undefined) {
4028
4035
  renderSystem.publishRecoveryGraphCandidate(recoveryGraphCandidate);
@@ -4069,6 +4076,13 @@ async function makeWebGPURenderer(
4069
4076
  buildPipeline,
4070
4077
  getMaterialShaderPipeline,
4071
4078
  getMaterialShaderPipelineEntry,
4079
+ getMaterialShaderArtifact: (materialShaderId, options = {}) =>
4080
+ resolveRendererMaterialShaderArtifact(
4081
+ materialShaderId,
4082
+ getShader(),
4083
+ pipelineState?.gpuDrivenPbrPrograms,
4084
+ options,
4085
+ ),
4072
4086
  getCachedMaterialShaderBindingContract,
4073
4087
  getParamSchema,
4074
4088
  getMaterialBindGroupLayout,