@forgeax/engine-render 0.1.7 → 0.1.20

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 (777) hide show
  1. package/README.md +140 -2
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/assembly/backend-contract.d.ts +12 -0
  4. package/dist/assembly/backend-contract.d.ts.map +1 -1
  5. package/dist/assembly/factory.d.ts +26 -5
  6. package/dist/assembly/factory.d.ts.map +1 -1
  7. package/dist/assembly/host-contract.d.ts +4 -0
  8. package/dist/assembly/host-contract.d.ts.map +1 -1
  9. package/dist/assembly/renderer-frame-transaction.d.ts +39 -0
  10. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -0
  11. package/dist/assembly/renderer-inspection.d.ts +22 -0
  12. package/dist/assembly/renderer-inspection.d.ts.map +1 -0
  13. package/dist/assembly/renderer-lifecycle.d.ts +4 -0
  14. package/dist/assembly/renderer-lifecycle.d.ts.map +1 -0
  15. package/dist/authoring.mjs +4 -2
  16. package/dist/authoring.mjs.map +1 -1
  17. package/dist/chunk-4XINDLAY.mjs +9404 -0
  18. package/dist/chunk-4XINDLAY.mjs.map +1 -0
  19. package/dist/chunk-CDTWTHZR.mjs +752 -0
  20. package/dist/chunk-CDTWTHZR.mjs.map +1 -0
  21. package/dist/chunk-FJ6P52EE.mjs +46 -0
  22. package/dist/chunk-FJ6P52EE.mjs.map +1 -0
  23. package/dist/chunk-JDWARUOI.mjs +44 -0
  24. package/dist/chunk-JDWARUOI.mjs.map +1 -0
  25. package/dist/chunk-M2MP4ZOU.mjs +73 -0
  26. package/dist/chunk-M2MP4ZOU.mjs.map +1 -0
  27. package/dist/chunk-PCHBPYML.mjs +385 -0
  28. package/dist/chunk-PCHBPYML.mjs.map +1 -0
  29. package/dist/chunk-RXZ3RGOU.mjs +53 -0
  30. package/dist/chunk-RXZ3RGOU.mjs.map +1 -0
  31. package/dist/{chunk-YEJIUW7D.mjs → chunk-TBQJOK2L.mjs} +356 -543
  32. package/dist/chunk-TBQJOK2L.mjs.map +1 -0
  33. package/dist/{chunk-CQXZC5XI.mjs → chunk-VFPLTKCU.mjs} +79 -69
  34. package/dist/chunk-VFPLTKCU.mjs.map +1 -0
  35. package/dist/components/atmosphere.d.ts +9 -0
  36. package/dist/components/atmosphere.d.ts.map +1 -0
  37. package/dist/components/camera.d.ts +2 -1
  38. package/dist/components/camera.d.ts.map +1 -1
  39. package/dist/components/fog.d.ts +8 -0
  40. package/dist/components/fog.d.ts.map +1 -0
  41. package/dist/components/index.d.ts +3 -0
  42. package/dist/components/index.d.ts.map +1 -1
  43. package/dist/components/light-helpers.d.ts.map +1 -1
  44. package/dist/components/motion-blur.d.ts +7 -0
  45. package/dist/components/motion-blur.d.ts.map +1 -0
  46. package/dist/components/spot-light.d.ts +12 -0
  47. package/dist/components/spot-light.d.ts.map +1 -1
  48. package/dist/construct-renderer.d.ts +2 -2
  49. package/dist/construct-renderer.d.ts.map +1 -1
  50. package/dist/construct-renderer.mjs +11146 -14507
  51. package/dist/construct-renderer.mjs.map +1 -1
  52. package/dist/device/device-scope.d.ts +4 -0
  53. package/dist/device/device-scope.d.ts.map +1 -1
  54. package/dist/device/gpu-residency.d.ts +19 -2
  55. package/dist/device/gpu-residency.d.ts.map +1 -1
  56. package/dist/environment/frame.d.ts +60 -0
  57. package/dist/environment/frame.d.ts.map +1 -0
  58. package/dist/environment/generation.d.ts +21 -0
  59. package/dist/environment/generation.d.ts.map +1 -0
  60. package/dist/environment/graph.d.ts +4 -0
  61. package/dist/environment/graph.d.ts.map +1 -0
  62. package/dist/environment/inspection.d.ts +48 -0
  63. package/dist/environment/inspection.d.ts.map +1 -0
  64. package/dist/environment/lifecycle.d.ts +39 -0
  65. package/dist/environment/lifecycle.d.ts.map +1 -0
  66. package/dist/environment/signature.d.ts +25 -0
  67. package/dist/environment/signature.d.ts.map +1 -0
  68. package/dist/errors/render.d.ts +204 -1
  69. package/dist/errors/render.d.ts.map +1 -1
  70. package/dist/extract/camera.d.ts +5 -0
  71. package/dist/extract/camera.d.ts.map +1 -0
  72. package/dist/extract/environment.d.ts +74 -0
  73. package/dist/extract/environment.d.ts.map +1 -0
  74. package/dist/extract/visibility.d.ts +1 -1
  75. package/dist/extract/visibility.d.ts.map +1 -1
  76. package/dist/features/host.d.ts +9 -2
  77. package/dist/features/host.d.ts.map +1 -1
  78. package/dist/features/motion-blur/motion-blur-feature.d.ts +16 -0
  79. package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -0
  80. package/dist/features/motion-blur/motion-blur-params.d.ts +25 -0
  81. package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -0
  82. package/dist/features/plan.d.ts +17 -2
  83. package/dist/features/plan.d.ts.map +1 -1
  84. package/dist/features/prepared-gpu-work.d.ts +3 -1
  85. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  86. package/dist/features/prepared-graphics.d.ts +2 -1
  87. package/dist/features/prepared-graphics.d.ts.map +1 -1
  88. package/dist/features/render-graph-raster.d.ts +2 -1
  89. package/dist/features/render-graph-raster.d.ts.map +1 -1
  90. package/dist/features/targets.d.ts +3 -0
  91. package/dist/features/targets.d.ts.map +1 -1
  92. package/dist/features/types.d.ts +12 -0
  93. package/dist/features/types.d.ts.map +1 -1
  94. package/dist/fullscreen-post-process-pass.d.ts +7 -1
  95. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  96. package/dist/gpu-scene.d.ts +2 -0
  97. package/dist/gpu-scene.d.ts.map +1 -1
  98. package/dist/gpu-usage.d.ts +2 -0
  99. package/dist/gpu-usage.d.ts.map +1 -1
  100. package/dist/ibl/skylight-bind-group.d.ts +5 -6
  101. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  102. package/dist/index.d.ts +24 -7
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.mjs +6 -4
  105. package/dist/index.mjs.map +1 -1
  106. package/dist/inspection-types.d.ts +43 -1
  107. package/dist/inspection-types.d.ts.map +1 -1
  108. package/dist/internal.d.ts +2 -0
  109. package/dist/internal.d.ts.map +1 -1
  110. package/dist/internal.mjs +2 -0
  111. package/dist/light-buffer-layout.d.ts +20 -12
  112. package/dist/light-buffer-layout.d.ts.map +1 -1
  113. package/dist/materials.d.ts +22 -0
  114. package/dist/materials.d.ts.map +1 -1
  115. package/dist/pbr-pipeline.d.ts +16 -9
  116. package/dist/pbr-pipeline.d.ts.map +1 -1
  117. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  118. package/dist/pipeline/standard-pipeline.d.ts +3 -0
  119. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  120. package/dist/pipeline/standard-post.d.ts +48 -0
  121. package/dist/pipeline/standard-post.d.ts.map +1 -0
  122. package/dist/pipeline/standard-profile.d.ts +12 -2
  123. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  124. package/dist/pipeline-builder.d.ts +18 -0
  125. package/dist/pipeline-builder.d.ts.map +1 -1
  126. package/dist/pipeline-spec.d.ts.map +1 -1
  127. package/dist/plugin.d.ts.map +1 -1
  128. package/dist/record/frame-lighting.d.ts +2 -0
  129. package/dist/record/frame-lighting.d.ts.map +1 -1
  130. package/dist/record/frame-snapshot.d.ts +137 -6
  131. package/dist/record/frame-snapshot.d.ts.map +1 -1
  132. package/dist/record/frame-targets.d.ts.map +1 -1
  133. package/dist/record/frame.d.ts +6 -3
  134. package/dist/record/frame.d.ts.map +1 -1
  135. package/dist/record/gpu-timing.d.ts +32 -0
  136. package/dist/record/gpu-timing.d.ts.map +1 -0
  137. package/dist/record/main-pass-geometry.d.ts +11 -0
  138. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  139. package/dist/record/main-pass-material.d.ts +9 -6
  140. package/dist/record/main-pass-material.d.ts.map +1 -1
  141. package/dist/record/main-pass-sprite-draws.d.ts +8 -1
  142. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  143. package/dist/record/main-pass.d.ts +2 -0
  144. package/dist/record/main-pass.d.ts.map +1 -1
  145. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  146. package/dist/record/render-context.d.ts +81 -19
  147. package/dist/record/render-context.d.ts.map +1 -1
  148. package/dist/record/shadow-pass.d.ts.map +1 -1
  149. package/dist/record/skybox-post-pass.d.ts +1 -11
  150. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  151. package/dist/record/typed-frame-graph.d.ts +36 -4
  152. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  153. package/dist/record/view-ubo.d.ts +4 -1
  154. package/dist/record/view-ubo.d.ts.map +1 -1
  155. package/dist/render-contract.d.ts +157 -8
  156. package/dist/render-contract.d.ts.map +1 -1
  157. package/dist/render-data.d.ts +10 -6
  158. package/dist/render-data.d.ts.map +1 -1
  159. package/dist/render-graph-primitives.d.ts.map +1 -1
  160. package/dist/render-pipeline.d.ts +86 -6
  161. package/dist/render-pipeline.d.ts.map +1 -1
  162. package/dist/render-system-extract.d.ts +84 -8
  163. package/dist/render-system-extract.d.ts.map +1 -1
  164. package/dist/render-system.d.ts +30 -5
  165. package/dist/render-system.d.ts.map +1 -1
  166. package/dist/renderer-inspect.d.ts +29 -0
  167. package/dist/renderer-inspect.d.ts.map +1 -0
  168. package/dist/scene/render-scene.d.ts +36 -2
  169. package/dist/scene/render-scene.d.ts.map +1 -1
  170. package/dist/temporal/coverage.d.ts +42 -0
  171. package/dist/temporal/coverage.d.ts.map +1 -0
  172. package/dist/temporal/frame-coordinator.d.ts +99 -0
  173. package/dist/temporal/frame-coordinator.d.ts.map +1 -0
  174. package/dist/temporal/frame.d.ts +44 -0
  175. package/dist/temporal/frame.d.ts.map +1 -0
  176. package/dist/temporal/gpu.d.ts +39 -0
  177. package/dist/temporal/gpu.d.ts.map +1 -0
  178. package/dist/temporal/history.d.ts +37 -0
  179. package/dist/temporal/history.d.ts.map +1 -0
  180. package/dist/temporal/index.d.ts +7 -0
  181. package/dist/temporal/index.d.ts.map +1 -0
  182. package/dist/temporal/index.mjs +8 -0
  183. package/dist/temporal/index.mjs.map +1 -0
  184. package/dist/temporal/inspection.d.ts +36 -0
  185. package/dist/temporal/inspection.d.ts.map +1 -0
  186. package/dist/temporal/scene-data-catalog.d.ts +48 -0
  187. package/dist/temporal/scene-data-catalog.d.ts.map +1 -0
  188. package/dist/temporal/scene-data.d.ts +43 -0
  189. package/dist/temporal/scene-data.d.ts.map +1 -0
  190. package/dist/temporal/standard-scene-data.d.ts +61 -0
  191. package/dist/temporal/standard-scene-data.d.ts.map +1 -0
  192. package/dist/temporal/taa-history-resources.d.ts +22 -0
  193. package/dist/temporal/taa-history-resources.d.ts.map +1 -0
  194. package/dist/temporal/taa-history-store.d.ts +27 -0
  195. package/dist/temporal/taa-history-store.d.ts.map +1 -0
  196. package/dist/temporal/taa-resolve.d.ts +15 -0
  197. package/dist/temporal/taa-resolve.d.ts.map +1 -0
  198. package/dist/temporal/temporal-view.d.ts +40 -0
  199. package/dist/temporal/temporal-view.d.ts.map +1 -0
  200. package/dist/temporal/transaction.d.ts +9 -0
  201. package/dist/temporal/transaction.d.ts.map +1 -0
  202. package/dist/temporal/view.d.ts +56 -0
  203. package/dist/temporal/view.d.ts.map +1 -0
  204. package/dist/transmission/backdrop.d.ts +147 -0
  205. package/dist/transmission/backdrop.d.ts.map +1 -0
  206. package/dist/transmission/inspection.d.ts +57 -0
  207. package/dist/transmission/inspection.d.ts.map +1 -0
  208. package/dist/transmission/oracle.d.ts +42 -0
  209. package/dist/transmission/oracle.d.ts.map +1 -0
  210. package/dist/transmission/projection.d.ts +37 -0
  211. package/dist/transmission/projection.d.ts.map +1 -0
  212. package/dist/typed-render-graph-primitives.d.ts +30 -2
  213. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  214. package/dist/typed-shadow-atlas.d.ts +10 -0
  215. package/dist/typed-shadow-atlas.d.ts.map +1 -0
  216. package/dist/typed-shadow-passes.d.ts.map +1 -1
  217. package/dist/volume/capability.d.ts +57 -0
  218. package/dist/volume/capability.d.ts.map +1 -0
  219. package/dist/volume/component.d.ts +61 -0
  220. package/dist/volume/component.d.ts.map +1 -0
  221. package/dist/volume/extract.d.ts +11 -0
  222. package/dist/volume/extract.d.ts.map +1 -0
  223. package/dist/volume/inspection.d.ts +38 -0
  224. package/dist/volume/inspection.d.ts.map +1 -0
  225. package/dist/volume/passes.d.ts +38 -0
  226. package/dist/volume/passes.d.ts.map +1 -0
  227. package/dist/volume/recovery.d.ts +38 -0
  228. package/dist/volume/recovery.d.ts.map +1 -0
  229. package/dist/volume/resources.d.ts +57 -0
  230. package/dist/volume/resources.d.ts.map +1 -0
  231. package/dist/volume/temporal.d.ts +35 -0
  232. package/dist/volume/temporal.d.ts.map +1 -0
  233. package/package.json +24 -19
  234. package/src/__tests__/ai-user-cold-start.integration.test.ts +59 -0
  235. package/src/__tests__/ai-user-public-surface.test-d.ts +45 -0
  236. package/src/__tests__/bloom-exact-zero.unit.test.ts +202 -0
  237. package/src/__tests__/bloom-lifecycle.integration.test.ts +125 -0
  238. package/src/__tests__/construct-renderer-identity.unit.test.ts +4 -3
  239. package/src/__tests__/detached-inspection.integration.test.ts +177 -0
  240. package/src/__tests__/environment-fog-lanes.integration.test.ts +219 -0
  241. package/src/__tests__/environment-generation.integration.test.ts +141 -0
  242. package/src/__tests__/environment-selection.unit.test.ts +245 -0
  243. package/src/__tests__/factory-contract.integration.test.ts +1380 -9
  244. package/src/__tests__/frame-plan-contract.unit.test.ts +90 -0
  245. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +7 -0
  246. package/src/__tests__/gpu-driven-production.integration.test.ts +74 -22
  247. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +37 -0
  248. package/src/__tests__/gpu-scene.unit.test.ts +10 -0
  249. package/src/__tests__/injection-kind-owner.test-d.ts +2 -1
  250. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +14 -14
  251. package/src/__tests__/material-contract-inventory.unit.test.ts +62 -0
  252. package/src/__tests__/material-cooked-projection.unit.test.ts +18 -146
  253. package/src/__tests__/materials-transmission-validation.unit.test.ts +36 -0
  254. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
  255. package/src/__tests__/morph-feature-plan.unit.test.ts +7 -0
  256. package/src/__tests__/motion-blur.pixel.integration.test.ts +121 -0
  257. package/src/__tests__/public-surface.test-d.ts +15 -1
  258. package/src/__tests__/render-error-code-owner.test-d.ts +14 -0
  259. package/src/__tests__/render-error-detail-ssot.test-d.ts +11 -2
  260. package/src/__tests__/render-error-exhaustive.test-d.ts +3 -0
  261. package/src/__tests__/render-feature-errors.unit.test.ts +33 -3
  262. package/src/__tests__/render-feature-gpu-work.integration.test.ts +44 -0
  263. package/src/__tests__/render-feature-plan.test-d.ts +22 -89
  264. package/src/__tests__/render-scene-projection.unit.test.ts +28 -0
  265. package/src/__tests__/render-temporal-environment-consumer.browser.test.ts +26 -0
  266. package/src/__tests__/render-temporal-environment-consumer.dawn.test.ts +21 -0
  267. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +423 -1
  268. package/src/__tests__/renderer-frame-transaction.integration.test.ts +405 -0
  269. package/src/__tests__/renderer-lifecycle.integration.test.ts +22 -0
  270. package/src/__tests__/scene-data-catalog.unit.test.ts +59 -0
  271. package/src/__tests__/scene-data-consumer.integration.test.ts +67 -0
  272. package/src/__tests__/scene-data-contract.test-d.ts +35 -0
  273. package/src/__tests__/scene-data-inspection.unit.test.ts +41 -0
  274. package/src/__tests__/scene-data-public-surface.test-d.ts +34 -0
  275. package/src/__tests__/shader-manifest-fixture.ts +111 -0
  276. package/src/__tests__/skinned-shadow-caster.test.ts +123 -0
  277. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +24 -3
  278. package/src/__tests__/standard-pipeline.dawn.test.ts +15 -0
  279. package/src/__tests__/standard-pipeline.integration.test.ts +543 -1
  280. package/src/__tests__/standard-profile-oracle.unit.test.ts +12 -2
  281. package/src/__tests__/standard-temporal-producer.integration.test.ts +35 -0
  282. package/src/__tests__/surface-configuration.unit.test.ts +211 -12
  283. package/src/__tests__/taa-transaction.integration.test.ts +30 -0
  284. package/src/__tests__/temporal-admission.integration.test.ts +40 -0
  285. package/src/__tests__/temporal-coverage.unit.test.ts +35 -0
  286. package/src/__tests__/temporal-demand.unit.test.ts +44 -0
  287. package/src/__tests__/temporal-frame-coordinator.unit.test.ts +64 -0
  288. package/src/__tests__/temporal-graph-topology.unit.test.ts +51 -0
  289. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +170 -0
  290. package/src/__tests__/temporal-performance.integration.test.ts +615 -0
  291. package/src/__tests__/temporal-reset-matrix.unit.test.ts +97 -0
  292. package/src/__tests__/temporal-transaction.integration.test.ts +60 -0
  293. package/src/__tests__/temporal-view.unit.test.ts +152 -0
  294. package/src/__tests__/transmission-prewarm.unit.test.ts +93 -0
  295. package/src/__tests__/typed-pipeline-topology.unit.test.ts +230 -1
  296. package/src/__tests__/urp-pipeline-targets.unit.test.ts +25 -1
  297. package/src/__tests__/urp-pipeline.unit.test.ts +4 -3
  298. package/src/__tests__/visibility-extract.integration.test.ts +27 -1
  299. package/src/__tests__/visibility-producer-matrix.integration.test.ts +10 -0
  300. package/src/__tests__/volume-optics-oracle.unit.test.ts +85 -0
  301. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +655 -0
  302. package/src/__tests__/volumetric-fog-component.unit.test.ts +71 -0
  303. package/src/__tests__/volumetric-fog-composite-stage-readback.dawn.test.ts +376 -0
  304. package/src/__tests__/volumetric-fog-errors.unit.test.ts +51 -0
  305. package/src/__tests__/volumetric-fog-inspection.unit.test.ts +77 -0
  306. package/src/__tests__/volumetric-fog-light-capability.unit.test.ts +19 -0
  307. package/src/__tests__/volumetric-fog-light-consumers.integration.test.ts +18 -0
  308. package/src/__tests__/volumetric-fog-light-errors.unit.test.ts +17 -0
  309. package/src/__tests__/volumetric-fog-light-topology.unit.test.ts +21 -0
  310. package/src/__tests__/volumetric-fog-light-transaction.integration.test.ts +53 -0
  311. package/src/__tests__/volumetric-fog-m9-watcher.integration.test.ts +27 -0
  312. package/src/__tests__/volumetric-fog-point-light-owner.integration.test.ts +44 -0
  313. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +49 -0
  314. package/src/__tests__/volumetric-fog-point-spot-contract.unit.test.ts +28 -0
  315. package/src/__tests__/volumetric-fog-point-spot-lkg.integration.test.ts +65 -0
  316. package/src/__tests__/volumetric-fog-point-spot-selection.integration.test.ts +49 -0
  317. package/src/__tests__/volumetric-fog-point-spot-temporal.unit.test.ts +55 -0
  318. package/src/__tests__/volumetric-fog-projector-contract.unit.test.ts +22 -0
  319. package/src/__tests__/volumetric-fog-projector-inspection.unit.test.ts +12 -0
  320. package/src/__tests__/volumetric-fog-projector-surface-volume.integration.test.ts +11 -0
  321. package/src/__tests__/volumetric-fog-projector-transaction.integration.test.ts +26 -0
  322. package/src/__tests__/volumetric-fog-recovery.integration.test.ts +47 -0
  323. package/src/__tests__/volumetric-fog-selected-light-resolution.integration.test.ts +63 -0
  324. package/src/__tests__/volumetric-fog-spot-closure.integration.test.ts +26 -0
  325. package/src/__tests__/volumetric-fog-spot-contract.test-d.ts +21 -0
  326. package/src/__tests__/volumetric-fog-spot-contract.unit.test.ts +27 -0
  327. package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +14 -0
  328. package/src/__tests__/volumetric-fog-spot-optics.integration.test.ts +29 -0
  329. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +550 -0
  330. package/src/__tests__/volumetric-fog-temporal-selected-light.unit.test.ts +33 -0
  331. package/src/__tests__/volumetric-fog-temporal.unit.test.ts +45 -0
  332. package/src/__tests__/volumetric-fog-topology.unit.test.ts +237 -0
  333. package/src/__tests__/volumetric-fog-transparent.integration.test.ts +27 -0
  334. package/src/assembly/backend-contract.ts +47 -1
  335. package/src/assembly/factory.ts +1480 -505
  336. package/src/assembly/host-contract.ts +4 -0
  337. package/src/assembly/renderer-frame-transaction.ts +75 -0
  338. package/src/assembly/renderer-inspection.ts +112 -0
  339. package/src/assembly/renderer-lifecycle.ts +6 -0
  340. package/src/components/__tests__/motion-blur.unit.test.ts +14 -0
  341. package/src/components/__tests__/schema-contract.unit.test.ts +6 -1
  342. package/src/components/atmosphere.ts +10 -0
  343. package/src/components/camera.ts +2 -1
  344. package/src/components/fog.ts +9 -0
  345. package/src/components/index.ts +3 -0
  346. package/src/components/light-helpers.ts +4 -0
  347. package/src/components/motion-blur.ts +8 -0
  348. package/src/components/spot-light.ts +22 -3
  349. package/src/construct-renderer.ts +40 -3
  350. package/src/device/device-scope.ts +15 -1
  351. package/src/device/gpu-residency.ts +168 -58
  352. package/src/environment/frame.ts +185 -0
  353. package/src/environment/generation.ts +23 -0
  354. package/src/environment/graph.ts +9 -0
  355. package/src/environment/inspection.ts +87 -0
  356. package/src/environment/lifecycle.ts +250 -0
  357. package/src/environment/signature.ts +55 -0
  358. package/src/errors/render.ts +404 -1
  359. package/src/extract/camera.ts +78 -0
  360. package/src/extract/environment.ts +171 -0
  361. package/src/extract/visibility.ts +38 -19
  362. package/src/features/host.ts +36 -5
  363. package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +67 -0
  364. package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +53 -0
  365. package/src/features/motion-blur/motion-blur-feature.ts +119 -0
  366. package/src/features/motion-blur/motion-blur-params.ts +89 -0
  367. package/src/features/plan.ts +32 -3
  368. package/src/features/prepared-gpu-work.ts +21 -6
  369. package/src/features/prepared-graphics.ts +2 -1
  370. package/src/features/render-graph-raster.ts +2 -1
  371. package/src/features/targets.ts +24 -2
  372. package/src/features/types.ts +13 -0
  373. package/src/fullscreen-post-process-pass.ts +34 -4
  374. package/src/gpu-scene.ts +24 -0
  375. package/src/gpu-usage.ts +2 -0
  376. package/src/ibl/skylight-bind-group.ts +30 -38
  377. package/src/index.ts +104 -7
  378. package/src/inspection-types.ts +48 -0
  379. package/src/internal.ts +5 -0
  380. package/src/light-buffer-layout.ts +59 -26
  381. package/src/materials.ts +119 -24
  382. package/src/pbr-pipeline.ts +91 -29
  383. package/src/pipeline/standard-forward-lane.ts +178 -101
  384. package/src/pipeline/standard-pipeline.ts +176 -105
  385. package/src/pipeline/standard-post.ts +373 -0
  386. package/src/pipeline/standard-profile.ts +26 -3
  387. package/src/pipeline-builder.ts +102 -12
  388. package/src/pipeline-spec.ts +9 -0
  389. package/src/plugin.ts +2 -0
  390. package/src/record/__tests__/fullscreen-temporal-routing.unit.test.ts +25 -0
  391. package/src/record/__tests__/main-pass-geometry.unit.test.ts +47 -0
  392. package/src/record/__tests__/material-bg-cache.unit.test.ts +12 -1
  393. package/src/record/__tests__/material-pipeline-pending-cache.unit.test.ts +22 -0
  394. package/src/record/__tests__/sprite-material-abi.unit.test.ts +75 -0
  395. package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +67 -0
  396. package/src/record/__tests__/temporal-payload.unit.test.ts +164 -0
  397. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +127 -0
  398. package/src/record/frame-lighting.ts +103 -89
  399. package/src/record/frame-snapshot.ts +170 -6
  400. package/src/record/frame-targets.ts +21 -8
  401. package/src/record/frame.ts +1014 -7
  402. package/src/record/gpu-timing.ts +224 -0
  403. package/src/record/main-pass-geometry.ts +86 -40
  404. package/src/record/main-pass-material.ts +120 -68
  405. package/src/record/main-pass-sprite-draws.ts +78 -101
  406. package/src/record/main-pass.ts +77 -4
  407. package/src/record/mesh-ssbo.ts +22 -3
  408. package/src/record/render-context.ts +194 -23
  409. package/src/record/shadow-pass.ts +197 -58
  410. package/src/record/skybox-post-pass.ts +97 -41
  411. package/src/record/typed-frame-graph.ts +704 -30
  412. package/src/record/view-ubo.ts +52 -4
  413. package/src/render-contract.ts +181 -6
  414. package/src/render-data.ts +50 -22
  415. package/src/render-graph-primitives.ts +18 -1
  416. package/src/render-pipeline.ts +144 -27
  417. package/src/render-system-extract.ts +522 -72
  418. package/src/render-system.ts +561 -41
  419. package/src/renderer-inspect.ts +66 -0
  420. package/src/scene/render-scene.ts +252 -7
  421. package/src/temporal/__tests__/taa-history-resources.unit.test.ts +45 -0
  422. package/src/temporal/coverage.ts +76 -0
  423. package/src/temporal/frame-coordinator.ts +311 -0
  424. package/src/temporal/frame.ts +128 -0
  425. package/src/temporal/gpu.ts +287 -0
  426. package/src/temporal/history.ts +92 -0
  427. package/src/temporal/index.ts +6 -0
  428. package/src/temporal/inspection.ts +97 -0
  429. package/src/temporal/scene-data-catalog.ts +200 -0
  430. package/src/temporal/scene-data.ts +149 -0
  431. package/src/temporal/standard-scene-data.ts +182 -0
  432. package/src/temporal/taa-history-resources.ts +128 -0
  433. package/src/temporal/taa-history-store.ts +79 -0
  434. package/src/temporal/taa-resolve.ts +34 -0
  435. package/src/temporal/temporal-view.ts +169 -0
  436. package/src/temporal/transaction.ts +17 -0
  437. package/src/temporal/view.ts +161 -0
  438. package/src/transmission/__tests__/capability.unit.test.ts +83 -0
  439. package/src/transmission/__tests__/composition.integration.test.ts +161 -0
  440. package/src/transmission/__tests__/inspection.unit.test.ts +241 -0
  441. package/src/transmission/__tests__/lifecycle.integration.test.ts +96 -0
  442. package/src/transmission/__tests__/oracle.unit.test.ts +131 -0
  443. package/src/transmission/__tests__/projection.unit.test.ts +71 -0
  444. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +437 -0
  445. package/src/transmission/__tests__/transmission-backdrop-topology.unit.test.ts +109 -0
  446. package/src/transmission/__tests__/transmission-backdrop.dawn.test.ts +105 -0
  447. package/src/transmission/backdrop.ts +572 -0
  448. package/src/transmission/inspection.ts +190 -0
  449. package/src/transmission/oracle.ts +141 -0
  450. package/src/transmission/projection.ts +83 -0
  451. package/src/typed-render-graph-primitives.ts +194 -11
  452. package/src/typed-shadow-atlas.ts +17 -0
  453. package/src/typed-shadow-passes.ts +6 -5
  454. package/src/volume/capability.ts +173 -0
  455. package/src/volume/component.ts +110 -0
  456. package/src/volume/extract.ts +21 -0
  457. package/src/volume/inspection.ts +78 -0
  458. package/src/volume/passes.ts +680 -0
  459. package/src/volume/recovery.ts +86 -0
  460. package/src/volume/resources.ts +236 -0
  461. package/src/volume/temporal.ts +89 -0
  462. package/dist/__tests__/authoring-path.unit.test.d.ts +0 -2
  463. package/dist/__tests__/authoring-path.unit.test.d.ts.map +0 -1
  464. package/dist/__tests__/backend-selection-rhi-capture.test-d.d.ts +0 -2
  465. package/dist/__tests__/backend-selection-rhi-capture.test-d.d.ts.map +0 -1
  466. package/dist/__tests__/callback-inference.test-d.d.ts +0 -2
  467. package/dist/__tests__/callback-inference.test-d.d.ts.map +0 -1
  468. package/dist/__tests__/cluster-binner.test.d.ts +0 -2
  469. package/dist/__tests__/cluster-binner.test.d.ts.map +0 -1
  470. package/dist/__tests__/cluster-grid-default-surface.unit.test.d.ts +0 -2
  471. package/dist/__tests__/cluster-grid-default-surface.unit.test.d.ts.map +0 -1
  472. package/dist/__tests__/construct-renderer-identity.unit.test.d.ts +0 -2
  473. package/dist/__tests__/construct-renderer-identity.unit.test.d.ts.map +0 -1
  474. package/dist/__tests__/device-scope-lifecycle.unit.test.d.ts +0 -2
  475. package/dist/__tests__/device-scope-lifecycle.unit.test.d.ts.map +0 -1
  476. package/dist/__tests__/device-scope-stale-matrix.unit.test.d.ts +0 -2
  477. package/dist/__tests__/device-scope-stale-matrix.unit.test.d.ts.map +0 -1
  478. package/dist/__tests__/direct-extract-consumer-contract.test-d.d.ts +0 -2
  479. package/dist/__tests__/direct-extract-consumer-contract.test-d.d.ts.map +0 -1
  480. package/dist/__tests__/direct-light-snapshot.unit.test.d.ts +0 -2
  481. package/dist/__tests__/direct-light-snapshot.unit.test.d.ts.map +0 -1
  482. package/dist/__tests__/factory-contract.integration.test.d.ts +0 -2
  483. package/dist/__tests__/factory-contract.integration.test.d.ts.map +0 -1
  484. package/dist/__tests__/fallback-row-stride-surface.unit.test.d.ts +0 -2
  485. package/dist/__tests__/fallback-row-stride-surface.unit.test.d.ts.map +0 -1
  486. package/dist/__tests__/frame-observation.contract.test.d.ts +0 -2
  487. package/dist/__tests__/frame-observation.contract.test.d.ts.map +0 -1
  488. package/dist/__tests__/fullscreen-feature-plan.unit.test.d.ts +0 -2
  489. package/dist/__tests__/fullscreen-feature-plan.unit.test.d.ts.map +0 -1
  490. package/dist/__tests__/glyph-text-layout-recovery.unit.test.d.ts +0 -2
  491. package/dist/__tests__/glyph-text-layout-recovery.unit.test.d.ts.map +0 -1
  492. package/dist/__tests__/gpu-driven-indirect-raster-evidence.d.ts +0 -6
  493. package/dist/__tests__/gpu-driven-indirect-raster-evidence.d.ts.map +0 -1
  494. package/dist/__tests__/gpu-driven-indirect-raster.browser.test.d.ts +0 -2
  495. package/dist/__tests__/gpu-driven-indirect-raster.browser.test.d.ts.map +0 -1
  496. package/dist/__tests__/gpu-driven-indirect-raster.dawn.test.d.ts +0 -2
  497. package/dist/__tests__/gpu-driven-indirect-raster.dawn.test.d.ts.map +0 -1
  498. package/dist/__tests__/gpu-driven-production.integration.test.d.ts +0 -2
  499. package/dist/__tests__/gpu-driven-production.integration.test.d.ts.map +0 -1
  500. package/dist/__tests__/gpu-driven-scaling.unit.test.d.ts +0 -2
  501. package/dist/__tests__/gpu-driven-scaling.unit.test.d.ts.map +0 -1
  502. package/dist/__tests__/gpu-driven-view-gpu-evidence.d.ts +0 -18
  503. package/dist/__tests__/gpu-driven-view-gpu-evidence.d.ts.map +0 -1
  504. package/dist/__tests__/gpu-driven-view-graph.integration.test.d.ts +0 -2
  505. package/dist/__tests__/gpu-driven-view-graph.integration.test.d.ts.map +0 -1
  506. package/dist/__tests__/gpu-driven-view-reference.d.ts +0 -15
  507. package/dist/__tests__/gpu-driven-view-reference.d.ts.map +0 -1
  508. package/dist/__tests__/gpu-driven-view.browser.test.d.ts +0 -2
  509. package/dist/__tests__/gpu-driven-view.browser.test.d.ts.map +0 -1
  510. package/dist/__tests__/gpu-driven-view.dawn.test.d.ts +0 -2
  511. package/dist/__tests__/gpu-driven-view.dawn.test.d.ts.map +0 -1
  512. package/dist/__tests__/gpu-driven-view.unit.test.d.ts +0 -2
  513. package/dist/__tests__/gpu-driven-view.unit.test.d.ts.map +0 -1
  514. package/dist/__tests__/gpu-resource-store-stride.unit.test.d.ts +0 -2
  515. package/dist/__tests__/gpu-resource-store-stride.unit.test.d.ts.map +0 -1
  516. package/dist/__tests__/gpu-scene-render-graph-gpu.d.ts +0 -7
  517. package/dist/__tests__/gpu-scene-render-graph-gpu.d.ts.map +0 -1
  518. package/dist/__tests__/gpu-scene-render-graph.browser.test.d.ts +0 -2
  519. package/dist/__tests__/gpu-scene-render-graph.browser.test.d.ts.map +0 -1
  520. package/dist/__tests__/gpu-scene-render-graph.dawn.test.d.ts +0 -2
  521. package/dist/__tests__/gpu-scene-render-graph.dawn.test.d.ts.map +0 -1
  522. package/dist/__tests__/gpu-scene-schema.unit.test.d.ts +0 -2
  523. package/dist/__tests__/gpu-scene-schema.unit.test.d.ts.map +0 -1
  524. package/dist/__tests__/gpu-scene.dawn.test.d.ts +0 -2
  525. package/dist/__tests__/gpu-scene.dawn.test.d.ts.map +0 -1
  526. package/dist/__tests__/gpu-scene.unit.test.d.ts +0 -2
  527. package/dist/__tests__/gpu-scene.unit.test.d.ts.map +0 -1
  528. package/dist/__tests__/ibl/face-uniforms.unit.test.d.ts +0 -2
  529. package/dist/__tests__/ibl/face-uniforms.unit.test.d.ts.map +0 -1
  530. package/dist/__tests__/infinite-grid-no-vertex-probe.unit.test.d.ts +0 -2
  531. package/dist/__tests__/infinite-grid-no-vertex-probe.unit.test.d.ts.map +0 -1
  532. package/dist/__tests__/injection-kind-owner.test-d.d.ts +0 -2
  533. package/dist/__tests__/injection-kind-owner.test-d.d.ts.map +0 -1
  534. package/dist/__tests__/ktx2-basis-gpu-consumer.dawn.test.d.ts +0 -2
  535. package/dist/__tests__/ktx2-basis-gpu-consumer.dawn.test.d.ts.map +0 -1
  536. package/dist/__tests__/light-buffer-layout.unit.test.d.ts +0 -2
  537. package/dist/__tests__/light-buffer-layout.unit.test.d.ts.map +0 -1
  538. package/dist/__tests__/material-contract-inventory.unit.test.d.ts +0 -2
  539. package/dist/__tests__/material-contract-inventory.unit.test.d.ts.map +0 -1
  540. package/dist/__tests__/material-cooked-projection.unit.test.d.ts +0 -2
  541. package/dist/__tests__/material-cooked-projection.unit.test.d.ts.map +0 -1
  542. package/dist/__tests__/material-snapshot-mutation.integration.test.d.ts +0 -2
  543. package/dist/__tests__/material-snapshot-mutation.integration.test.d.ts.map +0 -1
  544. package/dist/__tests__/material-static-dynamic-route.unit.test.d.ts +0 -2
  545. package/dist/__tests__/material-static-dynamic-route.unit.test.d.ts.map +0 -1
  546. package/dist/__tests__/material-stride-surface.unit.test.d.ts +0 -2
  547. package/dist/__tests__/material-stride-surface.unit.test.d.ts.map +0 -1
  548. package/dist/__tests__/mesh-buffer-usage-surface.unit.test.d.ts +0 -2
  549. package/dist/__tests__/mesh-buffer-usage-surface.unit.test.d.ts.map +0 -1
  550. package/dist/__tests__/mesh-material-bindings.unit.test.d.ts +0 -2
  551. package/dist/__tests__/mesh-material-bindings.unit.test.d.ts.map +0 -1
  552. package/dist/__tests__/mesh-stride-surface.unit.test.d.ts +0 -2
  553. package/dist/__tests__/mesh-stride-surface.unit.test.d.ts.map +0 -1
  554. package/dist/__tests__/morph-culling-reentry.dawn.test.d.ts +0 -2
  555. package/dist/__tests__/morph-culling-reentry.dawn.test.d.ts.map +0 -1
  556. package/dist/__tests__/morph-feature-gpu-work.integration.test.d.ts +0 -2
  557. package/dist/__tests__/morph-feature-gpu-work.integration.test.d.ts.map +0 -1
  558. package/dist/__tests__/morph-feature-plan.unit.test.d.ts +0 -2
  559. package/dist/__tests__/morph-feature-plan.unit.test.d.ts.map +0 -1
  560. package/dist/__tests__/morph-visual-evidence.integration.test.d.ts +0 -2
  561. package/dist/__tests__/morph-visual-evidence.integration.test.d.ts.map +0 -1
  562. package/dist/__tests__/nineslice-handle-surface.unit.test.d.ts +0 -2
  563. package/dist/__tests__/nineslice-handle-surface.unit.test.d.ts.map +0 -1
  564. package/dist/__tests__/post-process-error-policy-owner.unit.test.d.ts +0 -2
  565. package/dist/__tests__/post-process-error-policy-owner.unit.test.d.ts.map +0 -1
  566. package/dist/__tests__/prepared-buffer-kind-owner.test-d.d.ts +0 -2
  567. package/dist/__tests__/prepared-buffer-kind-owner.test-d.d.ts.map +0 -1
  568. package/dist/__tests__/prepared-graphics-capability.unit.test.d.ts +0 -2
  569. package/dist/__tests__/prepared-graphics-capability.unit.test.d.ts.map +0 -1
  570. package/dist/__tests__/prepared-graphics-generation.unit.test.d.ts +0 -2
  571. package/dist/__tests__/prepared-graphics-generation.unit.test.d.ts.map +0 -1
  572. package/dist/__tests__/prepared-graphics-lifecycle.unit.test.d.ts +0 -2
  573. package/dist/__tests__/prepared-graphics-lifecycle.unit.test.d.ts.map +0 -1
  574. package/dist/__tests__/prepared-graphics-pipeline-warmup.unit.test.d.ts +0 -2
  575. package/dist/__tests__/prepared-graphics-pipeline-warmup.unit.test.d.ts.map +0 -1
  576. package/dist/__tests__/prepared-graphics-resolver.unit.test.d.ts +0 -2
  577. package/dist/__tests__/prepared-graphics-resolver.unit.test.d.ts.map +0 -1
  578. package/dist/__tests__/prepared-graphics-snapshot.unit.test.d.ts +0 -2
  579. package/dist/__tests__/prepared-graphics-snapshot.unit.test.d.ts.map +0 -1
  580. package/dist/__tests__/prepared-graphics-store.unit.test.d.ts +0 -2
  581. package/dist/__tests__/prepared-graphics-store.unit.test.d.ts.map +0 -1
  582. package/dist/__tests__/prepared-graphics-validation.unit.test.d.ts +0 -2
  583. package/dist/__tests__/prepared-graphics-validation.unit.test.d.ts.map +0 -1
  584. package/dist/__tests__/profiler-default-off.test.d.ts +0 -2
  585. package/dist/__tests__/profiler-default-off.test.d.ts.map +0 -1
  586. package/dist/__tests__/profiler-phase-catalog.test.d.ts +0 -2
  587. package/dist/__tests__/profiler-phase-catalog.test.d.ts.map +0 -1
  588. package/dist/__tests__/profiler-phase-integration.test.d.ts +0 -2
  589. package/dist/__tests__/profiler-phase-integration.test.d.ts.map +0 -1
  590. package/dist/__tests__/profiler-phase-token.test.d.ts +0 -2
  591. package/dist/__tests__/profiler-phase-token.test.d.ts.map +0 -1
  592. package/dist/__tests__/public-surface.test-d.d.ts +0 -2
  593. package/dist/__tests__/public-surface.test-d.d.ts.map +0 -1
  594. package/dist/__tests__/recover-error-policy-owner.unit.test.d.ts +0 -2
  595. package/dist/__tests__/recover-error-policy-owner.unit.test.d.ts.map +0 -1
  596. package/dist/__tests__/render-error-code-owner.test-d.d.ts +0 -2
  597. package/dist/__tests__/render-error-code-owner.test-d.d.ts.map +0 -1
  598. package/dist/__tests__/render-error-detail-ssot.test-d.d.ts +0 -2
  599. package/dist/__tests__/render-error-detail-ssot.test-d.d.ts.map +0 -1
  600. package/dist/__tests__/render-error-exhaustive.test-d.d.ts +0 -2
  601. package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +0 -1
  602. package/dist/__tests__/render-feature-assembly.test-d.d.ts +0 -2
  603. package/dist/__tests__/render-feature-assembly.test-d.d.ts.map +0 -1
  604. package/dist/__tests__/render-feature-context.test-d.d.ts +0 -2
  605. package/dist/__tests__/render-feature-context.test-d.d.ts.map +0 -1
  606. package/dist/__tests__/render-feature-contract.test-d.d.ts +0 -2
  607. package/dist/__tests__/render-feature-contract.test-d.d.ts.map +0 -1
  608. package/dist/__tests__/render-feature-diagnostics.unit.test.d.ts +0 -2
  609. package/dist/__tests__/render-feature-diagnostics.unit.test.d.ts.map +0 -1
  610. package/dist/__tests__/render-feature-docs.unit.test.d.ts +0 -2
  611. package/dist/__tests__/render-feature-docs.unit.test.d.ts.map +0 -1
  612. package/dist/__tests__/render-feature-errors.unit.test.d.ts +0 -2
  613. package/dist/__tests__/render-feature-errors.unit.test.d.ts.map +0 -1
  614. package/dist/__tests__/render-feature-gpu-work.integration.test.d.ts +0 -2
  615. package/dist/__tests__/render-feature-gpu-work.integration.test.d.ts.map +0 -1
  616. package/dist/__tests__/render-feature-plan.test-d.d.ts +0 -2
  617. package/dist/__tests__/render-feature-plan.test-d.d.ts.map +0 -1
  618. package/dist/__tests__/render-feature-plan.unit.test.d.ts +0 -2
  619. package/dist/__tests__/render-feature-plan.unit.test.d.ts.map +0 -1
  620. package/dist/__tests__/render-feature-prepared-diagnostics.unit.test.d.ts +0 -2
  621. package/dist/__tests__/render-feature-prepared-diagnostics.unit.test.d.ts.map +0 -1
  622. package/dist/__tests__/render-feature-prepared-errors.test-d.d.ts +0 -2
  623. package/dist/__tests__/render-feature-prepared-errors.test-d.d.ts.map +0 -1
  624. package/dist/__tests__/render-feature-prepared-graphics-docs.unit.test.d.ts +0 -2
  625. package/dist/__tests__/render-feature-prepared-graphics-docs.unit.test.d.ts.map +0 -1
  626. package/dist/__tests__/render-feature-prepared-graphics-isolation.unit.test.d.ts +0 -2
  627. package/dist/__tests__/render-feature-prepared-graphics-isolation.unit.test.d.ts.map +0 -1
  628. package/dist/__tests__/render-feature-prepared-graphics-negative.test-d.d.ts +0 -2
  629. package/dist/__tests__/render-feature-prepared-graphics-negative.test-d.d.ts.map +0 -1
  630. package/dist/__tests__/render-feature-prepared-graphics.test-d.d.ts +0 -2
  631. package/dist/__tests__/render-feature-prepared-graphics.test-d.d.ts.map +0 -1
  632. package/dist/__tests__/render-feature-prepared-graphics.unit.test.d.ts +0 -2
  633. package/dist/__tests__/render-feature-prepared-graphics.unit.test.d.ts.map +0 -1
  634. package/dist/__tests__/render-feature-public-surface.test-d.d.ts +0 -2
  635. package/dist/__tests__/render-feature-public-surface.test-d.d.ts.map +0 -1
  636. package/dist/__tests__/render-feature-retry.unit.test.d.ts +0 -2
  637. package/dist/__tests__/render-feature-retry.unit.test.d.ts.map +0 -1
  638. package/dist/__tests__/render-feature-scheduling.unit.test.d.ts +0 -2
  639. package/dist/__tests__/render-feature-scheduling.unit.test.d.ts.map +0 -1
  640. package/dist/__tests__/render-feature-stage-failed-policy-owner.unit.test.d.ts +0 -2
  641. package/dist/__tests__/render-feature-stage-failed-policy-owner.unit.test.d.ts.map +0 -1
  642. package/dist/__tests__/render-feature-stage-fault-recovery.unit.test.d.ts +0 -2
  643. package/dist/__tests__/render-feature-stage-fault-recovery.unit.test.d.ts.map +0 -1
  644. package/dist/__tests__/render-feature-visibility-context.test-d.d.ts +0 -2
  645. package/dist/__tests__/render-feature-visibility-context.test-d.d.ts.map +0 -1
  646. package/dist/__tests__/render-feature-visibility-context.unit.test.d.ts +0 -2
  647. package/dist/__tests__/render-feature-visibility-context.unit.test.d.ts.map +0 -1
  648. package/dist/__tests__/render-feature-zero-work.unit.test.d.ts +0 -2
  649. package/dist/__tests__/render-feature-zero-work.unit.test.d.ts.map +0 -1
  650. package/dist/__tests__/render-graph-compute-raster-gpu.d.ts +0 -7
  651. package/dist/__tests__/render-graph-compute-raster-gpu.d.ts.map +0 -1
  652. package/dist/__tests__/render-graph-compute-raster.browser.test.d.ts +0 -2
  653. package/dist/__tests__/render-graph-compute-raster.browser.test.d.ts.map +0 -1
  654. package/dist/__tests__/render-graph-compute-raster.dawn.test.d.ts +0 -2
  655. package/dist/__tests__/render-graph-compute-raster.dawn.test.d.ts.map +0 -1
  656. package/dist/__tests__/render-graph-hzb-gpu.d.ts +0 -7
  657. package/dist/__tests__/render-graph-hzb-gpu.d.ts.map +0 -1
  658. package/dist/__tests__/render-graph-hzb.browser.test.d.ts +0 -2
  659. package/dist/__tests__/render-graph-hzb.browser.test.d.ts.map +0 -1
  660. package/dist/__tests__/render-graph-hzb.dawn.test.d.ts +0 -2
  661. package/dist/__tests__/render-graph-hzb.dawn.test.d.ts.map +0 -1
  662. package/dist/__tests__/render-graph-primitives.unit.test.d.ts +0 -2
  663. package/dist/__tests__/render-graph-primitives.unit.test.d.ts.map +0 -1
  664. package/dist/__tests__/render-graph-storage-indirect-gpu.d.ts +0 -12
  665. package/dist/__tests__/render-graph-storage-indirect-gpu.d.ts.map +0 -1
  666. package/dist/__tests__/render-graph-storage-indirect.browser.test.d.ts +0 -2
  667. package/dist/__tests__/render-graph-storage-indirect.browser.test.d.ts.map +0 -1
  668. package/dist/__tests__/render-graph-storage-indirect.dawn.test.d.ts +0 -2
  669. package/dist/__tests__/render-graph-storage-indirect.dawn.test.d.ts.map +0 -1
  670. package/dist/__tests__/render-scene-projection.unit.test.d.ts +0 -2
  671. package/dist/__tests__/render-scene-projection.unit.test.d.ts.map +0 -1
  672. package/dist/__tests__/renderer-factory-material-contract.unit.test.d.ts +0 -2
  673. package/dist/__tests__/renderer-factory-material-contract.unit.test.d.ts.map +0 -1
  674. package/dist/__tests__/renderer-lifecycle.integration.test.d.ts +0 -2
  675. package/dist/__tests__/renderer-lifecycle.integration.test.d.ts.map +0 -1
  676. package/dist/__tests__/rhi-backend-package-boundary.unit.test.d.ts +0 -2
  677. package/dist/__tests__/rhi-backend-package-boundary.unit.test.d.ts.map +0 -1
  678. package/dist/__tests__/scriptable-pack-consumer.test-d.d.ts +0 -2
  679. package/dist/__tests__/scriptable-pack-consumer.test-d.d.ts.map +0 -1
  680. package/dist/__tests__/shader-stage-surface.unit.test.d.ts +0 -2
  681. package/dist/__tests__/shader-stage-surface.unit.test.d.ts.map +0 -1
  682. package/dist/__tests__/ssao-config.unit.test.d.ts +0 -2
  683. package/dist/__tests__/ssao-config.unit.test.d.ts.map +0 -1
  684. package/dist/__tests__/ssao-kernel-surface.unit.test.d.ts +0 -2
  685. package/dist/__tests__/ssao-kernel-surface.unit.test.d.ts.map +0 -1
  686. package/dist/__tests__/standard-clustered-pipeline.unit.test.d.ts +0 -2
  687. package/dist/__tests__/standard-clustered-pipeline.unit.test.d.ts.map +0 -1
  688. package/dist/__tests__/standard-descriptors.test-d.d.ts +0 -2
  689. package/dist/__tests__/standard-descriptors.test-d.d.ts.map +0 -1
  690. package/dist/__tests__/standard-pipeline.dawn.test.d.ts +0 -2
  691. package/dist/__tests__/standard-pipeline.dawn.test.d.ts.map +0 -1
  692. package/dist/__tests__/standard-pipeline.integration.test.d.ts +0 -2
  693. package/dist/__tests__/standard-pipeline.integration.test.d.ts.map +0 -1
  694. package/dist/__tests__/standard-profile-oracle.unit.test.d.ts +0 -2
  695. package/dist/__tests__/standard-profile-oracle.unit.test.d.ts.map +0 -1
  696. package/dist/__tests__/surface-configuration.unit.test.d.ts +0 -2
  697. package/dist/__tests__/surface-configuration.unit.test.d.ts.map +0 -1
  698. package/dist/__tests__/texture-usage-surface.unit.test.d.ts +0 -2
  699. package/dist/__tests__/texture-usage-surface.unit.test.d.ts.map +0 -1
  700. package/dist/__tests__/typed-pipeline-topology.unit.test.d.ts +0 -2
  701. package/dist/__tests__/typed-pipeline-topology.unit.test.d.ts.map +0 -1
  702. package/dist/__tests__/urp-pipeline-targets.unit.test.d.ts +0 -2
  703. package/dist/__tests__/urp-pipeline-targets.unit.test.d.ts.map +0 -1
  704. package/dist/__tests__/urp-pipeline.unit.test.d.ts +0 -2
  705. package/dist/__tests__/urp-pipeline.unit.test.d.ts.map +0 -1
  706. package/dist/__tests__/vertex-color-pipeline.unit.test.d.ts +0 -2
  707. package/dist/__tests__/vertex-color-pipeline.unit.test.d.ts.map +0 -1
  708. package/dist/__tests__/vertex-layout-consumer.unit.test.d.ts +0 -2
  709. package/dist/__tests__/vertex-layout-consumer.unit.test.d.ts.map +0 -1
  710. package/dist/__tests__/visibility-diagnostics.integration.test.d.ts +0 -2
  711. package/dist/__tests__/visibility-diagnostics.integration.test.d.ts.map +0 -1
  712. package/dist/__tests__/visibility-extract.integration.test.d.ts +0 -2
  713. package/dist/__tests__/visibility-extract.integration.test.d.ts.map +0 -1
  714. package/dist/__tests__/visibility-extract.perf.test.d.ts +0 -2
  715. package/dist/__tests__/visibility-extract.perf.test.d.ts.map +0 -1
  716. package/dist/__tests__/visibility-feature-dedup.integration.test.d.ts +0 -2
  717. package/dist/__tests__/visibility-feature-dedup.integration.test.d.ts.map +0 -1
  718. package/dist/__tests__/visibility-hidden-report.unit.test.d.ts +0 -2
  719. package/dist/__tests__/visibility-hidden-report.unit.test.d.ts.map +0 -1
  720. package/dist/__tests__/visibility-instances.integration.test.d.ts +0 -2
  721. package/dist/__tests__/visibility-instances.integration.test.d.ts.map +0 -1
  722. package/dist/__tests__/visibility-multi-world.integration.test.d.ts +0 -2
  723. package/dist/__tests__/visibility-multi-world.integration.test.d.ts.map +0 -1
  724. package/dist/__tests__/visibility-producer-matrix.integration.test.d.ts +0 -2
  725. package/dist/__tests__/visibility-producer-matrix.integration.test.d.ts.map +0 -1
  726. package/dist/__tests__/visibility-producer-matrix.unit.test.d.ts +0 -2
  727. package/dist/__tests__/visibility-producer-matrix.unit.test.d.ts.map +0 -1
  728. package/dist/__tests__/visibility-public-surface.test-d.d.ts +0 -2
  729. package/dist/__tests__/visibility-public-surface.test-d.d.ts.map +0 -1
  730. package/dist/__tests__/visibility-resolver.unit.test.d.ts +0 -2
  731. package/dist/__tests__/visibility-resolver.unit.test.d.ts.map +0 -1
  732. package/dist/__tests__/visibility-resource-short-circuit.unit.test.d.ts +0 -2
  733. package/dist/__tests__/visibility-resource-short-circuit.unit.test.d.ts.map +0 -1
  734. package/dist/__tests__/visibility-snapshot-contract.unit.test.d.ts +0 -2
  735. package/dist/__tests__/visibility-snapshot-contract.unit.test.d.ts.map +0 -1
  736. package/dist/__tests__/vocabulary.unit.test.d.ts +0 -2
  737. package/dist/__tests__/vocabulary.unit.test.d.ts.map +0 -1
  738. package/dist/chunk-CQXZC5XI.mjs.map +0 -1
  739. package/dist/chunk-HCOGPHKS.mjs +0 -93
  740. package/dist/chunk-HCOGPHKS.mjs.map +0 -1
  741. package/dist/chunk-YEJIUW7D.mjs.map +0 -1
  742. package/dist/components/__tests__/points-lines-admission.unit.test.d.ts +0 -2
  743. package/dist/components/__tests__/points-lines-admission.unit.test.d.ts.map +0 -1
  744. package/dist/components/__tests__/points-lines-schema.unit.test.d.ts +0 -2
  745. package/dist/components/__tests__/points-lines-schema.unit.test.d.ts.map +0 -1
  746. package/dist/components/__tests__/schema-contract.unit.test.d.ts +0 -2
  747. package/dist/components/__tests__/schema-contract.unit.test.d.ts.map +0 -1
  748. package/dist/components/__tests__/schema-parity.unit.test.d.ts +0 -2
  749. package/dist/components/__tests__/schema-parity.unit.test.d.ts.map +0 -1
  750. package/dist/components/__tests__/visibility-schema.unit.test.d.ts +0 -2
  751. package/dist/components/__tests__/visibility-schema.unit.test.d.ts.map +0 -1
  752. package/dist/points-lines/__tests__/bounds.unit.test.d.ts +0 -2
  753. package/dist/points-lines/__tests__/bounds.unit.test.d.ts.map +0 -1
  754. package/dist/points-lines/__tests__/cache-recovery.unit.test.d.ts +0 -2
  755. package/dist/points-lines/__tests__/cache-recovery.unit.test.d.ts.map +0 -1
  756. package/dist/points-lines/__tests__/inspection-lanes.integration.test.d.ts +0 -2
  757. package/dist/points-lines/__tests__/inspection-lanes.integration.test.d.ts.map +0 -1
  758. package/dist/points-lines/__tests__/inspection.unit.test.d.ts +0 -2
  759. package/dist/points-lines/__tests__/inspection.unit.test.d.ts.map +0 -1
  760. package/dist/points-lines/__tests__/lane-structural.integration.test.d.ts +0 -2
  761. package/dist/points-lines/__tests__/lane-structural.integration.test.d.ts.map +0 -1
  762. package/dist/points-lines/__tests__/points-lines.bench.d.ts +0 -2
  763. package/dist/points-lines/__tests__/points-lines.bench.d.ts.map +0 -1
  764. package/dist/points-lines/__tests__/raster-lane.integration.test.d.ts +0 -2
  765. package/dist/points-lines/__tests__/raster-lane.integration.test.d.ts.map +0 -1
  766. package/dist/points-lines/__tests__/snapshot.unit.test.d.ts +0 -2
  767. package/dist/points-lines/__tests__/snapshot.unit.test.d.ts.map +0 -1
  768. package/dist/points-lines/__tests__/view-ubo.unit.test.d.ts +0 -2
  769. package/dist/points-lines/__tests__/view-ubo.unit.test.d.ts.map +0 -1
  770. package/dist/record/__tests__/material-bg-cache.unit.test.d.ts +0 -2
  771. package/dist/record/__tests__/material-bg-cache.unit.test.d.ts.map +0 -1
  772. package/dist/record/__tests__/material-slot-plan.unit.test.d.ts +0 -2
  773. package/dist/record/__tests__/material-slot-plan.unit.test.d.ts.map +0 -1
  774. package/dist/record/__tests__/typed-frame-graph-plan.unit.test.d.ts +0 -2
  775. package/dist/record/__tests__/typed-frame-graph-plan.unit.test.d.ts.map +0 -1
  776. package/dist/record/__tests__/video-texture-view.unit.test.d.ts +0 -2
  777. package/dist/record/__tests__/video-texture-view.unit.test.d.ts.map +0 -1
@@ -59,6 +59,7 @@ import type {
59
59
  import { err, ok, RhiError, validateDrawArgs } from '@forgeax/engine-rhi';
60
60
  import { registerPropagateTransforms } from '@forgeax/engine-scene';
61
61
  import {
62
+ DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
62
63
  findVariantByKey,
63
64
  type MaterialShaderEntry,
64
65
  type MaterialShaderManifestEntry,
@@ -77,9 +78,12 @@ import type {
77
78
  VertexAttributeMap,
78
79
  } from '@forgeax/engine-types';
79
80
  import { derive, handleSlot } from '@forgeax/engine-types';
81
+
82
+ export type { MaterialShaderManifestEntry } from '@forgeax/engine-shader';
83
+
80
84
  import { GpuResidencyCache, type MeshGpuHandles } from '../device/gpu-residency';
81
85
  import { createEngineMetrics } from '../engine-metrics';
82
- import { RecoverError, type RecoverFailure, RenderRecoveryError } from '../errors/recover';
86
+ import { RecoverError, type RecoverFailure } from '../errors/recover';
83
87
  import {
84
88
  FrameReceiptStaleError,
85
89
  type RenderError,
@@ -90,8 +94,9 @@ import {
90
94
  RenderFeatureStageFailedError,
91
95
  } from '../errors/render';
92
96
  import { createRenderFeatureHost, type RenderFeatureHost } from '../features/host';
97
+ import type { RenderFeatureMaterialShaderBindingContract } from '../features/plan';
93
98
  import { RENDER_FEATURE_VERTEX_LAYOUTS } from '../features/prepared-graphics';
94
- import type { RenderFeature } from '../features/types';
99
+ import type { RenderFeature, RenderFeatureShaderModuleMode } from '../features/types';
95
100
  import type { PostProcessShaderEntry } from '../fullscreen-post-process-pass';
96
101
  import { glyphTextLayoutSystem } from '../glyph-text-layout-system';
97
102
  import { GpuBuffer } from '../gpu-resource';
@@ -115,6 +120,7 @@ import {
115
120
  FALLBACK_BYTES_PER_ROW,
116
121
  type SkylightFallback,
117
122
  } from '../ibl/skylight-bind-group';
123
+ import type { BloomInspection } from '../inspection-types';
118
124
  import {
119
125
  DeviceScope,
120
126
  HealthListenerRegistry,
@@ -129,15 +135,19 @@ import {
129
135
  SPOT_LIGHT_STD430_BYTES,
130
136
  } from '../light-buffer-layout';
131
137
  import {
138
+ buildPbrMaterialUserRegionEntries,
132
139
  buildPbrPipelineLayouts,
133
140
  buildPbrSkinLayouts,
141
+ isSkinnedShadowCasterVariant,
134
142
  SKIN_MATERIAL_SHADER_ID,
135
143
  } from '../pbr-pipeline';
136
144
  import { STANDARD_CLUSTER_MEMBERSHIP_WGSL, standardPipeline } from '../pipeline/standard-pipeline';
145
+ import { inspectStandardBloomGraph } from '../pipeline/standard-post';
137
146
  import {
138
147
  DEFAULT_STANDARD_PROFILE,
139
148
  STANDARD_LIGHT_COUNTS,
140
149
  STANDARD_PIPELINE_ID,
150
+ STANDARD_POST_STAGE_NAMES,
141
151
  } from '../pipeline/standard-profile';
142
152
  import { buildPipelineForMaterialShader } from '../pipeline-builder';
143
153
  import {
@@ -155,6 +165,8 @@ import {
155
165
  } from '../pipeline-spec';
156
166
  import { POINTS_LINES_MATERIAL_SHADER_ID } from '../points-lines/record';
157
167
  import type { FrameObservationOptions } from '../record/frame';
168
+ import { GpuTimingCapture, type VolumeTimingObservation } from '../record/gpu-timing';
169
+ import type { BloomPersistentBundle } from '../record/render-context';
158
170
  import { SHADOW_CASTER_BUFFER_SIZE } from '../record/shadow-pass';
159
171
  import { POINTS_LINES_VIEW_BUFFER_SIZE, VIEW_UNIFORM_BUFFER_SIZE } from '../record/view-ubo';
160
172
  import type {
@@ -176,8 +188,9 @@ import type {
176
188
  RenderProfile,
177
189
  RenderResult,
178
190
  RenderWorldLease,
191
+ VolumetricFogShaderSources,
179
192
  } from '../render-contract';
180
- import { STANDARD_TONEMAP_FEATURE_ID } from '../render-contract';
193
+ import { STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from '../render-contract';
181
194
  import {
182
195
  configureSurface,
183
196
  createRenderSystem,
@@ -194,6 +207,8 @@ import {
194
207
  import { tilemapChunkExtractSystem } from '../tilemap-chunk-extract-system';
195
208
  import { EngineEnvironmentError, type RhiBackendPack } from './backend-contract';
196
209
  import type { RendererAssemblyImplementation, RendererHostImplementation } from './host-contract';
210
+ import { projectRendererOutputInspection } from './renderer-inspection';
211
+ import { isRendererStateOperational } from './renderer-lifecycle';
197
212
 
198
213
  export { assembleMaterialProjection } from './material/assembly';
199
214
  export {
@@ -232,6 +247,9 @@ function loadRhiPack(
232
247
  const createShaderModule = mod.createShaderModule as
233
248
  | RhiBackendPack['createShaderModule']
234
249
  | undefined;
250
+ const createShaderModuleImmediate = mod.createShaderModuleImmediate as
251
+ | RhiBackendPack['createShaderModuleImmediate']
252
+ | undefined;
235
253
  const translateErrorEventToRhiError = mod.translateErrorEventToRhiError as
236
254
  | RhiBackendPack['translateErrorEventToRhiError']
237
255
  | undefined;
@@ -241,6 +259,7 @@ function loadRhiPack(
241
259
  return {
242
260
  rhi,
243
261
  ...(createShaderModule === undefined ? {} : { createShaderModule }),
262
+ ...(createShaderModuleImmediate === undefined ? {} : { createShaderModuleImmediate }),
244
263
  ...(translateErrorEventToRhiError === undefined ? {} : { translateErrorEventToRhiError }),
245
264
  ...(rawDeviceAccessor === undefined ? {} : { _internal_getRawDevice: rawDeviceAccessor }),
246
265
  ...(instrumentation === undefined ? {} : { instrumentation }),
@@ -477,20 +496,67 @@ export type LayoutKind = 'pbr' | 'pbr-skin' | 'hdrp-pbr' | 'sprite-urp' | 'unlit
477
496
  * Return every device-capability HDRP PBR variant that needs a lazy module
478
497
  * label seeded during renderer boot. Color availability is intentionally not
479
498
  * filtered: colored and no-color meshes use distinct composed WGSL modules.
499
+ * Transmission is filtered by the same sampled-texture capability gate as
500
+ * Standard transmission because HDRP shares the material texture ABI.
480
501
  */
481
502
  export function selectHdrpPbrPrewarmVariants(
482
503
  manifestEntry: MaterialShaderManifestEntry | undefined,
483
504
  storageBufferCapable: boolean,
505
+ transmissionCapable: boolean,
506
+ projectorAvailable: boolean = true,
484
507
  ): readonly MaterialShaderManifestEntry['variants'][number][] {
485
508
  return (
486
509
  manifestEntry?.variants.filter(
487
510
  (variant) =>
488
511
  variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
489
- variant.defines.CLUSTER_FORWARD_AVAILABLE === true,
512
+ variant.defines.CLUSTER_FORWARD_AVAILABLE === true &&
513
+ (!('PROJECTOR_AVAILABLE' in variant.defines) ||
514
+ variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
515
+ (transmissionCapable || variant.defines.TRANSMISSION_AVAILABLE !== true),
490
516
  ) ?? []
491
517
  );
492
518
  }
493
519
 
520
+ /**
521
+ * Select both Standard transmission states from the manifest for exact-label
522
+ * prewarming. The requested keys come from declared variants; no key is
523
+ * synthesized and no source fallback is permitted.
524
+ */
525
+ export function selectStandardPbrTransmissionPrewarmVariants(
526
+ manifestEntry: MaterialShaderManifestEntry | undefined,
527
+ storageBufferCapable: boolean,
528
+ projectorAvailable: boolean = true,
529
+ ): readonly MaterialShaderManifestEntry['variants'][number][] {
530
+ if (manifestEntry === undefined) {
531
+ throw new Error('Standard material shader manifest row is missing');
532
+ }
533
+ const selected: MaterialShaderManifestEntry['variants'][number][] = [];
534
+ for (const transmissionAvailable of [false, true]) {
535
+ const declared = manifestEntry.variants.find(
536
+ (variant) =>
537
+ variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
538
+ variant.defines.CLUSTER_FORWARD_AVAILABLE === false &&
539
+ variant.defines.VERTEX_COLOR_AVAILABLE === false &&
540
+ (!('PROJECTOR_AVAILABLE' in variant.defines) ||
541
+ variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
542
+ variant.defines.TRANSMISSION_AVAILABLE === transmissionAvailable,
543
+ );
544
+ if (declared === undefined) {
545
+ throw new Error(
546
+ `Standard material shader manifest lacks exact TRANSMISSION_AVAILABLE=${transmissionAvailable} prewarm variant`,
547
+ );
548
+ }
549
+ const exact = findVariantByKey(manifestEntry, declared.definesKey);
550
+ if (exact === undefined) {
551
+ throw new Error(
552
+ `Standard material shader manifest exact lookup failed for TRANSMISSION_AVAILABLE=${transmissionAvailable}`,
553
+ );
554
+ }
555
+ selected.push(exact);
556
+ }
557
+ return selected;
558
+ }
559
+
494
560
  /**
495
561
  * Select the PBR variant that matches a fallback mesh with no COLOR_0 input.
496
562
  * Unknown/custom material IDs may still use the legacy PBR fallback, so that
@@ -500,24 +566,24 @@ export function selectNoColorPbrVariant(
500
566
  manifestEntry: MaterialShaderManifestEntry | undefined,
501
567
  storageBufferCapable: boolean,
502
568
  variantSet: string | undefined,
569
+ projectorAvailable: boolean = true,
503
570
  ): MaterialShaderManifestEntry['variants'][number] | undefined {
504
571
  const hdrpRequested =
505
- variantSet === '' || (variantSet?.includes('CLUSTER_FORWARD_AVAILABLE=true') ?? false);
572
+ variantSet === '' ||
573
+ // biome-ignore lint/complexity/useOptionalChain: G-14 requires an explicit undefined guard so no variant request becomes false.
574
+ (variantSet !== undefined && variantSet.includes('CLUSTER_FORWARD_AVAILABLE=true'));
506
575
  return manifestEntry?.variants.find(
507
576
  (variant) =>
508
577
  variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
509
578
  variant.defines.VERTEX_COLOR_AVAILABLE === false &&
579
+ (!('PROJECTOR_AVAILABLE' in variant.defines) ||
580
+ variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
510
581
  (!('CLUSTER_FORWARD_AVAILABLE' in variant.defines) ||
511
582
  variant.defines.CLUSTER_FORWARD_AVAILABLE === hdrpRequested),
512
583
  );
513
584
  }
514
585
 
515
- export type MaterialShaderBindingContract =
516
- | 'group-0'
517
- | 'group-0-resource'
518
- | 'view-only'
519
- | 'view-and-scene-depth'
520
- | 'render-material';
586
+ export type MaterialShaderBindingContract = RenderFeatureMaterialShaderBindingContract;
521
587
  export type MaterialShaderVertexInputContract = 'none' | 'render-material';
522
588
 
523
589
  export function resolveMaterialShaderBindingContract(
@@ -570,6 +636,46 @@ export function resolveMaterialShaderVertexInputContract(
570
636
  : 'none';
571
637
  }
572
638
 
639
+ /**
640
+ * Decide whether a custom material's user-region can use the shared PBR
641
+ * bind-group layout. Resource names are not part of a WebGPU layout contract:
642
+ * the only safe reuse test is that every derived binding in the custom schema
643
+ * is an identical prefix of the canonical user-region descriptor, including
644
+ * binding kind and its full shape (sample type, view dimension, sampler mode,
645
+ * dynamic-offset flags, and visibility). A texture3d/array or comparison /
646
+ * storage resource must therefore take the per-shader layout path even when
647
+ * its field count is small.
648
+ */
649
+ export function isSharedMaterialUserRegionCompatible(
650
+ paramSchema: readonly ParamSchemaEntry[],
651
+ ): boolean {
652
+ const shared = buildPbrMaterialUserRegionEntries(DEFAULT_STANDARD_PBR_PARAM_SCHEMA);
653
+ const candidate = buildPbrMaterialUserRegionEntries(paramSchema);
654
+ if (candidate.length > shared.length) return false;
655
+ return candidate.every((entry, index) => {
656
+ const canonical = shared[index];
657
+ if (canonical === undefined) return false;
658
+ return (
659
+ JSON.stringify({
660
+ binding: entry.binding,
661
+ visibility: entry.visibility,
662
+ buffer: entry.buffer ?? null,
663
+ sampler: entry.sampler ?? null,
664
+ texture: entry.texture ?? null,
665
+ storageTexture: entry.storageTexture ?? null,
666
+ }) ===
667
+ JSON.stringify({
668
+ binding: canonical.binding,
669
+ visibility: canonical.visibility,
670
+ buffer: canonical.buffer ?? null,
671
+ sampler: canonical.sampler ?? null,
672
+ texture: canonical.texture ?? null,
673
+ storageTexture: canonical.storageTexture ?? null,
674
+ })
675
+ );
676
+ });
677
+ }
678
+
573
679
  export function allowsUnlitPreparedFallback(
574
680
  depthFormatOverride: GPUTextureFormat | null | undefined,
575
681
  vertexLayout: string | undefined,
@@ -632,6 +738,13 @@ export function selectPipelineLayoutForVariant(
632
738
  if (layoutKind === 'unlit-urp') {
633
739
  return state.pbrPipelineLayout;
634
740
  }
741
+ // Static PBR callers can use the canonical all-true variant key (`''`)
742
+ // without becoming HDRP. Keep the shader variant identity independent from
743
+ // the pipeline-layout identity; shadow-caster static draws use this explicit
744
+ // kind so their ordinary mesh bind group remains compatible with the PSO.
745
+ if (layoutKind === 'pbr') {
746
+ return state.pbrPipelineLayout;
747
+ }
635
748
  // HDRP variant matches when:
636
749
  // - layoutKind === 'hdrp-pbr' (caller-driven, future-proof), OR
637
750
  // - variantSet is the canonical all-true key '' (boot-time HDRP
@@ -667,10 +780,13 @@ export function resolveMaterialShaderVariantSet(
667
780
  }>,
668
781
  backendKind: RhiDevice['caps']['backendKind'],
669
782
  storageBuffer: boolean,
783
+ projectorAvailable: boolean = true,
670
784
  ): string | undefined {
671
785
  const declaredAxes = new Set(variants.flatMap((variant) => Object.keys(variant.defines)));
672
786
  const declaresCapabilityVariants =
673
- declaredAxes.has('WEBGL2_COMPAT') || declaredAxes.has('STORAGE_BUFFER_AVAILABLE');
787
+ declaredAxes.has('WEBGL2_COMPAT') ||
788
+ declaredAxes.has('STORAGE_BUFFER_AVAILABLE') ||
789
+ declaredAxes.has('PROJECTOR_AVAILABLE');
674
790
  if (!declaresCapabilityVariants) return requestedVariantSet;
675
791
 
676
792
  const requested: Record<string, boolean> = {};
@@ -685,7 +801,19 @@ export function resolveMaterialShaderVariantSet(
685
801
  ? backendKind === 'wgpu-webgl2'
686
802
  : axis === 'STORAGE_BUFFER_AVAILABLE'
687
803
  ? backendKind !== 'wgpu-webgl2' && storageBuffer
688
- : (requested[axis] ?? true);
804
+ : axis === 'SKINNING_DISABLED'
805
+ ? (requested[axis] ?? true)
806
+ : axis === 'CLUSTER_FORWARD_AVAILABLE'
807
+ ? backendKind !== 'wgpu-webgl2' && (requested[axis] ?? false)
808
+ : axis === 'PER_INSTANCE_REGION'
809
+ ? requestedVariantSet === '' || (requested[axis] ?? false)
810
+ : axis === 'VERTEX_COLOR_AVAILABLE'
811
+ ? requestedVariantSet === '' || (requested[axis] ?? false)
812
+ : axis === 'TRANSMISSION_AVAILABLE'
813
+ ? (requested[axis] ?? false)
814
+ : axis === 'PROJECTOR_AVAILABLE'
815
+ ? projectorAvailable
816
+ : (requested[axis] ?? true);
689
817
  }
690
818
  const sorted = Object.entries(filtered).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
691
819
  return sorted.every(([, value]) => value)
@@ -875,11 +1003,38 @@ const COMPRESSION_FEATURES: GPUFeatureName[] = [
875
1003
  'texture-compression-astc',
876
1004
  ];
877
1005
 
1006
+ // The Standard transmission fragment ABI needs 17 sampled textures across its
1007
+ // material, IBL, clustered-light, and SSAO bindings. Ordinary Standard draws
1008
+ // use the non-transmission variant and remain valid on the WebGPU minimum of
1009
+ // 16; only request the elevated limit when the adapter can provide it.
1010
+ const STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES = 17;
1011
+
878
1012
  function deviceOptionsForAdapter(
879
- adapter: Pick<RhiAdapter, 'features'>,
880
- ): { requiredFeatures: GPUFeatureName[] } | undefined {
1013
+ adapter: Pick<RhiAdapter, 'features' | 'limits'>,
1014
+ options?: Pick<RendererOptions, 'captureGpuTimings'>,
1015
+ ):
1016
+ | {
1017
+ requiredFeatures?: GPUFeatureName[];
1018
+ requiredLimits?: GPUDeviceDescriptor['requiredLimits'];
1019
+ }
1020
+ | undefined {
881
1021
  const requiredFeatures = COMPRESSION_FEATURES.filter((feature) => adapter.features.has(feature));
882
- return requiredFeatures.length > 0 ? { requiredFeatures } : undefined;
1022
+ const supportsTransmission =
1023
+ (adapter.limits.maxSampledTexturesPerShaderStage ?? 0) >=
1024
+ STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
1025
+ if (options?.captureGpuTimings === true && adapter.features.has('timestamp-query')) {
1026
+ requiredFeatures.push('timestamp-query');
1027
+ }
1028
+ return {
1029
+ ...(requiredFeatures.length > 0 ? { requiredFeatures } : {}),
1030
+ ...(supportsTransmission
1031
+ ? {
1032
+ requiredLimits: {
1033
+ maxSampledTexturesPerShaderStage: STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
1034
+ },
1035
+ }
1036
+ : {}),
1037
+ };
883
1038
  }
884
1039
 
885
1040
  /**
@@ -929,7 +1084,7 @@ async function tryCreateWebGPURenderer(
929
1084
  const adapter = adapterResult.value;
930
1085
  // M4 w28: filter compression features from adapter.features (AC-07). The
931
1086
  // same helper is used by recover() below so both paths share one profile.
932
- const deviceOpts = deviceOptionsForAdapter(adapter);
1087
+ const deviceOpts = deviceOptionsForAdapter(adapter, options);
933
1088
  const result = await adapter.requestDevice(deviceOpts);
934
1089
  if (!result.ok) {
935
1090
  return { kind: 'rhi-err', error: result.error };
@@ -1059,6 +1214,8 @@ interface WebGPURendererInternals {
1059
1214
  * has wired the controller; identity stable across grow events.
1060
1215
  */
1061
1216
  meshSsboState?: MeshSsboState;
1217
+ /** Device-bound, manifest-owned volumetric utility shader sources. */
1218
+ volumetricFogShaders?: VolumetricFogShaderSources | undefined;
1062
1219
  /** Host-owned producer features; created before device-bound assembly starts. */
1063
1220
  featureHost?: RenderFeatureHost;
1064
1221
  /** App-owned overlay capability read by the typed graph at frame time. */
@@ -1097,16 +1254,34 @@ async function makeWebGPURenderer(
1097
1254
  // compile across both paths (charter P4 consistent abstraction +
1098
1255
  // architecture-principles #6 idempotency).
1099
1256
  let sharedShaderModuleAdapter: ShaderDeviceAdapterInternal | null = null;
1257
+ // Generated feature programs (currently VFX) opt into the handle-first
1258
+ // adapter. Keep it separate from the validated adapter so ordinary render
1259
+ // pipelines retain the compiler-readiness barrier that prevents first-frame
1260
+ // black output on Dawn.
1261
+ let sharedImmediateShaderModuleAdapter: ShaderDeviceAdapterInternal | null = null;
1100
1262
  const getShaderModuleAdapter = (): ShaderDeviceAdapterInternal => {
1101
1263
  if (sharedShaderModuleAdapter === null) {
1102
1264
  sharedShaderModuleAdapter = makeShaderDeviceAdapter(
1103
1265
  internals.device,
1104
1266
  internals.errorRegistry,
1105
1267
  internals.pack.createShaderModule,
1268
+ internals.pack.createShaderModuleImmediate,
1106
1269
  );
1107
1270
  }
1108
1271
  return sharedShaderModuleAdapter;
1109
1272
  };
1273
+ const getImmediateShaderModuleAdapter = (): ShaderDeviceAdapterInternal => {
1274
+ if (sharedImmediateShaderModuleAdapter === null) {
1275
+ sharedImmediateShaderModuleAdapter = makeShaderDeviceAdapter(
1276
+ internals.device,
1277
+ internals.errorRegistry,
1278
+ internals.pack.createShaderModule,
1279
+ internals.pack.createShaderModuleImmediate,
1280
+ 'immediate',
1281
+ );
1282
+ }
1283
+ return sharedImmediateShaderModuleAdapter;
1284
+ };
1110
1285
  const getShader = (): ShaderCatalog => {
1111
1286
  if (shaderInstance === null) {
1112
1287
  // feat-20260608-create-app-param-surface-trim / M2 / D-2 q5-A:
@@ -1326,13 +1501,13 @@ async function makeWebGPURenderer(
1326
1501
  >();
1327
1502
  const preparedMaterialPipelineLayoutCache = new Map<string, PipelineLayout>();
1328
1503
  // feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): per-shader material
1329
- // BGL + pipeline layout cache. A custom material shader declaring >3
1330
- // user-region textures (e.g. parallax `heightTexture`) needs a material BGL
1331
- // whose user-region + injection start differ from the shared built-in
1332
- // derived shared layout, plus a pipeline layout [view, perShaderMaterialBgl,
1333
- // meshArray, instances] that the PSO builds against. Built lazily on first
1334
- // request and cached by shaderId; 4-or-fewer-texture shaders never enter the cache
1335
- // (they reuse the shared layout, byte-for-byte the built-in shape — D-2).
1504
+ // BGL + pipeline layout cache. Every authored material shader owns the
1505
+ // user-region binding order emitted by its paramSchema, plus the shared
1506
+ // engine-injection suffix. A custom shader with four-or-fewer textures can
1507
+ // still differ from the standard PBR order (for example, baseColorTexture
1508
+ // followed directly by normalTexture), so it cannot safely reuse the
1509
+ // standard material BGL. Built lazily on first request and cached by
1510
+ // shaderId; built-in shader IDs continue to use the boot-time shared layout.
1336
1511
  type PerShaderMaterialLayout = {
1337
1512
  materialBgl: BindGroupLayout;
1338
1513
  pipelineLayout: PipelineLayout;
@@ -1343,6 +1518,16 @@ async function makeWebGPURenderer(
1343
1518
  readonly layout: PerShaderMaterialLayout | null;
1344
1519
  };
1345
1520
  const perShaderMaterialLayoutCache = new Map<string, PerShaderMaterialLayoutCacheEntry>();
1521
+ const isEngineOwnedMaterialShader = (materialShaderId: string): boolean =>
1522
+ materialShaderId === 'forgeax::default-unlit' ||
1523
+ materialShaderId === 'forgeax::default-shadow-caster' ||
1524
+ materialShaderId === 'forgeax::sprite' ||
1525
+ materialShaderId === 'forgeax::sprite-lit' ||
1526
+ materialShaderId === 'forgeax::default-sprite' ||
1527
+ materialShaderId === 'forgeax::msdf-text' ||
1528
+ materialShaderId === 'forgeax::default-standard-pbr' ||
1529
+ materialShaderId === 'forgeax::pbr-skin' ||
1530
+ materialShaderId === 'forgeax::default-standard-pbr-skin';
1346
1531
  // Material binding contracts are derived from WGSL source, but the lookup
1347
1532
  // is also used by the per-submesh bind-group path. Cache the derived value
1348
1533
  // per renderer so a frame does not re-run the comment stripping and regex
@@ -1477,7 +1662,9 @@ async function makeWebGPURenderer(
1477
1662
  if (cached !== undefined) return cached;
1478
1663
  if (pipelineState === null) return null;
1479
1664
  const materialBgl =
1480
- getOrBuildPerShaderMaterialLayout(materialShaderId)?.materialBgl ??
1665
+ (isEngineOwnedMaterialShader(materialShaderId)
1666
+ ? undefined
1667
+ : getOrBuildPerShaderMaterialLayout(materialShaderId)?.materialBgl) ??
1481
1668
  pipelineState.materialBindGroupLayout;
1482
1669
  const result = internals.device.createPipelineLayout({
1483
1670
  label: `prepared-material-pl-${materialShaderId}`,
@@ -1490,15 +1677,15 @@ async function makeWebGPURenderer(
1490
1677
  preparedMaterialPipelineLayoutCache.set(materialShaderId, result.value);
1491
1678
  return result.value;
1492
1679
  };
1493
- // Returns the per-shader { materialBgl, pipelineLayout } for a custom shader
1494
- // whose user-region texture count exceeds the built-in 4, or null when the
1495
- // shader resolves to the shared built-in layout (4 textures) / is
1496
- // unregistered / pipelineState is not ready. Pure-ish: builds + caches on
1497
- // miss via the same buildBindGroupLayoutDescriptor SSOT (kind
1498
- // 'pbr-material-merged' + materialParamSchema) used by buildPbrPipelineLayouts.
1680
+ // Returns the per-shader { materialBgl, pipelineLayout } for an authored
1681
+ // shader, or null when it is unregistered / pipelineState is not ready.
1682
+ // Pure-ish: builds + caches on miss via the same
1683
+ // buildBindGroupLayoutDescriptor SSOT (kind 'pbr-material-merged' +
1684
+ // materialParamSchema) used by buildPbrPipelineLayouts.
1499
1685
  const getOrBuildPerShaderMaterialLayout = (
1500
1686
  materialShaderId: string,
1501
1687
  ): PerShaderMaterialLayout | null => {
1688
+ if (isEngineOwnedMaterialShader(materialShaderId)) return null;
1502
1689
  const lookup = getShader().findMaterialArtifact(materialShaderId);
1503
1690
  if (!lookup.ok) return null;
1504
1691
  const cached = perShaderMaterialLayoutCache.get(materialShaderId);
@@ -1507,9 +1694,11 @@ async function makeWebGPURenderer(
1507
1694
  }
1508
1695
  if (pipelineState === null) return null;
1509
1696
  const paramSchema = lookup.value.paramSchema;
1510
- // 4-or-fewer user-region textures derive to the same shape as the shared
1511
- // built-in BGL -> reuse it (D-2; no per-shader entry).
1512
- if (derive(paramSchema).textureFieldNames.size <= 4) {
1697
+ // Reuse the shared BGL only when the complete derived resource shape is a
1698
+ // canonical prefix. Field names alone are not a layout contract: a
1699
+ // texture3d/array, comparison sampler, or storage buffer must never be
1700
+ // paired with the PBR texture2d/filtering descriptor by accident.
1701
+ if (isSharedMaterialUserRegionCompatible(paramSchema)) {
1513
1702
  perShaderMaterialLayoutCache.set(materialShaderId, {
1514
1703
  source: lookup.value.source,
1515
1704
  paramSchema,
@@ -1563,6 +1752,8 @@ async function makeWebGPURenderer(
1563
1752
  [],
1564
1753
  internals.device.caps.backendKind,
1565
1754
  internals.device.caps.storageBuffer,
1755
+ (internals.device.limits.maxSampledTexturesPerShaderStage ?? 0) >=
1756
+ STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
1566
1757
  );
1567
1758
  }
1568
1759
  const variants = manifestEntry.variants;
@@ -1571,13 +1762,17 @@ async function makeWebGPURenderer(
1571
1762
  byRequest = new Map();
1572
1763
  materialShaderVariantResolutionCache.set(variants, byRequest);
1573
1764
  }
1574
- const cacheKey = `${requestedVariantSet ?? '\u0000'}|${internals.device.caps.backendKind}|${internals.device.caps.storageBuffer ? '1' : '0'}`;
1765
+ const projectorAvailable =
1766
+ (internals.device.limits.maxSampledTexturesPerShaderStage ?? 0) >=
1767
+ STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
1768
+ const cacheKey = `${requestedVariantSet ?? '\u0000'}|${internals.device.caps.backendKind}|${internals.device.caps.storageBuffer ? '1' : '0'}|${projectorAvailable ? '1' : '0'}`;
1575
1769
  if (byRequest.has(cacheKey)) return byRequest.get(cacheKey);
1576
1770
  const resolved = resolveMaterialShaderVariantSet(
1577
1771
  requestedVariantSet,
1578
1772
  variants,
1579
1773
  internals.device.caps.backendKind,
1580
1774
  internals.device.caps.storageBuffer,
1775
+ projectorAvailable,
1581
1776
  );
1582
1777
  byRequest.set(cacheKey, resolved);
1583
1778
  return resolved;
@@ -1603,9 +1798,11 @@ async function makeWebGPURenderer(
1603
1798
  // `internals.device` / `internals.pack` are read live, so a rebuild after a
1604
1799
  // device swap compiles against the new device.
1605
1800
  let tonemapRegistered = false;
1606
- const buildPipeline = (): Promise<PipelineState> =>
1801
+ let temporalPostProcessesRegistered = false;
1802
+ const buildPipeline = (scope: DeviceScope = activeDeviceScope): Promise<PipelineState> =>
1607
1803
  buildReadyWebGPU(
1608
1804
  internals.device,
1805
+ scope,
1609
1806
  getShader,
1610
1807
  gpuStore,
1611
1808
  internals.pack.createShaderModule,
@@ -1644,13 +1841,34 @@ async function makeWebGPURenderer(
1644
1841
  // tonemap into this channel each frame (render-system-extract.ts w13).
1645
1842
  (source: string) => {
1646
1843
  if (tonemapRegistered) return;
1647
- renderSystem.registerBuiltinPostProcess(STANDARD_TONEMAP_FEATURE_ID, {
1844
+ renderSystem.registerBuiltinPostProcess(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, {
1648
1845
  source,
1649
1846
  params: { byteSize: 16, defaultValue: new Uint8Array(16) },
1650
1847
  reads: ['hdrColor'],
1651
1848
  });
1652
1849
  tonemapRegistered = true;
1653
1850
  },
1851
+ (entries: { readonly motionBlur?: string; readonly taaResolve?: string }) => {
1852
+ if (temporalPostProcessesRegistered) return;
1853
+ if (entries.motionBlur !== undefined) {
1854
+ renderSystem.registerBuiltinPostProcess('forgeax.motion-blur', {
1855
+ source: entries.motionBlur,
1856
+ params: { byteSize: 16, defaultValue: new Uint8Array(16) },
1857
+ reads: ['scene-color', 'scene-temporal'],
1858
+ });
1859
+ }
1860
+ if (entries.taaResolve !== undefined) {
1861
+ renderSystem.registerBuiltinPostProcess('forgeax.taa-resolve', {
1862
+ source: entries.taaResolve,
1863
+ params: { byteSize: 16, defaultValue: new Uint8Array(16) },
1864
+ reads: ['scene-color', 'scene-temporal', 'taa-history-color', 'taa-history-temporal'],
1865
+ });
1866
+ }
1867
+ temporalPostProcessesRegistered = true;
1868
+ },
1869
+ (sources: VolumetricFogShaderSources) => {
1870
+ internals.volumetricFogShaders = sources;
1871
+ },
1654
1872
  );
1655
1873
  const ready: Promise<Result<void, RhiError>> = buildPipeline().then(
1656
1874
  (state): Result<void, RhiError> => {
@@ -1696,6 +1914,8 @@ async function makeWebGPURenderer(
1696
1914
  pbrManifestEntry,
1697
1915
  internals.device.caps.storageBuffer,
1698
1916
  variantSet,
1917
+ (internals.device.limits.maxSampledTexturesPerShaderStage ?? 0) >=
1918
+ STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
1699
1919
  );
1700
1920
  if (noColorVariant !== undefined && noColorVariant.composedWgsl !== standardEntry.wgsl) {
1701
1921
  return { ...standardEntry, wgsl: noColorVariant.composedWgsl };
@@ -1730,6 +1950,8 @@ async function makeWebGPURenderer(
1730
1950
  meshAttributes?: VertexAttributeMap,
1731
1951
  vertexLayout?: string,
1732
1952
  vertexLayoutProjection?: VertexLayoutProjection,
1953
+ shaderModuleMode?: RenderFeatureShaderModuleMode,
1954
+ layoutKindOverride?: LayoutKind,
1733
1955
  ) => {
1734
1956
  // bug-20260611: derive LayoutKind from materialShaderId (caller-driven)
1735
1957
  // so the selector body stays free of literal shader-id strings (AC-09).
@@ -1741,17 +1963,17 @@ async function makeWebGPURenderer(
1741
1963
  // through the HDRP layout branch of the selector — sprite shaders have
1742
1964
  // no HDRP variant, plan-strategy R-1' guard.
1743
1965
  const layoutKind: LayoutKind | undefined =
1744
- materialShaderId === SKIN_MATERIAL_SHADER_ID
1966
+ layoutKindOverride ??
1967
+ (materialShaderId === SKIN_MATERIAL_SHADER_ID ||
1968
+ isSkinnedShadowCasterVariant(materialShaderId, variantSet)
1745
1969
  ? 'pbr-skin'
1746
1970
  : materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit'
1747
1971
  ? 'sprite-urp'
1748
1972
  : materialShaderId === 'forgeax::default-unlit'
1749
1973
  ? 'unlit-urp'
1750
- : undefined;
1974
+ : undefined);
1751
1975
  const usesSharedBootMaterialLayout =
1752
- materialShaderId === 'forgeax::default-standard-pbr' ||
1753
- materialShaderId === 'forgeax::default-unlit' ||
1754
- materialShaderId === SKIN_MATERIAL_SHADER_ID;
1976
+ materialShaderId !== undefined && isEngineOwnedMaterialShader(materialShaderId);
1755
1977
  // Built-in PBR/unlit/skin layouts are assembled at boot. Re-deriving a
1756
1978
  // per-shader layout from the expanded PBR schema would bypass the HDRP
1757
1979
  // group(2) selector and bind the URP mesh BGL to the cluster pipeline.
@@ -1759,12 +1981,11 @@ async function makeWebGPURenderer(
1759
1981
  !usesSharedBootMaterialLayout && layoutKind !== 'pbr-skin' && materialShaderId !== undefined
1760
1982
  ? getOrBuildPerShaderMaterialLayout(materialShaderId)
1761
1983
  : null;
1762
- // feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): a custom shader
1763
- // with >3 user-region textures owns a per-shader pipeline layout (its
1764
- // material BGL is wider than the shared built-in 18-entry). Skin / HDRP
1765
- // keep their dedicated layouts; everything else falls back to the shared
1766
- // URP/HDRP selector. 3-texture customs return null here -> shared layout
1767
- // (D-2 byte-for-byte).
1984
+ // feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): an authored
1985
+ // shader owns a per-shader pipeline layout derived from its paramSchema,
1986
+ // because its compact texture order may differ from standard PBR even
1987
+ // when it declares only a few textures. Skin / HDRP keep their dedicated
1988
+ // layouts; built-in material IDs remain on the shared boot-time layout.
1768
1989
  const bindingContract =
1769
1990
  materialShaderId === undefined
1770
1991
  ? 'render-material'
@@ -1900,9 +2121,13 @@ async function makeWebGPURenderer(
1900
2121
  ] satisfies readonly GPUVertexBufferLayout[]);
1901
2122
  return {
1902
2123
  device: internals.device,
1903
- shaderModuleFactory: getShaderModuleAdapter(),
2124
+ shaderModuleFactory:
2125
+ shaderModuleMode === 'immediate'
2126
+ ? getImmediateShaderModuleAdapter()
2127
+ : getShaderModuleAdapter(),
1904
2128
  pipelineLayout,
1905
2129
  vertexBuffers,
2130
+ ...(layoutKind === 'pbr-skin' ? { layoutKind } : {}),
1906
2131
  };
1907
2132
  };
1908
2133
  /**
@@ -1960,6 +2185,8 @@ async function makeWebGPURenderer(
1960
2185
  depthFormatOverride?: GPUTextureFormat | null,
1961
2186
  vertexLayout?: string,
1962
2187
  vertexLayoutProjection?: VertexLayoutProjection,
2188
+ shaderModuleMode?: RenderFeatureShaderModuleMode,
2189
+ layoutKindOverride?: LayoutKind,
1963
2190
  ): RenderPipeline | null => {
1964
2191
  const ctx = buildPipelineContext(
1965
2192
  variantSet,
@@ -1967,6 +2194,8 @@ async function makeWebGPURenderer(
1967
2194
  meshAttributes,
1968
2195
  vertexLayout,
1969
2196
  vertexLayoutProjection,
2197
+ shaderModuleMode,
2198
+ layoutKindOverride,
1970
2199
  );
1971
2200
  if (ctx === null) return null;
1972
2201
  // bug-20260612: route the LDR color format through pipelineState so the
@@ -1976,6 +2205,11 @@ async function makeWebGPURenderer(
1976
2205
  // the explicit null check).
1977
2206
  if (pipelineState === null) return null;
1978
2207
  const ldrColorFormat = colorFormatOverride ?? pipelineState.colorAttachmentFormat;
2208
+ const fragmentEntry =
2209
+ isHdr &&
2210
+ (materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit')
2211
+ ? 'fs_main_hdr'
2212
+ : undefined;
1979
2213
  const built = buildPipelineForMaterialShader(
1980
2214
  cacheKey,
1981
2215
  // The catalog entry is projected into the material-shader entry shape.
@@ -2009,7 +2243,7 @@ async function makeWebGPURenderer(
2009
2243
  }
2010
2244
  : undefined,
2011
2245
  undefined, // vertexEntry — default vs_main
2012
- undefined, // fragmentEntry — default fs_main
2246
+ fragmentEntry,
2013
2247
  undefined, // defines — none
2014
2248
  passKind,
2015
2249
  sampleCount,
@@ -2068,6 +2302,8 @@ async function makeWebGPURenderer(
2068
2302
  depthFormatOverride?: GPUTextureFormat | null,
2069
2303
  vertexLayout?: string,
2070
2304
  vertexLayoutProjection?: VertexLayoutProjection,
2305
+ shaderModuleMode: RenderFeatureShaderModuleMode = 'validated',
2306
+ layoutKindOverride?: LayoutKind,
2071
2307
  ): RenderPipeline | null => {
2072
2308
  // WebGL2 is a downlevel backend even when the record stage asks for the
2073
2309
  // native-capability variant string. Resolve the variant axes at this
@@ -2082,7 +2318,14 @@ async function makeWebGPURenderer(
2082
2318
  // (stored in materialShaderUvSetCounts during prepareMaterialShaders).
2083
2319
  const resolvedUvSetCount = shaderUvSetCount ?? materialShaderUvSetCounts.get(materialShaderId);
2084
2320
  const manifestEntry = findMaterialShaderManifestEntry(materialShaderId);
2085
- const resolvedVariantSet = resolveCachedMaterialShaderVariantSet(variantSet, manifestEntry);
2321
+ // Sprite's omitted request is the boot-selected default artifact. The
2322
+ // explicit empty key remains the PER_INSTANCE_REGION=true request used by
2323
+ // SpriteInstances, so only that path is capability-rewritten here.
2324
+ const resolvedVariantSet =
2325
+ variantSet === undefined &&
2326
+ (materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit')
2327
+ ? undefined
2328
+ : resolveCachedMaterialShaderVariantSet(variantSet, manifestEntry);
2086
2329
  // The record stage also composes engine capability axes for user shaders.
2087
2330
  // Keep only manifest-declared axes for non-engine material IDs: a plain
2088
2331
  // custom shader must not collapse that request to '' because '' is the
@@ -2227,6 +2470,7 @@ async function makeWebGPURenderer(
2227
2470
  depthFormatOverride,
2228
2471
  vertexLayout,
2229
2472
  vertexLayoutProjection,
2473
+ shaderModuleMode,
2230
2474
  );
2231
2475
  }
2232
2476
  const pbrEntry = findStandardPbrEntry(effectiveVariantSet);
@@ -2280,6 +2524,7 @@ async function makeWebGPURenderer(
2280
2524
  depthFormatOverride,
2281
2525
  vertexLayout,
2282
2526
  vertexLayoutProjection,
2527
+ shaderModuleMode,
2283
2528
  );
2284
2529
  }
2285
2530
  // feat-20260609 M4 / w31: resolve variant WGSL from manifest when
@@ -2370,6 +2615,8 @@ async function makeWebGPURenderer(
2370
2615
  depthFormatOverride,
2371
2616
  vertexLayout,
2372
2617
  vertexLayoutProjection,
2618
+ shaderModuleMode,
2619
+ layoutKindOverride,
2373
2620
  );
2374
2621
  };
2375
2622
  const getParamSchema = (materialShaderId: string) => {
@@ -2378,10 +2625,11 @@ async function makeWebGPURenderer(
2378
2625
  };
2379
2626
  // feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): expose the per-shader
2380
2627
  // material BGL so the record stage creates the material bind group against
2381
- // the matching layout (empty for group-0 shaders, wider for >3-texture
2382
- // custom shaders). Returns undefined for the ordinary 3-texture path so the
2383
- // record stage falls back to the shared built-in materialBindGroupLayout.
2628
+ // the matching paramSchema-derived layout. Returns undefined only for
2629
+ // group-0 / view-only contracts or an unavailable shader layout; built-in
2630
+ // material IDs continue to resolve through the shared layout path.
2384
2631
  const getMaterialBindGroupLayout = (materialShaderId: string): BindGroupLayout | undefined => {
2632
+ if (isEngineOwnedMaterialShader(materialShaderId)) return undefined;
2385
2633
  const contract = getCachedMaterialShaderBindingContract(materialShaderId);
2386
2634
  if (contract === 'group-0') {
2387
2635
  return getOrBuildGroup0MaterialLayout()?.materialBgl;
@@ -2458,7 +2706,11 @@ async function makeWebGPURenderer(
2458
2706
  fragment: {
2459
2707
  module: moduleResult.value,
2460
2708
  entryPoint: 'fs_main',
2461
- targets: [{ format: colorFormat }],
2709
+ targets:
2710
+ entry.source.includes('@location(1) temporal') &&
2711
+ entry.source.includes('TaaResolveOutput')
2712
+ ? [{ format: colorFormat }, { format: colorFormat }]
2713
+ : [{ format: colorFormat }],
2462
2714
  },
2463
2715
  primitive: { topology: 'triangle-list', cullMode: 'none', frontFace: 'ccw' },
2464
2716
  depthStencil: undefined,
@@ -2487,6 +2739,10 @@ async function makeWebGPURenderer(
2487
2739
  shaderModuleFactory: {
2488
2740
  createShaderModule: (descriptor) => getShaderModuleAdapter().createShaderModule(descriptor),
2489
2741
  },
2742
+ immediateShaderModuleFactory: {
2743
+ createShaderModule: (descriptor) =>
2744
+ getImmediateShaderModuleAdapter().createShaderModule(descriptor),
2745
+ },
2490
2746
  profiler: internals.options?.profiler,
2491
2747
  // feat-20260622-s5 M3 / w17: device + context read live off `internals` via
2492
2748
  // getters so the recover() rebuild (which swaps internals.device /
@@ -2499,6 +2755,9 @@ async function makeWebGPURenderer(
2499
2755
  get deviceScope() {
2500
2756
  return activeDeviceScope;
2501
2757
  },
2758
+ invalidateShaderModule: (label: string) => {
2759
+ sharedShaderModuleAdapter?.invalidateModule(label);
2760
+ },
2502
2761
  get context() {
2503
2762
  return internals.context;
2504
2763
  },
@@ -2535,7 +2794,22 @@ async function makeWebGPURenderer(
2535
2794
  get meshSsboState() {
2536
2795
  return internals.meshSsboState;
2537
2796
  },
2797
+ get volumetricFogShaders() {
2798
+ return internals.volumetricFogShaders;
2799
+ },
2538
2800
  });
2801
+ if (
2802
+ internals.device.caps.backendKind === 'null' &&
2803
+ Array.from(getShader().entries()).length === 0
2804
+ ) {
2805
+ // RhiNull validates graph topology only; keep the built-in identity
2806
+ // registered so structural frames do not fail before graph inspection.
2807
+ renderSystem.registerBuiltinPostProcess(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, {
2808
+ source: '',
2809
+ params: { byteSize: 16, defaultValue: new Uint8Array(16) },
2810
+ reads: ['hdrColor'],
2811
+ });
2812
+ }
2539
2813
  // The Standard pipeline is the sole built-in graph owner. Feature plans are
2540
2814
  // projected into that graph directly; no second post-process registry is
2541
2815
  // installed for fullscreen producers.
@@ -2548,7 +2822,8 @@ async function makeWebGPURenderer(
2548
2822
  const transformReleases = new Map<World, () => void>();
2549
2823
  const attachmentOwner = {};
2550
2824
  let frameId = 0;
2551
- let latestReceipt: FrameReceipt | undefined;
2825
+ const issuedReceipts = new WeakSet<FrameReceipt>();
2826
+ const receiptTimings = new WeakMap<FrameReceipt, Promise<VolumeTimingObservation>>();
2552
2827
  let surfaceReleased = false;
2553
2828
 
2554
2829
  const renderer: RendererAssemblyImplementation = {
@@ -2596,6 +2871,23 @@ async function makeWebGPURenderer(
2596
2871
  : health.reason === 'internal-fault' || !readySettled
2597
2872
  ? 'faulted'
2598
2873
  : 'alive';
2874
+ const surfaceStorage = pipelineState?.format ?? 'rgba8unorm';
2875
+ const surfaceDisplay = pipelineState?.colorAttachmentFormat ?? 'rgba8unorm';
2876
+ const observationId = `${STANDARD_OUTPUT_TRANSFORM_FEATURE_ID}:frame-${frameId}`;
2877
+ const outputInspection = projectRendererOutputInspection({
2878
+ graph: renderSystem.perFrameGraphInfo,
2879
+ surfaceAvailable: !surfaceReleased && pipelineState !== null,
2880
+ surfaceStorage,
2881
+ surfaceDisplay,
2882
+ ...(pipelineState?.surfaceProfile === undefined
2883
+ ? {}
2884
+ : { surfaceProfile: pipelineState.surfaceProfile }),
2885
+ rgba16floatRenderable: internals.device.caps.rgba16floatRenderable,
2886
+ ...(internals.context?.presentationProof === undefined
2887
+ ? {}
2888
+ : { presentationProof: internals.context.presentationProof }),
2889
+ });
2890
+ const volumetricFog = renderSystem.volumetricFog;
2599
2891
  return Object.freeze({
2600
2892
  state,
2601
2893
  surface: surfaceReleased ? 'released' : 'available',
@@ -2609,9 +2901,22 @@ async function makeWebGPURenderer(
2609
2901
  (internals.featureHost?.features ?? []).map((feature) => feature.identity),
2610
2902
  ),
2611
2903
  featureDiagnostics: internals.featureHost?.diagnostics() ?? Object.freeze([]),
2904
+ volumetricFog,
2612
2905
  frustumStats: Object.freeze({ ...renderSystem.frustumStats }),
2613
2906
  visibilityStats: Object.freeze({ ...renderSystem.visibilityStats }),
2614
2907
  renderScene: renderSystem.renderScene,
2908
+ environment: renderSystem.environment,
2909
+ temporal: renderSystem.temporal,
2910
+ bloom: renderSystem.bloom,
2911
+ ...(renderSystem.temporalTargetInspection === undefined
2912
+ ? {}
2913
+ : { temporalTarget: Object.freeze(renderSystem.temporalTargetInspection) }),
2914
+ ...(renderSystem.motionBlurInspection === undefined
2915
+ ? {}
2916
+ : { motionBlur: Object.freeze(renderSystem.motionBlurInspection) }),
2917
+ ...(renderSystem.transmission === undefined
2918
+ ? {}
2919
+ : { transmission: renderSystem.transmission }),
2615
2920
  meshMaterialBindings: Object.freeze(
2616
2921
  renderSystem.meshMaterialBindings.map((observation) =>
2617
2922
  Object.freeze({
@@ -2630,6 +2935,22 @@ async function makeWebGPURenderer(
2630
2935
  createBindGroup: renderSystem.bindGroupCounts.createBindGroup,
2631
2936
  keys: Object.freeze([...renderSystem.bindGroupCounts.keys]),
2632
2937
  }),
2938
+ ...outputInspection,
2939
+ observation: Object.freeze({
2940
+ observationId,
2941
+ frameId,
2942
+ ...(renderSystem.lastSuccessfulCameraAntialias === undefined
2943
+ ? {}
2944
+ : { antialias: renderSystem.lastSuccessfulCameraAntialias }),
2945
+ ...(outputInspection.surfaceProfile === undefined
2946
+ ? {}
2947
+ : { surfaceProfile: outputInspection.surfaceProfile }),
2948
+ rgba16floatRenderable: internals.device.caps.rgba16floatRenderable,
2949
+ passNames: Object.freeze([...outputInspection.graphPassNames]),
2950
+ ...(outputInspection.standardOutputColor === undefined
2951
+ ? {}
2952
+ : { standardOutputColor: Object.freeze({ ...outputInspection.standardOutputColor }) }),
2953
+ }),
2633
2954
  });
2634
2955
  },
2635
2956
  get device(): RhiDevice {
@@ -2720,6 +3041,12 @@ async function makeWebGPURenderer(
2720
3041
  observeCurrentFrame(options: FrameObservationOptions) {
2721
3042
  return renderSystem.observeCurrentFrame(options);
2722
3043
  },
3044
+ getCurrentGraphTarget(name: string) {
3045
+ return renderSystem.getCurrentGraphTarget(name);
3046
+ },
3047
+ requestGraphTargetCapture(request) {
3048
+ renderSystem.requestGraphTargetCapture(request);
3049
+ },
2723
3050
  get frustumStats() {
2724
3051
  return renderSystem.frustumStats;
2725
3052
  },
@@ -2991,14 +3318,34 @@ async function makeWebGPURenderer(
2991
3318
  // Configure context lazily on first draw (D-S1 single-point
2992
3319
  // exemption): GPUCanvasContext.configure({device}) needs a raw
2993
3320
  // The canvas context is configured through the RHI device scope.
2994
- ensureContextConfigured(internals, pipelineState, internals.errorRegistry);
3321
+ const contextConfigured = ensureContextConfigured(
3322
+ internals,
3323
+ pipelineState,
3324
+ internals.errorRegistry,
3325
+ );
3326
+ if (!contextConfigured.ok) return contextConfigured;
2995
3327
  // w24 — facade-level try/catch produces Result.err on unexpected throw
2996
3328
  // (D-P6 dual-channel preserved: per-stage RhiError continues to fan out
2997
3329
  // through onError separately; the facade Result is the synchronous
2998
3330
  // summary AI users can ignore or branch on).
2999
3331
  try {
3000
- const submitted = renderSystem.draw(worlds, drawOptions, renderReadLeases);
3332
+ const timingRequested =
3333
+ isFrameRequest &&
3334
+ (frameRequest?.profileFrame !== undefined ||
3335
+ internals.options?.captureGpuTimings === true);
3336
+ let timingCapture: GpuTimingCapture | undefined;
3337
+ let timingUnavailableReason = timingRequested
3338
+ ? 'timestamp capture is unavailable on this device'
3339
+ : 'timestamp capture was not requested for this frame';
3340
+ if (timingRequested) {
3341
+ const createdTiming = GpuTimingCapture.create(internals.device);
3342
+ if (createdTiming.ok) timingCapture = createdTiming.value;
3343
+ else
3344
+ timingUnavailableReason = `timestamp capture setup failed: ${createdTiming.error.code}`;
3345
+ }
3346
+ const submitted = renderSystem.draw(worlds, drawOptions, renderReadLeases, timingCapture);
3001
3347
  if (!submitted) {
3348
+ timingCapture?.discard();
3002
3349
  return err(
3003
3350
  new RendererContractFailureError(
3004
3351
  'draw',
@@ -3028,7 +3375,15 @@ async function makeWebGPURenderer(
3028
3375
  deviceGeneration: receiptGeneration,
3029
3376
  completed,
3030
3377
  });
3031
- latestReceipt = receipt;
3378
+ receiptTimings.set(
3379
+ receipt,
3380
+ timingCapture?.observation() ??
3381
+ Promise.resolve({
3382
+ status: 'unavailable',
3383
+ reason: timingUnavailableReason,
3384
+ }),
3385
+ );
3386
+ issuedReceipts.add(receipt);
3032
3387
  return ok(receipt);
3033
3388
  }
3034
3389
  return ok(undefined);
@@ -3053,10 +3408,8 @@ async function makeWebGPURenderer(
3053
3408
  request: FrameObservationRequest,
3054
3409
  ): Promise<RenderResult<FrameReceiptObservation, RenderError>> {
3055
3410
  if (
3056
- latestReceipt === undefined ||
3057
- latestReceipt !== receipt ||
3058
- latestReceipt.frameId !== receipt.frameId ||
3059
- latestReceipt.deviceGeneration !== receipt.deviceGeneration
3411
+ !issuedReceipts.has(receipt) ||
3412
+ receipt.deviceGeneration !== activeDeviceScope.generation
3060
3413
  ) {
3061
3414
  const currentGeneration = activeDeviceScope.generation;
3062
3415
  return err(
@@ -3079,11 +3432,19 @@ async function makeWebGPURenderer(
3079
3432
  }),
3080
3433
  );
3081
3434
  }
3435
+ const timings = request.include.includes('timings')
3436
+ ? await (receiptTimings.get(receipt) ??
3437
+ Promise.resolve({
3438
+ status: 'unavailable',
3439
+ reason: 'timing receipt was not retained',
3440
+ }))
3441
+ : undefined;
3082
3442
  return ok(
3083
3443
  Object.freeze({
3084
3444
  frameId: receipt.frameId,
3085
3445
  deviceGeneration: receipt.deviceGeneration,
3086
3446
  include: Object.freeze([...request.include]),
3447
+ ...(timings === undefined ? {} : { timings }),
3087
3448
  }),
3088
3449
  );
3089
3450
  },
@@ -3159,13 +3520,6 @@ async function makeWebGPURenderer(
3159
3520
  if (disposed) return ok(undefined);
3160
3521
  disposed = true;
3161
3522
  const cleanupFailures: RendererOperationCause[] = [];
3162
- try {
3163
- activeDeviceScope.dispose();
3164
- } catch (cause) {
3165
- const error = wrapDisposeError(cause, 'activeDeviceScope.dispose');
3166
- cleanupFailures.push(error);
3167
- internals.errorRegistry.fire(error);
3168
- }
3169
3523
  for (const world of attachedWorlds) {
3170
3524
  try {
3171
3525
  const derivedSystemName = derivedSystemNames.get(world);
@@ -3247,6 +3601,18 @@ async function makeWebGPURenderer(
3247
3601
  cleanupFailures.push(error);
3248
3602
  internals.errorRegistry.fire(error);
3249
3603
  }
3604
+ // Retire assembly-owned device resources only after the surface and
3605
+ // renderer-owned GPU pools have been released. The wgpu WebGL2 surface
3606
+ // still needs a live device while `unconfigure()` presents its pending
3607
+ // image; disposing the scope first lets wasm finalizers race that
3608
+ // presentation and can surface a parking_lot panic in WebKit.
3609
+ try {
3610
+ activeDeviceScope.dispose();
3611
+ } catch (cause) {
3612
+ const error = wrapDisposeError(cause, 'activeDeviceScope.dispose');
3613
+ cleanupFailures.push(error);
3614
+ internals.errorRegistry.fire(error);
3615
+ }
3250
3616
  // Step 5: the IBL cache is keyed by the retired DeviceScope generation.
3251
3617
  // It is no longer reachable through the active renderer scope, while
3252
3618
  // the device teardown owns the child GPU handles.
@@ -3326,6 +3692,7 @@ async function makeWebGPURenderer(
3326
3692
  // buffer (or crash the browser GPU process). Keep the ShaderCatalog's
3327
3693
  // CPU material/source catalogue, but rebuild this device-bound adapter.
3328
3694
  sharedShaderModuleAdapter = null;
3695
+ sharedImmediateShaderModuleAdapter = null;
3329
3696
  // The fullscreen post-process builder keeps one empty group-0 layout
3330
3697
  // outside RenderSystem's frame cache. It is also device-bound, so retain
3331
3698
  // the source/registration but force the next post-process pass to derive
@@ -3352,7 +3719,7 @@ async function makeWebGPURenderer(
3352
3719
  let device: RhiDevice;
3353
3720
  try {
3354
3721
  const deviceResult = await adapterResult.value.requestDevice(
3355
- deviceOptionsForAdapter(adapterResult.value),
3722
+ deviceOptionsForAdapter(adapterResult.value, internals.options),
3356
3723
  );
3357
3724
  if (!deviceResult.ok) {
3358
3725
  return err(new RecoverError('recover-device-unavailable'));
@@ -3372,6 +3739,13 @@ async function makeWebGPURenderer(
3372
3739
  // swap is observed without reconstructing the RenderSystem.
3373
3740
  internals.device = device;
3374
3741
  internals.context = ctxResult.value;
3742
+ // Build the replacement graph under a fresh renderer-owned scope. The
3743
+ // old committed scope remains authoritative until every rebuild step
3744
+ // succeeds, so a failed recovery cannot publish a half-built scope.
3745
+ const recoveryScope = DeviceScope.create(
3746
+ activeDeviceScope.generation + 1,
3747
+ activeDeviceScope.owner,
3748
+ );
3375
3749
  // Re-attach the device.lost fan-out to the new device's lost Promise,
3376
3750
  // reusing the SAME registries the host already subscribed to (SSOT
3377
3751
  // helper). When this device is lost again, health() flips to
@@ -3419,10 +3793,13 @@ async function makeWebGPURenderer(
3419
3793
  // the first post-recovery upload; unlike CPU asset payloads, these
3420
3794
  // handles cannot survive a device replacement.
3421
3795
  dynamicTextureStore.configureGpuDevice(adaptDynamicTextureDevice(internals.device));
3796
+ let candidatePipelineState: PipelineState | undefined;
3422
3797
  const abortDisposedRecovery = (): Result<void, RecoverFailure> | undefined => {
3423
3798
  if (!disposed) return undefined;
3424
3799
  gpuStore.destroyAll();
3800
+ candidatePipelineState?.perPassResources.drainBloomResources?.();
3425
3801
  renderSystem.disposeFrameState();
3802
+ recoveryScope.abandon();
3426
3803
  materialShaderPipelineCache.clear();
3427
3804
  perShaderMaterialLayoutCache.clear();
3428
3805
  group0ResourceLayouts.clear();
@@ -3441,7 +3818,7 @@ async function makeWebGPURenderer(
3441
3818
  );
3442
3819
  const abortedAfterShaders = abortDisposedRecovery();
3443
3820
  if (abortedAfterShaders !== undefined) return abortedAfterShaders;
3444
- pipelineState = await buildPipeline();
3821
+ candidatePipelineState = await buildPipeline(recoveryScope);
3445
3822
  const abortedAfterPipeline = abortDisposedRecovery();
3446
3823
  if (abortedAfterPipeline !== undefined) return abortedAfterPipeline;
3447
3824
  renderSystem.restorePostProcessResources();
@@ -3449,6 +3826,8 @@ async function makeWebGPURenderer(
3449
3826
  // Pipeline rebuild failed against the new device. Treat as a device
3450
3827
  // unavailability (the device was acquired but is not usable); health
3451
3828
  // stays `device-lost` so the host can retry.
3829
+ candidatePipelineState?.perPassResources.drainBloomResources?.();
3830
+ recoveryScope.abandon();
3452
3831
  const aborted = abortDisposedRecovery();
3453
3832
  if (aborted !== undefined) return aborted;
3454
3833
  return err(new RecoverError('recover-device-unavailable'));
@@ -3456,27 +3835,18 @@ async function makeWebGPURenderer(
3456
3835
  const abortedAfterPostProcess = abortDisposedRecovery();
3457
3836
  if (abortedAfterPostProcess !== undefined) return abortedAfterPostProcess;
3458
3837
 
3459
- const replacementResult = await activeDeviceScope.replace(
3460
- activeDeviceScope.generation + 1,
3461
- [],
3462
- );
3463
- if (!replacementResult.ok) {
3464
- const detail = replacementResult.error.primary.detail;
3465
- return err(
3466
- new RenderRecoveryError(
3467
- disposed ? 'recover-disposed-during-rebuild' : 'recover-lifecycle-failed',
3468
- {
3469
- owner: detail.owner,
3470
- generation: detail.generation,
3471
- recovery: disposed ? 'stop' : 'recover',
3472
- receipt: replacementResult.error.receipt,
3473
- cause: replacementResult.error,
3474
- },
3475
- ),
3476
- );
3838
+ // Promote only after the complete replacement build is ready. The old
3839
+ // scope is then retired exactly once; a failed attempt abandons its
3840
+ // candidate in the catch path and can be retried with a new generation.
3841
+ if (candidatePipelineState === undefined) {
3842
+ recoveryScope.abandon();
3843
+ return err(new RecoverError('recover-device-unavailable'));
3477
3844
  }
3478
- activeDeviceScope = replacementResult.value;
3845
+ const previousDeviceScope = activeDeviceScope;
3846
+ activeDeviceScope = recoveryScope;
3847
+ pipelineState = candidatePipelineState;
3479
3848
  gpuStore.bindDeviceScope(activeDeviceScope);
3849
+ previousDeviceScope.retire();
3480
3850
 
3481
3851
  // Re-evaluate feature capability gates against the replacement device.
3482
3852
  // Feature recovery is isolated from renderer recovery: a feature that is
@@ -3574,7 +3944,7 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
3574
3944
  );
3575
3945
  },
3576
3946
  setProfile: (profile) => {
3577
- if (currentState !== 'alive') {
3947
+ if (!isRendererStateOperational(currentState)) {
3578
3948
  return err(
3579
3949
  new RendererOperationError('renderer-state-invalid', {
3580
3950
  operation: 'set-profile',
@@ -3647,7 +4017,7 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
3647
4017
  const error = new RendererOperationError('recovery-failed', {
3648
4018
  operation: 'recover',
3649
4019
  oldGeneration,
3650
- cause: result.error,
4020
+ cause: structuredRendererCause(result.error, 'recover'),
3651
4021
  });
3652
4022
  emit(Object.freeze({ kind: 'error', error }));
3653
4023
  return err(error);
@@ -3690,13 +4060,24 @@ function structuredRendererCause(cause: unknown, operation: string): RendererOpe
3690
4060
  typeof candidate.expected === 'string' &&
3691
4061
  typeof candidate.hint === 'string'
3692
4062
  ) {
3693
- return candidate as RendererOperationCause;
4063
+ return {
4064
+ code: candidate.code,
4065
+ expected: candidate.expected,
4066
+ hint: candidate.hint,
4067
+ ...(candidate.detail === undefined ? {} : { detail: candidate.detail }),
4068
+ };
3694
4069
  }
3695
4070
  }
3696
- return new RendererContractFailureError(
4071
+ const contractFailure = new RendererContractFailureError(
3697
4072
  'draw',
3698
4073
  `${operation}: ${cause instanceof Error ? cause.message : String(cause)}`,
3699
4074
  );
4075
+ return {
4076
+ code: contractFailure.code,
4077
+ expected: contractFailure.expected,
4078
+ hint: contractFailure.hint,
4079
+ detail: contractFailure.detail,
4080
+ };
3700
4081
  }
3701
4082
 
3702
4083
  const RENDER_PROFILE_KEYS = new Set([
@@ -3711,6 +4092,7 @@ const RENDER_PROFILE_KEYS = new Set([
3711
4092
  'tone',
3712
4093
  'antialias',
3713
4094
  'sky',
4095
+ 'volumetricFog',
3714
4096
  'fallback',
3715
4097
  'postStages',
3716
4098
  ]);
@@ -3731,32 +4113,31 @@ function validateRenderProfile(profile: RenderProfile): string | undefined {
3731
4113
  if (profile.shadows !== 'off' && profile.shadows !== 'hard' && profile.shadows !== 'filtered') {
3732
4114
  return "RenderProfile.shadows must be 'off', 'hard', or 'filtered'";
3733
4115
  }
3734
- if (
3735
- profile.tone !== 'none' &&
3736
- profile.tone !== 'aces-filmic' &&
3737
- profile.tone !== 'agx' &&
3738
- profile.tone !== 'cineon' &&
3739
- profile.tone !== 'linear' &&
3740
- profile.tone !== 'neutral' &&
3741
- profile.tone !== 'reinhard' &&
3742
- profile.tone !== 'reinhard-extended'
3743
- ) {
3744
- return 'RenderProfile.tone is not a supported Standard tone';
3745
- }
3746
- if (
3747
- profile.antialias !== 'none' &&
3748
- profile.antialias !== 'fxaa' &&
3749
- profile.antialias !== 'msaa'
3750
- ) {
3751
- return "RenderProfile.antialias must be 'none', 'fxaa', or 'msaa'";
3752
- }
3753
4116
  if (profile.fallback !== 'native' && profile.fallback !== 'cpu-webgl2') {
3754
4117
  return "RenderProfile.fallback must be 'native' or 'cpu-webgl2'";
3755
4118
  }
3756
- for (const field of ['pbr', 'ibl', 'ssao', 'bloom', 'sky'] as const) {
4119
+ for (const field of ['pbr', 'ibl', 'ssao'] as const) {
3757
4120
  if (typeof profile[field] !== 'boolean') return `RenderProfile.${field} must be boolean`;
3758
4121
  }
3759
- const stages = ['transparent-blend', 'bloom', 'tone', 'fxaa', 'output'] as const;
4122
+ if (profile.volumetricFog !== undefined) {
4123
+ if (profile.volumetricFog.quality !== 'low' && profile.volumetricFog.quality !== 'high')
4124
+ return "RenderProfile.volumetricFog.quality must be 'low' or 'high'";
4125
+ if (profile.volumetricFog.depth !== 48 && profile.volumetricFog.depth !== 64) {
4126
+ return 'RenderProfile.volumetricFog.depth must be 48 or 64';
4127
+ }
4128
+ if (profile.volumetricFog.tileSize !== 4 && profile.volumetricFog.tileSize !== 16) {
4129
+ return 'RenderProfile.volumetricFog.tileSize must be 4 or 16';
4130
+ }
4131
+ if (profile.volumetricFog.quality === 'low' && profile.volumetricFog.depth !== 48)
4132
+ return 'RenderProfile.volumetricFog.low must use depth 48';
4133
+ if (profile.volumetricFog.quality === 'high' && profile.volumetricFog.depth !== 64)
4134
+ return 'RenderProfile.volumetricFog.high must use depth 64';
4135
+ if (profile.volumetricFog.quality === 'low' && profile.volumetricFog.tileSize !== 16)
4136
+ return 'RenderProfile.volumetricFog.low must use tileSize 16';
4137
+ if (profile.volumetricFog.quality === 'high' && profile.volumetricFog.tileSize !== 4)
4138
+ return 'RenderProfile.volumetricFog.high must use tileSize 4';
4139
+ }
4140
+ const stages = STANDARD_POST_STAGE_NAMES;
3760
4141
  if (
3761
4142
  !Array.isArray(profile.postStages) ||
3762
4143
  profile.postStages.length !== stages.length ||
@@ -3807,8 +4188,11 @@ interface ShaderDeviceAdapterInternal extends ShaderCatalogDevice {
3807
4188
  * `module-forgeax::default-shadow-caster` returns OK on frame 1.
3808
4189
  */
3809
4190
  seedModule(label: string, module: ShaderModule): void;
4191
+ invalidateModule(label: string): void;
3810
4192
  }
3811
4193
 
4194
+ type ShaderModuleAdapterMode = 'validated' | 'immediate';
4195
+
3812
4196
  function makeShaderDeviceAdapter(
3813
4197
  rhiDevice: RhiDevice,
3814
4198
  _errorRegistry: RhiErrorListenerRegistry,
@@ -3818,10 +4202,18 @@ function makeShaderDeviceAdapter(
3818
4202
  desc: { code: string; label?: string | undefined },
3819
4203
  ) => Promise<Result<ShaderModule, RhiError>>)
3820
4204
  | undefined,
4205
+ immediateCreateShaderModule:
4206
+ | ((
4207
+ device: RhiDevice,
4208
+ desc: { code: string; label?: string | undefined },
4209
+ ) => Result<ShaderModule, RhiError>)
4210
+ | undefined,
4211
+ mode: ShaderModuleAdapterMode = 'validated',
3821
4212
  ): ShaderDeviceAdapterInternal {
3822
4213
  const moduleCache = new Map<string, ShaderModule>();
3823
4214
  const errorCache = new Map<string, RhiError>();
3824
- const pending = new Set<string>();
4215
+ const pending = new Map<string, number>();
4216
+ const epochs = new Map<string, number>();
3825
4217
 
3826
4218
  return {
3827
4219
  createShaderModule(desc): Result<ShaderModule, RhiError> {
@@ -3831,18 +4223,39 @@ function makeShaderDeviceAdapter(
3831
4223
  const cachedError = errorCache.get(key);
3832
4224
  if (cachedError !== undefined) return err(cachedError);
3833
4225
 
4226
+ // WebGPU's raw createShaderModule is synchronous. Only the explicitly
4227
+ // selected immediate adapter uses that entry: generated feature programs
4228
+ // can avoid a slow getCompilationInfo() round trip, while the default
4229
+ // validated adapter keeps the compiler-readiness barrier for ordinary
4230
+ // render pipelines. The public async factory still owns diagnostics;
4231
+ // pipeline creation remains the validation point for the fast path.
4232
+ if (mode === 'immediate' && immediateCreateShaderModule !== undefined) {
4233
+ const immediateResult = immediateCreateShaderModule(rhiDevice, {
4234
+ code: desc.code,
4235
+ ...(desc.label === undefined ? {} : { label: desc.label }),
4236
+ });
4237
+ if (immediateResult.ok) {
4238
+ moduleCache.set(key, immediateResult.value);
4239
+ return immediateResult;
4240
+ }
4241
+ errorCache.set(key, immediateResult.error);
4242
+ return immediateResult;
4243
+ }
4244
+
3834
4245
  // Fire async creation; first sync call returns pending error (the AI consumer retry pattern).
3835
4246
  // M3 D-P4: rhi-webgpu supplies the async factory; rhi-wgpu (no top-level
3836
4247
  // async factory) falls back to the synchronous device.createShaderModule
3837
4248
  // entry which already returns Result<ShaderModule, RhiError>.
3838
4249
  if (!pending.has(key)) {
3839
- pending.add(key);
4250
+ const epoch = epochs.get(key) ?? 0;
4251
+ pending.set(key, epoch);
3840
4252
  const desc2: { code: string; label?: string | undefined } = { code: desc.code };
3841
4253
  if (desc.label !== undefined) desc2.label = desc.label;
3842
4254
  const asyncResult: Promise<Result<ShaderModule, RhiError>> = asyncCreateShaderModule
3843
4255
  ? asyncCreateShaderModule(rhiDevice, desc2)
3844
4256
  : invokeDeviceCreateShaderModule(rhiDevice, desc2);
3845
4257
  void asyncResult.then((result) => {
4258
+ if (pending.get(key) !== epoch || (epochs.get(key) ?? 0) !== epoch) return;
3846
4259
  if (result.ok) {
3847
4260
  moduleCache.set(key, result.value);
3848
4261
  } else {
@@ -3857,6 +4270,12 @@ function makeShaderDeviceAdapter(
3857
4270
  seedModule(label: string, module: ShaderModule): void {
3858
4271
  moduleCache.set(label, module);
3859
4272
  },
4273
+ invalidateModule(label: string): void {
4274
+ epochs.set(label, (epochs.get(label) ?? 0) + 1);
4275
+ moduleCache.delete(label);
4276
+ errorCache.delete(label);
4277
+ pending.delete(label);
4278
+ },
3860
4279
  };
3861
4280
  }
3862
4281
 
@@ -4102,7 +4521,11 @@ const HDR_COLOR_ATTACHMENT_FORMAT: GPUTextureFormat = 'rgba16float';
4102
4521
  // so it is intentionally absent here. A `mipmap:true` texture in an
4103
4522
  // un-prewarmed format surfaces a structured RhiError at record (never an
4104
4523
  // async stall) -- the falsifiable anchor for an incomplete prewarm list.
4105
- const MIPMAP_PREWARM_FORMATS: readonly GPUTextureFormat[] = ['rgba8unorm-srgb', 'rgba8unorm'];
4524
+ const MIPMAP_PREWARM_FORMATS: readonly GPUTextureFormat[] = [
4525
+ 'rgba8unorm-srgb',
4526
+ 'rgba8unorm',
4527
+ 'rgba16float',
4528
+ ];
4106
4529
 
4107
4530
  // feat-20260531-bloom-first-declarative-render-graph-pass / w13:
4108
4531
  // UBO sizes for bloom params (std140 layout).
@@ -4491,6 +4914,14 @@ async function prepareMaterialShaders(
4491
4914
  if (!capCheck.ok) throw capCheck.error;
4492
4915
  storageBufferCapable = !webgl2Downlevel && capCheck.value;
4493
4916
  }
4917
+ // The shared PBR view/material ABI consumes the WebGPU minimum of 16
4918
+ // fragment sampled textures. The authored SpotLight projector is the
4919
+ // optional 17th texture; select its shader/BGL pair from the same limit so
4920
+ // low-limit adapters keep analytic lighting instead of failing layout
4921
+ // creation at boot.
4922
+ const projectorAvailable =
4923
+ (rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
4924
+ STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
4494
4925
 
4495
4926
  // ── Step 1: manifest load ─────────────────────────────────────────────────
4496
4927
  const registry = getShader();
@@ -4516,6 +4947,13 @@ async function prepareMaterialShaders(
4516
4947
  const variantDefines: Record<string, boolean> = {
4517
4948
  STORAGE_BUFFER_AVAILABLE: storageBufferCapable,
4518
4949
  };
4950
+ // User material manifests may declare WEBGL2_COMPAT alongside the shared
4951
+ // mesh-storage axis. The fallback backend must select that authored
4952
+ // branch at boot; otherwise it asks for the impossible bare
4953
+ // `STORAGE_BUFFER_AVAILABLE=false` key and fails before the first frame.
4954
+ if (msEntry.variants.some((v) => 'WEBGL2_COMPAT' in v.defines)) {
4955
+ variantDefines.WEBGL2_COMPAT = webgl2Downlevel;
4956
+ }
4519
4957
  // Only include CLUSTER_FORWARD_AVAILABLE when the material shader declares
4520
4958
  // the axis (has a variant with this key), preserving backward compat for
4521
4959
  // entries without the pragma.
@@ -4525,6 +4963,18 @@ async function prepareMaterialShaders(
4525
4963
  if (msEntry.variants.some((v) => 'VERTEX_COLOR_AVAILABLE' in v.defines)) {
4526
4964
  variantDefines.VERTEX_COLOR_AVAILABLE = false;
4527
4965
  }
4966
+ if (
4967
+ msEntry.identifier === 'forgeax::default-standard-pbr' &&
4968
+ msEntry.variants.some((v) => 'PROJECTOR_AVAILABLE' in v.defines)
4969
+ ) {
4970
+ variantDefines.PROJECTOR_AVAILABLE = projectorAvailable;
4971
+ }
4972
+ if (
4973
+ msEntry.identifier === 'forgeax::default-standard-pbr' &&
4974
+ msEntry.variants.some((v) => 'TRANSMISSION_AVAILABLE' in v.defines)
4975
+ ) {
4976
+ variantDefines.TRANSMISSION_AVAILABLE = false;
4977
+ }
4528
4978
  // bug-20260708 M2 (a): PER_INSTANCE_REGION axis default = false at boot.
4529
4979
  // Per-entity spritePH path (`main-pass-sprite-draws.ts:511-513`) expects
4530
4980
  // `variantSet=undefined` to resolve to the PIR=false shader source (64B
@@ -4539,6 +4989,9 @@ async function prepareMaterialShaders(
4539
4989
  if (msEntry.variants.some((v) => 'PER_INSTANCE_REGION' in v.defines)) {
4540
4990
  variantDefines.PER_INSTANCE_REGION = false;
4541
4991
  }
4992
+ if (msEntry.variants.some((v) => 'SKINNING_DISABLED' in v.defines)) {
4993
+ variantDefines.SKINNING_DISABLED = true;
4994
+ }
4542
4995
  const sortedEntries = Object.entries(variantDefines).sort(([a], [b]) =>
4543
4996
  a < b ? -1 : a > b ? 1 : 0,
4544
4997
  );
@@ -4546,6 +4999,13 @@ async function prepareMaterialShaders(
4546
4999
  ? ''
4547
5000
  : sortedEntries.map(([k, v]) => `${k}=${v}`).join('+');
4548
5001
  const chosen = findVariantByKey(msEntry, definesKey);
5002
+ if (msEntry.variants.length > 0 && chosen === undefined) {
5003
+ throw new RhiError({
5004
+ code: 'shader-compile-failed',
5005
+ expected: `material shader '${msEntry.identifier}' declares the exact boot variant '${definesKey}'`,
5006
+ hint: 'regenerate the shader manifest with the complete variant Cartesian product',
5007
+ });
5008
+ }
4549
5009
  const wgsl = chosen?.composedWgsl ?? msEntry.composedWgsl;
4550
5010
  if (wgsl.length > 0) {
4551
5011
  const existing = registry.findMaterialArtifact(msEntry.identifier);
@@ -4627,6 +5087,7 @@ async function prepareMaterialShaders(
4627
5087
 
4628
5088
  async function buildReadyWebGPU(
4629
5089
  rhiDevice: RhiDevice,
5090
+ rendererScope: DeviceScope,
4630
5091
  getShader: () => ShaderCatalog,
4631
5092
  gpuStore: GpuResidencyCache,
4632
5093
  asyncCreateShaderModule:
@@ -4680,6 +5141,12 @@ async function buildReadyWebGPU(
4680
5141
  // tonemap source resolves inside this async function (after the manifest-load
4681
5142
  // await), by which point the synchronous `renderSystem` const is defined.
4682
5143
  registerBuiltinTonemap: (source: string) => void,
5144
+ registerBuiltinTemporalPostProcesses: (entries: {
5145
+ readonly motionBlur?: string;
5146
+ readonly taaResolve?: string;
5147
+ }) => void,
5148
+ /** Install manifest-owned volume utility sources only after all three modules compile. */
5149
+ setVolumetricFogShaderSources: (sources: VolumetricFogShaderSources) => void,
4683
5150
  ): Promise<PipelineState> {
4684
5151
  // bug-20260610: WebGL2 fallback path — read storage-buffer capability up
4685
5152
  // front so both Step 2 (shader-module compile, where we patch engine
@@ -4697,6 +5164,9 @@ async function buildReadyWebGPU(
4697
5164
  const capCheck = assertStorageBufferCap(capValue);
4698
5165
  storageBufferCapable = capCheck.ok ? !webgl2Downlevel && capCheck.value : !webgl2Downlevel;
4699
5166
  }
5167
+ const projectorAvailable =
5168
+ (rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
5169
+ STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
4700
5170
 
4701
5171
  // bug-20260612: choose swap-chain storage / view formats by backend.
4702
5172
  // Channel 2 (native WebGPU) follows navigator.gpu.getPreferredCanvasFormat();
@@ -4706,7 +5176,10 @@ async function buildReadyWebGPU(
4706
5176
  // pipeline target, configure() call, and color-attachment format —
4707
5177
  // single SSOT, no scattered branches. See selectSwapChainFormat
4708
5178
  // (above the SWAP_CHAIN_*_FORMAT historical constants).
4709
- const swapChainFormats = selectSwapChainFormat(storageBufferCapable);
5179
+ const surfaceViewFormats =
5180
+ (rhiDevice as unknown as { readonly surfaceViewFormats?: boolean }).surfaceViewFormats ??
5181
+ !webgl2Downlevel;
5182
+ const swapChainFormats = selectSwapChainFormat(storageBufferCapable, surfaceViewFormats);
4710
5183
  // The 'null' (headless RhiNull) backend has no UA preferred-canvas-format by
4711
5184
  // design; the rgba8unorm fallback is its intended steady state, not a
4712
5185
  // degraded one — firing 'rhi-not-available' there is territorially wrong
@@ -4811,6 +5284,7 @@ async function buildReadyWebGPU(
4811
5284
  let pbrEntry = findEngineManifestEntry('forgeax::default-standard-pbr');
4812
5285
  let unlitEntry = findEngineManifestEntry('forgeax::default-unlit');
4813
5286
  let tonemapEntry: ManifestEntry | undefined;
5287
+ let motionBlurEntry: ManifestEntry | undefined;
4814
5288
  let spriteEntry: ManifestEntry | undefined;
4815
5289
  // sprite-lit identification marker is the `spriteLitShadeAccum`
4816
5290
  // helper (defined in sprite-lit.wgsl, absent from sprite.wgsl). The
@@ -4820,6 +5294,7 @@ async function buildReadyWebGPU(
4820
5294
  // their paramSchema mirror) does not mis-classify sprite-lit as
4821
5295
  // sprite.
4822
5296
  let spriteLitEntry: ManifestEntry | undefined;
5297
+ let taaResolveEntry: ManifestEntry | undefined;
4823
5298
  let iblEquirectEntry: ManifestEntry | undefined;
4824
5299
  let iblIrradianceEntry: ManifestEntry | undefined;
4825
5300
  let iblPrefilterEntry: ManifestEntry | undefined;
@@ -4844,6 +5319,14 @@ async function buildReadyWebGPU(
4844
5319
  tonemapEntry ??= entry;
4845
5320
  continue;
4846
5321
  }
5322
+ if (entry.wgsl.includes('MotionBlurParams')) {
5323
+ motionBlurEntry ??= entry;
5324
+ continue;
5325
+ }
5326
+ if (entry.wgsl.includes('fs_taa_resolve')) {
5327
+ taaResolveEntry ??= entry;
5328
+ continue;
5329
+ }
4847
5330
  if (entry.wgsl.includes('spriteLitShadeAccum')) {
4848
5331
  // sprite-lit identification — must precede the `pivotAndSize`
4849
5332
  // marker since sprite-lit also carries that field (paramSchema
@@ -4985,10 +5468,12 @@ async function buildReadyWebGPU(
4985
5468
  (!('CLUSTER_FORWARD_AVAILABLE' in v.defines) ||
4986
5469
  v.defines.CLUSTER_FORWARD_AVAILABLE === false) &&
4987
5470
  (!('VERTEX_COLOR_AVAILABLE' in v.defines) ||
4988
- v.defines.VERTEX_COLOR_AVAILABLE === false),
5471
+ v.defines.VERTEX_COLOR_AVAILABLE === false) &&
5472
+ (!('PROJECTOR_AVAILABLE' in v.defines) ||
5473
+ v.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
5474
+ (!('SKINNING_DISABLED' in v.defines) || v.defines.SKINNING_DISABLED === true),
4989
5475
  );
4990
- const v =
4991
- exact ?? ms.variants.find((cand) => cand.defines.STORAGE_BUFFER_AVAILABLE === false);
5476
+ const v = exact === undefined ? undefined : findVariantByKey(ms, exact.definesKey);
4992
5477
  if (v !== undefined) {
4993
5478
  idToVariantWgsl.set(ms.identifier, v.composedWgsl);
4994
5479
  }
@@ -5028,13 +5513,15 @@ async function buildReadyWebGPU(
5028
5513
  (!('CLUSTER_FORWARD_AVAILABLE' in variant.defines) ||
5029
5514
  variant.defines.CLUSTER_FORWARD_AVAILABLE === false) &&
5030
5515
  (!('VERTEX_COLOR_AVAILABLE' in variant.defines) ||
5031
- variant.defines.VERTEX_COLOR_AVAILABLE === false),
5516
+ variant.defines.VERTEX_COLOR_AVAILABLE === false) &&
5517
+ (!('PROJECTOR_AVAILABLE' in variant.defines) ||
5518
+ variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
5519
+ (!('SKINNING_DISABLED' in variant.defines) ||
5520
+ variant.defines.SKINNING_DISABLED === true) &&
5521
+ (!('TRANSMISSION_AVAILABLE' in variant.defines) ||
5522
+ variant.defines.TRANSMISSION_AVAILABLE === false),
5032
5523
  );
5033
- const variant =
5034
- exact ??
5035
- ms.variants.find(
5036
- (candidate) => candidate.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable,
5037
- );
5524
+ const variant = exact === undefined ? undefined : findVariantByKey(ms, exact.definesKey);
5038
5525
  if (variant !== undefined) {
5039
5526
  idToRuntimeVariantWgsl.set(ms.identifier, variant.composedWgsl);
5040
5527
  }
@@ -5163,6 +5650,80 @@ async function buildReadyWebGPU(
5163
5650
  seedShaderModule(label, shaderResult.value);
5164
5651
  }
5165
5652
 
5653
+ // The volume graph is authored from ECS state and therefore cannot be
5654
+ // compiled while the static material manifest is assembled. Its pass
5655
+ // encoder nevertheless uses the same shared lazy shader adapter, so the
5656
+ // four utility modules must be prewarmed before the first synchronous
5657
+ // draw. The sources come from the shader manifest (the package-owned WGSL
5658
+ // files are the sole source of truth); compile all four first and only
5659
+ // then publish the labels and source bundle. A partial bundle would let a
5660
+ // later frame mix generations after a device recovery.
5661
+ if (rhiDevice.caps.compute && rhiDevice.caps.storageTexture) {
5662
+ const volumeSources = [
5663
+ ['inject', 'volume_inject', (source: string) => source.includes('fn volume_inject(')],
5664
+ ['temporal', 'volume_temporal', (source: string) => source.includes('fn volume_temporal(')],
5665
+ [
5666
+ 'integrate',
5667
+ 'volume_integrate',
5668
+ (source: string) => source.includes('fn volume_integrate('),
5669
+ ],
5670
+ ['composite', 'volume_composite', (source: string) => source.includes('fn volume_fs(')],
5671
+ ] as const;
5672
+ const resolved = volumeSources.map(([stage, label, matches]) => {
5673
+ const entry = manifestEntries.find((candidate) => matches(candidate.wgsl));
5674
+ return { stage, label, entry };
5675
+ });
5676
+ // Volumetric fog is an optional graph feature. Minimal hosts/tests may
5677
+ // intentionally ship only material/post-process entries; keep those
5678
+ // renderers bootable while still rejecting a partially declared volume
5679
+ // bundle (which would otherwise fail later as a lazy pending pipeline).
5680
+ const present = resolved.filter((candidate) => candidate.entry !== undefined).length;
5681
+ if (present === 0) {
5682
+ // No volume manifest entries: leave the source/module bundle unset and
5683
+ // let the topology capability gate keep volume work disabled.
5684
+ } else {
5685
+ const missing = resolved.find((candidate) => candidate.entry === undefined);
5686
+ if (missing !== undefined) {
5687
+ throw new RhiError({
5688
+ code: 'shader-compile-failed',
5689
+ expected: 'shader manifest contains all four volumetric fog utility entries',
5690
+ hint: `add the package-owned WGSL entry for '${missing.label}' to the engine shader manifest`,
5691
+ });
5692
+ }
5693
+ const compiled = [] as Array<{
5694
+ readonly stage: (typeof volumeSources)[number][0];
5695
+ readonly label: string;
5696
+ readonly source: string;
5697
+ readonly module: ShaderModule;
5698
+ }>;
5699
+ for (const { stage, label, entry } of resolved) {
5700
+ // `missing` above proves the narrowing for this loop; keep the guard
5701
+ // explicit so a future refactor cannot publish an undefined source.
5702
+ if (entry === undefined) throw new Error(`missing volumetric shader ${label}`);
5703
+ const shaderResult = await runShimStep(
5704
+ () =>
5705
+ asyncCreateShaderModule
5706
+ ? asyncCreateShaderModule(rhiDevice, { code: entry.wgsl, label })
5707
+ : invokeDeviceCreateShaderModule(rhiDevice, { code: entry.wgsl, label }),
5708
+ 'shader-compile-failed',
5709
+ `volumetric fog shader module '${label}' compiled`,
5710
+ `inspect manifest WGSL for '${label}' and check device.features`,
5711
+ );
5712
+ if (!shaderResult.ok) throw shaderResult.error;
5713
+ compiled.push({ stage, label, source: entry.wgsl, module: shaderResult.value });
5714
+ }
5715
+ for (const { label, module } of compiled) seedShaderModule(label, module);
5716
+ setVolumetricFogShaderSources({
5717
+ inject: compiled.find((candidate) => candidate.stage === 'inject')?.source as string,
5718
+ temporal: compiled.find((candidate) => candidate.stage === 'temporal')?.source as string,
5719
+ integrate: compiled.find((candidate) => candidate.stage === 'integrate')
5720
+ ?.source as string,
5721
+ composite: compiled.find((candidate) => candidate.stage === 'composite')
5722
+ ?.source as string,
5723
+ });
5724
+ }
5725
+ }
5726
+
5166
5727
  const pbrShaderResult = await runShimStep(
5167
5728
  () =>
5168
5729
  asyncCreateShaderModule
@@ -5190,6 +5751,16 @@ async function buildReadyWebGPU(
5190
5751
  if (pbrVariant !== undefined) {
5191
5752
  seedShaderModule(`module-forgeax::default-standard-pbr#${pbrVariant.definesKey}`, pbrModule);
5192
5753
  }
5754
+ const transmissionCapable =
5755
+ (rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
5756
+ STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
5757
+ const transmissionVariants = transmissionCapable
5758
+ ? selectStandardPbrTransmissionPrewarmVariants(
5759
+ pbrManifestEntry,
5760
+ storageBufferCapable,
5761
+ projectorAvailable,
5762
+ )
5763
+ : [];
5193
5764
 
5194
5765
  // HDRP uses the canonical all-true variant key (`''`) and the lazy
5195
5766
  // material pipeline adapter therefore requests a distinct module label.
@@ -5197,9 +5768,36 @@ async function buildReadyWebGPU(
5197
5768
  // label. Color availability is part of the geometry-owned variant axis, so
5198
5769
  // both colored and no-color HDRP sources must be retained.
5199
5770
  const prewarmedHdrpModules = new Map<string, ShaderModule>([[pbrEntry.wgsl, pbrModule]]);
5771
+ for (const transmissionVariant of transmissionVariants) {
5772
+ const moduleLabel = `module-forgeax::default-standard-pbr#${transmissionVariant.definesKey}`;
5773
+ let transmissionModule = prewarmedHdrpModules.get(transmissionVariant.composedWgsl);
5774
+ if (transmissionModule === undefined) {
5775
+ const transmissionModuleResult = await runShimStep(
5776
+ () =>
5777
+ asyncCreateShaderModule
5778
+ ? asyncCreateShaderModule(rhiDevice, {
5779
+ code: transmissionVariant.composedWgsl,
5780
+ label: moduleLabel,
5781
+ })
5782
+ : invokeDeviceCreateShaderModule(rhiDevice, {
5783
+ code: transmissionVariant.composedWgsl,
5784
+ label: moduleLabel,
5785
+ }),
5786
+ 'shader-compile-failed',
5787
+ 'Standard transmission shader module compiled',
5788
+ 'inspect the composed Standard transmission variant WGSL and check device.features',
5789
+ );
5790
+ if (!transmissionModuleResult.ok) throw transmissionModuleResult.error;
5791
+ transmissionModule = transmissionModuleResult.value;
5792
+ prewarmedHdrpModules.set(transmissionVariant.composedWgsl, transmissionModule);
5793
+ }
5794
+ seedShaderModule(moduleLabel, transmissionModule);
5795
+ }
5200
5796
  for (const hdrpVariant of selectHdrpPbrPrewarmVariants(
5201
5797
  pbrManifestEntry,
5202
5798
  storageBufferCapable,
5799
+ transmissionCapable,
5800
+ projectorAvailable,
5203
5801
  )) {
5204
5802
  const moduleLabel = `module-forgeax::default-standard-pbr#${hdrpVariant.definesKey}`;
5205
5803
  let hdrpModule = prewarmedHdrpModules.get(hdrpVariant.composedWgsl);
@@ -5335,6 +5933,49 @@ async function buildReadyWebGPU(
5335
5933
  // uses so the lazy build's createShaderModule call hits OK on
5336
5934
  // frame 1.
5337
5935
  seedShaderModule('module-forgeax::default-shadow-caster', shadowCasterShaderResult.value);
5936
+
5937
+ // The shadow caster now has independent storage and skinning axes. The
5938
+ // canonical all-true entry remains the ordinary static source through
5939
+ // SKINNING_DISABLED=true; skinned meshes request the explicit false
5940
+ // variant. Prewarm both for the active storage capability so neither
5941
+ // path loses its first shadow frame to the lazy module retry contract.
5942
+ const shadowCasterManifestEntry = Array.from(registry.materialShaderManifestEntries()).find(
5943
+ (candidate) => candidate.identifier === 'forgeax::default-shadow-caster',
5944
+ );
5945
+ const activeShadowVariants = shadowCasterManifestEntry?.variants.filter(
5946
+ (variant) =>
5947
+ variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
5948
+ typeof variant.defines.SKINNING_DISABLED === 'boolean',
5949
+ );
5950
+ for (const variant of activeShadowVariants ?? []) {
5951
+ if (variant.definesKey === '') {
5952
+ seedShaderModule(
5953
+ 'module-forgeax::default-shadow-caster#',
5954
+ shadowCasterShaderResult.value,
5955
+ );
5956
+ continue;
5957
+ }
5958
+ const variantShaderResult = await runShimStep(
5959
+ () =>
5960
+ asyncCreateShaderModule
5961
+ ? asyncCreateShaderModule(rhiDevice, {
5962
+ code: variant.composedWgsl,
5963
+ label: `shadow_caster#${variant.definesKey}`,
5964
+ })
5965
+ : invokeDeviceCreateShaderModule(rhiDevice, {
5966
+ code: variant.composedWgsl,
5967
+ label: `shadow_caster#${variant.definesKey}`,
5968
+ }),
5969
+ 'shader-compile-failed',
5970
+ `shadow_caster variant ${variant.definesKey || '<default>'} compiled`,
5971
+ 'inspect the selected shadow_caster storage/skinning variant and device.features',
5972
+ );
5973
+ if (!variantShaderResult.ok) throw variantShaderResult.error;
5974
+ seedShaderModule(
5975
+ `module-forgeax::default-shadow-caster#${variant.definesKey}`,
5976
+ variantShaderResult.value,
5977
+ );
5978
+ }
5338
5979
  }
5339
5980
 
5340
5981
  // feat-20260621 M-A3 (D-5): register the built-in tonemap onto the unified
@@ -5354,27 +5995,62 @@ async function buildReadyWebGPU(
5354
5995
  // forever. Mirror the shadow_caster prewarm: await-compile the tonemap module
5355
5996
  // here and `seedShaderModule` it under the exact label
5356
5997
  // `buildPostProcessPipeline` requests (`post-process-${id}-module`), so the
5357
- // first `getPostProcessPipeline(STANDARD_TONEMAP_FEATURE_ID, …)` hits the module cache
5998
+ // first `getPostProcessPipeline(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, …)` hits the module cache
5358
5999
  // synchronously and builds the pipeline on frame 1.
5359
6000
  const tonemapPrewarm = await runShimStep(
5360
6001
  () =>
5361
6002
  asyncCreateShaderModule
5362
6003
  ? asyncCreateShaderModule(rhiDevice, {
5363
6004
  code: tonemapEntry.wgsl,
5364
- label: `post-process-${STANDARD_TONEMAP_FEATURE_ID}-module`,
6005
+ label: `post-process-${STANDARD_OUTPUT_TRANSFORM_FEATURE_ID}-module`,
5365
6006
  })
5366
6007
  : invokeDeviceCreateShaderModule(rhiDevice, {
5367
6008
  code: tonemapEntry.wgsl,
5368
- label: `post-process-${STANDARD_TONEMAP_FEATURE_ID}-module`,
6009
+ label: `post-process-${STANDARD_OUTPUT_TRANSFORM_FEATURE_ID}-module`,
5369
6010
  }),
5370
6011
  'shader-compile-failed',
5371
6012
  'tonemap shader module compiled (unified post-process prewarm)',
5372
6013
  'inspect manifest tonemap entry composed wgsl; check device.features',
5373
6014
  );
5374
6015
  if (!tonemapPrewarm.ok) throw tonemapPrewarm.error;
5375
- seedShaderModule(`post-process-${STANDARD_TONEMAP_FEATURE_ID}-module`, tonemapPrewarm.value);
6016
+ seedShaderModule(
6017
+ `post-process-${STANDARD_OUTPUT_TRANSFORM_FEATURE_ID}-module`,
6018
+ tonemapPrewarm.value,
6019
+ );
5376
6020
  registerBuiltinTonemap(tonemapEntry.wgsl);
5377
6021
 
6022
+ // Temporal fullscreen passes execute synchronously from the compiled graph,
6023
+ // so their shader modules must be warmed before the initialization barrier
6024
+ // resolves. Otherwise the first tight draw loop sees an asynchronous
6025
+ // `rhi-not-available` result and leaves the temporal targets cleared even
6026
+ // though the graph topology is present.
6027
+ const prewarmTemporal = async (entry: ManifestEntry | undefined, id: string) => {
6028
+ if (entry === undefined) return;
6029
+ const prewarm = await runShimStep(
6030
+ () =>
6031
+ asyncCreateShaderModule
6032
+ ? asyncCreateShaderModule(rhiDevice, {
6033
+ code: entry.wgsl,
6034
+ label: `post-process-${id}-module`,
6035
+ })
6036
+ : invokeDeviceCreateShaderModule(rhiDevice, {
6037
+ code: entry.wgsl,
6038
+ label: `post-process-${id}-module`,
6039
+ }),
6040
+ 'shader-compile-failed',
6041
+ `${id} shader module compiled (unified post-process prewarm)`,
6042
+ `inspect manifest ${id} entry composed wgsl; check device.features`,
6043
+ );
6044
+ if (!prewarm.ok) throw prewarm.error;
6045
+ seedShaderModule(`post-process-${id}-module`, prewarm.value);
6046
+ };
6047
+ await prewarmTemporal(motionBlurEntry, 'forgeax.motion-blur');
6048
+ await prewarmTemporal(taaResolveEntry, 'forgeax.taa-resolve');
6049
+ registerBuiltinTemporalPostProcesses({
6050
+ ...(motionBlurEntry === undefined ? {} : { motionBlur: motionBlurEntry.wgsl }),
6051
+ ...(taaResolveEntry === undefined ? {} : { taaResolve: taaResolveEntry.wgsl }),
6052
+ });
6053
+
5378
6054
  // feat-20260520-2d-sprite-layer-mvp M-3 / w24: sprite shader module
5379
6055
  // is optional in the 3-tuple legacy manifest (back-compat for apps
5380
6056
  // that locked their manifest URL before this feat). With the
@@ -5410,6 +6086,36 @@ async function buildReadyWebGPU(
5410
6086
  // shares the generic per-MaterialShader lazy build path, which now
5411
6087
  // needs the explicit seed to preserve frame-1 readiness.
5412
6088
  seedShaderModule('module-forgeax::sprite', spriteShaderResult.value);
6089
+ const spriteManifestEntry = Array.from(registry.materialShaderManifestEntries()).find(
6090
+ (entry) => entry.identifier === 'forgeax::sprite',
6091
+ );
6092
+ const regionVariant = spriteManifestEntry?.variants.find(
6093
+ (variant) =>
6094
+ variant.defines.PER_INSTANCE_REGION === true &&
6095
+ variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable,
6096
+ );
6097
+ if (regionVariant !== undefined) {
6098
+ const regionShaderResult = await runShimStep(
6099
+ () =>
6100
+ asyncCreateShaderModule
6101
+ ? asyncCreateShaderModule(rhiDevice, {
6102
+ code: regionVariant.composedWgsl,
6103
+ label: `sprite#${regionVariant.definesKey}`,
6104
+ })
6105
+ : invokeDeviceCreateShaderModule(rhiDevice, {
6106
+ code: regionVariant.composedWgsl,
6107
+ label: `sprite#${regionVariant.definesKey}`,
6108
+ }),
6109
+ 'shader-compile-failed',
6110
+ `sprite variant ${regionVariant.definesKey || '<default>'} compiled`,
6111
+ 'inspect the selected sprite PER_INSTANCE_REGION variant and device.features',
6112
+ );
6113
+ if (!regionShaderResult.ok) throw regionShaderResult.error;
6114
+ seedShaderModule(
6115
+ `module-forgeax::sprite#${regionVariant.definesKey}`,
6116
+ regionShaderResult.value,
6117
+ );
6118
+ }
5413
6119
  }
5414
6120
 
5415
6121
  // feat-20260624-sprite-lit-shading-model-pure-2d-lighting M1' / t7:
@@ -5482,15 +6188,12 @@ async function buildReadyWebGPU(
5482
6188
  skyboxModule = skyboxShaderResult.value;
5483
6189
  }
5484
6190
 
5485
- // feat-20260531-bloom-first-declarative-render-graph-pass / w13:
5486
- // bloom shader modules are optional (apps with legacy manifests without
5487
- // bloom.wgsl continue to boot). Identified by the uniform-struct-name
5488
- // markers 'BloomBrightParams' / 'BloomBlurParams' / 'BloomCompositeParams'
5489
- // (see triage above; struct names survive naga_oil composition). When
5490
- // present, each module is compiled here; pipeline construction happens
5491
- // alongside the FXAA / tonemap pipelines below.
6191
+ // Bloom shader modules remain available to the lazy Bloom pipeline owner.
6192
+ // Their construction is still part of renderer readiness because the
6193
+ // backend may expose only an asynchronous module factory; the expensive
6194
+ // pipeline, binding, and parameter resources stay deferred until admission.
5492
6195
  if (bloomBrightEntry !== undefined) {
5493
- const brightShaderResult = await runShimStep(
6196
+ const result = await runShimStep(
5494
6197
  () =>
5495
6198
  asyncCreateShaderModule
5496
6199
  ? asyncCreateShaderModule(rhiDevice, {
@@ -5505,17 +6208,14 @@ async function buildReadyWebGPU(
5505
6208
  'bloom-bright shader module compiled',
5506
6209
  'inspect manifest bloom-bright entry composed wgsl; check device.features',
5507
6210
  );
5508
- if (!brightShaderResult.ok) throw brightShaderResult.error;
5509
- bloomBrightModule = brightShaderResult.value;
6211
+ if (!result.ok) throw result.error;
6212
+ bloomBrightModule = result.value;
5510
6213
  }
5511
6214
  if (bloomBlurEntry !== undefined) {
5512
- const blurShaderResult = await runShimStep(
6215
+ const result = await runShimStep(
5513
6216
  () =>
5514
6217
  asyncCreateShaderModule
5515
- ? asyncCreateShaderModule(rhiDevice, {
5516
- code: bloomBlurEntry.wgsl,
5517
- label: 'bloom-blur',
5518
- })
6218
+ ? asyncCreateShaderModule(rhiDevice, { code: bloomBlurEntry.wgsl, label: 'bloom-blur' })
5519
6219
  : invokeDeviceCreateShaderModule(rhiDevice, {
5520
6220
  code: bloomBlurEntry.wgsl,
5521
6221
  label: 'bloom-blur',
@@ -5524,11 +6224,11 @@ async function buildReadyWebGPU(
5524
6224
  'bloom-blur shader module compiled',
5525
6225
  'inspect manifest bloom-blur entry composed wgsl; check device.features',
5526
6226
  );
5527
- if (!blurShaderResult.ok) throw blurShaderResult.error;
5528
- bloomBlurModule = blurShaderResult.value;
6227
+ if (!result.ok) throw result.error;
6228
+ bloomBlurModule = result.value;
5529
6229
  }
5530
6230
  if (bloomCompositeEntry !== undefined) {
5531
- const compositeShaderResult = await runShimStep(
6231
+ const result = await runShimStep(
5532
6232
  () =>
5533
6233
  asyncCreateShaderModule
5534
6234
  ? asyncCreateShaderModule(rhiDevice, {
@@ -5543,8 +6243,8 @@ async function buildReadyWebGPU(
5543
6243
  'bloom-composite shader module compiled',
5544
6244
  'inspect manifest bloom-composite entry composed wgsl; check device.features',
5545
6245
  );
5546
- if (!compositeShaderResult.ok) throw compositeShaderResult.error;
5547
- bloomCompositeModule = compositeShaderResult.value;
6246
+ if (!result.ok) throw result.error;
6247
+ bloomCompositeModule = result.value;
5548
6248
  }
5549
6249
  // feat-20260612-hdrp-ssao M6 / w27: SSAO shader module compilation.
5550
6250
  // Optional manifest entry — absent on legacy manifests (zero-overhead
@@ -5576,11 +6276,11 @@ async function buildReadyWebGPU(
5576
6276
  // The factory builds 4 BindGroupLayouts (view + material + mesh-array +
5577
6277
  // instances) and the 4-slot pipeline layout in one call, throwing on each
5578
6278
  // device.createBindGroupLayout / device.createPipelineLayout failure.
5579
- // material BGL now carries 14 entries (material 0..6 + Skylight 7..13 via
6279
+ // material BGL now carries 24 entries (material 0..16 + Skylight 17..23 via
5580
6280
  // mergeSkylightIntoMaterialBgl) -- pipeline layout itself stays at 4 slots,
5581
6281
  // no @group(4) allocated (D-5 round-4 fix for the round-2 maxBindGroups=4
5582
6282
  // BLOCKER). The unlit pipeline currently shares this 14-entry layout
5583
- // (carrying fallback identity at binding 7..13 from the record stage);
6283
+ // (carrying fallback identity at binding 17..23 from the record stage);
5584
6284
  // future split via `buildUnlitMaterialBgl` is exposed for tests.
5585
6285
  //
5586
6286
  // feat-20260520-directional-light-shadow-mapping merge: view BGL entries
@@ -5591,7 +6291,13 @@ async function buildReadyWebGPU(
5591
6291
  // buildReadyWebGPU (above) so Step 2 (variant patch) and this layout
5592
6292
  // build read the same value.
5593
6293
  const pbrLayouts = runShimSyncStep(
5594
- () => ok(buildPbrPipelineLayouts(rhiDevice, { storageBuffer: storageBufferCapable })),
6294
+ () =>
6295
+ ok(
6296
+ buildPbrPipelineLayouts(rhiDevice, {
6297
+ storageBuffer: storageBufferCapable,
6298
+ projectorAvailable,
6299
+ }),
6300
+ ),
5595
6301
  'webgpu-runtime-error',
5596
6302
  'buildPbrPipelineLayouts succeeded',
5597
6303
  'check device.limits.maxBindingsPerBindGroup (need >=14) and maxBindGroupsPerPipelineLayout',
@@ -5614,7 +6320,10 @@ async function buildReadyWebGPU(
5614
6320
  // (URP layout) so the manifest entry's URP variant WGSL still builds a
5615
6321
  // valid PSO instead of hard-disabling the entire HDRP-variant build path.
5616
6322
  let hdrpPbrPipelineLayoutHandle: PipelineLayout | null = null;
5617
- {
6323
+ // The cluster layout is storage-buffer backed. On the WebGL2/fallback
6324
+ // capability tier its uniform-buffer projection exceeds the backend's
6325
+ // fragment-stage limit, and HDRP is not a valid route there anyway.
6326
+ if (storageBufferCapable) {
5618
6327
  const hdrpUnifiedBglDesc = createHdrpBindGroupLayoutDescriptor(storageBufferCapable);
5619
6328
  const hdrpUnifiedBglRes = rhiDevice.createBindGroupLayout(hdrpUnifiedBglDesc);
5620
6329
  if (hdrpUnifiedBglRes.ok) {
@@ -6922,6 +7631,59 @@ async function buildReadyWebGPU(
6922
7631
  let bloomBlurHParamsBufferHandle: Buffer | null = null;
6923
7632
  let bloomBlurVParamsBufferHandle: Buffer | null = null;
6924
7633
  let bloomCompositeParamsBufferHandle: Buffer | null = null;
7634
+ let bloomResourcesReady = false;
7635
+ let ensureBloomResources: (() => void) | undefined;
7636
+ let bloomActiveBundle: BloomPersistentBundle | undefined;
7637
+ let bloomCandidateBundle: BloomPersistentBundle | undefined;
7638
+ const retiringBloomBundles = new Set<BloomPersistentBundle>();
7639
+ let bloomGeneration = 0;
7640
+ let lastSuccessfulBloomReceipts:
7641
+ | import('../record/frame-snapshot').BloomFrameReceipts
7642
+ | undefined;
7643
+ const getBloomResourcesStable = (): BloomPersistentBundle | null =>
7644
+ bloomCandidateBundle ?? bloomActiveBundle ?? null;
7645
+ let commitBloomResources: (() => void) | undefined;
7646
+ let discardBloomResources: (() => void) | undefined;
7647
+ let retireBloomResources: ((completion: Promise<unknown>) => void) | undefined;
7648
+ let drainBloomResources: (() => void) | undefined;
7649
+ let inspectBloomResources:
7650
+ | ((graph?: import('@forgeax/engine-render-graph').CompiledRenderGraphInfo) => BloomInspection)
7651
+ | undefined;
7652
+ const ensureBloomResourcesStable = (): void => {
7653
+ ensureBloomResources?.();
7654
+ };
7655
+ const retireBloomResourcesStable = (completion: Promise<unknown>): void => {
7656
+ retireBloomResources?.(completion);
7657
+ };
7658
+ const commitBloomResourcesStable = (): void => {
7659
+ commitBloomResources?.();
7660
+ };
7661
+ const discardBloomResourcesStable = (): void => {
7662
+ discardBloomResources?.();
7663
+ };
7664
+ const drainBloomResourcesStable = (): void => {
7665
+ drainBloomResources?.();
7666
+ };
7667
+ const inspectBloomResourcesStable = (
7668
+ graph?: import('@forgeax/engine-render-graph').CompiledRenderGraphInfo,
7669
+ ): BloomInspection => {
7670
+ return (
7671
+ inspectBloomResources?.(graph) ?? {
7672
+ graphStatus: 'empty',
7673
+ enabled: false,
7674
+ targetCount: 0,
7675
+ targetBytes: 0,
7676
+ resourceCount: 0,
7677
+ passCount: 0,
7678
+ encodeCount: 0,
7679
+ bindGroupCount: 0,
7680
+ uploadCount: 0,
7681
+ residentChildBytes: 0,
7682
+ generation: 0,
7683
+ state: 'off',
7684
+ }
7685
+ );
7686
+ };
6925
7687
  // feat-20260612-hdrp-ssao M6 / w26 + w43: SSAO pipeline handles (D-A).
6926
7688
  // calc + blur RenderPipeline pair sharing a dedicated 6-entry BGL.
6927
7689
  // Optional — null when manifest lacks hdrp-ssao entry.
@@ -7222,361 +7984,560 @@ async function buildReadyWebGPU(
7222
7984
  // bloom-blur-v : same BGL, same module as blur-h, V-axis texelSize
7223
7985
  // bloom-composite: 2-tex + sampler + UBO@3 BGL, rgba16float target
7224
7986
 
7225
- // Shared bloom sampler: linear filter + clamp-to-edge (all 4 passes
7226
- // sample from textures using fullscreen triangle UVs).
7227
- if (bloomBrightModule !== null || bloomBlurModule !== null || bloomCompositeModule !== null) {
7228
- const bloomSamplerResult = runShimSyncStep(
7229
- () =>
7230
- rhiDevice.createSampler({
7231
- label: 'bloom-sampler',
7232
- magFilter: 'linear',
7233
- minFilter: 'linear',
7234
- mipmapFilter: 'linear',
7235
- addressModeU: 'clamp-to-edge',
7236
- addressModeV: 'clamp-to-edge',
7237
- }),
7238
- 'webgpu-runtime-error',
7239
- 'createSampler (bloom) succeeded',
7240
- 'check device.limits.maxSamplersPerShaderStage',
7241
- );
7242
- if (!bloomSamplerResult.ok) throw bloomSamplerResult.error;
7243
- bloomSamplerHandle = bloomSamplerResult.value;
7244
- }
7987
+ ensureBloomResources = (): void => {
7988
+ if (bloomResourcesReady) return;
7989
+ const candidateScope = rendererScope.createChild(`${rendererScope.owner}:standard-bloom`);
7990
+ const adopt = <T>(
7991
+ kind: Parameters<DeviceScope['_adopt']>[0],
7992
+ value: T,
7993
+ cleanup: (value: T) => void,
7994
+ ): T => {
7995
+ candidateScope._adopt(kind, value, cleanup);
7996
+ return value;
7997
+ };
7998
+ try {
7999
+ // Shared bloom sampler: linear filter + clamp-to-edge (all 4 passes
8000
+ // sample from textures using fullscreen triangle UVs).
8001
+ if (
8002
+ bloomBrightModule !== null ||
8003
+ bloomBlurModule !== null ||
8004
+ bloomCompositeModule !== null
8005
+ ) {
8006
+ const bloomSamplerResult = runShimSyncStep(
8007
+ () =>
8008
+ rhiDevice.createSampler({
8009
+ label: 'bloom-sampler',
8010
+ magFilter: 'linear',
8011
+ minFilter: 'linear',
8012
+ mipmapFilter: 'linear',
8013
+ addressModeU: 'clamp-to-edge',
8014
+ addressModeV: 'clamp-to-edge',
8015
+ }),
8016
+ 'webgpu-runtime-error',
8017
+ 'createSampler (bloom) succeeded',
8018
+ 'check device.limits.maxSamplersPerShaderStage',
8019
+ );
8020
+ if (!bloomSamplerResult.ok) throw bloomSamplerResult.error;
8021
+ bloomSamplerHandle = adopt('binding', bloomSamplerResult.value, () => undefined);
8022
+ }
7245
8023
 
7246
- // Bloom bright: 1-tex + sampler + UBO@2 BGL (D-4).
7247
- if (bloomBrightModule !== null && bloomSamplerHandle !== null) {
7248
- const brightBglResult = runShimSyncStep(
7249
- () =>
7250
- rhiDevice.createBindGroupLayout({
7251
- label: 'bloom-bright-bgl',
7252
- entries: [
7253
- {
7254
- binding: 0,
7255
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7256
- texture: { sampleType: 'float', viewDimension: '2d' },
8024
+ // Bloom bright: 1-tex + sampler + UBO@2 BGL (D-4).
8025
+ if (bloomBrightModule !== null && bloomSamplerHandle !== null) {
8026
+ const brightBglResult = runShimSyncStep(
8027
+ () =>
8028
+ rhiDevice.createBindGroupLayout({
8029
+ label: 'bloom-bright-bgl',
8030
+ entries: [
8031
+ {
8032
+ binding: 0,
8033
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
8034
+ texture: { sampleType: 'float', viewDimension: '2d' },
8035
+ },
8036
+ {
8037
+ binding: 1,
8038
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
8039
+ sampler: { type: 'filtering' },
8040
+ },
8041
+ {
8042
+ binding: 2,
8043
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
8044
+ buffer: { type: 'uniform' },
8045
+ },
8046
+ ],
8047
+ }),
8048
+ 'webgpu-runtime-error',
8049
+ 'createBindGroupLayout(bloom-bright) succeeded',
8050
+ 'check device.limits.maxBindGroupsPerPipelineLayout',
8051
+ );
8052
+ if (!brightBglResult.ok) throw brightBglResult.error;
8053
+ bloomBrightBglHandle = adopt('binding', brightBglResult.value, () => undefined);
8054
+
8055
+ const brightPlResult = runShimSyncStep(
8056
+ () =>
8057
+ rhiDevice.createPipelineLayout({
8058
+ label: 'bloom-bright-pl',
8059
+ bindGroupLayouts: [brightBglResult.value],
8060
+ }),
8061
+ 'webgpu-runtime-error',
8062
+ 'createPipelineLayout(bloom-bright) succeeded',
8063
+ 'verify bloom-bright BindGroupLayout matches shader @group(0) bindings',
8064
+ );
8065
+ if (!brightPlResult.ok) throw brightPlResult.error;
8066
+ adopt('binding', brightPlResult.value, () => undefined);
8067
+
8068
+ // M2-T4: bloom-bright pipeline via getOrBuildPipeline (lazy-build).
8069
+ {
8070
+ const brightSpec: PipelineSpec = {
8071
+ shader: {
8072
+ id: 'forgeax::post::bloom-bright',
8073
+ passKind: 'post-process',
8074
+ variantSet: undefined,
7257
8075
  },
7258
- {
7259
- binding: 1,
7260
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7261
- sampler: { type: 'filtering' },
8076
+ attachments: {
8077
+ colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
8078
+ depthFormat: undefined,
8079
+ sampleCount: 1,
7262
8080
  },
7263
- {
7264
- binding: 2,
7265
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7266
- buffer: { type: 'uniform' },
8081
+ geometry: {
8082
+ topology: 'triangle-list',
8083
+ stripIndexFormat: undefined,
8084
+ vertexLayout: {},
7267
8085
  },
7268
- ],
7269
- }),
7270
- 'webgpu-runtime-error',
7271
- 'createBindGroupLayout(bloom-bright) succeeded',
7272
- 'check device.limits.maxBindGroupsPerPipelineLayout',
7273
- );
7274
- if (!brightBglResult.ok) throw brightBglResult.error;
7275
- bloomBrightBglHandle = brightBglResult.value;
7276
-
7277
- const brightPlResult = runShimSyncStep(
7278
- () =>
7279
- rhiDevice.createPipelineLayout({
7280
- label: 'bloom-bright-pl',
7281
- bindGroupLayouts: [brightBglResult.value],
7282
- }),
7283
- 'webgpu-runtime-error',
7284
- 'createPipelineLayout(bloom-bright) succeeded',
7285
- 'verify bloom-bright BindGroupLayout matches shader @group(0) bindings',
7286
- );
7287
- if (!brightPlResult.ok) throw brightPlResult.error;
8086
+ renderState: { cullMode: 'none' },
8087
+ };
8088
+ const modules = {
8089
+ vertex: bloomBrightModule,
8090
+ fragment: bloomBrightModule,
8091
+ layout: brightPlResult.value,
8092
+ };
8093
+ try {
8094
+ bloomBrightPipelineHandle = getOrBuildPipeline(
8095
+ brightSpec,
8096
+ pipelineDeviceProvider,
8097
+ pipelineCache,
8098
+ modules,
8099
+ ) as RenderPipeline;
8100
+ bloomBrightPipelineHandle = adopt(
8101
+ 'pipeline',
8102
+ bloomBrightPipelineHandle,
8103
+ () => undefined,
8104
+ );
8105
+ } catch (err) {
8106
+ if (err instanceof PipelineSpecError) throw err;
8107
+ throw new PipelineSpecError({
8108
+ code: 'pipeline-build-failed',
8109
+ detail: { cause: err },
8110
+ hint: 'createRenderPipeline (bloom-bright fullscreen) failed; inspect gpuMessage',
8111
+ });
8112
+ }
8113
+ }
7288
8114
 
7289
- // M2-T4: bloom-bright pipeline via getOrBuildPipeline (lazy-build).
7290
- {
7291
- const brightSpec: PipelineSpec = {
7292
- shader: {
7293
- id: 'forgeax::post::bloom-bright',
7294
- passKind: 'post-process',
7295
- variantSet: undefined,
7296
- },
7297
- attachments: {
7298
- colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
7299
- depthFormat: undefined,
7300
- sampleCount: 1,
7301
- },
7302
- geometry: {
7303
- topology: 'triangle-list',
7304
- stripIndexFormat: undefined,
7305
- vertexLayout: {},
7306
- },
7307
- renderState: { cullMode: 'none' },
7308
- };
7309
- const modules = {
7310
- vertex: bloomBrightModule,
7311
- fragment: bloomBrightModule,
7312
- layout: brightPlResult.value,
7313
- };
7314
- try {
7315
- bloomBrightPipelineHandle = getOrBuildPipeline(
7316
- brightSpec,
7317
- pipelineDeviceProvider,
7318
- pipelineCache,
7319
- modules,
7320
- ) as RenderPipeline;
7321
- } catch (err) {
7322
- if (err instanceof PipelineSpecError) throw err;
7323
- throw new PipelineSpecError({
7324
- code: 'pipeline-build-failed',
7325
- detail: { cause: err },
7326
- hint: 'createRenderPipeline (bloom-bright fullscreen) failed; inspect gpuMessage',
8115
+ // Bright params UBO: 16 B std140 (threshold f32 + 12 B pad).
8116
+ const brightParamsResult = runShimSyncStep(
8117
+ () =>
8118
+ rhiDevice.createBuffer({
8119
+ label: 'bloom-bright-params-ubo',
8120
+ size: BRIGHT_PARAMS_BYTES,
8121
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
8122
+ mappedAtCreation: false,
8123
+ }),
8124
+ 'webgpu-runtime-error',
8125
+ 'createBuffer (bloom-bright params UBO) succeeded',
8126
+ 'check device.limits.maxUniformBufferBindingSize',
8127
+ );
8128
+ if (!brightParamsResult.ok) throw brightParamsResult.error;
8129
+ bloomBrightParamsBufferHandle = adopt('buffer', brightParamsResult.value, (value) => {
8130
+ rhiDevice.destroyBuffer(value);
7327
8131
  });
7328
8132
  }
7329
- }
7330
-
7331
- // Bright params UBO: 16 B std140 (threshold f32 + 12 B pad).
7332
- const brightParamsResult = runShimSyncStep(
7333
- () =>
7334
- rhiDevice.createBuffer({
7335
- label: 'bloom-bright-params-ubo',
7336
- size: BRIGHT_PARAMS_BYTES,
7337
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
7338
- mappedAtCreation: false,
7339
- }),
7340
- 'webgpu-runtime-error',
7341
- 'createBuffer (bloom-bright params UBO) succeeded',
7342
- 'check device.limits.maxUniformBufferBindingSize',
7343
- );
7344
- if (!brightParamsResult.ok) throw brightParamsResult.error;
7345
- bloomBrightParamsBufferHandle = brightParamsResult.value;
7346
- }
7347
8133
 
7348
- // Bloom blur H/V: same BGL (1-tex + sampler + UBO@2), same module,
7349
- // two separate pipelines with per-axis texelSize (D-1, D-4).
7350
- if (bloomBlurModule !== null && bloomSamplerHandle !== null) {
7351
- const blurBglResult = runShimSyncStep(
7352
- () =>
7353
- rhiDevice.createBindGroupLayout({
7354
- label: 'bloom-blur-bgl',
7355
- entries: [
7356
- {
7357
- binding: 0,
7358
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7359
- texture: { sampleType: 'float', viewDimension: '2d' },
8134
+ // Bloom blur H/V: same BGL (1-tex + sampler + UBO@2), same module,
8135
+ // two separate pipelines with per-axis texelSize (D-1, D-4).
8136
+ if (bloomBlurModule !== null && bloomSamplerHandle !== null) {
8137
+ const blurBglResult = runShimSyncStep(
8138
+ () =>
8139
+ rhiDevice.createBindGroupLayout({
8140
+ label: 'bloom-blur-bgl',
8141
+ entries: [
8142
+ {
8143
+ binding: 0,
8144
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
8145
+ texture: { sampleType: 'float', viewDimension: '2d' },
8146
+ },
8147
+ {
8148
+ binding: 1,
8149
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
8150
+ sampler: { type: 'filtering' },
8151
+ },
8152
+ {
8153
+ binding: 2,
8154
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
8155
+ buffer: { type: 'uniform' },
8156
+ },
8157
+ ],
8158
+ }),
8159
+ 'webgpu-runtime-error',
8160
+ 'createBindGroupLayout(bloom-blur) succeeded',
8161
+ 'check device.limits.maxBindGroupsPerPipelineLayout',
8162
+ );
8163
+ if (!blurBglResult.ok) throw blurBglResult.error;
8164
+ bloomBlurBglHandle = adopt('binding', blurBglResult.value, () => undefined);
8165
+
8166
+ const blurPlResult = runShimSyncStep(
8167
+ () =>
8168
+ rhiDevice.createPipelineLayout({
8169
+ label: 'bloom-blur-pl',
8170
+ bindGroupLayouts: [blurBglResult.value],
8171
+ }),
8172
+ 'webgpu-runtime-error',
8173
+ 'createPipelineLayout(bloom-blur) succeeded',
8174
+ 'verify bloom-blur BindGroupLayout matches shader @group(0) bindings',
8175
+ );
8176
+ if (!blurPlResult.ok) throw blurPlResult.error;
8177
+ adopt('binding', blurPlResult.value, () => undefined);
8178
+
8179
+ // M2-T4: bloom-blur H/V pipelines via getOrBuildPipeline (lazy-build).
8180
+ // H and V share the same PSO descriptor — only per-axis texelSize UBO
8181
+ // distinguishes them at record time. getOrBuildPipeline cache-hit on the
8182
+ // second call returns the same handle (identical spec, identical PSO).
8183
+ {
8184
+ const blurSpec: PipelineSpec = {
8185
+ shader: {
8186
+ id: 'forgeax::post::bloom-blur',
8187
+ passKind: 'post-process',
8188
+ variantSet: undefined,
7360
8189
  },
7361
- {
7362
- binding: 1,
7363
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7364
- sampler: { type: 'filtering' },
8190
+ attachments: {
8191
+ colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
8192
+ depthFormat: undefined,
8193
+ sampleCount: 1,
7365
8194
  },
7366
- {
7367
- binding: 2,
7368
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7369
- buffer: { type: 'uniform' },
8195
+ geometry: {
8196
+ topology: 'triangle-list',
8197
+ stripIndexFormat: undefined,
8198
+ vertexLayout: {},
7370
8199
  },
7371
- ],
7372
- }),
7373
- 'webgpu-runtime-error',
7374
- 'createBindGroupLayout(bloom-blur) succeeded',
7375
- 'check device.limits.maxBindGroupsPerPipelineLayout',
7376
- );
7377
- if (!blurBglResult.ok) throw blurBglResult.error;
7378
- bloomBlurBglHandle = blurBglResult.value;
8200
+ renderState: { cullMode: 'none' },
8201
+ };
8202
+ const modules = {
8203
+ vertex: bloomBlurModule,
8204
+ fragment: bloomBlurModule,
8205
+ layout: blurPlResult.value,
8206
+ label: 'bloom-blur-h-pipeline',
8207
+ };
8208
+ try {
8209
+ bloomBlurHPipelineHandle = getOrBuildPipeline(
8210
+ blurSpec,
8211
+ pipelineDeviceProvider,
8212
+ pipelineCache,
8213
+ modules,
8214
+ ) as RenderPipeline;
8215
+ bloomBlurHPipelineHandle = adopt(
8216
+ 'pipeline',
8217
+ bloomBlurHPipelineHandle,
8218
+ () => undefined,
8219
+ );
8220
+ } catch (err) {
8221
+ if (err instanceof PipelineSpecError) throw err;
8222
+ throw new PipelineSpecError({
8223
+ code: 'pipeline-build-failed',
8224
+ detail: { cause: err },
8225
+ hint: 'createRenderPipeline (bloom-blur-h fullscreen) failed; inspect gpuMessage',
8226
+ });
8227
+ }
8228
+ // V is cache-hit on the same spec (identical PSO; per-axis UBO differentiates at record time).
8229
+ bloomBlurVPipelineHandle = bloomBlurHPipelineHandle;
8230
+ }
7379
8231
 
7380
- const blurPlResult = runShimSyncStep(
7381
- () =>
7382
- rhiDevice.createPipelineLayout({
7383
- label: 'bloom-blur-pl',
7384
- bindGroupLayouts: [blurBglResult.value],
7385
- }),
7386
- 'webgpu-runtime-error',
7387
- 'createPipelineLayout(bloom-blur) succeeded',
7388
- 'verify bloom-blur BindGroupLayout matches shader @group(0) bindings',
7389
- );
7390
- if (!blurPlResult.ok) throw blurPlResult.error;
8232
+ // Blur params UBOs: 16 B std140 (texelSize.xy + radius + pad) each.
8233
+ // bug-20260625: one per axis -- H and V must not share a buffer (the
8234
+ // shared-buffer writeBuffer race made both passes blur vertically).
8235
+ const blurHParamsResult = runShimSyncStep(
8236
+ () =>
8237
+ rhiDevice.createBuffer({
8238
+ label: 'bloom-blur-h-params-ubo',
8239
+ size: BLUR_PARAMS_BYTES,
8240
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
8241
+ mappedAtCreation: false,
8242
+ }),
8243
+ 'webgpu-runtime-error',
8244
+ 'createBuffer (bloom-blur-h params UBO) succeeded',
8245
+ 'check device.limits.maxUniformBufferBindingSize',
8246
+ );
8247
+ if (!blurHParamsResult.ok) throw blurHParamsResult.error;
8248
+ bloomBlurHParamsBufferHandle = adopt('buffer', blurHParamsResult.value, (value) => {
8249
+ rhiDevice.destroyBuffer(value);
8250
+ });
7391
8251
 
7392
- // M2-T4: bloom-blur H/V pipelines via getOrBuildPipeline (lazy-build).
7393
- // H and V share the same PSO descriptor — only per-axis texelSize UBO
7394
- // distinguishes them at record time. getOrBuildPipeline cache-hit on the
7395
- // second call returns the same handle (identical spec, identical PSO).
7396
- {
7397
- const blurSpec: PipelineSpec = {
7398
- shader: {
7399
- id: 'forgeax::post::bloom-blur',
7400
- passKind: 'post-process',
7401
- variantSet: undefined,
7402
- },
7403
- attachments: {
7404
- colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
7405
- depthFormat: undefined,
7406
- sampleCount: 1,
7407
- },
7408
- geometry: {
7409
- topology: 'triangle-list',
7410
- stripIndexFormat: undefined,
7411
- vertexLayout: {},
7412
- },
7413
- renderState: { cullMode: 'none' },
7414
- };
7415
- const modules = {
7416
- vertex: bloomBlurModule,
7417
- fragment: bloomBlurModule,
7418
- layout: blurPlResult.value,
7419
- label: 'bloom-blur-h-pipeline',
7420
- };
7421
- try {
7422
- bloomBlurHPipelineHandle = getOrBuildPipeline(
7423
- blurSpec,
7424
- pipelineDeviceProvider,
7425
- pipelineCache,
7426
- modules,
7427
- ) as RenderPipeline;
7428
- } catch (err) {
7429
- if (err instanceof PipelineSpecError) throw err;
7430
- throw new PipelineSpecError({
7431
- code: 'pipeline-build-failed',
7432
- detail: { cause: err },
7433
- hint: 'createRenderPipeline (bloom-blur-h fullscreen) failed; inspect gpuMessage',
8252
+ const blurVParamsResult = runShimSyncStep(
8253
+ () =>
8254
+ rhiDevice.createBuffer({
8255
+ label: 'bloom-blur-v-params-ubo',
8256
+ size: BLUR_PARAMS_BYTES,
8257
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
8258
+ mappedAtCreation: false,
8259
+ }),
8260
+ 'webgpu-runtime-error',
8261
+ 'createBuffer (bloom-blur-v params UBO) succeeded',
8262
+ 'check device.limits.maxUniformBufferBindingSize',
8263
+ );
8264
+ if (!blurVParamsResult.ok) throw blurVParamsResult.error;
8265
+ bloomBlurVParamsBufferHandle = adopt('buffer', blurVParamsResult.value, (value) => {
8266
+ rhiDevice.destroyBuffer(value);
7434
8267
  });
7435
8268
  }
7436
- // V is cache-hit on the same spec (identical PSO; per-axis UBO differentiates at record time).
7437
- bloomBlurVPipelineHandle = bloomBlurHPipelineHandle;
7438
- }
7439
-
7440
- // Blur params UBOs: 16 B std140 (texelSize.xy + radius + pad) each.
7441
- // bug-20260625: one per axis -- H and V must not share a buffer (the
7442
- // shared-buffer writeBuffer race made both passes blur vertically).
7443
- const blurHParamsResult = runShimSyncStep(
7444
- () =>
7445
- rhiDevice.createBuffer({
7446
- label: 'bloom-blur-h-params-ubo',
7447
- size: BLUR_PARAMS_BYTES,
7448
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
7449
- mappedAtCreation: false,
7450
- }),
7451
- 'webgpu-runtime-error',
7452
- 'createBuffer (bloom-blur-h params UBO) succeeded',
7453
- 'check device.limits.maxUniformBufferBindingSize',
7454
- );
7455
- if (!blurHParamsResult.ok) throw blurHParamsResult.error;
7456
- bloomBlurHParamsBufferHandle = blurHParamsResult.value;
7457
-
7458
- const blurVParamsResult = runShimSyncStep(
7459
- () =>
7460
- rhiDevice.createBuffer({
7461
- label: 'bloom-blur-v-params-ubo',
7462
- size: BLUR_PARAMS_BYTES,
7463
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
7464
- mappedAtCreation: false,
7465
- }),
7466
- 'webgpu-runtime-error',
7467
- 'createBuffer (bloom-blur-v params UBO) succeeded',
7468
- 'check device.limits.maxUniformBufferBindingSize',
7469
- );
7470
- if (!blurVParamsResult.ok) throw blurVParamsResult.error;
7471
- bloomBlurVParamsBufferHandle = blurVParamsResult.value;
7472
- }
7473
8269
 
7474
- // Bloom composite: 2-tex + sampler + UBO@3 BGL (D-4, D-5).
7475
- if (bloomCompositeModule !== null && bloomSamplerHandle !== null) {
7476
- const compositeBglResult = runShimSyncStep(
7477
- () =>
7478
- rhiDevice.createBindGroupLayout({
7479
- label: 'bloom-composite-bgl',
7480
- entries: [
7481
- {
7482
- binding: 0,
7483
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7484
- texture: { sampleType: 'float', viewDimension: '2d' },
7485
- },
7486
- {
7487
- binding: 1,
7488
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7489
- texture: { sampleType: 'float', viewDimension: '2d' },
8270
+ // Bloom composite: 2-tex + sampler + UBO@3 BGL (D-4, D-5).
8271
+ if (bloomCompositeModule !== null && bloomSamplerHandle !== null) {
8272
+ const compositeBglResult = runShimSyncStep(
8273
+ () =>
8274
+ rhiDevice.createBindGroupLayout({
8275
+ label: 'bloom-composite-bgl',
8276
+ entries: [
8277
+ {
8278
+ binding: 0,
8279
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
8280
+ texture: { sampleType: 'float', viewDimension: '2d' },
8281
+ },
8282
+ {
8283
+ binding: 1,
8284
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
8285
+ texture: { sampleType: 'float', viewDimension: '2d' },
8286
+ },
8287
+ {
8288
+ binding: 2,
8289
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
8290
+ sampler: { type: 'filtering' },
8291
+ },
8292
+ {
8293
+ binding: 3,
8294
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
8295
+ buffer: { type: 'uniform' },
8296
+ },
8297
+ ],
8298
+ }),
8299
+ 'webgpu-runtime-error',
8300
+ 'createBindGroupLayout(bloom-composite) succeeded',
8301
+ 'check device.limits.maxBindGroupsPerPipelineLayout',
8302
+ );
8303
+ if (!compositeBglResult.ok) throw compositeBglResult.error;
8304
+ bloomCompositeBglHandle = adopt('binding', compositeBglResult.value, () => undefined);
8305
+
8306
+ const compositePlResult = runShimSyncStep(
8307
+ () =>
8308
+ rhiDevice.createPipelineLayout({
8309
+ label: 'bloom-composite-pl',
8310
+ bindGroupLayouts: [compositeBglResult.value],
8311
+ }),
8312
+ 'webgpu-runtime-error',
8313
+ 'createPipelineLayout(bloom-composite) succeeded',
8314
+ 'verify bloom-composite BindGroupLayout matches shader @group(0) bindings',
8315
+ );
8316
+ if (!compositePlResult.ok) throw compositePlResult.error;
8317
+ adopt('binding', compositePlResult.value, () => undefined);
8318
+
8319
+ // M2-T4: bloom-composite pipeline via getOrBuildPipeline (lazy-build).
8320
+ {
8321
+ const compositeSpec: PipelineSpec = {
8322
+ shader: {
8323
+ id: 'forgeax::post::bloom-composite',
8324
+ passKind: 'post-process',
8325
+ variantSet: undefined,
7490
8326
  },
7491
- {
7492
- binding: 2,
7493
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7494
- sampler: { type: 'filtering' },
8327
+ attachments: {
8328
+ colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
8329
+ depthFormat: undefined,
8330
+ sampleCount: 1,
7495
8331
  },
7496
- {
7497
- binding: 3,
7498
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7499
- buffer: { type: 'uniform' },
8332
+ geometry: {
8333
+ topology: 'triangle-list',
8334
+ stripIndexFormat: undefined,
8335
+ vertexLayout: {},
7500
8336
  },
7501
- ],
7502
- }),
7503
- 'webgpu-runtime-error',
7504
- 'createBindGroupLayout(bloom-composite) succeeded',
7505
- 'check device.limits.maxBindGroupsPerPipelineLayout',
7506
- );
7507
- if (!compositeBglResult.ok) throw compositeBglResult.error;
7508
- bloomCompositeBglHandle = compositeBglResult.value;
8337
+ renderState: { cullMode: 'none' },
8338
+ };
8339
+ const modules = {
8340
+ vertex: bloomCompositeModule,
8341
+ fragment: bloomCompositeModule,
8342
+ layout: compositePlResult.value,
8343
+ };
8344
+ try {
8345
+ bloomCompositePipelineHandle = getOrBuildPipeline(
8346
+ compositeSpec,
8347
+ pipelineDeviceProvider,
8348
+ pipelineCache,
8349
+ modules,
8350
+ ) as RenderPipeline;
8351
+ bloomCompositePipelineHandle = adopt(
8352
+ 'pipeline',
8353
+ bloomCompositePipelineHandle,
8354
+ () => undefined,
8355
+ );
8356
+ } catch (err) {
8357
+ if (err instanceof PipelineSpecError) throw err;
8358
+ throw new PipelineSpecError({
8359
+ code: 'pipeline-build-failed',
8360
+ detail: { cause: err },
8361
+ hint: 'createRenderPipeline (bloom-composite fullscreen) failed; inspect gpuMessage',
8362
+ });
8363
+ }
8364
+ }
7509
8365
 
7510
- const compositePlResult = runShimSyncStep(
7511
- () =>
7512
- rhiDevice.createPipelineLayout({
7513
- label: 'bloom-composite-pl',
7514
- bindGroupLayouts: [compositeBglResult.value],
7515
- }),
7516
- 'webgpu-runtime-error',
7517
- 'createPipelineLayout(bloom-composite) succeeded',
7518
- 'verify bloom-composite BindGroupLayout matches shader @group(0) bindings',
7519
- );
7520
- if (!compositePlResult.ok) throw compositePlResult.error;
8366
+ // Composite params UBO: 16 B std140 (intensity f32 + 12 B pad).
8367
+ const compositeParamsResult = runShimSyncStep(
8368
+ () =>
8369
+ rhiDevice.createBuffer({
8370
+ label: 'bloom-composite-params-ubo',
8371
+ size: COMPOSITE_PARAMS_BYTES,
8372
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
8373
+ mappedAtCreation: false,
8374
+ }),
8375
+ 'webgpu-runtime-error',
8376
+ 'createBuffer (bloom-composite params UBO) succeeded',
8377
+ 'check device.limits.maxUniformBufferBindingSize',
8378
+ );
8379
+ if (!compositeParamsResult.ok) throw compositeParamsResult.error;
8380
+ bloomCompositeParamsBufferHandle = adopt(
8381
+ 'buffer',
8382
+ compositeParamsResult.value,
8383
+ (value) => {
8384
+ rhiDevice.destroyBuffer(value);
8385
+ },
8386
+ );
8387
+ }
7521
8388
 
7522
- // M2-T4: bloom-composite pipeline via getOrBuildPipeline (lazy-build).
7523
- {
7524
- const compositeSpec: PipelineSpec = {
7525
- shader: {
7526
- id: 'forgeax::post::bloom-composite',
7527
- passKind: 'post-process',
7528
- variantSet: undefined,
7529
- },
7530
- attachments: {
7531
- colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
7532
- depthFormat: undefined,
7533
- sampleCount: 1,
7534
- },
7535
- geometry: {
7536
- topology: 'triangle-list',
7537
- stripIndexFormat: undefined,
7538
- vertexLayout: {},
7539
- },
7540
- renderState: { cullMode: 'none' },
8389
+ bloomCandidateBundle = {
8390
+ scope: candidateScope,
8391
+ generation: bloomGeneration + 1,
8392
+ bloomBrightPipeline: bloomBrightPipelineHandle,
8393
+ bloomBlurHPipeline: bloomBlurHPipelineHandle,
8394
+ bloomBlurVPipeline: bloomBlurVPipelineHandle,
8395
+ bloomCompositePipeline: bloomCompositePipelineHandle,
8396
+ bloomBrightBindGroupLayout: bloomBrightBglHandle,
8397
+ bloomBlurBindGroupLayout: bloomBlurBglHandle,
8398
+ bloomCompositeBindGroupLayout: bloomCompositeBglHandle,
8399
+ bloomSampler: bloomSamplerHandle,
8400
+ bloomBrightParamsBuffer: bloomBrightParamsBufferHandle,
8401
+ bloomBlurHParamsBuffer: bloomBlurHParamsBufferHandle,
8402
+ bloomBlurVParamsBuffer: bloomBlurVParamsBufferHandle,
8403
+ bloomCompositeParamsBuffer: bloomCompositeParamsBufferHandle,
7541
8404
  };
7542
- const modules = {
7543
- vertex: bloomCompositeModule,
7544
- fragment: bloomCompositeModule,
7545
- layout: compositePlResult.value,
8405
+ inspectBloomResources = (graph): BloomInspection => {
8406
+ const retiring = [...retiringBloomBundles].sort(
8407
+ (left, right) => left.generation - right.generation,
8408
+ );
8409
+ const active = bloomActiveBundle?.scope.isAlive() ? bloomActiveBundle : undefined;
8410
+ const liveRetiring = retiring.filter((bundle) => bundle.scope.state === 'retiring');
8411
+ const selected = active ?? liveRetiring[liveRetiring.length - 1];
8412
+ if (selected === undefined) {
8413
+ return {
8414
+ graphStatus: 'empty',
8415
+ enabled: false,
8416
+ targetCount: 0,
8417
+ targetBytes: 0,
8418
+ resourceCount: 0,
8419
+ passCount: 0,
8420
+ encodeCount: 0,
8421
+ bindGroupCount: 0,
8422
+ uploadCount: 0,
8423
+ residentChildBytes: 0,
8424
+ generation: 0,
8425
+ state: 'off',
8426
+ };
8427
+ }
8428
+ const state = active === undefined ? 'retiring' : 'active';
8429
+ const ownedBundles = [...(active === undefined ? [] : [active]), ...liveRetiring];
8430
+ const graphInspection = inspectStandardBloomGraph(graph);
8431
+ const receipts = lastSuccessfulBloomReceipts ?? {
8432
+ uploadCount: 0,
8433
+ bindGroupCount: 0,
8434
+ encodeCount: 0,
8435
+ };
8436
+ return {
8437
+ graphStatus: active === undefined ? 'empty' : graphInspection.status,
8438
+ enabled: active !== undefined,
8439
+ targetCount: active === undefined ? 0 : graphInspection.targetCount,
8440
+ targetBytes: active === undefined ? 0 : graphInspection.targetBytes,
8441
+ resourceCount: ownedBundles.reduce(
8442
+ (count, bundle) => count + bundle.scope.resourceDelta(),
8443
+ 0,
8444
+ ),
8445
+ passCount: active === undefined ? 0 : graphInspection.passCount,
8446
+ encodeCount: active === undefined ? 0 : receipts.encodeCount,
8447
+ bindGroupCount: active === undefined ? 0 : receipts.bindGroupCount,
8448
+ uploadCount: active === undefined ? 0 : receipts.uploadCount,
8449
+ residentChildBytes: 0,
8450
+ generation: selected?.generation ?? 0,
8451
+ state,
8452
+ };
7546
8453
  };
7547
- try {
7548
- bloomCompositePipelineHandle = getOrBuildPipeline(
7549
- compositeSpec,
7550
- pipelineDeviceProvider,
7551
- pipelineCache,
7552
- modules,
7553
- ) as RenderPipeline;
7554
- } catch (err) {
7555
- if (err instanceof PipelineSpecError) throw err;
7556
- throw new PipelineSpecError({
7557
- code: 'pipeline-build-failed',
7558
- detail: { cause: err },
7559
- hint: 'createRenderPipeline (bloom-composite fullscreen) failed; inspect gpuMessage',
7560
- });
7561
- }
8454
+ commitBloomResources = () => {
8455
+ if (bloomCandidateBundle?.scope !== candidateScope) return;
8456
+ bloomActiveBundle = bloomCandidateBundle;
8457
+ bloomCandidateBundle = undefined;
8458
+ bloomGeneration += 1;
8459
+ bloomResourcesReady = true;
8460
+ };
8461
+ discardBloomResources = () => {
8462
+ if (bloomCandidateBundle?.scope !== candidateScope) return;
8463
+ candidateScope.abandon();
8464
+ bloomCandidateBundle = undefined;
8465
+ bloomResourcesReady = false;
8466
+ };
8467
+ const reportBloomFenceFailure = (cause: unknown): void => {
8468
+ if (cause instanceof RhiError) {
8469
+ errorRegistry.fire(cause);
8470
+ return;
8471
+ }
8472
+ const detailError = {
8473
+ code: 'bloom-fence-rejected',
8474
+ message: String(cause),
8475
+ ...(cause instanceof Error ? { name: cause.name } : {}),
8476
+ };
8477
+ errorRegistry.fire(
8478
+ new RhiError({
8479
+ code: 'webgpu-runtime-error',
8480
+ expected: 'Bloom queue completion resolves after the submitted frame retires',
8481
+ hint: 'inspect renderer errors for the queue completion cause',
8482
+ detail: { error: detailError },
8483
+ }),
8484
+ );
8485
+ };
8486
+ retireBloomResources = (completion) => {
8487
+ const retiring = bloomActiveBundle;
8488
+ if (retiring === undefined) return;
8489
+ bloomActiveBundle = undefined;
8490
+ bloomResourcesReady = false;
8491
+ retiringBloomBundles.add(retiring);
8492
+ retiring.scope.beginRetire();
8493
+ void completion.then(
8494
+ () => {
8495
+ if (!retiringBloomBundles.has(retiring)) return;
8496
+ retiring.scope.retire();
8497
+ retiringBloomBundles.delete(retiring);
8498
+ },
8499
+ (cause) => {
8500
+ if (!retiringBloomBundles.has(retiring)) return;
8501
+ reportBloomFenceFailure(cause);
8502
+ retiring.scope.retire();
8503
+ retiringBloomBundles.delete(retiring);
8504
+ },
8505
+ );
8506
+ };
8507
+ drainBloomResources = () => {
8508
+ if (bloomCandidateBundle !== undefined) {
8509
+ bloomCandidateBundle.scope.abandon();
8510
+ bloomCandidateBundle = undefined;
8511
+ }
8512
+ if (bloomActiveBundle !== undefined) {
8513
+ bloomActiveBundle.scope.retire();
8514
+ bloomActiveBundle = undefined;
8515
+ }
8516
+ for (const retiring of retiringBloomBundles) retiring.scope.retire();
8517
+ retiringBloomBundles.clear();
8518
+ bloomResourcesReady = false;
8519
+ };
8520
+ } catch (cause) {
8521
+ candidateScope.abandon();
8522
+ if (bloomCandidateBundle?.scope === candidateScope) bloomCandidateBundle = undefined;
8523
+ commitBloomResources = undefined;
8524
+ discardBloomResources = undefined;
8525
+ drainBloomResources = undefined;
8526
+ bloomBrightPipelineHandle = null;
8527
+ bloomBlurHPipelineHandle = null;
8528
+ bloomBlurVPipelineHandle = null;
8529
+ bloomCompositePipelineHandle = null;
8530
+ bloomBrightBglHandle = null;
8531
+ bloomBlurBglHandle = null;
8532
+ bloomCompositeBglHandle = null;
8533
+ bloomSamplerHandle = null;
8534
+ bloomBrightParamsBufferHandle = null;
8535
+ bloomBlurHParamsBufferHandle = null;
8536
+ bloomBlurVParamsBufferHandle = null;
8537
+ bloomCompositeParamsBufferHandle = null;
8538
+ throw cause;
7562
8539
  }
7563
-
7564
- // Composite params UBO: 16 B std140 (intensity f32 + 12 B pad).
7565
- const compositeParamsResult = runShimSyncStep(
7566
- () =>
7567
- rhiDevice.createBuffer({
7568
- label: 'bloom-composite-params-ubo',
7569
- size: COMPOSITE_PARAMS_BYTES,
7570
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
7571
- mappedAtCreation: false,
7572
- }),
7573
- 'webgpu-runtime-error',
7574
- 'createBuffer (bloom-composite params UBO) succeeded',
7575
- 'check device.limits.maxUniformBufferBindingSize',
7576
- );
7577
- if (!compositeParamsResult.ok) throw compositeParamsResult.error;
7578
- bloomCompositeParamsBufferHandle = compositeParamsResult.value;
7579
- }
8540
+ };
7580
8541
 
7581
8542
  // ── feat-20260612-hdrp-ssao M6 / w26 + w43 + M8 / w37 ───────────────────
7582
8543
  //
@@ -7798,7 +8759,7 @@ async function buildReadyWebGPU(
7798
8759
  if (!prewarmRes.ok) throw prewarmRes.error;
7799
8760
  }
7800
8761
 
7801
- return {
8762
+ const pipelineState: PipelineState = {
7802
8763
  // feat-20260518-pbr-direct-lighting-mvp M5 / w22.10 (AC-06 dual->triple
7803
8764
  // pipeline + D-2 + D-10): three distinct GPU render-pipeline handles
7804
8765
  // backed by 2 distinct shader modules (unlit + pbr) x 2 vertex stride
@@ -7881,7 +8842,9 @@ async function buildReadyWebGPU(
7881
8842
  meshes: meshHandles,
7882
8843
  format: swapChainFormats.storage,
7883
8844
  colorAttachmentFormat: swapChainFormats.view,
8845
+ surfaceProfile: surfaceViewFormats ? 'dual-view' : 'raw-only',
7884
8846
  viewBindGroupLayout: viewBglResult.value,
8847
+ projectorAvailable,
7885
8848
  materialBindGroupLayout: materialBglResult.value,
7886
8849
  meshBindGroupLayout: meshArrayBglResult.value,
7887
8850
  viewUniformBuffer: viewUboResult.value,
@@ -7990,18 +8953,28 @@ async function buildReadyWebGPU(
7990
8953
  // (ensureLazyTexture, slot width/height tracking for size-drift rebuild).
7991
8954
  // BindGroup caches survive until the intermediate view is invalidated
7992
8955
  // by a resize (width/height drift forces null).
7993
- bloomBrightPipeline: bloomBrightPipelineHandle,
7994
- bloomBlurHPipeline: bloomBlurHPipelineHandle,
7995
- bloomBlurVPipeline: bloomBlurVPipelineHandle,
7996
- bloomCompositePipeline: bloomCompositePipelineHandle,
7997
- bloomBrightBindGroupLayout: bloomBrightBglHandle,
7998
- bloomBlurBindGroupLayout: bloomBlurBglHandle,
7999
- bloomCompositeBindGroupLayout: bloomCompositeBglHandle,
8000
- bloomSampler: bloomSamplerHandle,
8001
- bloomBrightParamsBuffer: bloomBrightParamsBufferHandle,
8002
- bloomBlurHParamsBuffer: bloomBlurHParamsBufferHandle,
8003
- bloomBlurVParamsBuffer: bloomBlurVParamsBufferHandle,
8004
- bloomCompositeParamsBuffer: bloomCompositeParamsBufferHandle,
8956
+ bloomBrightPipeline: null,
8957
+ bloomBlurHPipeline: null,
8958
+ bloomBlurVPipeline: null,
8959
+ bloomCompositePipeline: null,
8960
+ bloomBrightBindGroupLayout: null,
8961
+ bloomBlurBindGroupLayout: null,
8962
+ bloomCompositeBindGroupLayout: null,
8963
+ bloomSampler: null,
8964
+ bloomBrightParamsBuffer: null,
8965
+ bloomBlurHParamsBuffer: null,
8966
+ bloomBlurVParamsBuffer: null,
8967
+ bloomCompositeParamsBuffer: null,
8968
+ ensureBloomResources: ensureBloomResourcesStable,
8969
+ getBloomResources: getBloomResourcesStable,
8970
+ commitBloomResources: commitBloomResourcesStable,
8971
+ commitBloomFrameReceipts: (receipts) => {
8972
+ lastSuccessfulBloomReceipts = receipts;
8973
+ },
8974
+ discardBloomResources: discardBloomResourcesStable,
8975
+ retireBloomResources: retireBloomResourcesStable,
8976
+ drainBloomResources: drainBloomResourcesStable,
8977
+ inspectBloomResources: inspectBloomResourcesStable,
8005
8978
  bloomBrightTexture: null,
8006
8979
  bloomBrightView: null,
8007
8980
  bloomBrightWidth: 0,
@@ -8026,6 +8999,7 @@ async function buildReadyWebGPU(
8026
8999
  ssaoFallbackRawView: null,
8027
9000
  },
8028
9001
  };
9002
+ return pipelineState;
8029
9003
  }
8030
9004
 
8031
9005
  /**
@@ -8050,8 +9024,8 @@ function ensureContextConfigured(
8050
9024
  internals: WebGPURendererInternals,
8051
9025
  state: PipelineState,
8052
9026
  errorRegistry: RhiErrorListenerRegistry,
8053
- ): void {
8054
- if (state.perPassResources.configured) return;
9027
+ ): Result<void, RhiError> {
9028
+ if (state.perPassResources.configured) return ok(undefined);
8055
9029
  // M6 / w41 (feat-20260510-rhi-resource-creation): configure goes through
8056
9030
  // the forgeax `RhiCanvasContext` surface; the shim resolves the forgeax
8057
9031
  // RhiDevice back to the underlying raw GPUDevice via RAW_DEVICE_MAP
@@ -8064,7 +9038,7 @@ function ensureContextConfigured(
8064
9038
  const configuredDevice = internals.pack.instrumentation?.resolveSurfaceDevice?.(internals.device);
8065
9039
  if (configuredDevice !== undefined && !configuredDevice.ok) {
8066
9040
  errorRegistry.fire(configuredDevice.error);
8067
- return;
9041
+ return err(configuredDevice.error);
8068
9042
  }
8069
9043
  const cfgResult = configureSurface(
8070
9044
  internals.context,
@@ -8074,7 +9048,7 @@ function ensureContextConfigured(
8074
9048
  );
8075
9049
  if (!cfgResult.ok) {
8076
9050
  errorRegistry.fire(cfgResult.error);
8077
- return;
9051
+ return err(cfgResult.error);
8078
9052
  }
8079
9053
  state.perPassResources.configured = true;
8080
9054
  // bug-20260612: write the actually-configured swap-chain storage format
@@ -8087,6 +9061,7 @@ function ensureContextConfigured(
8087
9061
  // `${storage}-srgb` partner — derived, not duplicated.
8088
9062
  (globalThis as { __forgeaxSwapChainFormat?: GPUTextureFormat }).__forgeaxSwapChainFormat =
8089
9063
  state.format;
9064
+ return ok(undefined);
8090
9065
  }
8091
9066
 
8092
9067
  /**