@forgeax/engine-render 0.1.7 → 0.1.19

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 (683) 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 +12 -2
  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-FJ6P52EE.mjs +46 -0
  18. package/dist/chunk-FJ6P52EE.mjs.map +1 -0
  19. package/dist/chunk-IPXCABEP.mjs +73 -0
  20. package/dist/chunk-IPXCABEP.mjs.map +1 -0
  21. package/dist/chunk-JDWARUOI.mjs +44 -0
  22. package/dist/chunk-JDWARUOI.mjs.map +1 -0
  23. package/dist/{chunk-YEJIUW7D.mjs → chunk-LOSO4CTV.mjs} +296 -547
  24. package/dist/chunk-LOSO4CTV.mjs.map +1 -0
  25. package/dist/chunk-PCHBPYML.mjs +385 -0
  26. package/dist/chunk-PCHBPYML.mjs.map +1 -0
  27. package/dist/chunk-RJRUS55L.mjs +9323 -0
  28. package/dist/chunk-RJRUS55L.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-CQXZC5XI.mjs → chunk-VFPLTKCU.mjs} +79 -69
  32. package/dist/chunk-VFPLTKCU.mjs.map +1 -0
  33. package/dist/chunk-YTQ5C7YQ.mjs +584 -0
  34. package/dist/chunk-YTQ5C7YQ.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/motion-blur.d.ts +7 -0
  44. package/dist/components/motion-blur.d.ts.map +1 -0
  45. package/dist/construct-renderer.d.ts +2 -2
  46. package/dist/construct-renderer.d.ts.map +1 -1
  47. package/dist/construct-renderer.mjs +10914 -16392
  48. package/dist/construct-renderer.mjs.map +1 -1
  49. package/dist/device/device-scope.d.ts +4 -0
  50. package/dist/device/device-scope.d.ts.map +1 -1
  51. package/dist/environment/frame.d.ts +60 -0
  52. package/dist/environment/frame.d.ts.map +1 -0
  53. package/dist/environment/generation.d.ts +21 -0
  54. package/dist/environment/generation.d.ts.map +1 -0
  55. package/dist/environment/graph.d.ts +4 -0
  56. package/dist/environment/graph.d.ts.map +1 -0
  57. package/dist/environment/inspection.d.ts +48 -0
  58. package/dist/environment/inspection.d.ts.map +1 -0
  59. package/dist/environment/lifecycle.d.ts +39 -0
  60. package/dist/environment/lifecycle.d.ts.map +1 -0
  61. package/dist/environment/signature.d.ts +25 -0
  62. package/dist/environment/signature.d.ts.map +1 -0
  63. package/dist/errors/render.d.ts +136 -1
  64. package/dist/errors/render.d.ts.map +1 -1
  65. package/dist/extract/camera.d.ts +5 -0
  66. package/dist/extract/camera.d.ts.map +1 -0
  67. package/dist/extract/environment.d.ts +74 -0
  68. package/dist/extract/environment.d.ts.map +1 -0
  69. package/dist/extract/visibility.d.ts +1 -1
  70. package/dist/extract/visibility.d.ts.map +1 -1
  71. package/dist/features/host.d.ts +9 -2
  72. package/dist/features/host.d.ts.map +1 -1
  73. package/dist/features/motion-blur/motion-blur-feature.d.ts +16 -0
  74. package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -0
  75. package/dist/features/motion-blur/motion-blur-params.d.ts +25 -0
  76. package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -0
  77. package/dist/features/plan.d.ts +17 -2
  78. package/dist/features/plan.d.ts.map +1 -1
  79. package/dist/features/prepared-gpu-work.d.ts +3 -1
  80. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  81. package/dist/features/prepared-graphics.d.ts +2 -1
  82. package/dist/features/prepared-graphics.d.ts.map +1 -1
  83. package/dist/features/render-graph-raster.d.ts +2 -1
  84. package/dist/features/render-graph-raster.d.ts.map +1 -1
  85. package/dist/features/targets.d.ts +3 -0
  86. package/dist/features/targets.d.ts.map +1 -1
  87. package/dist/features/types.d.ts +12 -0
  88. package/dist/features/types.d.ts.map +1 -1
  89. package/dist/fullscreen-post-process-pass.d.ts +7 -1
  90. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  91. package/dist/gpu-scene.d.ts +2 -0
  92. package/dist/gpu-scene.d.ts.map +1 -1
  93. package/dist/ibl/skylight-bind-group.d.ts +5 -6
  94. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  95. package/dist/index.d.ts +17 -6
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.mjs +6 -4
  98. package/dist/index.mjs.map +1 -1
  99. package/dist/inspection-types.d.ts +43 -1
  100. package/dist/inspection-types.d.ts.map +1 -1
  101. package/dist/internal.d.ts +2 -0
  102. package/dist/internal.d.ts.map +1 -1
  103. package/dist/internal.mjs +2 -0
  104. package/dist/materials.d.ts +22 -0
  105. package/dist/materials.d.ts.map +1 -1
  106. package/dist/pbr-pipeline.d.ts +10 -6
  107. package/dist/pbr-pipeline.d.ts.map +1 -1
  108. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  109. package/dist/pipeline/standard-pipeline.d.ts +3 -0
  110. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  111. package/dist/pipeline/standard-post.d.ts +48 -0
  112. package/dist/pipeline/standard-post.d.ts.map +1 -0
  113. package/dist/pipeline/standard-profile.d.ts +3 -6
  114. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  115. package/dist/pipeline-builder.d.ts +18 -0
  116. package/dist/pipeline-builder.d.ts.map +1 -1
  117. package/dist/pipeline-spec.d.ts.map +1 -1
  118. package/dist/plugin.d.ts.map +1 -1
  119. package/dist/record/frame-snapshot.d.ts +100 -5
  120. package/dist/record/frame-snapshot.d.ts.map +1 -1
  121. package/dist/record/frame-targets.d.ts.map +1 -1
  122. package/dist/record/frame.d.ts +4 -2
  123. package/dist/record/frame.d.ts.map +1 -1
  124. package/dist/record/main-pass-geometry.d.ts +11 -0
  125. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  126. package/dist/record/main-pass-material.d.ts +8 -5
  127. package/dist/record/main-pass-material.d.ts.map +1 -1
  128. package/dist/record/main-pass-sprite-draws.d.ts +8 -1
  129. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  130. package/dist/record/main-pass.d.ts +2 -0
  131. package/dist/record/main-pass.d.ts.map +1 -1
  132. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  133. package/dist/record/render-context.d.ts +76 -18
  134. package/dist/record/render-context.d.ts.map +1 -1
  135. package/dist/record/shadow-pass.d.ts.map +1 -1
  136. package/dist/record/skybox-post-pass.d.ts +1 -11
  137. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  138. package/dist/record/typed-frame-graph.d.ts +26 -2
  139. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  140. package/dist/record/view-ubo.d.ts +4 -1
  141. package/dist/record/view-ubo.d.ts.map +1 -1
  142. package/dist/render-contract.d.ts +92 -8
  143. package/dist/render-contract.d.ts.map +1 -1
  144. package/dist/render-graph-primitives.d.ts.map +1 -1
  145. package/dist/render-pipeline.d.ts +50 -6
  146. package/dist/render-pipeline.d.ts.map +1 -1
  147. package/dist/render-system-extract.d.ts +29 -5
  148. package/dist/render-system-extract.d.ts.map +1 -1
  149. package/dist/render-system.d.ts +23 -4
  150. package/dist/render-system.d.ts.map +1 -1
  151. package/dist/scene/render-scene.d.ts +36 -2
  152. package/dist/scene/render-scene.d.ts.map +1 -1
  153. package/dist/temporal/coverage.d.ts +42 -0
  154. package/dist/temporal/coverage.d.ts.map +1 -0
  155. package/dist/temporal/frame-coordinator.d.ts +99 -0
  156. package/dist/temporal/frame-coordinator.d.ts.map +1 -0
  157. package/dist/temporal/gpu.d.ts +39 -0
  158. package/dist/temporal/gpu.d.ts.map +1 -0
  159. package/dist/temporal/history.d.ts +37 -0
  160. package/dist/temporal/history.d.ts.map +1 -0
  161. package/dist/temporal/index.d.ts +7 -0
  162. package/dist/temporal/index.d.ts.map +1 -0
  163. package/dist/temporal/index.mjs +8 -0
  164. package/dist/temporal/index.mjs.map +1 -0
  165. package/dist/temporal/inspection.d.ts +36 -0
  166. package/dist/temporal/inspection.d.ts.map +1 -0
  167. package/dist/temporal/scene-data-catalog.d.ts +48 -0
  168. package/dist/temporal/scene-data-catalog.d.ts.map +1 -0
  169. package/dist/temporal/scene-data.d.ts +43 -0
  170. package/dist/temporal/scene-data.d.ts.map +1 -0
  171. package/dist/temporal/standard-scene-data.d.ts +61 -0
  172. package/dist/temporal/standard-scene-data.d.ts.map +1 -0
  173. package/dist/temporal/taa-history-resources.d.ts +22 -0
  174. package/dist/temporal/taa-history-resources.d.ts.map +1 -0
  175. package/dist/temporal/taa-history-store.d.ts +27 -0
  176. package/dist/temporal/taa-history-store.d.ts.map +1 -0
  177. package/dist/temporal/taa-resolve.d.ts +15 -0
  178. package/dist/temporal/taa-resolve.d.ts.map +1 -0
  179. package/dist/temporal/temporal-view.d.ts +40 -0
  180. package/dist/temporal/temporal-view.d.ts.map +1 -0
  181. package/dist/temporal/transaction.d.ts +9 -0
  182. package/dist/temporal/transaction.d.ts.map +1 -0
  183. package/dist/temporal/view.d.ts +56 -0
  184. package/dist/temporal/view.d.ts.map +1 -0
  185. package/dist/transmission/backdrop.d.ts +147 -0
  186. package/dist/transmission/backdrop.d.ts.map +1 -0
  187. package/dist/transmission/inspection.d.ts +57 -0
  188. package/dist/transmission/inspection.d.ts.map +1 -0
  189. package/dist/transmission/oracle.d.ts +42 -0
  190. package/dist/transmission/oracle.d.ts.map +1 -0
  191. package/dist/transmission/projection.d.ts +37 -0
  192. package/dist/transmission/projection.d.ts.map +1 -0
  193. package/dist/typed-render-graph-primitives.d.ts +25 -2
  194. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  195. package/package.json +24 -19
  196. package/src/__tests__/ai-user-cold-start.integration.test.ts +59 -0
  197. package/src/__tests__/ai-user-public-surface.test-d.ts +45 -0
  198. package/src/__tests__/bloom-exact-zero.unit.test.ts +202 -0
  199. package/src/__tests__/bloom-lifecycle.integration.test.ts +125 -0
  200. package/src/__tests__/construct-renderer-identity.unit.test.ts +4 -3
  201. package/src/__tests__/detached-inspection.integration.test.ts +177 -0
  202. package/src/__tests__/environment-fog-lanes.integration.test.ts +219 -0
  203. package/src/__tests__/environment-generation.integration.test.ts +141 -0
  204. package/src/__tests__/environment-selection.unit.test.ts +245 -0
  205. package/src/__tests__/factory-contract.integration.test.ts +1380 -9
  206. package/src/__tests__/frame-plan-contract.unit.test.ts +90 -0
  207. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +7 -0
  208. package/src/__tests__/gpu-driven-production.integration.test.ts +74 -22
  209. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +37 -0
  210. package/src/__tests__/gpu-scene.unit.test.ts +10 -0
  211. package/src/__tests__/injection-kind-owner.test-d.ts +2 -1
  212. package/src/__tests__/material-contract-inventory.unit.test.ts +62 -0
  213. package/src/__tests__/material-cooked-projection.unit.test.ts +18 -146
  214. package/src/__tests__/materials-transmission-validation.unit.test.ts +36 -0
  215. package/src/__tests__/morph-feature-plan.unit.test.ts +7 -0
  216. package/src/__tests__/motion-blur.pixel.integration.test.ts +121 -0
  217. package/src/__tests__/public-surface.test-d.ts +15 -1
  218. package/src/__tests__/render-error-code-owner.test-d.ts +9 -0
  219. package/src/__tests__/render-error-detail-ssot.test-d.ts +11 -2
  220. package/src/__tests__/render-error-exhaustive.test-d.ts +3 -0
  221. package/src/__tests__/render-feature-errors.unit.test.ts +33 -3
  222. package/src/__tests__/render-feature-gpu-work.integration.test.ts +44 -0
  223. package/src/__tests__/render-feature-plan.test-d.ts +22 -89
  224. package/src/__tests__/render-scene-projection.unit.test.ts +28 -0
  225. package/src/__tests__/render-temporal-environment-consumer.browser.test.ts +26 -0
  226. package/src/__tests__/render-temporal-environment-consumer.dawn.test.ts +21 -0
  227. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +395 -1
  228. package/src/__tests__/renderer-frame-transaction.integration.test.ts +405 -0
  229. package/src/__tests__/renderer-lifecycle.integration.test.ts +22 -0
  230. package/src/__tests__/scene-data-catalog.unit.test.ts +59 -0
  231. package/src/__tests__/scene-data-consumer.integration.test.ts +67 -0
  232. package/src/__tests__/scene-data-contract.test-d.ts +35 -0
  233. package/src/__tests__/scene-data-inspection.unit.test.ts +41 -0
  234. package/src/__tests__/scene-data-public-surface.test-d.ts +34 -0
  235. package/src/__tests__/shader-manifest-fixture.ts +111 -0
  236. package/src/__tests__/skinned-shadow-caster.test.ts +102 -0
  237. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +24 -3
  238. package/src/__tests__/standard-pipeline.dawn.test.ts +15 -0
  239. package/src/__tests__/standard-pipeline.integration.test.ts +543 -1
  240. package/src/__tests__/standard-profile-oracle.unit.test.ts +10 -2
  241. package/src/__tests__/standard-temporal-producer.integration.test.ts +35 -0
  242. package/src/__tests__/surface-configuration.unit.test.ts +211 -12
  243. package/src/__tests__/taa-transaction.integration.test.ts +30 -0
  244. package/src/__tests__/temporal-admission.integration.test.ts +40 -0
  245. package/src/__tests__/temporal-coverage.unit.test.ts +35 -0
  246. package/src/__tests__/temporal-demand.unit.test.ts +44 -0
  247. package/src/__tests__/temporal-frame-coordinator.unit.test.ts +64 -0
  248. package/src/__tests__/temporal-graph-topology.unit.test.ts +51 -0
  249. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +170 -0
  250. package/src/__tests__/temporal-performance.integration.test.ts +615 -0
  251. package/src/__tests__/temporal-reset-matrix.unit.test.ts +97 -0
  252. package/src/__tests__/temporal-transaction.integration.test.ts +60 -0
  253. package/src/__tests__/temporal-view.unit.test.ts +152 -0
  254. package/src/__tests__/transmission-prewarm.unit.test.ts +93 -0
  255. package/src/__tests__/typed-pipeline-topology.unit.test.ts +230 -1
  256. package/src/__tests__/urp-pipeline-targets.unit.test.ts +25 -1
  257. package/src/__tests__/urp-pipeline.unit.test.ts +4 -3
  258. package/src/__tests__/visibility-extract.integration.test.ts +27 -1
  259. package/src/__tests__/visibility-producer-matrix.integration.test.ts +10 -0
  260. package/src/assembly/backend-contract.ts +47 -1
  261. package/src/assembly/factory.ts +1224 -505
  262. package/src/assembly/host-contract.ts +4 -0
  263. package/src/assembly/renderer-frame-transaction.ts +75 -0
  264. package/src/assembly/renderer-inspection.ts +112 -0
  265. package/src/assembly/renderer-lifecycle.ts +6 -0
  266. package/src/components/__tests__/motion-blur.unit.test.ts +14 -0
  267. package/src/components/__tests__/schema-contract.unit.test.ts +6 -1
  268. package/src/components/atmosphere.ts +10 -0
  269. package/src/components/camera.ts +2 -1
  270. package/src/components/fog.ts +9 -0
  271. package/src/components/index.ts +3 -0
  272. package/src/components/motion-blur.ts +8 -0
  273. package/src/construct-renderer.ts +40 -3
  274. package/src/device/device-scope.ts +15 -1
  275. package/src/environment/frame.ts +185 -0
  276. package/src/environment/generation.ts +23 -0
  277. package/src/environment/graph.ts +9 -0
  278. package/src/environment/inspection.ts +87 -0
  279. package/src/environment/lifecycle.ts +250 -0
  280. package/src/environment/signature.ts +55 -0
  281. package/src/errors/render.ts +285 -0
  282. package/src/extract/camera.ts +78 -0
  283. package/src/extract/environment.ts +171 -0
  284. package/src/extract/visibility.ts +38 -19
  285. package/src/features/host.ts +36 -5
  286. package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +67 -0
  287. package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +53 -0
  288. package/src/features/motion-blur/motion-blur-feature.ts +119 -0
  289. package/src/features/motion-blur/motion-blur-params.ts +89 -0
  290. package/src/features/plan.ts +32 -3
  291. package/src/features/prepared-gpu-work.ts +21 -6
  292. package/src/features/prepared-graphics.ts +2 -1
  293. package/src/features/render-graph-raster.ts +2 -1
  294. package/src/features/targets.ts +24 -2
  295. package/src/features/types.ts +13 -0
  296. package/src/fullscreen-post-process-pass.ts +34 -4
  297. package/src/gpu-scene.ts +24 -0
  298. package/src/ibl/skylight-bind-group.ts +30 -38
  299. package/src/index.ts +66 -6
  300. package/src/inspection-types.ts +48 -0
  301. package/src/internal.ts +5 -0
  302. package/src/materials.ts +119 -24
  303. package/src/pbr-pipeline.ts +58 -19
  304. package/src/pipeline/standard-forward-lane.ts +158 -101
  305. package/src/pipeline/standard-pipeline.ts +156 -105
  306. package/src/pipeline/standard-post.ts +373 -0
  307. package/src/pipeline/standard-profile.ts +11 -11
  308. package/src/pipeline-builder.ts +102 -12
  309. package/src/pipeline-spec.ts +9 -0
  310. package/src/plugin.ts +2 -0
  311. package/src/record/__tests__/fullscreen-temporal-routing.unit.test.ts +25 -0
  312. package/src/record/__tests__/main-pass-geometry.unit.test.ts +47 -0
  313. package/src/record/__tests__/material-bg-cache.unit.test.ts +12 -1
  314. package/src/record/__tests__/material-pipeline-pending-cache.unit.test.ts +22 -0
  315. package/src/record/__tests__/sprite-material-abi.unit.test.ts +75 -0
  316. package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +67 -0
  317. package/src/record/__tests__/temporal-payload.unit.test.ts +164 -0
  318. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +127 -0
  319. package/src/record/frame-snapshot.ts +129 -5
  320. package/src/record/frame-targets.ts +15 -6
  321. package/src/record/frame.ts +107 -3
  322. package/src/record/main-pass-geometry.ts +86 -40
  323. package/src/record/main-pass-material.ts +114 -66
  324. package/src/record/main-pass-sprite-draws.ts +78 -101
  325. package/src/record/main-pass.ts +77 -4
  326. package/src/record/mesh-ssbo.ts +22 -3
  327. package/src/record/render-context.ts +189 -23
  328. package/src/record/shadow-pass.ts +113 -23
  329. package/src/record/skybox-post-pass.ts +97 -41
  330. package/src/record/typed-frame-graph.ts +565 -26
  331. package/src/record/view-ubo.ts +52 -4
  332. package/src/render-contract.ts +109 -6
  333. package/src/render-graph-primitives.ts +18 -1
  334. package/src/render-pipeline.ts +100 -27
  335. package/src/render-system-extract.ts +193 -69
  336. package/src/render-system.ts +482 -41
  337. package/src/scene/render-scene.ts +250 -7
  338. package/src/temporal/__tests__/taa-history-resources.unit.test.ts +45 -0
  339. package/src/temporal/coverage.ts +76 -0
  340. package/src/temporal/frame-coordinator.ts +311 -0
  341. package/src/temporal/gpu.ts +287 -0
  342. package/src/temporal/history.ts +92 -0
  343. package/src/temporal/index.ts +6 -0
  344. package/src/temporal/inspection.ts +97 -0
  345. package/src/temporal/scene-data-catalog.ts +200 -0
  346. package/src/temporal/scene-data.ts +149 -0
  347. package/src/temporal/standard-scene-data.ts +182 -0
  348. package/src/temporal/taa-history-resources.ts +128 -0
  349. package/src/temporal/taa-history-store.ts +79 -0
  350. package/src/temporal/taa-resolve.ts +34 -0
  351. package/src/temporal/temporal-view.ts +169 -0
  352. package/src/temporal/transaction.ts +17 -0
  353. package/src/temporal/view.ts +161 -0
  354. package/src/transmission/__tests__/capability.unit.test.ts +83 -0
  355. package/src/transmission/__tests__/composition.integration.test.ts +161 -0
  356. package/src/transmission/__tests__/inspection.unit.test.ts +241 -0
  357. package/src/transmission/__tests__/lifecycle.integration.test.ts +96 -0
  358. package/src/transmission/__tests__/oracle.unit.test.ts +131 -0
  359. package/src/transmission/__tests__/projection.unit.test.ts +71 -0
  360. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +438 -0
  361. package/src/transmission/__tests__/transmission-backdrop-topology.unit.test.ts +109 -0
  362. package/src/transmission/__tests__/transmission-backdrop.dawn.test.ts +105 -0
  363. package/src/transmission/backdrop.ts +572 -0
  364. package/src/transmission/inspection.ts +190 -0
  365. package/src/transmission/oracle.ts +141 -0
  366. package/src/transmission/projection.ts +83 -0
  367. package/src/typed-render-graph-primitives.ts +162 -9
  368. package/dist/__tests__/authoring-path.unit.test.d.ts +0 -2
  369. package/dist/__tests__/authoring-path.unit.test.d.ts.map +0 -1
  370. package/dist/__tests__/backend-selection-rhi-capture.test-d.d.ts +0 -2
  371. package/dist/__tests__/backend-selection-rhi-capture.test-d.d.ts.map +0 -1
  372. package/dist/__tests__/callback-inference.test-d.d.ts +0 -2
  373. package/dist/__tests__/callback-inference.test-d.d.ts.map +0 -1
  374. package/dist/__tests__/cluster-binner.test.d.ts +0 -2
  375. package/dist/__tests__/cluster-binner.test.d.ts.map +0 -1
  376. package/dist/__tests__/cluster-grid-default-surface.unit.test.d.ts +0 -2
  377. package/dist/__tests__/cluster-grid-default-surface.unit.test.d.ts.map +0 -1
  378. package/dist/__tests__/construct-renderer-identity.unit.test.d.ts +0 -2
  379. package/dist/__tests__/construct-renderer-identity.unit.test.d.ts.map +0 -1
  380. package/dist/__tests__/device-scope-lifecycle.unit.test.d.ts +0 -2
  381. package/dist/__tests__/device-scope-lifecycle.unit.test.d.ts.map +0 -1
  382. package/dist/__tests__/device-scope-stale-matrix.unit.test.d.ts +0 -2
  383. package/dist/__tests__/device-scope-stale-matrix.unit.test.d.ts.map +0 -1
  384. package/dist/__tests__/direct-extract-consumer-contract.test-d.d.ts +0 -2
  385. package/dist/__tests__/direct-extract-consumer-contract.test-d.d.ts.map +0 -1
  386. package/dist/__tests__/direct-light-snapshot.unit.test.d.ts +0 -2
  387. package/dist/__tests__/direct-light-snapshot.unit.test.d.ts.map +0 -1
  388. package/dist/__tests__/factory-contract.integration.test.d.ts +0 -2
  389. package/dist/__tests__/factory-contract.integration.test.d.ts.map +0 -1
  390. package/dist/__tests__/fallback-row-stride-surface.unit.test.d.ts +0 -2
  391. package/dist/__tests__/fallback-row-stride-surface.unit.test.d.ts.map +0 -1
  392. package/dist/__tests__/frame-observation.contract.test.d.ts +0 -2
  393. package/dist/__tests__/frame-observation.contract.test.d.ts.map +0 -1
  394. package/dist/__tests__/fullscreen-feature-plan.unit.test.d.ts +0 -2
  395. package/dist/__tests__/fullscreen-feature-plan.unit.test.d.ts.map +0 -1
  396. package/dist/__tests__/glyph-text-layout-recovery.unit.test.d.ts +0 -2
  397. package/dist/__tests__/glyph-text-layout-recovery.unit.test.d.ts.map +0 -1
  398. package/dist/__tests__/gpu-driven-indirect-raster-evidence.d.ts +0 -6
  399. package/dist/__tests__/gpu-driven-indirect-raster-evidence.d.ts.map +0 -1
  400. package/dist/__tests__/gpu-driven-indirect-raster.browser.test.d.ts +0 -2
  401. package/dist/__tests__/gpu-driven-indirect-raster.browser.test.d.ts.map +0 -1
  402. package/dist/__tests__/gpu-driven-indirect-raster.dawn.test.d.ts +0 -2
  403. package/dist/__tests__/gpu-driven-indirect-raster.dawn.test.d.ts.map +0 -1
  404. package/dist/__tests__/gpu-driven-production.integration.test.d.ts +0 -2
  405. package/dist/__tests__/gpu-driven-production.integration.test.d.ts.map +0 -1
  406. package/dist/__tests__/gpu-driven-scaling.unit.test.d.ts +0 -2
  407. package/dist/__tests__/gpu-driven-scaling.unit.test.d.ts.map +0 -1
  408. package/dist/__tests__/gpu-driven-view-gpu-evidence.d.ts +0 -18
  409. package/dist/__tests__/gpu-driven-view-gpu-evidence.d.ts.map +0 -1
  410. package/dist/__tests__/gpu-driven-view-graph.integration.test.d.ts +0 -2
  411. package/dist/__tests__/gpu-driven-view-graph.integration.test.d.ts.map +0 -1
  412. package/dist/__tests__/gpu-driven-view-reference.d.ts +0 -15
  413. package/dist/__tests__/gpu-driven-view-reference.d.ts.map +0 -1
  414. package/dist/__tests__/gpu-driven-view.browser.test.d.ts +0 -2
  415. package/dist/__tests__/gpu-driven-view.browser.test.d.ts.map +0 -1
  416. package/dist/__tests__/gpu-driven-view.dawn.test.d.ts +0 -2
  417. package/dist/__tests__/gpu-driven-view.dawn.test.d.ts.map +0 -1
  418. package/dist/__tests__/gpu-driven-view.unit.test.d.ts +0 -2
  419. package/dist/__tests__/gpu-driven-view.unit.test.d.ts.map +0 -1
  420. package/dist/__tests__/gpu-resource-store-stride.unit.test.d.ts +0 -2
  421. package/dist/__tests__/gpu-resource-store-stride.unit.test.d.ts.map +0 -1
  422. package/dist/__tests__/gpu-scene-render-graph-gpu.d.ts +0 -7
  423. package/dist/__tests__/gpu-scene-render-graph-gpu.d.ts.map +0 -1
  424. package/dist/__tests__/gpu-scene-render-graph.browser.test.d.ts +0 -2
  425. package/dist/__tests__/gpu-scene-render-graph.browser.test.d.ts.map +0 -1
  426. package/dist/__tests__/gpu-scene-render-graph.dawn.test.d.ts +0 -2
  427. package/dist/__tests__/gpu-scene-render-graph.dawn.test.d.ts.map +0 -1
  428. package/dist/__tests__/gpu-scene-schema.unit.test.d.ts +0 -2
  429. package/dist/__tests__/gpu-scene-schema.unit.test.d.ts.map +0 -1
  430. package/dist/__tests__/gpu-scene.dawn.test.d.ts +0 -2
  431. package/dist/__tests__/gpu-scene.dawn.test.d.ts.map +0 -1
  432. package/dist/__tests__/gpu-scene.unit.test.d.ts +0 -2
  433. package/dist/__tests__/gpu-scene.unit.test.d.ts.map +0 -1
  434. package/dist/__tests__/ibl/face-uniforms.unit.test.d.ts +0 -2
  435. package/dist/__tests__/ibl/face-uniforms.unit.test.d.ts.map +0 -1
  436. package/dist/__tests__/infinite-grid-no-vertex-probe.unit.test.d.ts +0 -2
  437. package/dist/__tests__/infinite-grid-no-vertex-probe.unit.test.d.ts.map +0 -1
  438. package/dist/__tests__/injection-kind-owner.test-d.d.ts +0 -2
  439. package/dist/__tests__/injection-kind-owner.test-d.d.ts.map +0 -1
  440. package/dist/__tests__/ktx2-basis-gpu-consumer.dawn.test.d.ts +0 -2
  441. package/dist/__tests__/ktx2-basis-gpu-consumer.dawn.test.d.ts.map +0 -1
  442. package/dist/__tests__/light-buffer-layout.unit.test.d.ts +0 -2
  443. package/dist/__tests__/light-buffer-layout.unit.test.d.ts.map +0 -1
  444. package/dist/__tests__/material-contract-inventory.unit.test.d.ts +0 -2
  445. package/dist/__tests__/material-contract-inventory.unit.test.d.ts.map +0 -1
  446. package/dist/__tests__/material-cooked-projection.unit.test.d.ts +0 -2
  447. package/dist/__tests__/material-cooked-projection.unit.test.d.ts.map +0 -1
  448. package/dist/__tests__/material-snapshot-mutation.integration.test.d.ts +0 -2
  449. package/dist/__tests__/material-snapshot-mutation.integration.test.d.ts.map +0 -1
  450. package/dist/__tests__/material-static-dynamic-route.unit.test.d.ts +0 -2
  451. package/dist/__tests__/material-static-dynamic-route.unit.test.d.ts.map +0 -1
  452. package/dist/__tests__/material-stride-surface.unit.test.d.ts +0 -2
  453. package/dist/__tests__/material-stride-surface.unit.test.d.ts.map +0 -1
  454. package/dist/__tests__/mesh-buffer-usage-surface.unit.test.d.ts +0 -2
  455. package/dist/__tests__/mesh-buffer-usage-surface.unit.test.d.ts.map +0 -1
  456. package/dist/__tests__/mesh-material-bindings.unit.test.d.ts +0 -2
  457. package/dist/__tests__/mesh-material-bindings.unit.test.d.ts.map +0 -1
  458. package/dist/__tests__/mesh-stride-surface.unit.test.d.ts +0 -2
  459. package/dist/__tests__/mesh-stride-surface.unit.test.d.ts.map +0 -1
  460. package/dist/__tests__/morph-culling-reentry.dawn.test.d.ts +0 -2
  461. package/dist/__tests__/morph-culling-reentry.dawn.test.d.ts.map +0 -1
  462. package/dist/__tests__/morph-feature-gpu-work.integration.test.d.ts +0 -2
  463. package/dist/__tests__/morph-feature-gpu-work.integration.test.d.ts.map +0 -1
  464. package/dist/__tests__/morph-feature-plan.unit.test.d.ts +0 -2
  465. package/dist/__tests__/morph-feature-plan.unit.test.d.ts.map +0 -1
  466. package/dist/__tests__/morph-visual-evidence.integration.test.d.ts +0 -2
  467. package/dist/__tests__/morph-visual-evidence.integration.test.d.ts.map +0 -1
  468. package/dist/__tests__/nineslice-handle-surface.unit.test.d.ts +0 -2
  469. package/dist/__tests__/nineslice-handle-surface.unit.test.d.ts.map +0 -1
  470. package/dist/__tests__/post-process-error-policy-owner.unit.test.d.ts +0 -2
  471. package/dist/__tests__/post-process-error-policy-owner.unit.test.d.ts.map +0 -1
  472. package/dist/__tests__/prepared-buffer-kind-owner.test-d.d.ts +0 -2
  473. package/dist/__tests__/prepared-buffer-kind-owner.test-d.d.ts.map +0 -1
  474. package/dist/__tests__/prepared-graphics-capability.unit.test.d.ts +0 -2
  475. package/dist/__tests__/prepared-graphics-capability.unit.test.d.ts.map +0 -1
  476. package/dist/__tests__/prepared-graphics-generation.unit.test.d.ts +0 -2
  477. package/dist/__tests__/prepared-graphics-generation.unit.test.d.ts.map +0 -1
  478. package/dist/__tests__/prepared-graphics-lifecycle.unit.test.d.ts +0 -2
  479. package/dist/__tests__/prepared-graphics-lifecycle.unit.test.d.ts.map +0 -1
  480. package/dist/__tests__/prepared-graphics-pipeline-warmup.unit.test.d.ts +0 -2
  481. package/dist/__tests__/prepared-graphics-pipeline-warmup.unit.test.d.ts.map +0 -1
  482. package/dist/__tests__/prepared-graphics-resolver.unit.test.d.ts +0 -2
  483. package/dist/__tests__/prepared-graphics-resolver.unit.test.d.ts.map +0 -1
  484. package/dist/__tests__/prepared-graphics-snapshot.unit.test.d.ts +0 -2
  485. package/dist/__tests__/prepared-graphics-snapshot.unit.test.d.ts.map +0 -1
  486. package/dist/__tests__/prepared-graphics-store.unit.test.d.ts +0 -2
  487. package/dist/__tests__/prepared-graphics-store.unit.test.d.ts.map +0 -1
  488. package/dist/__tests__/prepared-graphics-validation.unit.test.d.ts +0 -2
  489. package/dist/__tests__/prepared-graphics-validation.unit.test.d.ts.map +0 -1
  490. package/dist/__tests__/profiler-default-off.test.d.ts +0 -2
  491. package/dist/__tests__/profiler-default-off.test.d.ts.map +0 -1
  492. package/dist/__tests__/profiler-phase-catalog.test.d.ts +0 -2
  493. package/dist/__tests__/profiler-phase-catalog.test.d.ts.map +0 -1
  494. package/dist/__tests__/profiler-phase-integration.test.d.ts +0 -2
  495. package/dist/__tests__/profiler-phase-integration.test.d.ts.map +0 -1
  496. package/dist/__tests__/profiler-phase-token.test.d.ts +0 -2
  497. package/dist/__tests__/profiler-phase-token.test.d.ts.map +0 -1
  498. package/dist/__tests__/public-surface.test-d.d.ts +0 -2
  499. package/dist/__tests__/public-surface.test-d.d.ts.map +0 -1
  500. package/dist/__tests__/recover-error-policy-owner.unit.test.d.ts +0 -2
  501. package/dist/__tests__/recover-error-policy-owner.unit.test.d.ts.map +0 -1
  502. package/dist/__tests__/render-error-code-owner.test-d.d.ts +0 -2
  503. package/dist/__tests__/render-error-code-owner.test-d.d.ts.map +0 -1
  504. package/dist/__tests__/render-error-detail-ssot.test-d.d.ts +0 -2
  505. package/dist/__tests__/render-error-detail-ssot.test-d.d.ts.map +0 -1
  506. package/dist/__tests__/render-error-exhaustive.test-d.d.ts +0 -2
  507. package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +0 -1
  508. package/dist/__tests__/render-feature-assembly.test-d.d.ts +0 -2
  509. package/dist/__tests__/render-feature-assembly.test-d.d.ts.map +0 -1
  510. package/dist/__tests__/render-feature-context.test-d.d.ts +0 -2
  511. package/dist/__tests__/render-feature-context.test-d.d.ts.map +0 -1
  512. package/dist/__tests__/render-feature-contract.test-d.d.ts +0 -2
  513. package/dist/__tests__/render-feature-contract.test-d.d.ts.map +0 -1
  514. package/dist/__tests__/render-feature-diagnostics.unit.test.d.ts +0 -2
  515. package/dist/__tests__/render-feature-diagnostics.unit.test.d.ts.map +0 -1
  516. package/dist/__tests__/render-feature-docs.unit.test.d.ts +0 -2
  517. package/dist/__tests__/render-feature-docs.unit.test.d.ts.map +0 -1
  518. package/dist/__tests__/render-feature-errors.unit.test.d.ts +0 -2
  519. package/dist/__tests__/render-feature-errors.unit.test.d.ts.map +0 -1
  520. package/dist/__tests__/render-feature-gpu-work.integration.test.d.ts +0 -2
  521. package/dist/__tests__/render-feature-gpu-work.integration.test.d.ts.map +0 -1
  522. package/dist/__tests__/render-feature-plan.test-d.d.ts +0 -2
  523. package/dist/__tests__/render-feature-plan.test-d.d.ts.map +0 -1
  524. package/dist/__tests__/render-feature-plan.unit.test.d.ts +0 -2
  525. package/dist/__tests__/render-feature-plan.unit.test.d.ts.map +0 -1
  526. package/dist/__tests__/render-feature-prepared-diagnostics.unit.test.d.ts +0 -2
  527. package/dist/__tests__/render-feature-prepared-diagnostics.unit.test.d.ts.map +0 -1
  528. package/dist/__tests__/render-feature-prepared-errors.test-d.d.ts +0 -2
  529. package/dist/__tests__/render-feature-prepared-errors.test-d.d.ts.map +0 -1
  530. package/dist/__tests__/render-feature-prepared-graphics-docs.unit.test.d.ts +0 -2
  531. package/dist/__tests__/render-feature-prepared-graphics-docs.unit.test.d.ts.map +0 -1
  532. package/dist/__tests__/render-feature-prepared-graphics-isolation.unit.test.d.ts +0 -2
  533. package/dist/__tests__/render-feature-prepared-graphics-isolation.unit.test.d.ts.map +0 -1
  534. package/dist/__tests__/render-feature-prepared-graphics-negative.test-d.d.ts +0 -2
  535. package/dist/__tests__/render-feature-prepared-graphics-negative.test-d.d.ts.map +0 -1
  536. package/dist/__tests__/render-feature-prepared-graphics.test-d.d.ts +0 -2
  537. package/dist/__tests__/render-feature-prepared-graphics.test-d.d.ts.map +0 -1
  538. package/dist/__tests__/render-feature-prepared-graphics.unit.test.d.ts +0 -2
  539. package/dist/__tests__/render-feature-prepared-graphics.unit.test.d.ts.map +0 -1
  540. package/dist/__tests__/render-feature-public-surface.test-d.d.ts +0 -2
  541. package/dist/__tests__/render-feature-public-surface.test-d.d.ts.map +0 -1
  542. package/dist/__tests__/render-feature-retry.unit.test.d.ts +0 -2
  543. package/dist/__tests__/render-feature-retry.unit.test.d.ts.map +0 -1
  544. package/dist/__tests__/render-feature-scheduling.unit.test.d.ts +0 -2
  545. package/dist/__tests__/render-feature-scheduling.unit.test.d.ts.map +0 -1
  546. package/dist/__tests__/render-feature-stage-failed-policy-owner.unit.test.d.ts +0 -2
  547. package/dist/__tests__/render-feature-stage-failed-policy-owner.unit.test.d.ts.map +0 -1
  548. package/dist/__tests__/render-feature-stage-fault-recovery.unit.test.d.ts +0 -2
  549. package/dist/__tests__/render-feature-stage-fault-recovery.unit.test.d.ts.map +0 -1
  550. package/dist/__tests__/render-feature-visibility-context.test-d.d.ts +0 -2
  551. package/dist/__tests__/render-feature-visibility-context.test-d.d.ts.map +0 -1
  552. package/dist/__tests__/render-feature-visibility-context.unit.test.d.ts +0 -2
  553. package/dist/__tests__/render-feature-visibility-context.unit.test.d.ts.map +0 -1
  554. package/dist/__tests__/render-feature-zero-work.unit.test.d.ts +0 -2
  555. package/dist/__tests__/render-feature-zero-work.unit.test.d.ts.map +0 -1
  556. package/dist/__tests__/render-graph-compute-raster-gpu.d.ts +0 -7
  557. package/dist/__tests__/render-graph-compute-raster-gpu.d.ts.map +0 -1
  558. package/dist/__tests__/render-graph-compute-raster.browser.test.d.ts +0 -2
  559. package/dist/__tests__/render-graph-compute-raster.browser.test.d.ts.map +0 -1
  560. package/dist/__tests__/render-graph-compute-raster.dawn.test.d.ts +0 -2
  561. package/dist/__tests__/render-graph-compute-raster.dawn.test.d.ts.map +0 -1
  562. package/dist/__tests__/render-graph-hzb-gpu.d.ts +0 -7
  563. package/dist/__tests__/render-graph-hzb-gpu.d.ts.map +0 -1
  564. package/dist/__tests__/render-graph-hzb.browser.test.d.ts +0 -2
  565. package/dist/__tests__/render-graph-hzb.browser.test.d.ts.map +0 -1
  566. package/dist/__tests__/render-graph-hzb.dawn.test.d.ts +0 -2
  567. package/dist/__tests__/render-graph-hzb.dawn.test.d.ts.map +0 -1
  568. package/dist/__tests__/render-graph-primitives.unit.test.d.ts +0 -2
  569. package/dist/__tests__/render-graph-primitives.unit.test.d.ts.map +0 -1
  570. package/dist/__tests__/render-graph-storage-indirect-gpu.d.ts +0 -12
  571. package/dist/__tests__/render-graph-storage-indirect-gpu.d.ts.map +0 -1
  572. package/dist/__tests__/render-graph-storage-indirect.browser.test.d.ts +0 -2
  573. package/dist/__tests__/render-graph-storage-indirect.browser.test.d.ts.map +0 -1
  574. package/dist/__tests__/render-graph-storage-indirect.dawn.test.d.ts +0 -2
  575. package/dist/__tests__/render-graph-storage-indirect.dawn.test.d.ts.map +0 -1
  576. package/dist/__tests__/render-scene-projection.unit.test.d.ts +0 -2
  577. package/dist/__tests__/render-scene-projection.unit.test.d.ts.map +0 -1
  578. package/dist/__tests__/renderer-factory-material-contract.unit.test.d.ts +0 -2
  579. package/dist/__tests__/renderer-factory-material-contract.unit.test.d.ts.map +0 -1
  580. package/dist/__tests__/renderer-lifecycle.integration.test.d.ts +0 -2
  581. package/dist/__tests__/renderer-lifecycle.integration.test.d.ts.map +0 -1
  582. package/dist/__tests__/rhi-backend-package-boundary.unit.test.d.ts +0 -2
  583. package/dist/__tests__/rhi-backend-package-boundary.unit.test.d.ts.map +0 -1
  584. package/dist/__tests__/scriptable-pack-consumer.test-d.d.ts +0 -2
  585. package/dist/__tests__/scriptable-pack-consumer.test-d.d.ts.map +0 -1
  586. package/dist/__tests__/shader-stage-surface.unit.test.d.ts +0 -2
  587. package/dist/__tests__/shader-stage-surface.unit.test.d.ts.map +0 -1
  588. package/dist/__tests__/ssao-config.unit.test.d.ts +0 -2
  589. package/dist/__tests__/ssao-config.unit.test.d.ts.map +0 -1
  590. package/dist/__tests__/ssao-kernel-surface.unit.test.d.ts +0 -2
  591. package/dist/__tests__/ssao-kernel-surface.unit.test.d.ts.map +0 -1
  592. package/dist/__tests__/standard-clustered-pipeline.unit.test.d.ts +0 -2
  593. package/dist/__tests__/standard-clustered-pipeline.unit.test.d.ts.map +0 -1
  594. package/dist/__tests__/standard-descriptors.test-d.d.ts +0 -2
  595. package/dist/__tests__/standard-descriptors.test-d.d.ts.map +0 -1
  596. package/dist/__tests__/standard-pipeline.dawn.test.d.ts +0 -2
  597. package/dist/__tests__/standard-pipeline.dawn.test.d.ts.map +0 -1
  598. package/dist/__tests__/standard-pipeline.integration.test.d.ts +0 -2
  599. package/dist/__tests__/standard-pipeline.integration.test.d.ts.map +0 -1
  600. package/dist/__tests__/standard-profile-oracle.unit.test.d.ts +0 -2
  601. package/dist/__tests__/standard-profile-oracle.unit.test.d.ts.map +0 -1
  602. package/dist/__tests__/surface-configuration.unit.test.d.ts +0 -2
  603. package/dist/__tests__/surface-configuration.unit.test.d.ts.map +0 -1
  604. package/dist/__tests__/texture-usage-surface.unit.test.d.ts +0 -2
  605. package/dist/__tests__/texture-usage-surface.unit.test.d.ts.map +0 -1
  606. package/dist/__tests__/typed-pipeline-topology.unit.test.d.ts +0 -2
  607. package/dist/__tests__/typed-pipeline-topology.unit.test.d.ts.map +0 -1
  608. package/dist/__tests__/urp-pipeline-targets.unit.test.d.ts +0 -2
  609. package/dist/__tests__/urp-pipeline-targets.unit.test.d.ts.map +0 -1
  610. package/dist/__tests__/urp-pipeline.unit.test.d.ts +0 -2
  611. package/dist/__tests__/urp-pipeline.unit.test.d.ts.map +0 -1
  612. package/dist/__tests__/vertex-color-pipeline.unit.test.d.ts +0 -2
  613. package/dist/__tests__/vertex-color-pipeline.unit.test.d.ts.map +0 -1
  614. package/dist/__tests__/vertex-layout-consumer.unit.test.d.ts +0 -2
  615. package/dist/__tests__/vertex-layout-consumer.unit.test.d.ts.map +0 -1
  616. package/dist/__tests__/visibility-diagnostics.integration.test.d.ts +0 -2
  617. package/dist/__tests__/visibility-diagnostics.integration.test.d.ts.map +0 -1
  618. package/dist/__tests__/visibility-extract.integration.test.d.ts +0 -2
  619. package/dist/__tests__/visibility-extract.integration.test.d.ts.map +0 -1
  620. package/dist/__tests__/visibility-extract.perf.test.d.ts +0 -2
  621. package/dist/__tests__/visibility-extract.perf.test.d.ts.map +0 -1
  622. package/dist/__tests__/visibility-feature-dedup.integration.test.d.ts +0 -2
  623. package/dist/__tests__/visibility-feature-dedup.integration.test.d.ts.map +0 -1
  624. package/dist/__tests__/visibility-hidden-report.unit.test.d.ts +0 -2
  625. package/dist/__tests__/visibility-hidden-report.unit.test.d.ts.map +0 -1
  626. package/dist/__tests__/visibility-instances.integration.test.d.ts +0 -2
  627. package/dist/__tests__/visibility-instances.integration.test.d.ts.map +0 -1
  628. package/dist/__tests__/visibility-multi-world.integration.test.d.ts +0 -2
  629. package/dist/__tests__/visibility-multi-world.integration.test.d.ts.map +0 -1
  630. package/dist/__tests__/visibility-producer-matrix.integration.test.d.ts +0 -2
  631. package/dist/__tests__/visibility-producer-matrix.integration.test.d.ts.map +0 -1
  632. package/dist/__tests__/visibility-producer-matrix.unit.test.d.ts +0 -2
  633. package/dist/__tests__/visibility-producer-matrix.unit.test.d.ts.map +0 -1
  634. package/dist/__tests__/visibility-public-surface.test-d.d.ts +0 -2
  635. package/dist/__tests__/visibility-public-surface.test-d.d.ts.map +0 -1
  636. package/dist/__tests__/visibility-resolver.unit.test.d.ts +0 -2
  637. package/dist/__tests__/visibility-resolver.unit.test.d.ts.map +0 -1
  638. package/dist/__tests__/visibility-resource-short-circuit.unit.test.d.ts +0 -2
  639. package/dist/__tests__/visibility-resource-short-circuit.unit.test.d.ts.map +0 -1
  640. package/dist/__tests__/visibility-snapshot-contract.unit.test.d.ts +0 -2
  641. package/dist/__tests__/visibility-snapshot-contract.unit.test.d.ts.map +0 -1
  642. package/dist/__tests__/vocabulary.unit.test.d.ts +0 -2
  643. package/dist/__tests__/vocabulary.unit.test.d.ts.map +0 -1
  644. package/dist/chunk-CQXZC5XI.mjs.map +0 -1
  645. package/dist/chunk-HCOGPHKS.mjs +0 -93
  646. package/dist/chunk-HCOGPHKS.mjs.map +0 -1
  647. package/dist/chunk-YEJIUW7D.mjs.map +0 -1
  648. package/dist/components/__tests__/points-lines-admission.unit.test.d.ts +0 -2
  649. package/dist/components/__tests__/points-lines-admission.unit.test.d.ts.map +0 -1
  650. package/dist/components/__tests__/points-lines-schema.unit.test.d.ts +0 -2
  651. package/dist/components/__tests__/points-lines-schema.unit.test.d.ts.map +0 -1
  652. package/dist/components/__tests__/schema-contract.unit.test.d.ts +0 -2
  653. package/dist/components/__tests__/schema-contract.unit.test.d.ts.map +0 -1
  654. package/dist/components/__tests__/schema-parity.unit.test.d.ts +0 -2
  655. package/dist/components/__tests__/schema-parity.unit.test.d.ts.map +0 -1
  656. package/dist/components/__tests__/visibility-schema.unit.test.d.ts +0 -2
  657. package/dist/components/__tests__/visibility-schema.unit.test.d.ts.map +0 -1
  658. package/dist/points-lines/__tests__/bounds.unit.test.d.ts +0 -2
  659. package/dist/points-lines/__tests__/bounds.unit.test.d.ts.map +0 -1
  660. package/dist/points-lines/__tests__/cache-recovery.unit.test.d.ts +0 -2
  661. package/dist/points-lines/__tests__/cache-recovery.unit.test.d.ts.map +0 -1
  662. package/dist/points-lines/__tests__/inspection-lanes.integration.test.d.ts +0 -2
  663. package/dist/points-lines/__tests__/inspection-lanes.integration.test.d.ts.map +0 -1
  664. package/dist/points-lines/__tests__/inspection.unit.test.d.ts +0 -2
  665. package/dist/points-lines/__tests__/inspection.unit.test.d.ts.map +0 -1
  666. package/dist/points-lines/__tests__/lane-structural.integration.test.d.ts +0 -2
  667. package/dist/points-lines/__tests__/lane-structural.integration.test.d.ts.map +0 -1
  668. package/dist/points-lines/__tests__/points-lines.bench.d.ts +0 -2
  669. package/dist/points-lines/__tests__/points-lines.bench.d.ts.map +0 -1
  670. package/dist/points-lines/__tests__/raster-lane.integration.test.d.ts +0 -2
  671. package/dist/points-lines/__tests__/raster-lane.integration.test.d.ts.map +0 -1
  672. package/dist/points-lines/__tests__/snapshot.unit.test.d.ts +0 -2
  673. package/dist/points-lines/__tests__/snapshot.unit.test.d.ts.map +0 -1
  674. package/dist/points-lines/__tests__/view-ubo.unit.test.d.ts +0 -2
  675. package/dist/points-lines/__tests__/view-ubo.unit.test.d.ts.map +0 -1
  676. package/dist/record/__tests__/material-bg-cache.unit.test.d.ts +0 -2
  677. package/dist/record/__tests__/material-bg-cache.unit.test.d.ts.map +0 -1
  678. package/dist/record/__tests__/material-slot-plan.unit.test.d.ts +0 -2
  679. package/dist/record/__tests__/material-slot-plan.unit.test.d.ts.map +0 -1
  680. package/dist/record/__tests__/typed-frame-graph-plan.unit.test.d.ts +0 -2
  681. package/dist/record/__tests__/typed-frame-graph-plan.unit.test.d.ts.map +0 -1
  682. package/dist/record/__tests__/video-texture-view.unit.test.d.ts +0 -2
  683. package/dist/record/__tests__/video-texture-view.unit.test.d.ts.map +0 -1
@@ -77,9 +77,12 @@ import type {
77
77
  VertexAttributeMap,
78
78
  } from '@forgeax/engine-types';
79
79
  import { derive, handleSlot } from '@forgeax/engine-types';
80
+
81
+ export type { MaterialShaderManifestEntry } from '@forgeax/engine-shader';
82
+
80
83
  import { GpuResidencyCache, type MeshGpuHandles } from '../device/gpu-residency';
81
84
  import { createEngineMetrics } from '../engine-metrics';
82
- import { RecoverError, type RecoverFailure, RenderRecoveryError } from '../errors/recover';
85
+ import { RecoverError, type RecoverFailure } from '../errors/recover';
83
86
  import {
84
87
  FrameReceiptStaleError,
85
88
  type RenderError,
@@ -90,8 +93,9 @@ import {
90
93
  RenderFeatureStageFailedError,
91
94
  } from '../errors/render';
92
95
  import { createRenderFeatureHost, type RenderFeatureHost } from '../features/host';
96
+ import type { RenderFeatureMaterialShaderBindingContract } from '../features/plan';
93
97
  import { RENDER_FEATURE_VERTEX_LAYOUTS } from '../features/prepared-graphics';
94
- import type { RenderFeature } from '../features/types';
98
+ import type { RenderFeature, RenderFeatureShaderModuleMode } from '../features/types';
95
99
  import type { PostProcessShaderEntry } from '../fullscreen-post-process-pass';
96
100
  import { glyphTextLayoutSystem } from '../glyph-text-layout-system';
97
101
  import { GpuBuffer } from '../gpu-resource';
@@ -115,6 +119,7 @@ import {
115
119
  FALLBACK_BYTES_PER_ROW,
116
120
  type SkylightFallback,
117
121
  } from '../ibl/skylight-bind-group';
122
+ import type { BloomInspection } from '../inspection-types';
118
123
  import {
119
124
  DeviceScope,
120
125
  HealthListenerRegistry,
@@ -131,13 +136,16 @@ import {
131
136
  import {
132
137
  buildPbrPipelineLayouts,
133
138
  buildPbrSkinLayouts,
139
+ isSkinnedShadowCasterVariant,
134
140
  SKIN_MATERIAL_SHADER_ID,
135
141
  } from '../pbr-pipeline';
136
142
  import { STANDARD_CLUSTER_MEMBERSHIP_WGSL, standardPipeline } from '../pipeline/standard-pipeline';
143
+ import { inspectStandardBloomGraph } from '../pipeline/standard-post';
137
144
  import {
138
145
  DEFAULT_STANDARD_PROFILE,
139
146
  STANDARD_LIGHT_COUNTS,
140
147
  STANDARD_PIPELINE_ID,
148
+ STANDARD_POST_STAGE_NAMES,
141
149
  } from '../pipeline/standard-profile';
142
150
  import { buildPipelineForMaterialShader } from '../pipeline-builder';
143
151
  import {
@@ -155,6 +163,7 @@ import {
155
163
  } from '../pipeline-spec';
156
164
  import { POINTS_LINES_MATERIAL_SHADER_ID } from '../points-lines/record';
157
165
  import type { FrameObservationOptions } from '../record/frame';
166
+ import type { BloomPersistentBundle } from '../record/render-context';
158
167
  import { SHADOW_CASTER_BUFFER_SIZE } from '../record/shadow-pass';
159
168
  import { POINTS_LINES_VIEW_BUFFER_SIZE, VIEW_UNIFORM_BUFFER_SIZE } from '../record/view-ubo';
160
169
  import type {
@@ -177,7 +186,7 @@ import type {
177
186
  RenderResult,
178
187
  RenderWorldLease,
179
188
  } from '../render-contract';
180
- import { STANDARD_TONEMAP_FEATURE_ID } from '../render-contract';
189
+ import { STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from '../render-contract';
181
190
  import {
182
191
  configureSurface,
183
192
  createRenderSystem,
@@ -194,6 +203,8 @@ import {
194
203
  import { tilemapChunkExtractSystem } from '../tilemap-chunk-extract-system';
195
204
  import { EngineEnvironmentError, type RhiBackendPack } from './backend-contract';
196
205
  import type { RendererAssemblyImplementation, RendererHostImplementation } from './host-contract';
206
+ import { projectRendererOutputInspection } from './renderer-inspection';
207
+ import { isRendererStateOperational } from './renderer-lifecycle';
197
208
 
198
209
  export { assembleMaterialProjection } from './material/assembly';
199
210
  export {
@@ -232,6 +243,9 @@ function loadRhiPack(
232
243
  const createShaderModule = mod.createShaderModule as
233
244
  | RhiBackendPack['createShaderModule']
234
245
  | undefined;
246
+ const createShaderModuleImmediate = mod.createShaderModuleImmediate as
247
+ | RhiBackendPack['createShaderModuleImmediate']
248
+ | undefined;
235
249
  const translateErrorEventToRhiError = mod.translateErrorEventToRhiError as
236
250
  | RhiBackendPack['translateErrorEventToRhiError']
237
251
  | undefined;
@@ -241,6 +255,7 @@ function loadRhiPack(
241
255
  return {
242
256
  rhi,
243
257
  ...(createShaderModule === undefined ? {} : { createShaderModule }),
258
+ ...(createShaderModuleImmediate === undefined ? {} : { createShaderModuleImmediate }),
244
259
  ...(translateErrorEventToRhiError === undefined ? {} : { translateErrorEventToRhiError }),
245
260
  ...(rawDeviceAccessor === undefined ? {} : { _internal_getRawDevice: rawDeviceAccessor }),
246
261
  ...(instrumentation === undefined ? {} : { instrumentation }),
@@ -477,20 +492,61 @@ export type LayoutKind = 'pbr' | 'pbr-skin' | 'hdrp-pbr' | 'sprite-urp' | 'unlit
477
492
  * Return every device-capability HDRP PBR variant that needs a lazy module
478
493
  * label seeded during renderer boot. Color availability is intentionally not
479
494
  * filtered: colored and no-color meshes use distinct composed WGSL modules.
495
+ * Transmission is filtered by the same sampled-texture capability gate as
496
+ * Standard transmission because HDRP shares the material texture ABI.
480
497
  */
481
498
  export function selectHdrpPbrPrewarmVariants(
482
499
  manifestEntry: MaterialShaderManifestEntry | undefined,
483
500
  storageBufferCapable: boolean,
501
+ transmissionCapable: boolean,
484
502
  ): readonly MaterialShaderManifestEntry['variants'][number][] {
485
503
  return (
486
504
  manifestEntry?.variants.filter(
487
505
  (variant) =>
488
506
  variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
489
- variant.defines.CLUSTER_FORWARD_AVAILABLE === true,
507
+ variant.defines.CLUSTER_FORWARD_AVAILABLE === true &&
508
+ (transmissionCapable || variant.defines.TRANSMISSION_AVAILABLE !== true),
490
509
  ) ?? []
491
510
  );
492
511
  }
493
512
 
513
+ /**
514
+ * Select both Standard transmission states from the manifest for exact-label
515
+ * prewarming. The requested keys come from declared variants; no key is
516
+ * synthesized and no source fallback is permitted.
517
+ */
518
+ export function selectStandardPbrTransmissionPrewarmVariants(
519
+ manifestEntry: MaterialShaderManifestEntry | undefined,
520
+ storageBufferCapable: boolean,
521
+ ): readonly MaterialShaderManifestEntry['variants'][number][] {
522
+ if (manifestEntry === undefined) {
523
+ throw new Error('Standard material shader manifest row is missing');
524
+ }
525
+ const selected: MaterialShaderManifestEntry['variants'][number][] = [];
526
+ for (const transmissionAvailable of [false, true]) {
527
+ const declared = manifestEntry.variants.find(
528
+ (variant) =>
529
+ variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
530
+ variant.defines.CLUSTER_FORWARD_AVAILABLE === false &&
531
+ variant.defines.VERTEX_COLOR_AVAILABLE === false &&
532
+ variant.defines.TRANSMISSION_AVAILABLE === transmissionAvailable,
533
+ );
534
+ if (declared === undefined) {
535
+ throw new Error(
536
+ `Standard material shader manifest lacks exact TRANSMISSION_AVAILABLE=${transmissionAvailable} prewarm variant`,
537
+ );
538
+ }
539
+ const exact = findVariantByKey(manifestEntry, declared.definesKey);
540
+ if (exact === undefined) {
541
+ throw new Error(
542
+ `Standard material shader manifest exact lookup failed for TRANSMISSION_AVAILABLE=${transmissionAvailable}`,
543
+ );
544
+ }
545
+ selected.push(exact);
546
+ }
547
+ return selected;
548
+ }
549
+
494
550
  /**
495
551
  * Select the PBR variant that matches a fallback mesh with no COLOR_0 input.
496
552
  * Unknown/custom material IDs may still use the legacy PBR fallback, so that
@@ -502,7 +558,9 @@ export function selectNoColorPbrVariant(
502
558
  variantSet: string | undefined,
503
559
  ): MaterialShaderManifestEntry['variants'][number] | undefined {
504
560
  const hdrpRequested =
505
- variantSet === '' || (variantSet?.includes('CLUSTER_FORWARD_AVAILABLE=true') ?? false);
561
+ variantSet === '' ||
562
+ // biome-ignore lint/complexity/useOptionalChain: G-14 requires an explicit undefined guard so no variant request becomes false.
563
+ (variantSet !== undefined && variantSet.includes('CLUSTER_FORWARD_AVAILABLE=true'));
506
564
  return manifestEntry?.variants.find(
507
565
  (variant) =>
508
566
  variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
@@ -512,12 +570,7 @@ export function selectNoColorPbrVariant(
512
570
  );
513
571
  }
514
572
 
515
- export type MaterialShaderBindingContract =
516
- | 'group-0'
517
- | 'group-0-resource'
518
- | 'view-only'
519
- | 'view-and-scene-depth'
520
- | 'render-material';
573
+ export type MaterialShaderBindingContract = RenderFeatureMaterialShaderBindingContract;
521
574
  export type MaterialShaderVertexInputContract = 'none' | 'render-material';
522
575
 
523
576
  export function resolveMaterialShaderBindingContract(
@@ -632,6 +685,13 @@ export function selectPipelineLayoutForVariant(
632
685
  if (layoutKind === 'unlit-urp') {
633
686
  return state.pbrPipelineLayout;
634
687
  }
688
+ // Static PBR callers can use the canonical all-true variant key (`''`)
689
+ // without becoming HDRP. Keep the shader variant identity independent from
690
+ // the pipeline-layout identity; shadow-caster static draws use this explicit
691
+ // kind so their ordinary mesh bind group remains compatible with the PSO.
692
+ if (layoutKind === 'pbr') {
693
+ return state.pbrPipelineLayout;
694
+ }
635
695
  // HDRP variant matches when:
636
696
  // - layoutKind === 'hdrp-pbr' (caller-driven, future-proof), OR
637
697
  // - variantSet is the canonical all-true key '' (boot-time HDRP
@@ -685,7 +745,17 @@ export function resolveMaterialShaderVariantSet(
685
745
  ? backendKind === 'wgpu-webgl2'
686
746
  : axis === 'STORAGE_BUFFER_AVAILABLE'
687
747
  ? backendKind !== 'wgpu-webgl2' && storageBuffer
688
- : (requested[axis] ?? true);
748
+ : axis === 'SKINNING_DISABLED'
749
+ ? (requested[axis] ?? true)
750
+ : axis === 'CLUSTER_FORWARD_AVAILABLE'
751
+ ? backendKind !== 'wgpu-webgl2' && (requested[axis] ?? false)
752
+ : axis === 'PER_INSTANCE_REGION'
753
+ ? requestedVariantSet === '' || (requested[axis] ?? false)
754
+ : axis === 'VERTEX_COLOR_AVAILABLE'
755
+ ? requestedVariantSet === '' || (requested[axis] ?? false)
756
+ : axis === 'TRANSMISSION_AVAILABLE'
757
+ ? (requested[axis] ?? false)
758
+ : (requested[axis] ?? true);
689
759
  }
690
760
  const sorted = Object.entries(filtered).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
691
761
  return sorted.every(([, value]) => value)
@@ -875,11 +945,32 @@ const COMPRESSION_FEATURES: GPUFeatureName[] = [
875
945
  'texture-compression-astc',
876
946
  ];
877
947
 
878
- function deviceOptionsForAdapter(
879
- adapter: Pick<RhiAdapter, 'features'>,
880
- ): { requiredFeatures: GPUFeatureName[] } | undefined {
948
+ // The Standard transmission fragment ABI needs 17 sampled textures across its
949
+ // material, IBL, clustered-light, and SSAO bindings. Ordinary Standard draws
950
+ // use the non-transmission variant and remain valid on the WebGPU minimum of
951
+ // 16; only request the elevated limit when the adapter can provide it.
952
+ const STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES = 17;
953
+
954
+ function deviceOptionsForAdapter(adapter: Pick<RhiAdapter, 'features' | 'limits'>):
955
+ | {
956
+ requiredFeatures?: GPUFeatureName[];
957
+ requiredLimits?: GPUDeviceDescriptor['requiredLimits'];
958
+ }
959
+ | undefined {
881
960
  const requiredFeatures = COMPRESSION_FEATURES.filter((feature) => adapter.features.has(feature));
882
- return requiredFeatures.length > 0 ? { requiredFeatures } : undefined;
961
+ const supportsTransmission =
962
+ (adapter.limits.maxSampledTexturesPerShaderStage ?? 0) >=
963
+ STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
964
+ return {
965
+ ...(requiredFeatures.length > 0 ? { requiredFeatures } : {}),
966
+ ...(supportsTransmission
967
+ ? {
968
+ requiredLimits: {
969
+ maxSampledTexturesPerShaderStage: STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
970
+ },
971
+ }
972
+ : {}),
973
+ };
883
974
  }
884
975
 
885
976
  /**
@@ -1097,16 +1188,34 @@ async function makeWebGPURenderer(
1097
1188
  // compile across both paths (charter P4 consistent abstraction +
1098
1189
  // architecture-principles #6 idempotency).
1099
1190
  let sharedShaderModuleAdapter: ShaderDeviceAdapterInternal | null = null;
1191
+ // Generated feature programs (currently VFX) opt into the handle-first
1192
+ // adapter. Keep it separate from the validated adapter so ordinary render
1193
+ // pipelines retain the compiler-readiness barrier that prevents first-frame
1194
+ // black output on Dawn.
1195
+ let sharedImmediateShaderModuleAdapter: ShaderDeviceAdapterInternal | null = null;
1100
1196
  const getShaderModuleAdapter = (): ShaderDeviceAdapterInternal => {
1101
1197
  if (sharedShaderModuleAdapter === null) {
1102
1198
  sharedShaderModuleAdapter = makeShaderDeviceAdapter(
1103
1199
  internals.device,
1104
1200
  internals.errorRegistry,
1105
1201
  internals.pack.createShaderModule,
1202
+ internals.pack.createShaderModuleImmediate,
1106
1203
  );
1107
1204
  }
1108
1205
  return sharedShaderModuleAdapter;
1109
1206
  };
1207
+ const getImmediateShaderModuleAdapter = (): ShaderDeviceAdapterInternal => {
1208
+ if (sharedImmediateShaderModuleAdapter === null) {
1209
+ sharedImmediateShaderModuleAdapter = makeShaderDeviceAdapter(
1210
+ internals.device,
1211
+ internals.errorRegistry,
1212
+ internals.pack.createShaderModule,
1213
+ internals.pack.createShaderModuleImmediate,
1214
+ 'immediate',
1215
+ );
1216
+ }
1217
+ return sharedImmediateShaderModuleAdapter;
1218
+ };
1110
1219
  const getShader = (): ShaderCatalog => {
1111
1220
  if (shaderInstance === null) {
1112
1221
  // feat-20260608-create-app-param-surface-trim / M2 / D-2 q5-A:
@@ -1326,13 +1435,13 @@ async function makeWebGPURenderer(
1326
1435
  >();
1327
1436
  const preparedMaterialPipelineLayoutCache = new Map<string, PipelineLayout>();
1328
1437
  // 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).
1438
+ // BGL + pipeline layout cache. Every authored material shader owns the
1439
+ // user-region binding order emitted by its paramSchema, plus the shared
1440
+ // engine-injection suffix. A custom shader with four-or-fewer textures can
1441
+ // still differ from the standard PBR order (for example, baseColorTexture
1442
+ // followed directly by normalTexture), so it cannot safely reuse the
1443
+ // standard material BGL. Built lazily on first request and cached by
1444
+ // shaderId; built-in shader IDs continue to use the boot-time shared layout.
1336
1445
  type PerShaderMaterialLayout = {
1337
1446
  materialBgl: BindGroupLayout;
1338
1447
  pipelineLayout: PipelineLayout;
@@ -1343,6 +1452,16 @@ async function makeWebGPURenderer(
1343
1452
  readonly layout: PerShaderMaterialLayout | null;
1344
1453
  };
1345
1454
  const perShaderMaterialLayoutCache = new Map<string, PerShaderMaterialLayoutCacheEntry>();
1455
+ const isEngineOwnedMaterialShader = (materialShaderId: string): boolean =>
1456
+ materialShaderId === 'forgeax::default-unlit' ||
1457
+ materialShaderId === 'forgeax::default-shadow-caster' ||
1458
+ materialShaderId === 'forgeax::sprite' ||
1459
+ materialShaderId === 'forgeax::sprite-lit' ||
1460
+ materialShaderId === 'forgeax::default-sprite' ||
1461
+ materialShaderId === 'forgeax::msdf-text' ||
1462
+ materialShaderId === 'forgeax::default-standard-pbr' ||
1463
+ materialShaderId === 'forgeax::pbr-skin' ||
1464
+ materialShaderId === 'forgeax::default-standard-pbr-skin';
1346
1465
  // Material binding contracts are derived from WGSL source, but the lookup
1347
1466
  // is also used by the per-submesh bind-group path. Cache the derived value
1348
1467
  // per renderer so a frame does not re-run the comment stripping and regex
@@ -1477,7 +1596,9 @@ async function makeWebGPURenderer(
1477
1596
  if (cached !== undefined) return cached;
1478
1597
  if (pipelineState === null) return null;
1479
1598
  const materialBgl =
1480
- getOrBuildPerShaderMaterialLayout(materialShaderId)?.materialBgl ??
1599
+ (isEngineOwnedMaterialShader(materialShaderId)
1600
+ ? undefined
1601
+ : getOrBuildPerShaderMaterialLayout(materialShaderId)?.materialBgl) ??
1481
1602
  pipelineState.materialBindGroupLayout;
1482
1603
  const result = internals.device.createPipelineLayout({
1483
1604
  label: `prepared-material-pl-${materialShaderId}`,
@@ -1490,15 +1611,15 @@ async function makeWebGPURenderer(
1490
1611
  preparedMaterialPipelineLayoutCache.set(materialShaderId, result.value);
1491
1612
  return result.value;
1492
1613
  };
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.
1614
+ // Returns the per-shader { materialBgl, pipelineLayout } for an authored
1615
+ // shader, or null when it is unregistered / pipelineState is not ready.
1616
+ // Pure-ish: builds + caches on miss via the same
1617
+ // buildBindGroupLayoutDescriptor SSOT (kind 'pbr-material-merged' +
1618
+ // materialParamSchema) used by buildPbrPipelineLayouts.
1499
1619
  const getOrBuildPerShaderMaterialLayout = (
1500
1620
  materialShaderId: string,
1501
1621
  ): PerShaderMaterialLayout | null => {
1622
+ if (isEngineOwnedMaterialShader(materialShaderId)) return null;
1502
1623
  const lookup = getShader().findMaterialArtifact(materialShaderId);
1503
1624
  if (!lookup.ok) return null;
1504
1625
  const cached = perShaderMaterialLayoutCache.get(materialShaderId);
@@ -1507,16 +1628,6 @@ async function makeWebGPURenderer(
1507
1628
  }
1508
1629
  if (pipelineState === null) return null;
1509
1630
  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) {
1513
- perShaderMaterialLayoutCache.set(materialShaderId, {
1514
- source: lookup.value.source,
1515
- paramSchema,
1516
- layout: null,
1517
- });
1518
- return null;
1519
- }
1520
1631
  const spec: PipelineSpec = {
1521
1632
  shader: { id: materialShaderId, passKind: 'forward', variantSet: undefined },
1522
1633
  attachments: { colorFormats: [], depthFormat: undefined, sampleCount: 1 },
@@ -1603,9 +1714,11 @@ async function makeWebGPURenderer(
1603
1714
  // `internals.device` / `internals.pack` are read live, so a rebuild after a
1604
1715
  // device swap compiles against the new device.
1605
1716
  let tonemapRegistered = false;
1606
- const buildPipeline = (): Promise<PipelineState> =>
1717
+ let temporalPostProcessesRegistered = false;
1718
+ const buildPipeline = (scope: DeviceScope = activeDeviceScope): Promise<PipelineState> =>
1607
1719
  buildReadyWebGPU(
1608
1720
  internals.device,
1721
+ scope,
1609
1722
  getShader,
1610
1723
  gpuStore,
1611
1724
  internals.pack.createShaderModule,
@@ -1644,13 +1757,31 @@ async function makeWebGPURenderer(
1644
1757
  // tonemap into this channel each frame (render-system-extract.ts w13).
1645
1758
  (source: string) => {
1646
1759
  if (tonemapRegistered) return;
1647
- renderSystem.registerBuiltinPostProcess(STANDARD_TONEMAP_FEATURE_ID, {
1760
+ renderSystem.registerBuiltinPostProcess(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, {
1648
1761
  source,
1649
1762
  params: { byteSize: 16, defaultValue: new Uint8Array(16) },
1650
1763
  reads: ['hdrColor'],
1651
1764
  });
1652
1765
  tonemapRegistered = true;
1653
1766
  },
1767
+ (entries: { readonly motionBlur?: string; readonly taaResolve?: string }) => {
1768
+ if (temporalPostProcessesRegistered) return;
1769
+ if (entries.motionBlur !== undefined) {
1770
+ renderSystem.registerBuiltinPostProcess('forgeax.motion-blur', {
1771
+ source: entries.motionBlur,
1772
+ params: { byteSize: 16, defaultValue: new Uint8Array(16) },
1773
+ reads: ['scene-color', 'scene-temporal'],
1774
+ });
1775
+ }
1776
+ if (entries.taaResolve !== undefined) {
1777
+ renderSystem.registerBuiltinPostProcess('forgeax.taa-resolve', {
1778
+ source: entries.taaResolve,
1779
+ params: { byteSize: 16, defaultValue: new Uint8Array(16) },
1780
+ reads: ['scene-color', 'scene-temporal', 'taa-history-color', 'taa-history-temporal'],
1781
+ });
1782
+ }
1783
+ temporalPostProcessesRegistered = true;
1784
+ },
1654
1785
  );
1655
1786
  const ready: Promise<Result<void, RhiError>> = buildPipeline().then(
1656
1787
  (state): Result<void, RhiError> => {
@@ -1730,6 +1861,8 @@ async function makeWebGPURenderer(
1730
1861
  meshAttributes?: VertexAttributeMap,
1731
1862
  vertexLayout?: string,
1732
1863
  vertexLayoutProjection?: VertexLayoutProjection,
1864
+ shaderModuleMode?: RenderFeatureShaderModuleMode,
1865
+ layoutKindOverride?: LayoutKind,
1733
1866
  ) => {
1734
1867
  // bug-20260611: derive LayoutKind from materialShaderId (caller-driven)
1735
1868
  // so the selector body stays free of literal shader-id strings (AC-09).
@@ -1741,17 +1874,17 @@ async function makeWebGPURenderer(
1741
1874
  // through the HDRP layout branch of the selector — sprite shaders have
1742
1875
  // no HDRP variant, plan-strategy R-1' guard.
1743
1876
  const layoutKind: LayoutKind | undefined =
1744
- materialShaderId === SKIN_MATERIAL_SHADER_ID
1877
+ layoutKindOverride ??
1878
+ (materialShaderId === SKIN_MATERIAL_SHADER_ID ||
1879
+ isSkinnedShadowCasterVariant(materialShaderId, variantSet)
1745
1880
  ? 'pbr-skin'
1746
1881
  : materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit'
1747
1882
  ? 'sprite-urp'
1748
1883
  : materialShaderId === 'forgeax::default-unlit'
1749
1884
  ? 'unlit-urp'
1750
- : undefined;
1885
+ : undefined);
1751
1886
  const usesSharedBootMaterialLayout =
1752
- materialShaderId === 'forgeax::default-standard-pbr' ||
1753
- materialShaderId === 'forgeax::default-unlit' ||
1754
- materialShaderId === SKIN_MATERIAL_SHADER_ID;
1887
+ materialShaderId !== undefined && isEngineOwnedMaterialShader(materialShaderId);
1755
1888
  // Built-in PBR/unlit/skin layouts are assembled at boot. Re-deriving a
1756
1889
  // per-shader layout from the expanded PBR schema would bypass the HDRP
1757
1890
  // group(2) selector and bind the URP mesh BGL to the cluster pipeline.
@@ -1759,12 +1892,11 @@ async function makeWebGPURenderer(
1759
1892
  !usesSharedBootMaterialLayout && layoutKind !== 'pbr-skin' && materialShaderId !== undefined
1760
1893
  ? getOrBuildPerShaderMaterialLayout(materialShaderId)
1761
1894
  : 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).
1895
+ // feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): an authored
1896
+ // shader owns a per-shader pipeline layout derived from its paramSchema,
1897
+ // because its compact texture order may differ from standard PBR even
1898
+ // when it declares only a few textures. Skin / HDRP keep their dedicated
1899
+ // layouts; built-in material IDs remain on the shared boot-time layout.
1768
1900
  const bindingContract =
1769
1901
  materialShaderId === undefined
1770
1902
  ? 'render-material'
@@ -1900,9 +2032,13 @@ async function makeWebGPURenderer(
1900
2032
  ] satisfies readonly GPUVertexBufferLayout[]);
1901
2033
  return {
1902
2034
  device: internals.device,
1903
- shaderModuleFactory: getShaderModuleAdapter(),
2035
+ shaderModuleFactory:
2036
+ shaderModuleMode === 'immediate'
2037
+ ? getImmediateShaderModuleAdapter()
2038
+ : getShaderModuleAdapter(),
1904
2039
  pipelineLayout,
1905
2040
  vertexBuffers,
2041
+ ...(layoutKind === 'pbr-skin' ? { layoutKind } : {}),
1906
2042
  };
1907
2043
  };
1908
2044
  /**
@@ -1960,6 +2096,8 @@ async function makeWebGPURenderer(
1960
2096
  depthFormatOverride?: GPUTextureFormat | null,
1961
2097
  vertexLayout?: string,
1962
2098
  vertexLayoutProjection?: VertexLayoutProjection,
2099
+ shaderModuleMode?: RenderFeatureShaderModuleMode,
2100
+ layoutKindOverride?: LayoutKind,
1963
2101
  ): RenderPipeline | null => {
1964
2102
  const ctx = buildPipelineContext(
1965
2103
  variantSet,
@@ -1967,6 +2105,8 @@ async function makeWebGPURenderer(
1967
2105
  meshAttributes,
1968
2106
  vertexLayout,
1969
2107
  vertexLayoutProjection,
2108
+ shaderModuleMode,
2109
+ layoutKindOverride,
1970
2110
  );
1971
2111
  if (ctx === null) return null;
1972
2112
  // bug-20260612: route the LDR color format through pipelineState so the
@@ -1976,6 +2116,11 @@ async function makeWebGPURenderer(
1976
2116
  // the explicit null check).
1977
2117
  if (pipelineState === null) return null;
1978
2118
  const ldrColorFormat = colorFormatOverride ?? pipelineState.colorAttachmentFormat;
2119
+ const fragmentEntry =
2120
+ isHdr &&
2121
+ (materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit')
2122
+ ? 'fs_main_hdr'
2123
+ : undefined;
1979
2124
  const built = buildPipelineForMaterialShader(
1980
2125
  cacheKey,
1981
2126
  // The catalog entry is projected into the material-shader entry shape.
@@ -2009,7 +2154,7 @@ async function makeWebGPURenderer(
2009
2154
  }
2010
2155
  : undefined,
2011
2156
  undefined, // vertexEntry — default vs_main
2012
- undefined, // fragmentEntry — default fs_main
2157
+ fragmentEntry,
2013
2158
  undefined, // defines — none
2014
2159
  passKind,
2015
2160
  sampleCount,
@@ -2068,6 +2213,8 @@ async function makeWebGPURenderer(
2068
2213
  depthFormatOverride?: GPUTextureFormat | null,
2069
2214
  vertexLayout?: string,
2070
2215
  vertexLayoutProjection?: VertexLayoutProjection,
2216
+ shaderModuleMode: RenderFeatureShaderModuleMode = 'validated',
2217
+ layoutKindOverride?: LayoutKind,
2071
2218
  ): RenderPipeline | null => {
2072
2219
  // WebGL2 is a downlevel backend even when the record stage asks for the
2073
2220
  // native-capability variant string. Resolve the variant axes at this
@@ -2082,7 +2229,14 @@ async function makeWebGPURenderer(
2082
2229
  // (stored in materialShaderUvSetCounts during prepareMaterialShaders).
2083
2230
  const resolvedUvSetCount = shaderUvSetCount ?? materialShaderUvSetCounts.get(materialShaderId);
2084
2231
  const manifestEntry = findMaterialShaderManifestEntry(materialShaderId);
2085
- const resolvedVariantSet = resolveCachedMaterialShaderVariantSet(variantSet, manifestEntry);
2232
+ // Sprite's omitted request is the boot-selected default artifact. The
2233
+ // explicit empty key remains the PER_INSTANCE_REGION=true request used by
2234
+ // SpriteInstances, so only that path is capability-rewritten here.
2235
+ const resolvedVariantSet =
2236
+ variantSet === undefined &&
2237
+ (materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit')
2238
+ ? undefined
2239
+ : resolveCachedMaterialShaderVariantSet(variantSet, manifestEntry);
2086
2240
  // The record stage also composes engine capability axes for user shaders.
2087
2241
  // Keep only manifest-declared axes for non-engine material IDs: a plain
2088
2242
  // custom shader must not collapse that request to '' because '' is the
@@ -2227,6 +2381,7 @@ async function makeWebGPURenderer(
2227
2381
  depthFormatOverride,
2228
2382
  vertexLayout,
2229
2383
  vertexLayoutProjection,
2384
+ shaderModuleMode,
2230
2385
  );
2231
2386
  }
2232
2387
  const pbrEntry = findStandardPbrEntry(effectiveVariantSet);
@@ -2280,6 +2435,7 @@ async function makeWebGPURenderer(
2280
2435
  depthFormatOverride,
2281
2436
  vertexLayout,
2282
2437
  vertexLayoutProjection,
2438
+ shaderModuleMode,
2283
2439
  );
2284
2440
  }
2285
2441
  // feat-20260609 M4 / w31: resolve variant WGSL from manifest when
@@ -2370,6 +2526,8 @@ async function makeWebGPURenderer(
2370
2526
  depthFormatOverride,
2371
2527
  vertexLayout,
2372
2528
  vertexLayoutProjection,
2529
+ shaderModuleMode,
2530
+ layoutKindOverride,
2373
2531
  );
2374
2532
  };
2375
2533
  const getParamSchema = (materialShaderId: string) => {
@@ -2378,10 +2536,11 @@ async function makeWebGPURenderer(
2378
2536
  };
2379
2537
  // feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): expose the per-shader
2380
2538
  // 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.
2539
+ // the matching paramSchema-derived layout. Returns undefined only for
2540
+ // group-0 / view-only contracts or an unavailable shader layout; built-in
2541
+ // material IDs continue to resolve through the shared layout path.
2384
2542
  const getMaterialBindGroupLayout = (materialShaderId: string): BindGroupLayout | undefined => {
2543
+ if (isEngineOwnedMaterialShader(materialShaderId)) return undefined;
2385
2544
  const contract = getCachedMaterialShaderBindingContract(materialShaderId);
2386
2545
  if (contract === 'group-0') {
2387
2546
  return getOrBuildGroup0MaterialLayout()?.materialBgl;
@@ -2458,7 +2617,11 @@ async function makeWebGPURenderer(
2458
2617
  fragment: {
2459
2618
  module: moduleResult.value,
2460
2619
  entryPoint: 'fs_main',
2461
- targets: [{ format: colorFormat }],
2620
+ targets:
2621
+ entry.source.includes('@location(1) temporal') &&
2622
+ entry.source.includes('TaaResolveOutput')
2623
+ ? [{ format: colorFormat }, { format: colorFormat }]
2624
+ : [{ format: colorFormat }],
2462
2625
  },
2463
2626
  primitive: { topology: 'triangle-list', cullMode: 'none', frontFace: 'ccw' },
2464
2627
  depthStencil: undefined,
@@ -2487,6 +2650,10 @@ async function makeWebGPURenderer(
2487
2650
  shaderModuleFactory: {
2488
2651
  createShaderModule: (descriptor) => getShaderModuleAdapter().createShaderModule(descriptor),
2489
2652
  },
2653
+ immediateShaderModuleFactory: {
2654
+ createShaderModule: (descriptor) =>
2655
+ getImmediateShaderModuleAdapter().createShaderModule(descriptor),
2656
+ },
2490
2657
  profiler: internals.options?.profiler,
2491
2658
  // feat-20260622-s5 M3 / w17: device + context read live off `internals` via
2492
2659
  // getters so the recover() rebuild (which swaps internals.device /
@@ -2499,6 +2666,9 @@ async function makeWebGPURenderer(
2499
2666
  get deviceScope() {
2500
2667
  return activeDeviceScope;
2501
2668
  },
2669
+ invalidateShaderModule: (label: string) => {
2670
+ sharedShaderModuleAdapter?.invalidateModule(label);
2671
+ },
2502
2672
  get context() {
2503
2673
  return internals.context;
2504
2674
  },
@@ -2536,6 +2706,18 @@ async function makeWebGPURenderer(
2536
2706
  return internals.meshSsboState;
2537
2707
  },
2538
2708
  });
2709
+ if (
2710
+ internals.device.caps.backendKind === 'null' &&
2711
+ Array.from(getShader().entries()).length === 0
2712
+ ) {
2713
+ // RhiNull validates graph topology only; keep the built-in identity
2714
+ // registered so structural frames do not fail before graph inspection.
2715
+ renderSystem.registerBuiltinPostProcess(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, {
2716
+ source: '',
2717
+ params: { byteSize: 16, defaultValue: new Uint8Array(16) },
2718
+ reads: ['hdrColor'],
2719
+ });
2720
+ }
2539
2721
  // The Standard pipeline is the sole built-in graph owner. Feature plans are
2540
2722
  // projected into that graph directly; no second post-process registry is
2541
2723
  // installed for fullscreen producers.
@@ -2596,6 +2778,22 @@ async function makeWebGPURenderer(
2596
2778
  : health.reason === 'internal-fault' || !readySettled
2597
2779
  ? 'faulted'
2598
2780
  : 'alive';
2781
+ const surfaceStorage = pipelineState?.format ?? 'rgba8unorm';
2782
+ const surfaceDisplay = pipelineState?.colorAttachmentFormat ?? 'rgba8unorm';
2783
+ const observationId = `${STANDARD_OUTPUT_TRANSFORM_FEATURE_ID}:frame-${frameId}`;
2784
+ const outputInspection = projectRendererOutputInspection({
2785
+ graph: renderSystem.perFrameGraphInfo,
2786
+ surfaceAvailable: !surfaceReleased && pipelineState !== null,
2787
+ surfaceStorage,
2788
+ surfaceDisplay,
2789
+ ...(pipelineState?.surfaceProfile === undefined
2790
+ ? {}
2791
+ : { surfaceProfile: pipelineState.surfaceProfile }),
2792
+ rgba16floatRenderable: internals.device.caps.rgba16floatRenderable,
2793
+ ...(internals.context?.presentationProof === undefined
2794
+ ? {}
2795
+ : { presentationProof: internals.context.presentationProof }),
2796
+ });
2599
2797
  return Object.freeze({
2600
2798
  state,
2601
2799
  surface: surfaceReleased ? 'released' : 'available',
@@ -2612,6 +2810,18 @@ async function makeWebGPURenderer(
2612
2810
  frustumStats: Object.freeze({ ...renderSystem.frustumStats }),
2613
2811
  visibilityStats: Object.freeze({ ...renderSystem.visibilityStats }),
2614
2812
  renderScene: renderSystem.renderScene,
2813
+ environment: renderSystem.environment,
2814
+ temporal: renderSystem.temporal,
2815
+ bloom: renderSystem.bloom,
2816
+ ...(renderSystem.temporalTargetInspection === undefined
2817
+ ? {}
2818
+ : { temporalTarget: Object.freeze(renderSystem.temporalTargetInspection) }),
2819
+ ...(renderSystem.motionBlurInspection === undefined
2820
+ ? {}
2821
+ : { motionBlur: Object.freeze(renderSystem.motionBlurInspection) }),
2822
+ ...(renderSystem.transmission === undefined
2823
+ ? {}
2824
+ : { transmission: renderSystem.transmission }),
2615
2825
  meshMaterialBindings: Object.freeze(
2616
2826
  renderSystem.meshMaterialBindings.map((observation) =>
2617
2827
  Object.freeze({
@@ -2630,6 +2840,22 @@ async function makeWebGPURenderer(
2630
2840
  createBindGroup: renderSystem.bindGroupCounts.createBindGroup,
2631
2841
  keys: Object.freeze([...renderSystem.bindGroupCounts.keys]),
2632
2842
  }),
2843
+ ...outputInspection,
2844
+ observation: Object.freeze({
2845
+ observationId,
2846
+ frameId,
2847
+ ...(renderSystem.lastSuccessfulCameraAntialias === undefined
2848
+ ? {}
2849
+ : { antialias: renderSystem.lastSuccessfulCameraAntialias }),
2850
+ ...(outputInspection.surfaceProfile === undefined
2851
+ ? {}
2852
+ : { surfaceProfile: outputInspection.surfaceProfile }),
2853
+ rgba16floatRenderable: internals.device.caps.rgba16floatRenderable,
2854
+ passNames: Object.freeze([...outputInspection.graphPassNames]),
2855
+ ...(outputInspection.standardOutputColor === undefined
2856
+ ? {}
2857
+ : { standardOutputColor: Object.freeze({ ...outputInspection.standardOutputColor }) }),
2858
+ }),
2633
2859
  });
2634
2860
  },
2635
2861
  get device(): RhiDevice {
@@ -2720,6 +2946,12 @@ async function makeWebGPURenderer(
2720
2946
  observeCurrentFrame(options: FrameObservationOptions) {
2721
2947
  return renderSystem.observeCurrentFrame(options);
2722
2948
  },
2949
+ getCurrentGraphTarget(name: string) {
2950
+ return renderSystem.getCurrentGraphTarget(name);
2951
+ },
2952
+ requestGraphTargetCapture(request) {
2953
+ renderSystem.requestGraphTargetCapture(request);
2954
+ },
2723
2955
  get frustumStats() {
2724
2956
  return renderSystem.frustumStats;
2725
2957
  },
@@ -2991,7 +3223,12 @@ async function makeWebGPURenderer(
2991
3223
  // Configure context lazily on first draw (D-S1 single-point
2992
3224
  // exemption): GPUCanvasContext.configure({device}) needs a raw
2993
3225
  // The canvas context is configured through the RHI device scope.
2994
- ensureContextConfigured(internals, pipelineState, internals.errorRegistry);
3226
+ const contextConfigured = ensureContextConfigured(
3227
+ internals,
3228
+ pipelineState,
3229
+ internals.errorRegistry,
3230
+ );
3231
+ if (!contextConfigured.ok) return contextConfigured;
2995
3232
  // w24 — facade-level try/catch produces Result.err on unexpected throw
2996
3233
  // (D-P6 dual-channel preserved: per-stage RhiError continues to fan out
2997
3234
  // through onError separately; the facade Result is the synchronous
@@ -3159,13 +3396,6 @@ async function makeWebGPURenderer(
3159
3396
  if (disposed) return ok(undefined);
3160
3397
  disposed = true;
3161
3398
  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
3399
  for (const world of attachedWorlds) {
3170
3400
  try {
3171
3401
  const derivedSystemName = derivedSystemNames.get(world);
@@ -3247,6 +3477,18 @@ async function makeWebGPURenderer(
3247
3477
  cleanupFailures.push(error);
3248
3478
  internals.errorRegistry.fire(error);
3249
3479
  }
3480
+ // Retire assembly-owned device resources only after the surface and
3481
+ // renderer-owned GPU pools have been released. The wgpu WebGL2 surface
3482
+ // still needs a live device while `unconfigure()` presents its pending
3483
+ // image; disposing the scope first lets wasm finalizers race that
3484
+ // presentation and can surface a parking_lot panic in WebKit.
3485
+ try {
3486
+ activeDeviceScope.dispose();
3487
+ } catch (cause) {
3488
+ const error = wrapDisposeError(cause, 'activeDeviceScope.dispose');
3489
+ cleanupFailures.push(error);
3490
+ internals.errorRegistry.fire(error);
3491
+ }
3250
3492
  // Step 5: the IBL cache is keyed by the retired DeviceScope generation.
3251
3493
  // It is no longer reachable through the active renderer scope, while
3252
3494
  // the device teardown owns the child GPU handles.
@@ -3326,6 +3568,7 @@ async function makeWebGPURenderer(
3326
3568
  // buffer (or crash the browser GPU process). Keep the ShaderCatalog's
3327
3569
  // CPU material/source catalogue, but rebuild this device-bound adapter.
3328
3570
  sharedShaderModuleAdapter = null;
3571
+ sharedImmediateShaderModuleAdapter = null;
3329
3572
  // The fullscreen post-process builder keeps one empty group-0 layout
3330
3573
  // outside RenderSystem's frame cache. It is also device-bound, so retain
3331
3574
  // the source/registration but force the next post-process pass to derive
@@ -3372,6 +3615,13 @@ async function makeWebGPURenderer(
3372
3615
  // swap is observed without reconstructing the RenderSystem.
3373
3616
  internals.device = device;
3374
3617
  internals.context = ctxResult.value;
3618
+ // Build the replacement graph under a fresh renderer-owned scope. The
3619
+ // old committed scope remains authoritative until every rebuild step
3620
+ // succeeds, so a failed recovery cannot publish a half-built scope.
3621
+ const recoveryScope = DeviceScope.create(
3622
+ activeDeviceScope.generation + 1,
3623
+ activeDeviceScope.owner,
3624
+ );
3375
3625
  // Re-attach the device.lost fan-out to the new device's lost Promise,
3376
3626
  // reusing the SAME registries the host already subscribed to (SSOT
3377
3627
  // helper). When this device is lost again, health() flips to
@@ -3419,10 +3669,13 @@ async function makeWebGPURenderer(
3419
3669
  // the first post-recovery upload; unlike CPU asset payloads, these
3420
3670
  // handles cannot survive a device replacement.
3421
3671
  dynamicTextureStore.configureGpuDevice(adaptDynamicTextureDevice(internals.device));
3672
+ let candidatePipelineState: PipelineState | undefined;
3422
3673
  const abortDisposedRecovery = (): Result<void, RecoverFailure> | undefined => {
3423
3674
  if (!disposed) return undefined;
3424
3675
  gpuStore.destroyAll();
3676
+ candidatePipelineState?.perPassResources.drainBloomResources?.();
3425
3677
  renderSystem.disposeFrameState();
3678
+ recoveryScope.abandon();
3426
3679
  materialShaderPipelineCache.clear();
3427
3680
  perShaderMaterialLayoutCache.clear();
3428
3681
  group0ResourceLayouts.clear();
@@ -3441,7 +3694,7 @@ async function makeWebGPURenderer(
3441
3694
  );
3442
3695
  const abortedAfterShaders = abortDisposedRecovery();
3443
3696
  if (abortedAfterShaders !== undefined) return abortedAfterShaders;
3444
- pipelineState = await buildPipeline();
3697
+ candidatePipelineState = await buildPipeline(recoveryScope);
3445
3698
  const abortedAfterPipeline = abortDisposedRecovery();
3446
3699
  if (abortedAfterPipeline !== undefined) return abortedAfterPipeline;
3447
3700
  renderSystem.restorePostProcessResources();
@@ -3449,6 +3702,8 @@ async function makeWebGPURenderer(
3449
3702
  // Pipeline rebuild failed against the new device. Treat as a device
3450
3703
  // unavailability (the device was acquired but is not usable); health
3451
3704
  // stays `device-lost` so the host can retry.
3705
+ candidatePipelineState?.perPassResources.drainBloomResources?.();
3706
+ recoveryScope.abandon();
3452
3707
  const aborted = abortDisposedRecovery();
3453
3708
  if (aborted !== undefined) return aborted;
3454
3709
  return err(new RecoverError('recover-device-unavailable'));
@@ -3456,27 +3711,18 @@ async function makeWebGPURenderer(
3456
3711
  const abortedAfterPostProcess = abortDisposedRecovery();
3457
3712
  if (abortedAfterPostProcess !== undefined) return abortedAfterPostProcess;
3458
3713
 
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
- );
3714
+ // Promote only after the complete replacement build is ready. The old
3715
+ // scope is then retired exactly once; a failed attempt abandons its
3716
+ // candidate in the catch path and can be retried with a new generation.
3717
+ if (candidatePipelineState === undefined) {
3718
+ recoveryScope.abandon();
3719
+ return err(new RecoverError('recover-device-unavailable'));
3477
3720
  }
3478
- activeDeviceScope = replacementResult.value;
3721
+ const previousDeviceScope = activeDeviceScope;
3722
+ activeDeviceScope = recoveryScope;
3723
+ pipelineState = candidatePipelineState;
3479
3724
  gpuStore.bindDeviceScope(activeDeviceScope);
3725
+ previousDeviceScope.retire();
3480
3726
 
3481
3727
  // Re-evaluate feature capability gates against the replacement device.
3482
3728
  // Feature recovery is isolated from renderer recovery: a feature that is
@@ -3574,7 +3820,7 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
3574
3820
  );
3575
3821
  },
3576
3822
  setProfile: (profile) => {
3577
- if (currentState !== 'alive') {
3823
+ if (!isRendererStateOperational(currentState)) {
3578
3824
  return err(
3579
3825
  new RendererOperationError('renderer-state-invalid', {
3580
3826
  operation: 'set-profile',
@@ -3647,7 +3893,7 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
3647
3893
  const error = new RendererOperationError('recovery-failed', {
3648
3894
  operation: 'recover',
3649
3895
  oldGeneration,
3650
- cause: result.error,
3896
+ cause: structuredRendererCause(result.error, 'recover'),
3651
3897
  });
3652
3898
  emit(Object.freeze({ kind: 'error', error }));
3653
3899
  return err(error);
@@ -3690,13 +3936,24 @@ function structuredRendererCause(cause: unknown, operation: string): RendererOpe
3690
3936
  typeof candidate.expected === 'string' &&
3691
3937
  typeof candidate.hint === 'string'
3692
3938
  ) {
3693
- return candidate as RendererOperationCause;
3939
+ return {
3940
+ code: candidate.code,
3941
+ expected: candidate.expected,
3942
+ hint: candidate.hint,
3943
+ ...(candidate.detail === undefined ? {} : { detail: candidate.detail }),
3944
+ };
3694
3945
  }
3695
3946
  }
3696
- return new RendererContractFailureError(
3947
+ const contractFailure = new RendererContractFailureError(
3697
3948
  'draw',
3698
3949
  `${operation}: ${cause instanceof Error ? cause.message : String(cause)}`,
3699
3950
  );
3951
+ return {
3952
+ code: contractFailure.code,
3953
+ expected: contractFailure.expected,
3954
+ hint: contractFailure.hint,
3955
+ detail: contractFailure.detail,
3956
+ };
3700
3957
  }
3701
3958
 
3702
3959
  const RENDER_PROFILE_KEYS = new Set([
@@ -3707,10 +3964,6 @@ const RENDER_PROFILE_KEYS = new Set([
3707
3964
  'pbr',
3708
3965
  'ibl',
3709
3966
  'ssao',
3710
- 'bloom',
3711
- 'tone',
3712
- 'antialias',
3713
- 'sky',
3714
3967
  'fallback',
3715
3968
  'postStages',
3716
3969
  ]);
@@ -3731,32 +3984,13 @@ function validateRenderProfile(profile: RenderProfile): string | undefined {
3731
3984
  if (profile.shadows !== 'off' && profile.shadows !== 'hard' && profile.shadows !== 'filtered') {
3732
3985
  return "RenderProfile.shadows must be 'off', 'hard', or 'filtered'";
3733
3986
  }
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
3987
  if (profile.fallback !== 'native' && profile.fallback !== 'cpu-webgl2') {
3754
3988
  return "RenderProfile.fallback must be 'native' or 'cpu-webgl2'";
3755
3989
  }
3756
- for (const field of ['pbr', 'ibl', 'ssao', 'bloom', 'sky'] as const) {
3990
+ for (const field of ['pbr', 'ibl', 'ssao'] as const) {
3757
3991
  if (typeof profile[field] !== 'boolean') return `RenderProfile.${field} must be boolean`;
3758
3992
  }
3759
- const stages = ['transparent-blend', 'bloom', 'tone', 'fxaa', 'output'] as const;
3993
+ const stages = STANDARD_POST_STAGE_NAMES;
3760
3994
  if (
3761
3995
  !Array.isArray(profile.postStages) ||
3762
3996
  profile.postStages.length !== stages.length ||
@@ -3807,8 +4041,11 @@ interface ShaderDeviceAdapterInternal extends ShaderCatalogDevice {
3807
4041
  * `module-forgeax::default-shadow-caster` returns OK on frame 1.
3808
4042
  */
3809
4043
  seedModule(label: string, module: ShaderModule): void;
4044
+ invalidateModule(label: string): void;
3810
4045
  }
3811
4046
 
4047
+ type ShaderModuleAdapterMode = 'validated' | 'immediate';
4048
+
3812
4049
  function makeShaderDeviceAdapter(
3813
4050
  rhiDevice: RhiDevice,
3814
4051
  _errorRegistry: RhiErrorListenerRegistry,
@@ -3818,10 +4055,18 @@ function makeShaderDeviceAdapter(
3818
4055
  desc: { code: string; label?: string | undefined },
3819
4056
  ) => Promise<Result<ShaderModule, RhiError>>)
3820
4057
  | undefined,
4058
+ immediateCreateShaderModule:
4059
+ | ((
4060
+ device: RhiDevice,
4061
+ desc: { code: string; label?: string | undefined },
4062
+ ) => Result<ShaderModule, RhiError>)
4063
+ | undefined,
4064
+ mode: ShaderModuleAdapterMode = 'validated',
3821
4065
  ): ShaderDeviceAdapterInternal {
3822
4066
  const moduleCache = new Map<string, ShaderModule>();
3823
4067
  const errorCache = new Map<string, RhiError>();
3824
- const pending = new Set<string>();
4068
+ const pending = new Map<string, number>();
4069
+ const epochs = new Map<string, number>();
3825
4070
 
3826
4071
  return {
3827
4072
  createShaderModule(desc): Result<ShaderModule, RhiError> {
@@ -3831,18 +4076,39 @@ function makeShaderDeviceAdapter(
3831
4076
  const cachedError = errorCache.get(key);
3832
4077
  if (cachedError !== undefined) return err(cachedError);
3833
4078
 
4079
+ // WebGPU's raw createShaderModule is synchronous. Only the explicitly
4080
+ // selected immediate adapter uses that entry: generated feature programs
4081
+ // can avoid a slow getCompilationInfo() round trip, while the default
4082
+ // validated adapter keeps the compiler-readiness barrier for ordinary
4083
+ // render pipelines. The public async factory still owns diagnostics;
4084
+ // pipeline creation remains the validation point for the fast path.
4085
+ if (mode === 'immediate' && immediateCreateShaderModule !== undefined) {
4086
+ const immediateResult = immediateCreateShaderModule(rhiDevice, {
4087
+ code: desc.code,
4088
+ ...(desc.label === undefined ? {} : { label: desc.label }),
4089
+ });
4090
+ if (immediateResult.ok) {
4091
+ moduleCache.set(key, immediateResult.value);
4092
+ return immediateResult;
4093
+ }
4094
+ errorCache.set(key, immediateResult.error);
4095
+ return immediateResult;
4096
+ }
4097
+
3834
4098
  // Fire async creation; first sync call returns pending error (the AI consumer retry pattern).
3835
4099
  // M3 D-P4: rhi-webgpu supplies the async factory; rhi-wgpu (no top-level
3836
4100
  // async factory) falls back to the synchronous device.createShaderModule
3837
4101
  // entry which already returns Result<ShaderModule, RhiError>.
3838
4102
  if (!pending.has(key)) {
3839
- pending.add(key);
4103
+ const epoch = epochs.get(key) ?? 0;
4104
+ pending.set(key, epoch);
3840
4105
  const desc2: { code: string; label?: string | undefined } = { code: desc.code };
3841
4106
  if (desc.label !== undefined) desc2.label = desc.label;
3842
4107
  const asyncResult: Promise<Result<ShaderModule, RhiError>> = asyncCreateShaderModule
3843
4108
  ? asyncCreateShaderModule(rhiDevice, desc2)
3844
4109
  : invokeDeviceCreateShaderModule(rhiDevice, desc2);
3845
4110
  void asyncResult.then((result) => {
4111
+ if (pending.get(key) !== epoch || (epochs.get(key) ?? 0) !== epoch) return;
3846
4112
  if (result.ok) {
3847
4113
  moduleCache.set(key, result.value);
3848
4114
  } else {
@@ -3857,6 +4123,12 @@ function makeShaderDeviceAdapter(
3857
4123
  seedModule(label: string, module: ShaderModule): void {
3858
4124
  moduleCache.set(label, module);
3859
4125
  },
4126
+ invalidateModule(label: string): void {
4127
+ epochs.set(label, (epochs.get(label) ?? 0) + 1);
4128
+ moduleCache.delete(label);
4129
+ errorCache.delete(label);
4130
+ pending.delete(label);
4131
+ },
3860
4132
  };
3861
4133
  }
3862
4134
 
@@ -4102,7 +4374,11 @@ const HDR_COLOR_ATTACHMENT_FORMAT: GPUTextureFormat = 'rgba16float';
4102
4374
  // so it is intentionally absent here. A `mipmap:true` texture in an
4103
4375
  // un-prewarmed format surfaces a structured RhiError at record (never an
4104
4376
  // async stall) -- the falsifiable anchor for an incomplete prewarm list.
4105
- const MIPMAP_PREWARM_FORMATS: readonly GPUTextureFormat[] = ['rgba8unorm-srgb', 'rgba8unorm'];
4377
+ const MIPMAP_PREWARM_FORMATS: readonly GPUTextureFormat[] = [
4378
+ 'rgba8unorm-srgb',
4379
+ 'rgba8unorm',
4380
+ 'rgba16float',
4381
+ ];
4106
4382
 
4107
4383
  // feat-20260531-bloom-first-declarative-render-graph-pass / w13:
4108
4384
  // UBO sizes for bloom params (std140 layout).
@@ -4516,6 +4792,13 @@ async function prepareMaterialShaders(
4516
4792
  const variantDefines: Record<string, boolean> = {
4517
4793
  STORAGE_BUFFER_AVAILABLE: storageBufferCapable,
4518
4794
  };
4795
+ // User material manifests may declare WEBGL2_COMPAT alongside the shared
4796
+ // mesh-storage axis. The fallback backend must select that authored
4797
+ // branch at boot; otherwise it asks for the impossible bare
4798
+ // `STORAGE_BUFFER_AVAILABLE=false` key and fails before the first frame.
4799
+ if (msEntry.variants.some((v) => 'WEBGL2_COMPAT' in v.defines)) {
4800
+ variantDefines.WEBGL2_COMPAT = webgl2Downlevel;
4801
+ }
4519
4802
  // Only include CLUSTER_FORWARD_AVAILABLE when the material shader declares
4520
4803
  // the axis (has a variant with this key), preserving backward compat for
4521
4804
  // entries without the pragma.
@@ -4525,6 +4808,12 @@ async function prepareMaterialShaders(
4525
4808
  if (msEntry.variants.some((v) => 'VERTEX_COLOR_AVAILABLE' in v.defines)) {
4526
4809
  variantDefines.VERTEX_COLOR_AVAILABLE = false;
4527
4810
  }
4811
+ if (
4812
+ msEntry.identifier === 'forgeax::default-standard-pbr' &&
4813
+ msEntry.variants.some((v) => 'TRANSMISSION_AVAILABLE' in v.defines)
4814
+ ) {
4815
+ variantDefines.TRANSMISSION_AVAILABLE = false;
4816
+ }
4528
4817
  // bug-20260708 M2 (a): PER_INSTANCE_REGION axis default = false at boot.
4529
4818
  // Per-entity spritePH path (`main-pass-sprite-draws.ts:511-513`) expects
4530
4819
  // `variantSet=undefined` to resolve to the PIR=false shader source (64B
@@ -4539,6 +4828,9 @@ async function prepareMaterialShaders(
4539
4828
  if (msEntry.variants.some((v) => 'PER_INSTANCE_REGION' in v.defines)) {
4540
4829
  variantDefines.PER_INSTANCE_REGION = false;
4541
4830
  }
4831
+ if (msEntry.variants.some((v) => 'SKINNING_DISABLED' in v.defines)) {
4832
+ variantDefines.SKINNING_DISABLED = true;
4833
+ }
4542
4834
  const sortedEntries = Object.entries(variantDefines).sort(([a], [b]) =>
4543
4835
  a < b ? -1 : a > b ? 1 : 0,
4544
4836
  );
@@ -4546,6 +4838,13 @@ async function prepareMaterialShaders(
4546
4838
  ? ''
4547
4839
  : sortedEntries.map(([k, v]) => `${k}=${v}`).join('+');
4548
4840
  const chosen = findVariantByKey(msEntry, definesKey);
4841
+ if (msEntry.variants.length > 0 && chosen === undefined) {
4842
+ throw new RhiError({
4843
+ code: 'shader-compile-failed',
4844
+ expected: `material shader '${msEntry.identifier}' declares the exact boot variant '${definesKey}'`,
4845
+ hint: 'regenerate the shader manifest with the complete variant Cartesian product',
4846
+ });
4847
+ }
4549
4848
  const wgsl = chosen?.composedWgsl ?? msEntry.composedWgsl;
4550
4849
  if (wgsl.length > 0) {
4551
4850
  const existing = registry.findMaterialArtifact(msEntry.identifier);
@@ -4627,6 +4926,7 @@ async function prepareMaterialShaders(
4627
4926
 
4628
4927
  async function buildReadyWebGPU(
4629
4928
  rhiDevice: RhiDevice,
4929
+ rendererScope: DeviceScope,
4630
4930
  getShader: () => ShaderCatalog,
4631
4931
  gpuStore: GpuResidencyCache,
4632
4932
  asyncCreateShaderModule:
@@ -4680,6 +4980,10 @@ async function buildReadyWebGPU(
4680
4980
  // tonemap source resolves inside this async function (after the manifest-load
4681
4981
  // await), by which point the synchronous `renderSystem` const is defined.
4682
4982
  registerBuiltinTonemap: (source: string) => void,
4983
+ registerBuiltinTemporalPostProcesses: (entries: {
4984
+ readonly motionBlur?: string;
4985
+ readonly taaResolve?: string;
4986
+ }) => void,
4683
4987
  ): Promise<PipelineState> {
4684
4988
  // bug-20260610: WebGL2 fallback path — read storage-buffer capability up
4685
4989
  // front so both Step 2 (shader-module compile, where we patch engine
@@ -4706,7 +5010,10 @@ async function buildReadyWebGPU(
4706
5010
  // pipeline target, configure() call, and color-attachment format —
4707
5011
  // single SSOT, no scattered branches. See selectSwapChainFormat
4708
5012
  // (above the SWAP_CHAIN_*_FORMAT historical constants).
4709
- const swapChainFormats = selectSwapChainFormat(storageBufferCapable);
5013
+ const surfaceViewFormats =
5014
+ (rhiDevice as unknown as { readonly surfaceViewFormats?: boolean }).surfaceViewFormats ??
5015
+ !webgl2Downlevel;
5016
+ const swapChainFormats = selectSwapChainFormat(storageBufferCapable, surfaceViewFormats);
4710
5017
  // The 'null' (headless RhiNull) backend has no UA preferred-canvas-format by
4711
5018
  // design; the rgba8unorm fallback is its intended steady state, not a
4712
5019
  // degraded one — firing 'rhi-not-available' there is territorially wrong
@@ -4811,6 +5118,7 @@ async function buildReadyWebGPU(
4811
5118
  let pbrEntry = findEngineManifestEntry('forgeax::default-standard-pbr');
4812
5119
  let unlitEntry = findEngineManifestEntry('forgeax::default-unlit');
4813
5120
  let tonemapEntry: ManifestEntry | undefined;
5121
+ let motionBlurEntry: ManifestEntry | undefined;
4814
5122
  let spriteEntry: ManifestEntry | undefined;
4815
5123
  // sprite-lit identification marker is the `spriteLitShadeAccum`
4816
5124
  // helper (defined in sprite-lit.wgsl, absent from sprite.wgsl). The
@@ -4820,6 +5128,7 @@ async function buildReadyWebGPU(
4820
5128
  // their paramSchema mirror) does not mis-classify sprite-lit as
4821
5129
  // sprite.
4822
5130
  let spriteLitEntry: ManifestEntry | undefined;
5131
+ let taaResolveEntry: ManifestEntry | undefined;
4823
5132
  let iblEquirectEntry: ManifestEntry | undefined;
4824
5133
  let iblIrradianceEntry: ManifestEntry | undefined;
4825
5134
  let iblPrefilterEntry: ManifestEntry | undefined;
@@ -4844,6 +5153,14 @@ async function buildReadyWebGPU(
4844
5153
  tonemapEntry ??= entry;
4845
5154
  continue;
4846
5155
  }
5156
+ if (entry.wgsl.includes('MotionBlurParams')) {
5157
+ motionBlurEntry ??= entry;
5158
+ continue;
5159
+ }
5160
+ if (entry.wgsl.includes('fs_taa_resolve')) {
5161
+ taaResolveEntry ??= entry;
5162
+ continue;
5163
+ }
4847
5164
  if (entry.wgsl.includes('spriteLitShadeAccum')) {
4848
5165
  // sprite-lit identification — must precede the `pivotAndSize`
4849
5166
  // marker since sprite-lit also carries that field (paramSchema
@@ -4985,10 +5302,10 @@ async function buildReadyWebGPU(
4985
5302
  (!('CLUSTER_FORWARD_AVAILABLE' in v.defines) ||
4986
5303
  v.defines.CLUSTER_FORWARD_AVAILABLE === false) &&
4987
5304
  (!('VERTEX_COLOR_AVAILABLE' in v.defines) ||
4988
- v.defines.VERTEX_COLOR_AVAILABLE === false),
5305
+ v.defines.VERTEX_COLOR_AVAILABLE === false) &&
5306
+ (!('SKINNING_DISABLED' in v.defines) || v.defines.SKINNING_DISABLED === true),
4989
5307
  );
4990
- const v =
4991
- exact ?? ms.variants.find((cand) => cand.defines.STORAGE_BUFFER_AVAILABLE === false);
5308
+ const v = exact === undefined ? undefined : findVariantByKey(ms, exact.definesKey);
4992
5309
  if (v !== undefined) {
4993
5310
  idToVariantWgsl.set(ms.identifier, v.composedWgsl);
4994
5311
  }
@@ -5028,13 +5345,13 @@ async function buildReadyWebGPU(
5028
5345
  (!('CLUSTER_FORWARD_AVAILABLE' in variant.defines) ||
5029
5346
  variant.defines.CLUSTER_FORWARD_AVAILABLE === false) &&
5030
5347
  (!('VERTEX_COLOR_AVAILABLE' in variant.defines) ||
5031
- variant.defines.VERTEX_COLOR_AVAILABLE === false),
5348
+ variant.defines.VERTEX_COLOR_AVAILABLE === false) &&
5349
+ (!('SKINNING_DISABLED' in variant.defines) ||
5350
+ variant.defines.SKINNING_DISABLED === true) &&
5351
+ (!('TRANSMISSION_AVAILABLE' in variant.defines) ||
5352
+ variant.defines.TRANSMISSION_AVAILABLE === false),
5032
5353
  );
5033
- const variant =
5034
- exact ??
5035
- ms.variants.find(
5036
- (candidate) => candidate.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable,
5037
- );
5354
+ const variant = exact === undefined ? undefined : findVariantByKey(ms, exact.definesKey);
5038
5355
  if (variant !== undefined) {
5039
5356
  idToRuntimeVariantWgsl.set(ms.identifier, variant.composedWgsl);
5040
5357
  }
@@ -5190,6 +5507,12 @@ async function buildReadyWebGPU(
5190
5507
  if (pbrVariant !== undefined) {
5191
5508
  seedShaderModule(`module-forgeax::default-standard-pbr#${pbrVariant.definesKey}`, pbrModule);
5192
5509
  }
5510
+ const transmissionCapable =
5511
+ (rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
5512
+ STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
5513
+ const transmissionVariants = transmissionCapable
5514
+ ? selectStandardPbrTransmissionPrewarmVariants(pbrManifestEntry, storageBufferCapable)
5515
+ : [];
5193
5516
 
5194
5517
  // HDRP uses the canonical all-true variant key (`''`) and the lazy
5195
5518
  // material pipeline adapter therefore requests a distinct module label.
@@ -5197,9 +5520,35 @@ async function buildReadyWebGPU(
5197
5520
  // label. Color availability is part of the geometry-owned variant axis, so
5198
5521
  // both colored and no-color HDRP sources must be retained.
5199
5522
  const prewarmedHdrpModules = new Map<string, ShaderModule>([[pbrEntry.wgsl, pbrModule]]);
5523
+ for (const transmissionVariant of transmissionVariants) {
5524
+ const moduleLabel = `module-forgeax::default-standard-pbr#${transmissionVariant.definesKey}`;
5525
+ let transmissionModule = prewarmedHdrpModules.get(transmissionVariant.composedWgsl);
5526
+ if (transmissionModule === undefined) {
5527
+ const transmissionModuleResult = await runShimStep(
5528
+ () =>
5529
+ asyncCreateShaderModule
5530
+ ? asyncCreateShaderModule(rhiDevice, {
5531
+ code: transmissionVariant.composedWgsl,
5532
+ label: moduleLabel,
5533
+ })
5534
+ : invokeDeviceCreateShaderModule(rhiDevice, {
5535
+ code: transmissionVariant.composedWgsl,
5536
+ label: moduleLabel,
5537
+ }),
5538
+ 'shader-compile-failed',
5539
+ 'Standard transmission shader module compiled',
5540
+ 'inspect the composed Standard transmission variant WGSL and check device.features',
5541
+ );
5542
+ if (!transmissionModuleResult.ok) throw transmissionModuleResult.error;
5543
+ transmissionModule = transmissionModuleResult.value;
5544
+ prewarmedHdrpModules.set(transmissionVariant.composedWgsl, transmissionModule);
5545
+ }
5546
+ seedShaderModule(moduleLabel, transmissionModule);
5547
+ }
5200
5548
  for (const hdrpVariant of selectHdrpPbrPrewarmVariants(
5201
5549
  pbrManifestEntry,
5202
5550
  storageBufferCapable,
5551
+ transmissionCapable,
5203
5552
  )) {
5204
5553
  const moduleLabel = `module-forgeax::default-standard-pbr#${hdrpVariant.definesKey}`;
5205
5554
  let hdrpModule = prewarmedHdrpModules.get(hdrpVariant.composedWgsl);
@@ -5335,6 +5684,49 @@ async function buildReadyWebGPU(
5335
5684
  // uses so the lazy build's createShaderModule call hits OK on
5336
5685
  // frame 1.
5337
5686
  seedShaderModule('module-forgeax::default-shadow-caster', shadowCasterShaderResult.value);
5687
+
5688
+ // The shadow caster now has independent storage and skinning axes. The
5689
+ // canonical all-true entry remains the ordinary static source through
5690
+ // SKINNING_DISABLED=true; skinned meshes request the explicit false
5691
+ // variant. Prewarm both for the active storage capability so neither
5692
+ // path loses its first shadow frame to the lazy module retry contract.
5693
+ const shadowCasterManifestEntry = Array.from(registry.materialShaderManifestEntries()).find(
5694
+ (candidate) => candidate.identifier === 'forgeax::default-shadow-caster',
5695
+ );
5696
+ const activeShadowVariants = shadowCasterManifestEntry?.variants.filter(
5697
+ (variant) =>
5698
+ variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
5699
+ typeof variant.defines.SKINNING_DISABLED === 'boolean',
5700
+ );
5701
+ for (const variant of activeShadowVariants ?? []) {
5702
+ if (variant.definesKey === '') {
5703
+ seedShaderModule(
5704
+ 'module-forgeax::default-shadow-caster#',
5705
+ shadowCasterShaderResult.value,
5706
+ );
5707
+ continue;
5708
+ }
5709
+ const variantShaderResult = await runShimStep(
5710
+ () =>
5711
+ asyncCreateShaderModule
5712
+ ? asyncCreateShaderModule(rhiDevice, {
5713
+ code: variant.composedWgsl,
5714
+ label: `shadow_caster#${variant.definesKey}`,
5715
+ })
5716
+ : invokeDeviceCreateShaderModule(rhiDevice, {
5717
+ code: variant.composedWgsl,
5718
+ label: `shadow_caster#${variant.definesKey}`,
5719
+ }),
5720
+ 'shader-compile-failed',
5721
+ `shadow_caster variant ${variant.definesKey || '<default>'} compiled`,
5722
+ 'inspect the selected shadow_caster storage/skinning variant and device.features',
5723
+ );
5724
+ if (!variantShaderResult.ok) throw variantShaderResult.error;
5725
+ seedShaderModule(
5726
+ `module-forgeax::default-shadow-caster#${variant.definesKey}`,
5727
+ variantShaderResult.value,
5728
+ );
5729
+ }
5338
5730
  }
5339
5731
 
5340
5732
  // feat-20260621 M-A3 (D-5): register the built-in tonemap onto the unified
@@ -5354,27 +5746,62 @@ async function buildReadyWebGPU(
5354
5746
  // forever. Mirror the shadow_caster prewarm: await-compile the tonemap module
5355
5747
  // here and `seedShaderModule` it under the exact label
5356
5748
  // `buildPostProcessPipeline` requests (`post-process-${id}-module`), so the
5357
- // first `getPostProcessPipeline(STANDARD_TONEMAP_FEATURE_ID, …)` hits the module cache
5749
+ // first `getPostProcessPipeline(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, …)` hits the module cache
5358
5750
  // synchronously and builds the pipeline on frame 1.
5359
5751
  const tonemapPrewarm = await runShimStep(
5360
5752
  () =>
5361
5753
  asyncCreateShaderModule
5362
5754
  ? asyncCreateShaderModule(rhiDevice, {
5363
5755
  code: tonemapEntry.wgsl,
5364
- label: `post-process-${STANDARD_TONEMAP_FEATURE_ID}-module`,
5756
+ label: `post-process-${STANDARD_OUTPUT_TRANSFORM_FEATURE_ID}-module`,
5365
5757
  })
5366
5758
  : invokeDeviceCreateShaderModule(rhiDevice, {
5367
5759
  code: tonemapEntry.wgsl,
5368
- label: `post-process-${STANDARD_TONEMAP_FEATURE_ID}-module`,
5760
+ label: `post-process-${STANDARD_OUTPUT_TRANSFORM_FEATURE_ID}-module`,
5369
5761
  }),
5370
5762
  'shader-compile-failed',
5371
5763
  'tonemap shader module compiled (unified post-process prewarm)',
5372
5764
  'inspect manifest tonemap entry composed wgsl; check device.features',
5373
5765
  );
5374
5766
  if (!tonemapPrewarm.ok) throw tonemapPrewarm.error;
5375
- seedShaderModule(`post-process-${STANDARD_TONEMAP_FEATURE_ID}-module`, tonemapPrewarm.value);
5767
+ seedShaderModule(
5768
+ `post-process-${STANDARD_OUTPUT_TRANSFORM_FEATURE_ID}-module`,
5769
+ tonemapPrewarm.value,
5770
+ );
5376
5771
  registerBuiltinTonemap(tonemapEntry.wgsl);
5377
5772
 
5773
+ // Temporal fullscreen passes execute synchronously from the compiled graph,
5774
+ // so their shader modules must be warmed before the initialization barrier
5775
+ // resolves. Otherwise the first tight draw loop sees an asynchronous
5776
+ // `rhi-not-available` result and leaves the temporal targets cleared even
5777
+ // though the graph topology is present.
5778
+ const prewarmTemporal = async (entry: ManifestEntry | undefined, id: string) => {
5779
+ if (entry === undefined) return;
5780
+ const prewarm = await runShimStep(
5781
+ () =>
5782
+ asyncCreateShaderModule
5783
+ ? asyncCreateShaderModule(rhiDevice, {
5784
+ code: entry.wgsl,
5785
+ label: `post-process-${id}-module`,
5786
+ })
5787
+ : invokeDeviceCreateShaderModule(rhiDevice, {
5788
+ code: entry.wgsl,
5789
+ label: `post-process-${id}-module`,
5790
+ }),
5791
+ 'shader-compile-failed',
5792
+ `${id} shader module compiled (unified post-process prewarm)`,
5793
+ `inspect manifest ${id} entry composed wgsl; check device.features`,
5794
+ );
5795
+ if (!prewarm.ok) throw prewarm.error;
5796
+ seedShaderModule(`post-process-${id}-module`, prewarm.value);
5797
+ };
5798
+ await prewarmTemporal(motionBlurEntry, 'forgeax.motion-blur');
5799
+ await prewarmTemporal(taaResolveEntry, 'forgeax.taa-resolve');
5800
+ registerBuiltinTemporalPostProcesses({
5801
+ ...(motionBlurEntry === undefined ? {} : { motionBlur: motionBlurEntry.wgsl }),
5802
+ ...(taaResolveEntry === undefined ? {} : { taaResolve: taaResolveEntry.wgsl }),
5803
+ });
5804
+
5378
5805
  // feat-20260520-2d-sprite-layer-mvp M-3 / w24: sprite shader module
5379
5806
  // is optional in the 3-tuple legacy manifest (back-compat for apps
5380
5807
  // that locked their manifest URL before this feat). With the
@@ -5410,6 +5837,36 @@ async function buildReadyWebGPU(
5410
5837
  // shares the generic per-MaterialShader lazy build path, which now
5411
5838
  // needs the explicit seed to preserve frame-1 readiness.
5412
5839
  seedShaderModule('module-forgeax::sprite', spriteShaderResult.value);
5840
+ const spriteManifestEntry = Array.from(registry.materialShaderManifestEntries()).find(
5841
+ (entry) => entry.identifier === 'forgeax::sprite',
5842
+ );
5843
+ const regionVariant = spriteManifestEntry?.variants.find(
5844
+ (variant) =>
5845
+ variant.defines.PER_INSTANCE_REGION === true &&
5846
+ variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable,
5847
+ );
5848
+ if (regionVariant !== undefined) {
5849
+ const regionShaderResult = await runShimStep(
5850
+ () =>
5851
+ asyncCreateShaderModule
5852
+ ? asyncCreateShaderModule(rhiDevice, {
5853
+ code: regionVariant.composedWgsl,
5854
+ label: `sprite#${regionVariant.definesKey}`,
5855
+ })
5856
+ : invokeDeviceCreateShaderModule(rhiDevice, {
5857
+ code: regionVariant.composedWgsl,
5858
+ label: `sprite#${regionVariant.definesKey}`,
5859
+ }),
5860
+ 'shader-compile-failed',
5861
+ `sprite variant ${regionVariant.definesKey || '<default>'} compiled`,
5862
+ 'inspect the selected sprite PER_INSTANCE_REGION variant and device.features',
5863
+ );
5864
+ if (!regionShaderResult.ok) throw regionShaderResult.error;
5865
+ seedShaderModule(
5866
+ `module-forgeax::sprite#${regionVariant.definesKey}`,
5867
+ regionShaderResult.value,
5868
+ );
5869
+ }
5413
5870
  }
5414
5871
 
5415
5872
  // feat-20260624-sprite-lit-shading-model-pure-2d-lighting M1' / t7:
@@ -5482,15 +5939,12 @@ async function buildReadyWebGPU(
5482
5939
  skyboxModule = skyboxShaderResult.value;
5483
5940
  }
5484
5941
 
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.
5942
+ // Bloom shader modules remain available to the lazy Bloom pipeline owner.
5943
+ // Their construction is still part of renderer readiness because the
5944
+ // backend may expose only an asynchronous module factory; the expensive
5945
+ // pipeline, binding, and parameter resources stay deferred until admission.
5492
5946
  if (bloomBrightEntry !== undefined) {
5493
- const brightShaderResult = await runShimStep(
5947
+ const result = await runShimStep(
5494
5948
  () =>
5495
5949
  asyncCreateShaderModule
5496
5950
  ? asyncCreateShaderModule(rhiDevice, {
@@ -5505,17 +5959,14 @@ async function buildReadyWebGPU(
5505
5959
  'bloom-bright shader module compiled',
5506
5960
  'inspect manifest bloom-bright entry composed wgsl; check device.features',
5507
5961
  );
5508
- if (!brightShaderResult.ok) throw brightShaderResult.error;
5509
- bloomBrightModule = brightShaderResult.value;
5962
+ if (!result.ok) throw result.error;
5963
+ bloomBrightModule = result.value;
5510
5964
  }
5511
5965
  if (bloomBlurEntry !== undefined) {
5512
- const blurShaderResult = await runShimStep(
5966
+ const result = await runShimStep(
5513
5967
  () =>
5514
5968
  asyncCreateShaderModule
5515
- ? asyncCreateShaderModule(rhiDevice, {
5516
- code: bloomBlurEntry.wgsl,
5517
- label: 'bloom-blur',
5518
- })
5969
+ ? asyncCreateShaderModule(rhiDevice, { code: bloomBlurEntry.wgsl, label: 'bloom-blur' })
5519
5970
  : invokeDeviceCreateShaderModule(rhiDevice, {
5520
5971
  code: bloomBlurEntry.wgsl,
5521
5972
  label: 'bloom-blur',
@@ -5524,11 +5975,11 @@ async function buildReadyWebGPU(
5524
5975
  'bloom-blur shader module compiled',
5525
5976
  'inspect manifest bloom-blur entry composed wgsl; check device.features',
5526
5977
  );
5527
- if (!blurShaderResult.ok) throw blurShaderResult.error;
5528
- bloomBlurModule = blurShaderResult.value;
5978
+ if (!result.ok) throw result.error;
5979
+ bloomBlurModule = result.value;
5529
5980
  }
5530
5981
  if (bloomCompositeEntry !== undefined) {
5531
- const compositeShaderResult = await runShimStep(
5982
+ const result = await runShimStep(
5532
5983
  () =>
5533
5984
  asyncCreateShaderModule
5534
5985
  ? asyncCreateShaderModule(rhiDevice, {
@@ -5543,8 +5994,8 @@ async function buildReadyWebGPU(
5543
5994
  'bloom-composite shader module compiled',
5544
5995
  'inspect manifest bloom-composite entry composed wgsl; check device.features',
5545
5996
  );
5546
- if (!compositeShaderResult.ok) throw compositeShaderResult.error;
5547
- bloomCompositeModule = compositeShaderResult.value;
5997
+ if (!result.ok) throw result.error;
5998
+ bloomCompositeModule = result.value;
5548
5999
  }
5549
6000
  // feat-20260612-hdrp-ssao M6 / w27: SSAO shader module compilation.
5550
6001
  // Optional manifest entry — absent on legacy manifests (zero-overhead
@@ -5576,11 +6027,11 @@ async function buildReadyWebGPU(
5576
6027
  // The factory builds 4 BindGroupLayouts (view + material + mesh-array +
5577
6028
  // instances) and the 4-slot pipeline layout in one call, throwing on each
5578
6029
  // device.createBindGroupLayout / device.createPipelineLayout failure.
5579
- // material BGL now carries 14 entries (material 0..6 + Skylight 7..13 via
6030
+ // material BGL now carries 24 entries (material 0..16 + Skylight 17..23 via
5580
6031
  // mergeSkylightIntoMaterialBgl) -- pipeline layout itself stays at 4 slots,
5581
6032
  // no @group(4) allocated (D-5 round-4 fix for the round-2 maxBindGroups=4
5582
6033
  // BLOCKER). The unlit pipeline currently shares this 14-entry layout
5583
- // (carrying fallback identity at binding 7..13 from the record stage);
6034
+ // (carrying fallback identity at binding 17..23 from the record stage);
5584
6035
  // future split via `buildUnlitMaterialBgl` is exposed for tests.
5585
6036
  //
5586
6037
  // feat-20260520-directional-light-shadow-mapping merge: view BGL entries
@@ -5614,7 +6065,10 @@ async function buildReadyWebGPU(
5614
6065
  // (URP layout) so the manifest entry's URP variant WGSL still builds a
5615
6066
  // valid PSO instead of hard-disabling the entire HDRP-variant build path.
5616
6067
  let hdrpPbrPipelineLayoutHandle: PipelineLayout | null = null;
5617
- {
6068
+ // The cluster layout is storage-buffer backed. On the WebGL2/fallback
6069
+ // capability tier its uniform-buffer projection exceeds the backend's
6070
+ // fragment-stage limit, and HDRP is not a valid route there anyway.
6071
+ if (storageBufferCapable) {
5618
6072
  const hdrpUnifiedBglDesc = createHdrpBindGroupLayoutDescriptor(storageBufferCapable);
5619
6073
  const hdrpUnifiedBglRes = rhiDevice.createBindGroupLayout(hdrpUnifiedBglDesc);
5620
6074
  if (hdrpUnifiedBglRes.ok) {
@@ -6922,6 +7376,59 @@ async function buildReadyWebGPU(
6922
7376
  let bloomBlurHParamsBufferHandle: Buffer | null = null;
6923
7377
  let bloomBlurVParamsBufferHandle: Buffer | null = null;
6924
7378
  let bloomCompositeParamsBufferHandle: Buffer | null = null;
7379
+ let bloomResourcesReady = false;
7380
+ let ensureBloomResources: (() => void) | undefined;
7381
+ let bloomActiveBundle: BloomPersistentBundle | undefined;
7382
+ let bloomCandidateBundle: BloomPersistentBundle | undefined;
7383
+ const retiringBloomBundles = new Set<BloomPersistentBundle>();
7384
+ let bloomGeneration = 0;
7385
+ let lastSuccessfulBloomReceipts:
7386
+ | import('../record/frame-snapshot').BloomFrameReceipts
7387
+ | undefined;
7388
+ const getBloomResourcesStable = (): BloomPersistentBundle | null =>
7389
+ bloomCandidateBundle ?? bloomActiveBundle ?? null;
7390
+ let commitBloomResources: (() => void) | undefined;
7391
+ let discardBloomResources: (() => void) | undefined;
7392
+ let retireBloomResources: ((completion: Promise<unknown>) => void) | undefined;
7393
+ let drainBloomResources: (() => void) | undefined;
7394
+ let inspectBloomResources:
7395
+ | ((graph?: import('@forgeax/engine-render-graph').CompiledRenderGraphInfo) => BloomInspection)
7396
+ | undefined;
7397
+ const ensureBloomResourcesStable = (): void => {
7398
+ ensureBloomResources?.();
7399
+ };
7400
+ const retireBloomResourcesStable = (completion: Promise<unknown>): void => {
7401
+ retireBloomResources?.(completion);
7402
+ };
7403
+ const commitBloomResourcesStable = (): void => {
7404
+ commitBloomResources?.();
7405
+ };
7406
+ const discardBloomResourcesStable = (): void => {
7407
+ discardBloomResources?.();
7408
+ };
7409
+ const drainBloomResourcesStable = (): void => {
7410
+ drainBloomResources?.();
7411
+ };
7412
+ const inspectBloomResourcesStable = (
7413
+ graph?: import('@forgeax/engine-render-graph').CompiledRenderGraphInfo,
7414
+ ): BloomInspection => {
7415
+ return (
7416
+ inspectBloomResources?.(graph) ?? {
7417
+ graphStatus: 'empty',
7418
+ enabled: false,
7419
+ targetCount: 0,
7420
+ targetBytes: 0,
7421
+ resourceCount: 0,
7422
+ passCount: 0,
7423
+ encodeCount: 0,
7424
+ bindGroupCount: 0,
7425
+ uploadCount: 0,
7426
+ residentChildBytes: 0,
7427
+ generation: 0,
7428
+ state: 'off',
7429
+ }
7430
+ );
7431
+ };
6925
7432
  // feat-20260612-hdrp-ssao M6 / w26 + w43: SSAO pipeline handles (D-A).
6926
7433
  // calc + blur RenderPipeline pair sharing a dedicated 6-entry BGL.
6927
7434
  // Optional — null when manifest lacks hdrp-ssao entry.
@@ -7222,361 +7729,560 @@ async function buildReadyWebGPU(
7222
7729
  // bloom-blur-v : same BGL, same module as blur-h, V-axis texelSize
7223
7730
  // bloom-composite: 2-tex + sampler + UBO@3 BGL, rgba16float target
7224
7731
 
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
- }
7732
+ ensureBloomResources = (): void => {
7733
+ if (bloomResourcesReady) return;
7734
+ const candidateScope = rendererScope.createChild(`${rendererScope.owner}:standard-bloom`);
7735
+ const adopt = <T>(
7736
+ kind: Parameters<DeviceScope['_adopt']>[0],
7737
+ value: T,
7738
+ cleanup: (value: T) => void,
7739
+ ): T => {
7740
+ candidateScope._adopt(kind, value, cleanup);
7741
+ return value;
7742
+ };
7743
+ try {
7744
+ // Shared bloom sampler: linear filter + clamp-to-edge (all 4 passes
7745
+ // sample from textures using fullscreen triangle UVs).
7746
+ if (
7747
+ bloomBrightModule !== null ||
7748
+ bloomBlurModule !== null ||
7749
+ bloomCompositeModule !== null
7750
+ ) {
7751
+ const bloomSamplerResult = runShimSyncStep(
7752
+ () =>
7753
+ rhiDevice.createSampler({
7754
+ label: 'bloom-sampler',
7755
+ magFilter: 'linear',
7756
+ minFilter: 'linear',
7757
+ mipmapFilter: 'linear',
7758
+ addressModeU: 'clamp-to-edge',
7759
+ addressModeV: 'clamp-to-edge',
7760
+ }),
7761
+ 'webgpu-runtime-error',
7762
+ 'createSampler (bloom) succeeded',
7763
+ 'check device.limits.maxSamplersPerShaderStage',
7764
+ );
7765
+ if (!bloomSamplerResult.ok) throw bloomSamplerResult.error;
7766
+ bloomSamplerHandle = adopt('binding', bloomSamplerResult.value, () => undefined);
7767
+ }
7245
7768
 
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' },
7769
+ // Bloom bright: 1-tex + sampler + UBO@2 BGL (D-4).
7770
+ if (bloomBrightModule !== null && bloomSamplerHandle !== null) {
7771
+ const brightBglResult = runShimSyncStep(
7772
+ () =>
7773
+ rhiDevice.createBindGroupLayout({
7774
+ label: 'bloom-bright-bgl',
7775
+ entries: [
7776
+ {
7777
+ binding: 0,
7778
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
7779
+ texture: { sampleType: 'float', viewDimension: '2d' },
7780
+ },
7781
+ {
7782
+ binding: 1,
7783
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
7784
+ sampler: { type: 'filtering' },
7785
+ },
7786
+ {
7787
+ binding: 2,
7788
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
7789
+ buffer: { type: 'uniform' },
7790
+ },
7791
+ ],
7792
+ }),
7793
+ 'webgpu-runtime-error',
7794
+ 'createBindGroupLayout(bloom-bright) succeeded',
7795
+ 'check device.limits.maxBindGroupsPerPipelineLayout',
7796
+ );
7797
+ if (!brightBglResult.ok) throw brightBglResult.error;
7798
+ bloomBrightBglHandle = adopt('binding', brightBglResult.value, () => undefined);
7799
+
7800
+ const brightPlResult = runShimSyncStep(
7801
+ () =>
7802
+ rhiDevice.createPipelineLayout({
7803
+ label: 'bloom-bright-pl',
7804
+ bindGroupLayouts: [brightBglResult.value],
7805
+ }),
7806
+ 'webgpu-runtime-error',
7807
+ 'createPipelineLayout(bloom-bright) succeeded',
7808
+ 'verify bloom-bright BindGroupLayout matches shader @group(0) bindings',
7809
+ );
7810
+ if (!brightPlResult.ok) throw brightPlResult.error;
7811
+ adopt('binding', brightPlResult.value, () => undefined);
7812
+
7813
+ // M2-T4: bloom-bright pipeline via getOrBuildPipeline (lazy-build).
7814
+ {
7815
+ const brightSpec: PipelineSpec = {
7816
+ shader: {
7817
+ id: 'forgeax::post::bloom-bright',
7818
+ passKind: 'post-process',
7819
+ variantSet: undefined,
7257
7820
  },
7258
- {
7259
- binding: 1,
7260
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7261
- sampler: { type: 'filtering' },
7821
+ attachments: {
7822
+ colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
7823
+ depthFormat: undefined,
7824
+ sampleCount: 1,
7262
7825
  },
7263
- {
7264
- binding: 2,
7265
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7266
- buffer: { type: 'uniform' },
7826
+ geometry: {
7827
+ topology: 'triangle-list',
7828
+ stripIndexFormat: undefined,
7829
+ vertexLayout: {},
7267
7830
  },
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;
7831
+ renderState: { cullMode: 'none' },
7832
+ };
7833
+ const modules = {
7834
+ vertex: bloomBrightModule,
7835
+ fragment: bloomBrightModule,
7836
+ layout: brightPlResult.value,
7837
+ };
7838
+ try {
7839
+ bloomBrightPipelineHandle = getOrBuildPipeline(
7840
+ brightSpec,
7841
+ pipelineDeviceProvider,
7842
+ pipelineCache,
7843
+ modules,
7844
+ ) as RenderPipeline;
7845
+ bloomBrightPipelineHandle = adopt(
7846
+ 'pipeline',
7847
+ bloomBrightPipelineHandle,
7848
+ () => undefined,
7849
+ );
7850
+ } catch (err) {
7851
+ if (err instanceof PipelineSpecError) throw err;
7852
+ throw new PipelineSpecError({
7853
+ code: 'pipeline-build-failed',
7854
+ detail: { cause: err },
7855
+ hint: 'createRenderPipeline (bloom-bright fullscreen) failed; inspect gpuMessage',
7856
+ });
7857
+ }
7858
+ }
7288
7859
 
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',
7860
+ // Bright params UBO: 16 B std140 (threshold f32 + 12 B pad).
7861
+ const brightParamsResult = runShimSyncStep(
7862
+ () =>
7863
+ rhiDevice.createBuffer({
7864
+ label: 'bloom-bright-params-ubo',
7865
+ size: BRIGHT_PARAMS_BYTES,
7866
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
7867
+ mappedAtCreation: false,
7868
+ }),
7869
+ 'webgpu-runtime-error',
7870
+ 'createBuffer (bloom-bright params UBO) succeeded',
7871
+ 'check device.limits.maxUniformBufferBindingSize',
7872
+ );
7873
+ if (!brightParamsResult.ok) throw brightParamsResult.error;
7874
+ bloomBrightParamsBufferHandle = adopt('buffer', brightParamsResult.value, (value) => {
7875
+ rhiDevice.destroyBuffer(value);
7327
7876
  });
7328
7877
  }
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
7878
 
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' },
7879
+ // Bloom blur H/V: same BGL (1-tex + sampler + UBO@2), same module,
7880
+ // two separate pipelines with per-axis texelSize (D-1, D-4).
7881
+ if (bloomBlurModule !== null && bloomSamplerHandle !== null) {
7882
+ const blurBglResult = runShimSyncStep(
7883
+ () =>
7884
+ rhiDevice.createBindGroupLayout({
7885
+ label: 'bloom-blur-bgl',
7886
+ entries: [
7887
+ {
7888
+ binding: 0,
7889
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
7890
+ texture: { sampleType: 'float', viewDimension: '2d' },
7891
+ },
7892
+ {
7893
+ binding: 1,
7894
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
7895
+ sampler: { type: 'filtering' },
7896
+ },
7897
+ {
7898
+ binding: 2,
7899
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
7900
+ buffer: { type: 'uniform' },
7901
+ },
7902
+ ],
7903
+ }),
7904
+ 'webgpu-runtime-error',
7905
+ 'createBindGroupLayout(bloom-blur) succeeded',
7906
+ 'check device.limits.maxBindGroupsPerPipelineLayout',
7907
+ );
7908
+ if (!blurBglResult.ok) throw blurBglResult.error;
7909
+ bloomBlurBglHandle = adopt('binding', blurBglResult.value, () => undefined);
7910
+
7911
+ const blurPlResult = runShimSyncStep(
7912
+ () =>
7913
+ rhiDevice.createPipelineLayout({
7914
+ label: 'bloom-blur-pl',
7915
+ bindGroupLayouts: [blurBglResult.value],
7916
+ }),
7917
+ 'webgpu-runtime-error',
7918
+ 'createPipelineLayout(bloom-blur) succeeded',
7919
+ 'verify bloom-blur BindGroupLayout matches shader @group(0) bindings',
7920
+ );
7921
+ if (!blurPlResult.ok) throw blurPlResult.error;
7922
+ adopt('binding', blurPlResult.value, () => undefined);
7923
+
7924
+ // M2-T4: bloom-blur H/V pipelines via getOrBuildPipeline (lazy-build).
7925
+ // H and V share the same PSO descriptor — only per-axis texelSize UBO
7926
+ // distinguishes them at record time. getOrBuildPipeline cache-hit on the
7927
+ // second call returns the same handle (identical spec, identical PSO).
7928
+ {
7929
+ const blurSpec: PipelineSpec = {
7930
+ shader: {
7931
+ id: 'forgeax::post::bloom-blur',
7932
+ passKind: 'post-process',
7933
+ variantSet: undefined,
7360
7934
  },
7361
- {
7362
- binding: 1,
7363
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7364
- sampler: { type: 'filtering' },
7935
+ attachments: {
7936
+ colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
7937
+ depthFormat: undefined,
7938
+ sampleCount: 1,
7365
7939
  },
7366
- {
7367
- binding: 2,
7368
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7369
- buffer: { type: 'uniform' },
7940
+ geometry: {
7941
+ topology: 'triangle-list',
7942
+ stripIndexFormat: undefined,
7943
+ vertexLayout: {},
7370
7944
  },
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;
7945
+ renderState: { cullMode: 'none' },
7946
+ };
7947
+ const modules = {
7948
+ vertex: bloomBlurModule,
7949
+ fragment: bloomBlurModule,
7950
+ layout: blurPlResult.value,
7951
+ label: 'bloom-blur-h-pipeline',
7952
+ };
7953
+ try {
7954
+ bloomBlurHPipelineHandle = getOrBuildPipeline(
7955
+ blurSpec,
7956
+ pipelineDeviceProvider,
7957
+ pipelineCache,
7958
+ modules,
7959
+ ) as RenderPipeline;
7960
+ bloomBlurHPipelineHandle = adopt(
7961
+ 'pipeline',
7962
+ bloomBlurHPipelineHandle,
7963
+ () => undefined,
7964
+ );
7965
+ } catch (err) {
7966
+ if (err instanceof PipelineSpecError) throw err;
7967
+ throw new PipelineSpecError({
7968
+ code: 'pipeline-build-failed',
7969
+ detail: { cause: err },
7970
+ hint: 'createRenderPipeline (bloom-blur-h fullscreen) failed; inspect gpuMessage',
7971
+ });
7972
+ }
7973
+ // V is cache-hit on the same spec (identical PSO; per-axis UBO differentiates at record time).
7974
+ bloomBlurVPipelineHandle = bloomBlurHPipelineHandle;
7975
+ }
7379
7976
 
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;
7977
+ // Blur params UBOs: 16 B std140 (texelSize.xy + radius + pad) each.
7978
+ // bug-20260625: one per axis -- H and V must not share a buffer (the
7979
+ // shared-buffer writeBuffer race made both passes blur vertically).
7980
+ const blurHParamsResult = runShimSyncStep(
7981
+ () =>
7982
+ rhiDevice.createBuffer({
7983
+ label: 'bloom-blur-h-params-ubo',
7984
+ size: BLUR_PARAMS_BYTES,
7985
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
7986
+ mappedAtCreation: false,
7987
+ }),
7988
+ 'webgpu-runtime-error',
7989
+ 'createBuffer (bloom-blur-h params UBO) succeeded',
7990
+ 'check device.limits.maxUniformBufferBindingSize',
7991
+ );
7992
+ if (!blurHParamsResult.ok) throw blurHParamsResult.error;
7993
+ bloomBlurHParamsBufferHandle = adopt('buffer', blurHParamsResult.value, (value) => {
7994
+ rhiDevice.destroyBuffer(value);
7995
+ });
7391
7996
 
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',
7997
+ const blurVParamsResult = runShimSyncStep(
7998
+ () =>
7999
+ rhiDevice.createBuffer({
8000
+ label: 'bloom-blur-v-params-ubo',
8001
+ size: BLUR_PARAMS_BYTES,
8002
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
8003
+ mappedAtCreation: false,
8004
+ }),
8005
+ 'webgpu-runtime-error',
8006
+ 'createBuffer (bloom-blur-v params UBO) succeeded',
8007
+ 'check device.limits.maxUniformBufferBindingSize',
8008
+ );
8009
+ if (!blurVParamsResult.ok) throw blurVParamsResult.error;
8010
+ bloomBlurVParamsBufferHandle = adopt('buffer', blurVParamsResult.value, (value) => {
8011
+ rhiDevice.destroyBuffer(value);
7434
8012
  });
7435
8013
  }
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
8014
 
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
-
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' },
8015
+ // Bloom composite: 2-tex + sampler + UBO@3 BGL (D-4, D-5).
8016
+ if (bloomCompositeModule !== null && bloomSamplerHandle !== null) {
8017
+ const compositeBglResult = runShimSyncStep(
8018
+ () =>
8019
+ rhiDevice.createBindGroupLayout({
8020
+ label: 'bloom-composite-bgl',
8021
+ entries: [
8022
+ {
8023
+ binding: 0,
8024
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
8025
+ texture: { sampleType: 'float', viewDimension: '2d' },
8026
+ },
8027
+ {
8028
+ binding: 1,
8029
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
8030
+ texture: { sampleType: 'float', viewDimension: '2d' },
8031
+ },
8032
+ {
8033
+ binding: 2,
8034
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
8035
+ sampler: { type: 'filtering' },
8036
+ },
8037
+ {
8038
+ binding: 3,
8039
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
8040
+ buffer: { type: 'uniform' },
8041
+ },
8042
+ ],
8043
+ }),
8044
+ 'webgpu-runtime-error',
8045
+ 'createBindGroupLayout(bloom-composite) succeeded',
8046
+ 'check device.limits.maxBindGroupsPerPipelineLayout',
8047
+ );
8048
+ if (!compositeBglResult.ok) throw compositeBglResult.error;
8049
+ bloomCompositeBglHandle = adopt('binding', compositeBglResult.value, () => undefined);
8050
+
8051
+ const compositePlResult = runShimSyncStep(
8052
+ () =>
8053
+ rhiDevice.createPipelineLayout({
8054
+ label: 'bloom-composite-pl',
8055
+ bindGroupLayouts: [compositeBglResult.value],
8056
+ }),
8057
+ 'webgpu-runtime-error',
8058
+ 'createPipelineLayout(bloom-composite) succeeded',
8059
+ 'verify bloom-composite BindGroupLayout matches shader @group(0) bindings',
8060
+ );
8061
+ if (!compositePlResult.ok) throw compositePlResult.error;
8062
+ adopt('binding', compositePlResult.value, () => undefined);
8063
+
8064
+ // M2-T4: bloom-composite pipeline via getOrBuildPipeline (lazy-build).
8065
+ {
8066
+ const compositeSpec: PipelineSpec = {
8067
+ shader: {
8068
+ id: 'forgeax::post::bloom-composite',
8069
+ passKind: 'post-process',
8070
+ variantSet: undefined,
7490
8071
  },
7491
- {
7492
- binding: 2,
7493
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7494
- sampler: { type: 'filtering' },
8072
+ attachments: {
8073
+ colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
8074
+ depthFormat: undefined,
8075
+ sampleCount: 1,
7495
8076
  },
7496
- {
7497
- binding: 3,
7498
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7499
- buffer: { type: 'uniform' },
8077
+ geometry: {
8078
+ topology: 'triangle-list',
8079
+ stripIndexFormat: undefined,
8080
+ vertexLayout: {},
7500
8081
  },
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;
8082
+ renderState: { cullMode: 'none' },
8083
+ };
8084
+ const modules = {
8085
+ vertex: bloomCompositeModule,
8086
+ fragment: bloomCompositeModule,
8087
+ layout: compositePlResult.value,
8088
+ };
8089
+ try {
8090
+ bloomCompositePipelineHandle = getOrBuildPipeline(
8091
+ compositeSpec,
8092
+ pipelineDeviceProvider,
8093
+ pipelineCache,
8094
+ modules,
8095
+ ) as RenderPipeline;
8096
+ bloomCompositePipelineHandle = adopt(
8097
+ 'pipeline',
8098
+ bloomCompositePipelineHandle,
8099
+ () => undefined,
8100
+ );
8101
+ } catch (err) {
8102
+ if (err instanceof PipelineSpecError) throw err;
8103
+ throw new PipelineSpecError({
8104
+ code: 'pipeline-build-failed',
8105
+ detail: { cause: err },
8106
+ hint: 'createRenderPipeline (bloom-composite fullscreen) failed; inspect gpuMessage',
8107
+ });
8108
+ }
8109
+ }
7509
8110
 
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;
8111
+ // Composite params UBO: 16 B std140 (intensity f32 + 12 B pad).
8112
+ const compositeParamsResult = runShimSyncStep(
8113
+ () =>
8114
+ rhiDevice.createBuffer({
8115
+ label: 'bloom-composite-params-ubo',
8116
+ size: COMPOSITE_PARAMS_BYTES,
8117
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
8118
+ mappedAtCreation: false,
8119
+ }),
8120
+ 'webgpu-runtime-error',
8121
+ 'createBuffer (bloom-composite params UBO) succeeded',
8122
+ 'check device.limits.maxUniformBufferBindingSize',
8123
+ );
8124
+ if (!compositeParamsResult.ok) throw compositeParamsResult.error;
8125
+ bloomCompositeParamsBufferHandle = adopt(
8126
+ 'buffer',
8127
+ compositeParamsResult.value,
8128
+ (value) => {
8129
+ rhiDevice.destroyBuffer(value);
8130
+ },
8131
+ );
8132
+ }
7521
8133
 
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' },
8134
+ bloomCandidateBundle = {
8135
+ scope: candidateScope,
8136
+ generation: bloomGeneration + 1,
8137
+ bloomBrightPipeline: bloomBrightPipelineHandle,
8138
+ bloomBlurHPipeline: bloomBlurHPipelineHandle,
8139
+ bloomBlurVPipeline: bloomBlurVPipelineHandle,
8140
+ bloomCompositePipeline: bloomCompositePipelineHandle,
8141
+ bloomBrightBindGroupLayout: bloomBrightBglHandle,
8142
+ bloomBlurBindGroupLayout: bloomBlurBglHandle,
8143
+ bloomCompositeBindGroupLayout: bloomCompositeBglHandle,
8144
+ bloomSampler: bloomSamplerHandle,
8145
+ bloomBrightParamsBuffer: bloomBrightParamsBufferHandle,
8146
+ bloomBlurHParamsBuffer: bloomBlurHParamsBufferHandle,
8147
+ bloomBlurVParamsBuffer: bloomBlurVParamsBufferHandle,
8148
+ bloomCompositeParamsBuffer: bloomCompositeParamsBufferHandle,
7541
8149
  };
7542
- const modules = {
7543
- vertex: bloomCompositeModule,
7544
- fragment: bloomCompositeModule,
7545
- layout: compositePlResult.value,
8150
+ inspectBloomResources = (graph): BloomInspection => {
8151
+ const retiring = [...retiringBloomBundles].sort(
8152
+ (left, right) => left.generation - right.generation,
8153
+ );
8154
+ const active = bloomActiveBundle?.scope.isAlive() ? bloomActiveBundle : undefined;
8155
+ const liveRetiring = retiring.filter((bundle) => bundle.scope.state === 'retiring');
8156
+ const selected = active ?? liveRetiring[liveRetiring.length - 1];
8157
+ if (selected === undefined) {
8158
+ return {
8159
+ graphStatus: 'empty',
8160
+ enabled: false,
8161
+ targetCount: 0,
8162
+ targetBytes: 0,
8163
+ resourceCount: 0,
8164
+ passCount: 0,
8165
+ encodeCount: 0,
8166
+ bindGroupCount: 0,
8167
+ uploadCount: 0,
8168
+ residentChildBytes: 0,
8169
+ generation: 0,
8170
+ state: 'off',
8171
+ };
8172
+ }
8173
+ const state = active === undefined ? 'retiring' : 'active';
8174
+ const ownedBundles = [...(active === undefined ? [] : [active]), ...liveRetiring];
8175
+ const graphInspection = inspectStandardBloomGraph(graph);
8176
+ const receipts = lastSuccessfulBloomReceipts ?? {
8177
+ uploadCount: 0,
8178
+ bindGroupCount: 0,
8179
+ encodeCount: 0,
8180
+ };
8181
+ return {
8182
+ graphStatus: active === undefined ? 'empty' : graphInspection.status,
8183
+ enabled: active !== undefined,
8184
+ targetCount: active === undefined ? 0 : graphInspection.targetCount,
8185
+ targetBytes: active === undefined ? 0 : graphInspection.targetBytes,
8186
+ resourceCount: ownedBundles.reduce(
8187
+ (count, bundle) => count + bundle.scope.resourceDelta(),
8188
+ 0,
8189
+ ),
8190
+ passCount: active === undefined ? 0 : graphInspection.passCount,
8191
+ encodeCount: active === undefined ? 0 : receipts.encodeCount,
8192
+ bindGroupCount: active === undefined ? 0 : receipts.bindGroupCount,
8193
+ uploadCount: active === undefined ? 0 : receipts.uploadCount,
8194
+ residentChildBytes: 0,
8195
+ generation: selected?.generation ?? 0,
8196
+ state,
8197
+ };
7546
8198
  };
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
- }
8199
+ commitBloomResources = () => {
8200
+ if (bloomCandidateBundle?.scope !== candidateScope) return;
8201
+ bloomActiveBundle = bloomCandidateBundle;
8202
+ bloomCandidateBundle = undefined;
8203
+ bloomGeneration += 1;
8204
+ bloomResourcesReady = true;
8205
+ };
8206
+ discardBloomResources = () => {
8207
+ if (bloomCandidateBundle?.scope !== candidateScope) return;
8208
+ candidateScope.abandon();
8209
+ bloomCandidateBundle = undefined;
8210
+ bloomResourcesReady = false;
8211
+ };
8212
+ const reportBloomFenceFailure = (cause: unknown): void => {
8213
+ if (cause instanceof RhiError) {
8214
+ errorRegistry.fire(cause);
8215
+ return;
8216
+ }
8217
+ const detailError = {
8218
+ code: 'bloom-fence-rejected',
8219
+ message: String(cause),
8220
+ ...(cause instanceof Error ? { name: cause.name } : {}),
8221
+ };
8222
+ errorRegistry.fire(
8223
+ new RhiError({
8224
+ code: 'webgpu-runtime-error',
8225
+ expected: 'Bloom queue completion resolves after the submitted frame retires',
8226
+ hint: 'inspect renderer errors for the queue completion cause',
8227
+ detail: { error: detailError },
8228
+ }),
8229
+ );
8230
+ };
8231
+ retireBloomResources = (completion) => {
8232
+ const retiring = bloomActiveBundle;
8233
+ if (retiring === undefined) return;
8234
+ bloomActiveBundle = undefined;
8235
+ bloomResourcesReady = false;
8236
+ retiringBloomBundles.add(retiring);
8237
+ retiring.scope.beginRetire();
8238
+ void completion.then(
8239
+ () => {
8240
+ if (!retiringBloomBundles.has(retiring)) return;
8241
+ retiring.scope.retire();
8242
+ retiringBloomBundles.delete(retiring);
8243
+ },
8244
+ (cause) => {
8245
+ if (!retiringBloomBundles.has(retiring)) return;
8246
+ reportBloomFenceFailure(cause);
8247
+ retiring.scope.retire();
8248
+ retiringBloomBundles.delete(retiring);
8249
+ },
8250
+ );
8251
+ };
8252
+ drainBloomResources = () => {
8253
+ if (bloomCandidateBundle !== undefined) {
8254
+ bloomCandidateBundle.scope.abandon();
8255
+ bloomCandidateBundle = undefined;
8256
+ }
8257
+ if (bloomActiveBundle !== undefined) {
8258
+ bloomActiveBundle.scope.retire();
8259
+ bloomActiveBundle = undefined;
8260
+ }
8261
+ for (const retiring of retiringBloomBundles) retiring.scope.retire();
8262
+ retiringBloomBundles.clear();
8263
+ bloomResourcesReady = false;
8264
+ };
8265
+ } catch (cause) {
8266
+ candidateScope.abandon();
8267
+ if (bloomCandidateBundle?.scope === candidateScope) bloomCandidateBundle = undefined;
8268
+ commitBloomResources = undefined;
8269
+ discardBloomResources = undefined;
8270
+ drainBloomResources = undefined;
8271
+ bloomBrightPipelineHandle = null;
8272
+ bloomBlurHPipelineHandle = null;
8273
+ bloomBlurVPipelineHandle = null;
8274
+ bloomCompositePipelineHandle = null;
8275
+ bloomBrightBglHandle = null;
8276
+ bloomBlurBglHandle = null;
8277
+ bloomCompositeBglHandle = null;
8278
+ bloomSamplerHandle = null;
8279
+ bloomBrightParamsBufferHandle = null;
8280
+ bloomBlurHParamsBufferHandle = null;
8281
+ bloomBlurVParamsBufferHandle = null;
8282
+ bloomCompositeParamsBufferHandle = null;
8283
+ throw cause;
7562
8284
  }
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
- }
8285
+ };
7580
8286
 
7581
8287
  // ── feat-20260612-hdrp-ssao M6 / w26 + w43 + M8 / w37 ───────────────────
7582
8288
  //
@@ -7798,7 +8504,7 @@ async function buildReadyWebGPU(
7798
8504
  if (!prewarmRes.ok) throw prewarmRes.error;
7799
8505
  }
7800
8506
 
7801
- return {
8507
+ const pipelineState: PipelineState = {
7802
8508
  // feat-20260518-pbr-direct-lighting-mvp M5 / w22.10 (AC-06 dual->triple
7803
8509
  // pipeline + D-2 + D-10): three distinct GPU render-pipeline handles
7804
8510
  // backed by 2 distinct shader modules (unlit + pbr) x 2 vertex stride
@@ -7881,6 +8587,7 @@ async function buildReadyWebGPU(
7881
8587
  meshes: meshHandles,
7882
8588
  format: swapChainFormats.storage,
7883
8589
  colorAttachmentFormat: swapChainFormats.view,
8590
+ surfaceProfile: surfaceViewFormats ? 'dual-view' : 'raw-only',
7884
8591
  viewBindGroupLayout: viewBglResult.value,
7885
8592
  materialBindGroupLayout: materialBglResult.value,
7886
8593
  meshBindGroupLayout: meshArrayBglResult.value,
@@ -7990,18 +8697,28 @@ async function buildReadyWebGPU(
7990
8697
  // (ensureLazyTexture, slot width/height tracking for size-drift rebuild).
7991
8698
  // BindGroup caches survive until the intermediate view is invalidated
7992
8699
  // 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,
8700
+ bloomBrightPipeline: null,
8701
+ bloomBlurHPipeline: null,
8702
+ bloomBlurVPipeline: null,
8703
+ bloomCompositePipeline: null,
8704
+ bloomBrightBindGroupLayout: null,
8705
+ bloomBlurBindGroupLayout: null,
8706
+ bloomCompositeBindGroupLayout: null,
8707
+ bloomSampler: null,
8708
+ bloomBrightParamsBuffer: null,
8709
+ bloomBlurHParamsBuffer: null,
8710
+ bloomBlurVParamsBuffer: null,
8711
+ bloomCompositeParamsBuffer: null,
8712
+ ensureBloomResources: ensureBloomResourcesStable,
8713
+ getBloomResources: getBloomResourcesStable,
8714
+ commitBloomResources: commitBloomResourcesStable,
8715
+ commitBloomFrameReceipts: (receipts) => {
8716
+ lastSuccessfulBloomReceipts = receipts;
8717
+ },
8718
+ discardBloomResources: discardBloomResourcesStable,
8719
+ retireBloomResources: retireBloomResourcesStable,
8720
+ drainBloomResources: drainBloomResourcesStable,
8721
+ inspectBloomResources: inspectBloomResourcesStable,
8005
8722
  bloomBrightTexture: null,
8006
8723
  bloomBrightView: null,
8007
8724
  bloomBrightWidth: 0,
@@ -8026,6 +8743,7 @@ async function buildReadyWebGPU(
8026
8743
  ssaoFallbackRawView: null,
8027
8744
  },
8028
8745
  };
8746
+ return pipelineState;
8029
8747
  }
8030
8748
 
8031
8749
  /**
@@ -8050,8 +8768,8 @@ function ensureContextConfigured(
8050
8768
  internals: WebGPURendererInternals,
8051
8769
  state: PipelineState,
8052
8770
  errorRegistry: RhiErrorListenerRegistry,
8053
- ): void {
8054
- if (state.perPassResources.configured) return;
8771
+ ): Result<void, RhiError> {
8772
+ if (state.perPassResources.configured) return ok(undefined);
8055
8773
  // M6 / w41 (feat-20260510-rhi-resource-creation): configure goes through
8056
8774
  // the forgeax `RhiCanvasContext` surface; the shim resolves the forgeax
8057
8775
  // RhiDevice back to the underlying raw GPUDevice via RAW_DEVICE_MAP
@@ -8064,7 +8782,7 @@ function ensureContextConfigured(
8064
8782
  const configuredDevice = internals.pack.instrumentation?.resolveSurfaceDevice?.(internals.device);
8065
8783
  if (configuredDevice !== undefined && !configuredDevice.ok) {
8066
8784
  errorRegistry.fire(configuredDevice.error);
8067
- return;
8785
+ return err(configuredDevice.error);
8068
8786
  }
8069
8787
  const cfgResult = configureSurface(
8070
8788
  internals.context,
@@ -8074,7 +8792,7 @@ function ensureContextConfigured(
8074
8792
  );
8075
8793
  if (!cfgResult.ok) {
8076
8794
  errorRegistry.fire(cfgResult.error);
8077
- return;
8795
+ return err(cfgResult.error);
8078
8796
  }
8079
8797
  state.perPassResources.configured = true;
8080
8798
  // bug-20260612: write the actually-configured swap-chain storage format
@@ -8087,6 +8805,7 @@ function ensureContextConfigured(
8087
8805
  // `${storage}-srgb` partner — derived, not duplicated.
8088
8806
  (globalThis as { __forgeaxSwapChainFormat?: GPUTextureFormat }).__forgeaxSwapChainFormat =
8089
8807
  state.format;
8808
+ return ok(undefined);
8090
8809
  }
8091
8810
 
8092
8811
  /**