@forgeax/engine-render 0.1.27 → 0.1.29

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