@forgeax/engine-render 0.1.6 → 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 (713) hide show
  1. package/README.md +279 -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 +19 -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/material/assembly.d.ts.map +1 -1
  10. package/dist/assembly/renderer-frame-transaction.d.ts +39 -0
  11. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -0
  12. package/dist/assembly/renderer-inspection.d.ts +22 -0
  13. package/dist/assembly/renderer-inspection.d.ts.map +1 -0
  14. package/dist/assembly/renderer-lifecycle.d.ts +4 -0
  15. package/dist/assembly/renderer-lifecycle.d.ts.map +1 -0
  16. package/dist/authoring.d.ts +1 -0
  17. package/dist/authoring.d.ts.map +1 -1
  18. package/dist/authoring.mjs +4 -2
  19. package/dist/authoring.mjs.map +1 -1
  20. package/dist/chunk-FJ6P52EE.mjs +46 -0
  21. package/dist/chunk-FJ6P52EE.mjs.map +1 -0
  22. package/dist/chunk-IPXCABEP.mjs +73 -0
  23. package/dist/chunk-IPXCABEP.mjs.map +1 -0
  24. package/dist/chunk-JDWARUOI.mjs +44 -0
  25. package/dist/chunk-JDWARUOI.mjs.map +1 -0
  26. package/dist/chunk-LOSO4CTV.mjs +901 -0
  27. package/dist/chunk-LOSO4CTV.mjs.map +1 -0
  28. package/dist/chunk-PCHBPYML.mjs +385 -0
  29. package/dist/chunk-PCHBPYML.mjs.map +1 -0
  30. package/dist/chunk-RJRUS55L.mjs +9323 -0
  31. package/dist/chunk-RJRUS55L.mjs.map +1 -0
  32. package/dist/{chunk-QC4VW3XJ.mjs → chunk-RXZ3RGOU.mjs} +4 -30
  33. package/dist/chunk-RXZ3RGOU.mjs.map +1 -0
  34. package/dist/{chunk-CQXZC5XI.mjs → chunk-VFPLTKCU.mjs} +79 -69
  35. package/dist/chunk-VFPLTKCU.mjs.map +1 -0
  36. package/dist/{chunk-SCRSAK5B.mjs → chunk-YTQ5C7YQ.mjs} +231 -127
  37. package/dist/chunk-YTQ5C7YQ.mjs.map +1 -0
  38. package/dist/components/atmosphere.d.ts +9 -0
  39. package/dist/components/atmosphere.d.ts.map +1 -0
  40. package/dist/components/camera.d.ts +2 -1
  41. package/dist/components/camera.d.ts.map +1 -1
  42. package/dist/components/fog.d.ts +8 -0
  43. package/dist/components/fog.d.ts.map +1 -0
  44. package/dist/components/index.d.ts +5 -0
  45. package/dist/components/index.d.ts.map +1 -1
  46. package/dist/components/lines.d.ts +5 -0
  47. package/dist/components/lines.d.ts.map +1 -0
  48. package/dist/components/motion-blur.d.ts +7 -0
  49. package/dist/components/motion-blur.d.ts.map +1 -0
  50. package/dist/components/points.d.ts +15 -0
  51. package/dist/components/points.d.ts.map +1 -0
  52. package/dist/construct-renderer.d.ts +2 -2
  53. package/dist/construct-renderer.d.ts.map +1 -1
  54. package/dist/construct-renderer.mjs +11419 -16442
  55. package/dist/construct-renderer.mjs.map +1 -1
  56. package/dist/device/device-scope.d.ts +4 -0
  57. package/dist/device/device-scope.d.ts.map +1 -1
  58. package/dist/environment/frame.d.ts +60 -0
  59. package/dist/environment/frame.d.ts.map +1 -0
  60. package/dist/environment/generation.d.ts +21 -0
  61. package/dist/environment/generation.d.ts.map +1 -0
  62. package/dist/environment/graph.d.ts +4 -0
  63. package/dist/environment/graph.d.ts.map +1 -0
  64. package/dist/environment/inspection.d.ts +48 -0
  65. package/dist/environment/inspection.d.ts.map +1 -0
  66. package/dist/environment/lifecycle.d.ts +39 -0
  67. package/dist/environment/lifecycle.d.ts.map +1 -0
  68. package/dist/environment/signature.d.ts +25 -0
  69. package/dist/environment/signature.d.ts.map +1 -0
  70. package/dist/errors/render.d.ts +217 -1
  71. package/dist/errors/render.d.ts.map +1 -1
  72. package/dist/extract/camera.d.ts +5 -0
  73. package/dist/extract/camera.d.ts.map +1 -0
  74. package/dist/extract/environment.d.ts +74 -0
  75. package/dist/extract/environment.d.ts.map +1 -0
  76. package/dist/extract/visibility.d.ts +1 -1
  77. package/dist/extract/visibility.d.ts.map +1 -1
  78. package/dist/features/host.d.ts +9 -2
  79. package/dist/features/host.d.ts.map +1 -1
  80. package/dist/features/motion-blur/motion-blur-feature.d.ts +16 -0
  81. package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -0
  82. package/dist/features/motion-blur/motion-blur-params.d.ts +25 -0
  83. package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -0
  84. package/dist/features/plan.d.ts +17 -2
  85. package/dist/features/plan.d.ts.map +1 -1
  86. package/dist/features/prepared-gpu-work.d.ts +3 -1
  87. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  88. package/dist/features/prepared-graphics.d.ts +2 -1
  89. package/dist/features/prepared-graphics.d.ts.map +1 -1
  90. package/dist/features/render-graph-raster.d.ts +7 -2
  91. package/dist/features/render-graph-raster.d.ts.map +1 -1
  92. package/dist/features/targets.d.ts +3 -0
  93. package/dist/features/targets.d.ts.map +1 -1
  94. package/dist/features/types.d.ts +12 -0
  95. package/dist/features/types.d.ts.map +1 -1
  96. package/dist/fullscreen-post-process-pass.d.ts +7 -1
  97. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  98. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  99. package/dist/gpu-scene.d.ts +2 -0
  100. package/dist/gpu-scene.d.ts.map +1 -1
  101. package/dist/ibl/skylight-bind-group.d.ts +5 -6
  102. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  103. package/dist/index.d.ts +21 -6
  104. package/dist/index.d.ts.map +1 -1
  105. package/dist/index.mjs +8 -4
  106. package/dist/index.mjs.map +1 -1
  107. package/dist/inspection-types.d.ts +46 -1
  108. package/dist/inspection-types.d.ts.map +1 -1
  109. package/dist/internal.d.ts +2 -0
  110. package/dist/internal.d.ts.map +1 -1
  111. package/dist/internal.mjs +2 -0
  112. package/dist/materials.d.ts +22 -0
  113. package/dist/materials.d.ts.map +1 -1
  114. package/dist/pbr-pipeline.d.ts +12 -6
  115. package/dist/pbr-pipeline.d.ts.map +1 -1
  116. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  117. package/dist/pipeline/standard-pipeline.d.ts +3 -0
  118. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  119. package/dist/pipeline/standard-post.d.ts +48 -0
  120. package/dist/pipeline/standard-post.d.ts.map +1 -0
  121. package/dist/pipeline/standard-profile.d.ts +3 -6
  122. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  123. package/dist/pipeline-builder.d.ts +18 -0
  124. package/dist/pipeline-builder.d.ts.map +1 -1
  125. package/dist/pipeline-spec.d.ts.map +1 -1
  126. package/dist/plugin.d.ts.map +1 -1
  127. package/dist/points-lines/admission.d.ts +36 -0
  128. package/dist/points-lines/admission.d.ts.map +1 -0
  129. package/dist/points-lines/bounds.d.ts +8 -0
  130. package/dist/points-lines/bounds.d.ts.map +1 -0
  131. package/dist/points-lines/expansion-cache.d.ts +28 -0
  132. package/dist/points-lines/expansion-cache.d.ts.map +1 -0
  133. package/dist/points-lines/inspection.d.ts +53 -0
  134. package/dist/points-lines/inspection.d.ts.map +1 -0
  135. package/dist/points-lines/prepare.d.ts +67 -0
  136. package/dist/points-lines/prepare.d.ts.map +1 -0
  137. package/dist/points-lines/record.d.ts +60 -0
  138. package/dist/points-lines/record.d.ts.map +1 -0
  139. package/dist/points-lines/snapshot.d.ts +49 -0
  140. package/dist/points-lines/snapshot.d.ts.map +1 -0
  141. package/dist/record/frame-lighting.d.ts.map +1 -1
  142. package/dist/record/frame-snapshot.d.ts +100 -5
  143. package/dist/record/frame-snapshot.d.ts.map +1 -1
  144. package/dist/record/frame-targets.d.ts.map +1 -1
  145. package/dist/record/frame.d.ts +4 -2
  146. package/dist/record/frame.d.ts.map +1 -1
  147. package/dist/record/main-pass-geometry.d.ts +18 -0
  148. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  149. package/dist/record/main-pass-material.d.ts +8 -5
  150. package/dist/record/main-pass-material.d.ts.map +1 -1
  151. package/dist/record/main-pass-sprite-draws.d.ts +8 -1
  152. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  153. package/dist/record/main-pass.d.ts +2 -0
  154. package/dist/record/main-pass.d.ts.map +1 -1
  155. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  156. package/dist/record/render-context.d.ts +93 -18
  157. package/dist/record/render-context.d.ts.map +1 -1
  158. package/dist/record/shadow-pass.d.ts.map +1 -1
  159. package/dist/record/skybox-post-pass.d.ts +1 -11
  160. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  161. package/dist/record/typed-frame-graph.d.ts +26 -2
  162. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  163. package/dist/record/view-ubo.d.ts +10 -1
  164. package/dist/record/view-ubo.d.ts.map +1 -1
  165. package/dist/render-contract.d.ts +102 -10
  166. package/dist/render-contract.d.ts.map +1 -1
  167. package/dist/render-graph-primitives.d.ts.map +1 -1
  168. package/dist/render-pipeline.d.ts +50 -6
  169. package/dist/render-pipeline.d.ts.map +1 -1
  170. package/dist/render-system-extract.d.ts +32 -5
  171. package/dist/render-system-extract.d.ts.map +1 -1
  172. package/dist/render-system.d.ts +27 -5
  173. package/dist/render-system.d.ts.map +1 -1
  174. package/dist/scene/render-scene-types.d.ts +3 -0
  175. package/dist/scene/render-scene-types.d.ts.map +1 -1
  176. package/dist/scene/render-scene.d.ts +43 -2
  177. package/dist/scene/render-scene.d.ts.map +1 -1
  178. package/dist/temporal/coverage.d.ts +42 -0
  179. package/dist/temporal/coverage.d.ts.map +1 -0
  180. package/dist/temporal/frame-coordinator.d.ts +99 -0
  181. package/dist/temporal/frame-coordinator.d.ts.map +1 -0
  182. package/dist/temporal/gpu.d.ts +39 -0
  183. package/dist/temporal/gpu.d.ts.map +1 -0
  184. package/dist/temporal/history.d.ts +37 -0
  185. package/dist/temporal/history.d.ts.map +1 -0
  186. package/dist/temporal/index.d.ts +7 -0
  187. package/dist/temporal/index.d.ts.map +1 -0
  188. package/dist/temporal/index.mjs +8 -0
  189. package/dist/temporal/index.mjs.map +1 -0
  190. package/dist/temporal/inspection.d.ts +36 -0
  191. package/dist/temporal/inspection.d.ts.map +1 -0
  192. package/dist/temporal/scene-data-catalog.d.ts +48 -0
  193. package/dist/temporal/scene-data-catalog.d.ts.map +1 -0
  194. package/dist/temporal/scene-data.d.ts +43 -0
  195. package/dist/temporal/scene-data.d.ts.map +1 -0
  196. package/dist/temporal/standard-scene-data.d.ts +61 -0
  197. package/dist/temporal/standard-scene-data.d.ts.map +1 -0
  198. package/dist/temporal/taa-history-resources.d.ts +22 -0
  199. package/dist/temporal/taa-history-resources.d.ts.map +1 -0
  200. package/dist/temporal/taa-history-store.d.ts +27 -0
  201. package/dist/temporal/taa-history-store.d.ts.map +1 -0
  202. package/dist/temporal/taa-resolve.d.ts +15 -0
  203. package/dist/temporal/taa-resolve.d.ts.map +1 -0
  204. package/dist/temporal/temporal-view.d.ts +40 -0
  205. package/dist/temporal/temporal-view.d.ts.map +1 -0
  206. package/dist/temporal/transaction.d.ts +9 -0
  207. package/dist/temporal/transaction.d.ts.map +1 -0
  208. package/dist/temporal/view.d.ts +56 -0
  209. package/dist/temporal/view.d.ts.map +1 -0
  210. package/dist/transmission/backdrop.d.ts +147 -0
  211. package/dist/transmission/backdrop.d.ts.map +1 -0
  212. package/dist/transmission/inspection.d.ts +57 -0
  213. package/dist/transmission/inspection.d.ts.map +1 -0
  214. package/dist/transmission/oracle.d.ts +42 -0
  215. package/dist/transmission/oracle.d.ts.map +1 -0
  216. package/dist/transmission/projection.d.ts +37 -0
  217. package/dist/transmission/projection.d.ts.map +1 -0
  218. package/dist/typed-render-graph-primitives.d.ts +25 -2
  219. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  220. package/package.json +24 -19
  221. package/src/__tests__/ai-user-cold-start.integration.test.ts +59 -0
  222. package/src/__tests__/ai-user-public-surface.test-d.ts +45 -0
  223. package/src/__tests__/authoring-path.unit.test.ts +9 -0
  224. package/src/__tests__/bloom-exact-zero.unit.test.ts +202 -0
  225. package/src/__tests__/bloom-lifecycle.integration.test.ts +125 -0
  226. package/src/__tests__/construct-renderer-identity.unit.test.ts +4 -3
  227. package/src/__tests__/detached-inspection.integration.test.ts +177 -0
  228. package/src/__tests__/environment-fog-lanes.integration.test.ts +219 -0
  229. package/src/__tests__/environment-generation.integration.test.ts +141 -0
  230. package/src/__tests__/environment-selection.unit.test.ts +245 -0
  231. package/src/__tests__/factory-contract.integration.test.ts +1380 -9
  232. package/src/__tests__/frame-plan-contract.unit.test.ts +90 -0
  233. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +7 -0
  234. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +12 -2
  235. package/src/__tests__/gpu-driven-production.integration.test.ts +74 -22
  236. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +37 -0
  237. package/src/__tests__/gpu-scene.unit.test.ts +10 -0
  238. package/src/__tests__/injection-kind-owner.test-d.ts +2 -1
  239. package/src/__tests__/material-contract-inventory.unit.test.ts +62 -0
  240. package/src/__tests__/material-cooked-projection.unit.test.ts +18 -124
  241. package/src/__tests__/material-static-dynamic-route.unit.test.ts +28 -28
  242. package/src/__tests__/materials-transmission-validation.unit.test.ts +36 -0
  243. package/src/__tests__/morph-feature-plan.unit.test.ts +7 -0
  244. package/src/__tests__/motion-blur.pixel.integration.test.ts +121 -0
  245. package/src/__tests__/public-surface.test-d.ts +15 -1
  246. package/src/__tests__/render-error-code-owner.test-d.ts +15 -0
  247. package/src/__tests__/render-error-detail-ssot.test-d.ts +11 -2
  248. package/src/__tests__/render-error-exhaustive.test-d.ts +22 -0
  249. package/src/__tests__/render-feature-errors.unit.test.ts +33 -3
  250. package/src/__tests__/render-feature-gpu-work.integration.test.ts +44 -0
  251. package/src/__tests__/render-feature-plan.test-d.ts +22 -89
  252. package/src/__tests__/render-scene-projection.unit.test.ts +28 -0
  253. package/src/__tests__/render-temporal-environment-consumer.browser.test.ts +26 -0
  254. package/src/__tests__/render-temporal-environment-consumer.dawn.test.ts +21 -0
  255. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +433 -1
  256. package/src/__tests__/renderer-frame-transaction.integration.test.ts +405 -0
  257. package/src/__tests__/renderer-lifecycle.integration.test.ts +22 -0
  258. package/src/__tests__/scene-data-catalog.unit.test.ts +59 -0
  259. package/src/__tests__/scene-data-consumer.integration.test.ts +67 -0
  260. package/src/__tests__/scene-data-contract.test-d.ts +35 -0
  261. package/src/__tests__/scene-data-inspection.unit.test.ts +41 -0
  262. package/src/__tests__/scene-data-public-surface.test-d.ts +34 -0
  263. package/src/__tests__/shader-manifest-fixture.ts +111 -0
  264. package/src/__tests__/skinned-shadow-caster.test.ts +102 -0
  265. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +24 -3
  266. package/src/__tests__/standard-pipeline.dawn.test.ts +15 -0
  267. package/src/__tests__/standard-pipeline.integration.test.ts +543 -1
  268. package/src/__tests__/standard-profile-oracle.unit.test.ts +10 -2
  269. package/src/__tests__/standard-temporal-producer.integration.test.ts +35 -0
  270. package/src/__tests__/surface-configuration.unit.test.ts +211 -12
  271. package/src/__tests__/taa-transaction.integration.test.ts +30 -0
  272. package/src/__tests__/temporal-admission.integration.test.ts +40 -0
  273. package/src/__tests__/temporal-coverage.unit.test.ts +35 -0
  274. package/src/__tests__/temporal-demand.unit.test.ts +44 -0
  275. package/src/__tests__/temporal-frame-coordinator.unit.test.ts +64 -0
  276. package/src/__tests__/temporal-graph-topology.unit.test.ts +51 -0
  277. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +170 -0
  278. package/src/__tests__/temporal-performance.integration.test.ts +615 -0
  279. package/src/__tests__/temporal-reset-matrix.unit.test.ts +97 -0
  280. package/src/__tests__/temporal-transaction.integration.test.ts +60 -0
  281. package/src/__tests__/temporal-view.unit.test.ts +152 -0
  282. package/src/__tests__/transmission-prewarm.unit.test.ts +93 -0
  283. package/src/__tests__/typed-pipeline-topology.unit.test.ts +230 -1
  284. package/src/__tests__/urp-pipeline-targets.unit.test.ts +25 -1
  285. package/src/__tests__/urp-pipeline.unit.test.ts +4 -3
  286. package/src/__tests__/visibility-extract.integration.test.ts +27 -1
  287. package/src/__tests__/visibility-producer-matrix.integration.test.ts +10 -0
  288. package/src/assembly/backend-contract.ts +47 -1
  289. package/src/assembly/factory.ts +1295 -507
  290. package/src/assembly/host-contract.ts +4 -0
  291. package/src/assembly/material/assembly.ts +2 -5
  292. package/src/assembly/renderer-frame-transaction.ts +75 -0
  293. package/src/assembly/renderer-inspection.ts +112 -0
  294. package/src/assembly/renderer-lifecycle.ts +6 -0
  295. package/src/authoring.ts +1 -0
  296. package/src/components/__tests__/motion-blur.unit.test.ts +14 -0
  297. package/src/components/__tests__/points-lines-admission.unit.test.ts +219 -0
  298. package/src/components/__tests__/points-lines-schema.unit.test.ts +56 -0
  299. package/src/components/__tests__/schema-contract.unit.test.ts +6 -1
  300. package/src/components/atmosphere.ts +10 -0
  301. package/src/components/camera.ts +2 -1
  302. package/src/components/fog.ts +9 -0
  303. package/src/components/index.ts +10 -0
  304. package/src/components/lines.ts +18 -0
  305. package/src/components/motion-blur.ts +8 -0
  306. package/src/components/points.ts +40 -0
  307. package/src/construct-renderer.ts +40 -3
  308. package/src/device/device-scope.ts +15 -1
  309. package/src/environment/frame.ts +185 -0
  310. package/src/environment/generation.ts +23 -0
  311. package/src/environment/graph.ts +9 -0
  312. package/src/environment/inspection.ts +87 -0
  313. package/src/environment/lifecycle.ts +250 -0
  314. package/src/environment/signature.ts +55 -0
  315. package/src/errors/render.ts +429 -1
  316. package/src/extract/camera.ts +78 -0
  317. package/src/extract/environment.ts +171 -0
  318. package/src/extract/visibility.ts +38 -19
  319. package/src/features/host.ts +36 -5
  320. package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +67 -0
  321. package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +53 -0
  322. package/src/features/motion-blur/motion-blur-feature.ts +119 -0
  323. package/src/features/motion-blur/motion-blur-params.ts +89 -0
  324. package/src/features/plan.ts +32 -3
  325. package/src/features/prepared-gpu-work.ts +21 -6
  326. package/src/features/prepared-graphics.ts +2 -1
  327. package/src/features/render-graph-raster.ts +24 -4
  328. package/src/features/targets.ts +24 -2
  329. package/src/features/types.ts +13 -0
  330. package/src/fullscreen-post-process-pass.ts +34 -4
  331. package/src/gpu-driven/production-raster.ts +6 -1
  332. package/src/gpu-scene.ts +24 -0
  333. package/src/ibl/skylight-bind-group.ts +30 -38
  334. package/src/index.ts +83 -6
  335. package/src/inspection-types.ts +52 -0
  336. package/src/internal.ts +5 -0
  337. package/src/materials.ts +119 -24
  338. package/src/pbr-pipeline.ts +66 -20
  339. package/src/pipeline/standard-forward-lane.ts +158 -101
  340. package/src/pipeline/standard-pipeline.ts +156 -105
  341. package/src/pipeline/standard-post.ts +373 -0
  342. package/src/pipeline/standard-profile.ts +11 -11
  343. package/src/pipeline-builder.ts +102 -12
  344. package/src/pipeline-spec.ts +9 -0
  345. package/src/plugin.ts +6 -0
  346. package/src/points-lines/__tests__/bounds.unit.test.ts +21 -0
  347. package/src/points-lines/__tests__/cache-recovery.unit.test.ts +157 -0
  348. package/src/points-lines/__tests__/inspection-lanes.integration.test.ts +146 -0
  349. package/src/points-lines/__tests__/inspection.unit.test.ts +173 -0
  350. package/src/points-lines/__tests__/lane-structural.integration.test.ts +122 -0
  351. package/src/points-lines/__tests__/points-lines.bench.ts +400 -0
  352. package/src/points-lines/__tests__/raster-lane.integration.test.ts +44 -0
  353. package/src/points-lines/__tests__/snapshot.unit.test.ts +101 -0
  354. package/src/points-lines/__tests__/view-ubo.unit.test.ts +73 -0
  355. package/src/points-lines/admission.ts +238 -0
  356. package/src/points-lines/bounds.ts +23 -0
  357. package/src/points-lines/expansion-cache.ts +215 -0
  358. package/src/points-lines/inspection.ts +97 -0
  359. package/src/points-lines/prepare.ts +177 -0
  360. package/src/points-lines/record.ts +125 -0
  361. package/src/points-lines/snapshot.ts +130 -0
  362. package/src/record/__tests__/fullscreen-temporal-routing.unit.test.ts +25 -0
  363. package/src/record/__tests__/main-pass-geometry.unit.test.ts +47 -0
  364. package/src/record/__tests__/material-bg-cache.unit.test.ts +12 -1
  365. package/src/record/__tests__/material-pipeline-pending-cache.unit.test.ts +22 -0
  366. package/src/record/__tests__/sprite-material-abi.unit.test.ts +75 -0
  367. package/src/record/__tests__/standard-pbr-ubo-layout.unit.test.ts +67 -0
  368. package/src/record/__tests__/temporal-payload.unit.test.ts +164 -0
  369. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +127 -0
  370. package/src/record/frame-lighting.ts +14 -2
  371. package/src/record/frame-snapshot.ts +129 -5
  372. package/src/record/frame-targets.ts +15 -6
  373. package/src/record/frame.ts +119 -4
  374. package/src/record/main-pass-geometry.ts +178 -40
  375. package/src/record/main-pass-material.ts +114 -66
  376. package/src/record/main-pass-sprite-draws.ts +79 -102
  377. package/src/record/main-pass.ts +83 -7
  378. package/src/record/mesh-ssbo.ts +22 -3
  379. package/src/record/render-context.ts +209 -23
  380. package/src/record/shadow-pass.ts +129 -28
  381. package/src/record/skybox-post-pass.ts +99 -42
  382. package/src/record/typed-frame-graph.ts +582 -42
  383. package/src/record/view-ubo.ts +99 -4
  384. package/src/render-contract.ts +121 -7
  385. package/src/render-graph-primitives.ts +18 -1
  386. package/src/render-pipeline.ts +100 -27
  387. package/src/render-system-extract.ts +264 -77
  388. package/src/render-system.ts +786 -43
  389. package/src/scene/render-scene-types.ts +3 -0
  390. package/src/scene/render-scene.ts +307 -9
  391. package/src/temporal/__tests__/taa-history-resources.unit.test.ts +45 -0
  392. package/src/temporal/coverage.ts +76 -0
  393. package/src/temporal/frame-coordinator.ts +311 -0
  394. package/src/temporal/gpu.ts +287 -0
  395. package/src/temporal/history.ts +92 -0
  396. package/src/temporal/index.ts +6 -0
  397. package/src/temporal/inspection.ts +97 -0
  398. package/src/temporal/scene-data-catalog.ts +200 -0
  399. package/src/temporal/scene-data.ts +149 -0
  400. package/src/temporal/standard-scene-data.ts +182 -0
  401. package/src/temporal/taa-history-resources.ts +128 -0
  402. package/src/temporal/taa-history-store.ts +79 -0
  403. package/src/temporal/taa-resolve.ts +34 -0
  404. package/src/temporal/temporal-view.ts +169 -0
  405. package/src/temporal/transaction.ts +17 -0
  406. package/src/temporal/view.ts +161 -0
  407. package/src/transmission/__tests__/capability.unit.test.ts +83 -0
  408. package/src/transmission/__tests__/composition.integration.test.ts +161 -0
  409. package/src/transmission/__tests__/inspection.unit.test.ts +241 -0
  410. package/src/transmission/__tests__/lifecycle.integration.test.ts +96 -0
  411. package/src/transmission/__tests__/oracle.unit.test.ts +131 -0
  412. package/src/transmission/__tests__/projection.unit.test.ts +71 -0
  413. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +438 -0
  414. package/src/transmission/__tests__/transmission-backdrop-topology.unit.test.ts +109 -0
  415. package/src/transmission/__tests__/transmission-backdrop.dawn.test.ts +105 -0
  416. package/src/transmission/backdrop.ts +572 -0
  417. package/src/transmission/inspection.ts +190 -0
  418. package/src/transmission/oracle.ts +141 -0
  419. package/src/transmission/projection.ts +83 -0
  420. package/src/typed-render-graph-primitives.ts +162 -9
  421. package/dist/__tests__/authoring-path.unit.test.d.ts +0 -2
  422. package/dist/__tests__/authoring-path.unit.test.d.ts.map +0 -1
  423. package/dist/__tests__/backend-selection-rhi-capture.test-d.d.ts +0 -2
  424. package/dist/__tests__/backend-selection-rhi-capture.test-d.d.ts.map +0 -1
  425. package/dist/__tests__/callback-inference.test-d.d.ts +0 -2
  426. package/dist/__tests__/callback-inference.test-d.d.ts.map +0 -1
  427. package/dist/__tests__/cluster-binner.test.d.ts +0 -2
  428. package/dist/__tests__/cluster-binner.test.d.ts.map +0 -1
  429. package/dist/__tests__/cluster-grid-default-surface.unit.test.d.ts +0 -2
  430. package/dist/__tests__/cluster-grid-default-surface.unit.test.d.ts.map +0 -1
  431. package/dist/__tests__/construct-renderer-identity.unit.test.d.ts +0 -2
  432. package/dist/__tests__/construct-renderer-identity.unit.test.d.ts.map +0 -1
  433. package/dist/__tests__/device-scope-lifecycle.unit.test.d.ts +0 -2
  434. package/dist/__tests__/device-scope-lifecycle.unit.test.d.ts.map +0 -1
  435. package/dist/__tests__/device-scope-stale-matrix.unit.test.d.ts +0 -2
  436. package/dist/__tests__/device-scope-stale-matrix.unit.test.d.ts.map +0 -1
  437. package/dist/__tests__/direct-extract-consumer-contract.test-d.d.ts +0 -2
  438. package/dist/__tests__/direct-extract-consumer-contract.test-d.d.ts.map +0 -1
  439. package/dist/__tests__/direct-light-snapshot.unit.test.d.ts +0 -2
  440. package/dist/__tests__/direct-light-snapshot.unit.test.d.ts.map +0 -1
  441. package/dist/__tests__/factory-contract.integration.test.d.ts +0 -2
  442. package/dist/__tests__/factory-contract.integration.test.d.ts.map +0 -1
  443. package/dist/__tests__/fallback-row-stride-surface.unit.test.d.ts +0 -2
  444. package/dist/__tests__/fallback-row-stride-surface.unit.test.d.ts.map +0 -1
  445. package/dist/__tests__/frame-observation.contract.test.d.ts +0 -2
  446. package/dist/__tests__/frame-observation.contract.test.d.ts.map +0 -1
  447. package/dist/__tests__/fullscreen-feature-plan.unit.test.d.ts +0 -2
  448. package/dist/__tests__/fullscreen-feature-plan.unit.test.d.ts.map +0 -1
  449. package/dist/__tests__/glyph-text-layout-recovery.unit.test.d.ts +0 -2
  450. package/dist/__tests__/glyph-text-layout-recovery.unit.test.d.ts.map +0 -1
  451. package/dist/__tests__/gpu-driven-indirect-raster-evidence.d.ts +0 -6
  452. package/dist/__tests__/gpu-driven-indirect-raster-evidence.d.ts.map +0 -1
  453. package/dist/__tests__/gpu-driven-indirect-raster.browser.test.d.ts +0 -2
  454. package/dist/__tests__/gpu-driven-indirect-raster.browser.test.d.ts.map +0 -1
  455. package/dist/__tests__/gpu-driven-indirect-raster.dawn.test.d.ts +0 -2
  456. package/dist/__tests__/gpu-driven-indirect-raster.dawn.test.d.ts.map +0 -1
  457. package/dist/__tests__/gpu-driven-production.integration.test.d.ts +0 -2
  458. package/dist/__tests__/gpu-driven-production.integration.test.d.ts.map +0 -1
  459. package/dist/__tests__/gpu-driven-scaling.unit.test.d.ts +0 -2
  460. package/dist/__tests__/gpu-driven-scaling.unit.test.d.ts.map +0 -1
  461. package/dist/__tests__/gpu-driven-view-gpu-evidence.d.ts +0 -18
  462. package/dist/__tests__/gpu-driven-view-gpu-evidence.d.ts.map +0 -1
  463. package/dist/__tests__/gpu-driven-view-graph.integration.test.d.ts +0 -2
  464. package/dist/__tests__/gpu-driven-view-graph.integration.test.d.ts.map +0 -1
  465. package/dist/__tests__/gpu-driven-view-reference.d.ts +0 -15
  466. package/dist/__tests__/gpu-driven-view-reference.d.ts.map +0 -1
  467. package/dist/__tests__/gpu-driven-view.browser.test.d.ts +0 -2
  468. package/dist/__tests__/gpu-driven-view.browser.test.d.ts.map +0 -1
  469. package/dist/__tests__/gpu-driven-view.dawn.test.d.ts +0 -2
  470. package/dist/__tests__/gpu-driven-view.dawn.test.d.ts.map +0 -1
  471. package/dist/__tests__/gpu-driven-view.unit.test.d.ts +0 -2
  472. package/dist/__tests__/gpu-driven-view.unit.test.d.ts.map +0 -1
  473. package/dist/__tests__/gpu-resource-store-stride.unit.test.d.ts +0 -2
  474. package/dist/__tests__/gpu-resource-store-stride.unit.test.d.ts.map +0 -1
  475. package/dist/__tests__/gpu-scene-render-graph-gpu.d.ts +0 -7
  476. package/dist/__tests__/gpu-scene-render-graph-gpu.d.ts.map +0 -1
  477. package/dist/__tests__/gpu-scene-render-graph.browser.test.d.ts +0 -2
  478. package/dist/__tests__/gpu-scene-render-graph.browser.test.d.ts.map +0 -1
  479. package/dist/__tests__/gpu-scene-render-graph.dawn.test.d.ts +0 -2
  480. package/dist/__tests__/gpu-scene-render-graph.dawn.test.d.ts.map +0 -1
  481. package/dist/__tests__/gpu-scene-schema.unit.test.d.ts +0 -2
  482. package/dist/__tests__/gpu-scene-schema.unit.test.d.ts.map +0 -1
  483. package/dist/__tests__/gpu-scene.dawn.test.d.ts +0 -2
  484. package/dist/__tests__/gpu-scene.dawn.test.d.ts.map +0 -1
  485. package/dist/__tests__/gpu-scene.unit.test.d.ts +0 -2
  486. package/dist/__tests__/gpu-scene.unit.test.d.ts.map +0 -1
  487. package/dist/__tests__/ibl/face-uniforms.unit.test.d.ts +0 -2
  488. package/dist/__tests__/ibl/face-uniforms.unit.test.d.ts.map +0 -1
  489. package/dist/__tests__/infinite-grid-no-vertex-probe.unit.test.d.ts +0 -2
  490. package/dist/__tests__/infinite-grid-no-vertex-probe.unit.test.d.ts.map +0 -1
  491. package/dist/__tests__/injection-kind-owner.test-d.d.ts +0 -2
  492. package/dist/__tests__/injection-kind-owner.test-d.d.ts.map +0 -1
  493. package/dist/__tests__/ktx2-basis-gpu-consumer.dawn.test.d.ts +0 -2
  494. package/dist/__tests__/ktx2-basis-gpu-consumer.dawn.test.d.ts.map +0 -1
  495. package/dist/__tests__/light-buffer-layout.unit.test.d.ts +0 -2
  496. package/dist/__tests__/light-buffer-layout.unit.test.d.ts.map +0 -1
  497. package/dist/__tests__/material-contract-inventory.unit.test.d.ts +0 -2
  498. package/dist/__tests__/material-contract-inventory.unit.test.d.ts.map +0 -1
  499. package/dist/__tests__/material-cooked-projection.unit.test.d.ts +0 -2
  500. package/dist/__tests__/material-cooked-projection.unit.test.d.ts.map +0 -1
  501. package/dist/__tests__/material-snapshot-mutation.integration.test.d.ts +0 -2
  502. package/dist/__tests__/material-snapshot-mutation.integration.test.d.ts.map +0 -1
  503. package/dist/__tests__/material-static-dynamic-route.unit.test.d.ts +0 -2
  504. package/dist/__tests__/material-static-dynamic-route.unit.test.d.ts.map +0 -1
  505. package/dist/__tests__/material-stride-surface.unit.test.d.ts +0 -2
  506. package/dist/__tests__/material-stride-surface.unit.test.d.ts.map +0 -1
  507. package/dist/__tests__/mesh-buffer-usage-surface.unit.test.d.ts +0 -2
  508. package/dist/__tests__/mesh-buffer-usage-surface.unit.test.d.ts.map +0 -1
  509. package/dist/__tests__/mesh-material-bindings.unit.test.d.ts +0 -2
  510. package/dist/__tests__/mesh-material-bindings.unit.test.d.ts.map +0 -1
  511. package/dist/__tests__/mesh-stride-surface.unit.test.d.ts +0 -2
  512. package/dist/__tests__/mesh-stride-surface.unit.test.d.ts.map +0 -1
  513. package/dist/__tests__/morph-culling-reentry.dawn.test.d.ts +0 -2
  514. package/dist/__tests__/morph-culling-reentry.dawn.test.d.ts.map +0 -1
  515. package/dist/__tests__/morph-feature-gpu-work.integration.test.d.ts +0 -2
  516. package/dist/__tests__/morph-feature-gpu-work.integration.test.d.ts.map +0 -1
  517. package/dist/__tests__/morph-feature-plan.unit.test.d.ts +0 -2
  518. package/dist/__tests__/morph-feature-plan.unit.test.d.ts.map +0 -1
  519. package/dist/__tests__/morph-visual-evidence.integration.test.d.ts +0 -2
  520. package/dist/__tests__/morph-visual-evidence.integration.test.d.ts.map +0 -1
  521. package/dist/__tests__/nineslice-handle-surface.unit.test.d.ts +0 -2
  522. package/dist/__tests__/nineslice-handle-surface.unit.test.d.ts.map +0 -1
  523. package/dist/__tests__/post-process-error-policy-owner.unit.test.d.ts +0 -2
  524. package/dist/__tests__/post-process-error-policy-owner.unit.test.d.ts.map +0 -1
  525. package/dist/__tests__/prepared-buffer-kind-owner.test-d.d.ts +0 -2
  526. package/dist/__tests__/prepared-buffer-kind-owner.test-d.d.ts.map +0 -1
  527. package/dist/__tests__/prepared-graphics-capability.unit.test.d.ts +0 -2
  528. package/dist/__tests__/prepared-graphics-capability.unit.test.d.ts.map +0 -1
  529. package/dist/__tests__/prepared-graphics-generation.unit.test.d.ts +0 -2
  530. package/dist/__tests__/prepared-graphics-generation.unit.test.d.ts.map +0 -1
  531. package/dist/__tests__/prepared-graphics-lifecycle.unit.test.d.ts +0 -2
  532. package/dist/__tests__/prepared-graphics-lifecycle.unit.test.d.ts.map +0 -1
  533. package/dist/__tests__/prepared-graphics-pipeline-warmup.unit.test.d.ts +0 -2
  534. package/dist/__tests__/prepared-graphics-pipeline-warmup.unit.test.d.ts.map +0 -1
  535. package/dist/__tests__/prepared-graphics-resolver.unit.test.d.ts +0 -2
  536. package/dist/__tests__/prepared-graphics-resolver.unit.test.d.ts.map +0 -1
  537. package/dist/__tests__/prepared-graphics-snapshot.unit.test.d.ts +0 -2
  538. package/dist/__tests__/prepared-graphics-snapshot.unit.test.d.ts.map +0 -1
  539. package/dist/__tests__/prepared-graphics-store.unit.test.d.ts +0 -2
  540. package/dist/__tests__/prepared-graphics-store.unit.test.d.ts.map +0 -1
  541. package/dist/__tests__/prepared-graphics-validation.unit.test.d.ts +0 -2
  542. package/dist/__tests__/prepared-graphics-validation.unit.test.d.ts.map +0 -1
  543. package/dist/__tests__/profiler-default-off.test.d.ts +0 -2
  544. package/dist/__tests__/profiler-default-off.test.d.ts.map +0 -1
  545. package/dist/__tests__/profiler-phase-catalog.test.d.ts +0 -2
  546. package/dist/__tests__/profiler-phase-catalog.test.d.ts.map +0 -1
  547. package/dist/__tests__/profiler-phase-integration.test.d.ts +0 -2
  548. package/dist/__tests__/profiler-phase-integration.test.d.ts.map +0 -1
  549. package/dist/__tests__/profiler-phase-token.test.d.ts +0 -2
  550. package/dist/__tests__/profiler-phase-token.test.d.ts.map +0 -1
  551. package/dist/__tests__/public-surface.test-d.d.ts +0 -2
  552. package/dist/__tests__/public-surface.test-d.d.ts.map +0 -1
  553. package/dist/__tests__/recover-error-policy-owner.unit.test.d.ts +0 -2
  554. package/dist/__tests__/recover-error-policy-owner.unit.test.d.ts.map +0 -1
  555. package/dist/__tests__/render-error-code-owner.test-d.d.ts +0 -2
  556. package/dist/__tests__/render-error-code-owner.test-d.d.ts.map +0 -1
  557. package/dist/__tests__/render-error-detail-ssot.test-d.d.ts +0 -2
  558. package/dist/__tests__/render-error-detail-ssot.test-d.d.ts.map +0 -1
  559. package/dist/__tests__/render-error-exhaustive.test-d.d.ts +0 -2
  560. package/dist/__tests__/render-error-exhaustive.test-d.d.ts.map +0 -1
  561. package/dist/__tests__/render-feature-assembly.test-d.d.ts +0 -2
  562. package/dist/__tests__/render-feature-assembly.test-d.d.ts.map +0 -1
  563. package/dist/__tests__/render-feature-context.test-d.d.ts +0 -2
  564. package/dist/__tests__/render-feature-context.test-d.d.ts.map +0 -1
  565. package/dist/__tests__/render-feature-contract.test-d.d.ts +0 -2
  566. package/dist/__tests__/render-feature-contract.test-d.d.ts.map +0 -1
  567. package/dist/__tests__/render-feature-diagnostics.unit.test.d.ts +0 -2
  568. package/dist/__tests__/render-feature-diagnostics.unit.test.d.ts.map +0 -1
  569. package/dist/__tests__/render-feature-docs.unit.test.d.ts +0 -2
  570. package/dist/__tests__/render-feature-docs.unit.test.d.ts.map +0 -1
  571. package/dist/__tests__/render-feature-errors.unit.test.d.ts +0 -2
  572. package/dist/__tests__/render-feature-errors.unit.test.d.ts.map +0 -1
  573. package/dist/__tests__/render-feature-gpu-work.integration.test.d.ts +0 -2
  574. package/dist/__tests__/render-feature-gpu-work.integration.test.d.ts.map +0 -1
  575. package/dist/__tests__/render-feature-plan.test-d.d.ts +0 -2
  576. package/dist/__tests__/render-feature-plan.test-d.d.ts.map +0 -1
  577. package/dist/__tests__/render-feature-plan.unit.test.d.ts +0 -2
  578. package/dist/__tests__/render-feature-plan.unit.test.d.ts.map +0 -1
  579. package/dist/__tests__/render-feature-prepared-diagnostics.unit.test.d.ts +0 -2
  580. package/dist/__tests__/render-feature-prepared-diagnostics.unit.test.d.ts.map +0 -1
  581. package/dist/__tests__/render-feature-prepared-errors.test-d.d.ts +0 -2
  582. package/dist/__tests__/render-feature-prepared-errors.test-d.d.ts.map +0 -1
  583. package/dist/__tests__/render-feature-prepared-graphics-docs.unit.test.d.ts +0 -2
  584. package/dist/__tests__/render-feature-prepared-graphics-docs.unit.test.d.ts.map +0 -1
  585. package/dist/__tests__/render-feature-prepared-graphics-isolation.unit.test.d.ts +0 -2
  586. package/dist/__tests__/render-feature-prepared-graphics-isolation.unit.test.d.ts.map +0 -1
  587. package/dist/__tests__/render-feature-prepared-graphics-negative.test-d.d.ts +0 -2
  588. package/dist/__tests__/render-feature-prepared-graphics-negative.test-d.d.ts.map +0 -1
  589. package/dist/__tests__/render-feature-prepared-graphics.test-d.d.ts +0 -2
  590. package/dist/__tests__/render-feature-prepared-graphics.test-d.d.ts.map +0 -1
  591. package/dist/__tests__/render-feature-prepared-graphics.unit.test.d.ts +0 -2
  592. package/dist/__tests__/render-feature-prepared-graphics.unit.test.d.ts.map +0 -1
  593. package/dist/__tests__/render-feature-public-surface.test-d.d.ts +0 -2
  594. package/dist/__tests__/render-feature-public-surface.test-d.d.ts.map +0 -1
  595. package/dist/__tests__/render-feature-retry.unit.test.d.ts +0 -2
  596. package/dist/__tests__/render-feature-retry.unit.test.d.ts.map +0 -1
  597. package/dist/__tests__/render-feature-scheduling.unit.test.d.ts +0 -2
  598. package/dist/__tests__/render-feature-scheduling.unit.test.d.ts.map +0 -1
  599. package/dist/__tests__/render-feature-stage-failed-policy-owner.unit.test.d.ts +0 -2
  600. package/dist/__tests__/render-feature-stage-failed-policy-owner.unit.test.d.ts.map +0 -1
  601. package/dist/__tests__/render-feature-stage-fault-recovery.unit.test.d.ts +0 -2
  602. package/dist/__tests__/render-feature-stage-fault-recovery.unit.test.d.ts.map +0 -1
  603. package/dist/__tests__/render-feature-visibility-context.test-d.d.ts +0 -2
  604. package/dist/__tests__/render-feature-visibility-context.test-d.d.ts.map +0 -1
  605. package/dist/__tests__/render-feature-visibility-context.unit.test.d.ts +0 -2
  606. package/dist/__tests__/render-feature-visibility-context.unit.test.d.ts.map +0 -1
  607. package/dist/__tests__/render-feature-zero-work.unit.test.d.ts +0 -2
  608. package/dist/__tests__/render-feature-zero-work.unit.test.d.ts.map +0 -1
  609. package/dist/__tests__/render-graph-compute-raster-gpu.d.ts +0 -7
  610. package/dist/__tests__/render-graph-compute-raster-gpu.d.ts.map +0 -1
  611. package/dist/__tests__/render-graph-compute-raster.browser.test.d.ts +0 -2
  612. package/dist/__tests__/render-graph-compute-raster.browser.test.d.ts.map +0 -1
  613. package/dist/__tests__/render-graph-compute-raster.dawn.test.d.ts +0 -2
  614. package/dist/__tests__/render-graph-compute-raster.dawn.test.d.ts.map +0 -1
  615. package/dist/__tests__/render-graph-hzb-gpu.d.ts +0 -7
  616. package/dist/__tests__/render-graph-hzb-gpu.d.ts.map +0 -1
  617. package/dist/__tests__/render-graph-hzb.browser.test.d.ts +0 -2
  618. package/dist/__tests__/render-graph-hzb.browser.test.d.ts.map +0 -1
  619. package/dist/__tests__/render-graph-hzb.dawn.test.d.ts +0 -2
  620. package/dist/__tests__/render-graph-hzb.dawn.test.d.ts.map +0 -1
  621. package/dist/__tests__/render-graph-primitives.unit.test.d.ts +0 -2
  622. package/dist/__tests__/render-graph-primitives.unit.test.d.ts.map +0 -1
  623. package/dist/__tests__/render-graph-storage-indirect-gpu.d.ts +0 -12
  624. package/dist/__tests__/render-graph-storage-indirect-gpu.d.ts.map +0 -1
  625. package/dist/__tests__/render-graph-storage-indirect.browser.test.d.ts +0 -2
  626. package/dist/__tests__/render-graph-storage-indirect.browser.test.d.ts.map +0 -1
  627. package/dist/__tests__/render-graph-storage-indirect.dawn.test.d.ts +0 -2
  628. package/dist/__tests__/render-graph-storage-indirect.dawn.test.d.ts.map +0 -1
  629. package/dist/__tests__/render-scene-projection.unit.test.d.ts +0 -2
  630. package/dist/__tests__/render-scene-projection.unit.test.d.ts.map +0 -1
  631. package/dist/__tests__/renderer-factory-material-contract.unit.test.d.ts +0 -2
  632. package/dist/__tests__/renderer-factory-material-contract.unit.test.d.ts.map +0 -1
  633. package/dist/__tests__/renderer-lifecycle.integration.test.d.ts +0 -2
  634. package/dist/__tests__/renderer-lifecycle.integration.test.d.ts.map +0 -1
  635. package/dist/__tests__/rhi-backend-package-boundary.unit.test.d.ts +0 -2
  636. package/dist/__tests__/rhi-backend-package-boundary.unit.test.d.ts.map +0 -1
  637. package/dist/__tests__/scriptable-pack-consumer.test-d.d.ts +0 -2
  638. package/dist/__tests__/scriptable-pack-consumer.test-d.d.ts.map +0 -1
  639. package/dist/__tests__/shader-stage-surface.unit.test.d.ts +0 -2
  640. package/dist/__tests__/shader-stage-surface.unit.test.d.ts.map +0 -1
  641. package/dist/__tests__/ssao-config.unit.test.d.ts +0 -2
  642. package/dist/__tests__/ssao-config.unit.test.d.ts.map +0 -1
  643. package/dist/__tests__/ssao-kernel-surface.unit.test.d.ts +0 -2
  644. package/dist/__tests__/ssao-kernel-surface.unit.test.d.ts.map +0 -1
  645. package/dist/__tests__/standard-clustered-pipeline.unit.test.d.ts +0 -2
  646. package/dist/__tests__/standard-clustered-pipeline.unit.test.d.ts.map +0 -1
  647. package/dist/__tests__/standard-descriptors.test-d.d.ts +0 -2
  648. package/dist/__tests__/standard-descriptors.test-d.d.ts.map +0 -1
  649. package/dist/__tests__/standard-pipeline.dawn.test.d.ts +0 -2
  650. package/dist/__tests__/standard-pipeline.dawn.test.d.ts.map +0 -1
  651. package/dist/__tests__/standard-pipeline.integration.test.d.ts +0 -2
  652. package/dist/__tests__/standard-pipeline.integration.test.d.ts.map +0 -1
  653. package/dist/__tests__/standard-profile-oracle.unit.test.d.ts +0 -2
  654. package/dist/__tests__/standard-profile-oracle.unit.test.d.ts.map +0 -1
  655. package/dist/__tests__/surface-configuration.unit.test.d.ts +0 -2
  656. package/dist/__tests__/surface-configuration.unit.test.d.ts.map +0 -1
  657. package/dist/__tests__/texture-usage-surface.unit.test.d.ts +0 -2
  658. package/dist/__tests__/texture-usage-surface.unit.test.d.ts.map +0 -1
  659. package/dist/__tests__/typed-pipeline-topology.unit.test.d.ts +0 -2
  660. package/dist/__tests__/typed-pipeline-topology.unit.test.d.ts.map +0 -1
  661. package/dist/__tests__/urp-pipeline-targets.unit.test.d.ts +0 -2
  662. package/dist/__tests__/urp-pipeline-targets.unit.test.d.ts.map +0 -1
  663. package/dist/__tests__/urp-pipeline.unit.test.d.ts +0 -2
  664. package/dist/__tests__/urp-pipeline.unit.test.d.ts.map +0 -1
  665. package/dist/__tests__/vertex-color-pipeline.unit.test.d.ts +0 -2
  666. package/dist/__tests__/vertex-color-pipeline.unit.test.d.ts.map +0 -1
  667. package/dist/__tests__/vertex-layout-consumer.unit.test.d.ts +0 -2
  668. package/dist/__tests__/vertex-layout-consumer.unit.test.d.ts.map +0 -1
  669. package/dist/__tests__/visibility-diagnostics.integration.test.d.ts +0 -2
  670. package/dist/__tests__/visibility-diagnostics.integration.test.d.ts.map +0 -1
  671. package/dist/__tests__/visibility-extract.integration.test.d.ts +0 -2
  672. package/dist/__tests__/visibility-extract.integration.test.d.ts.map +0 -1
  673. package/dist/__tests__/visibility-extract.perf.test.d.ts +0 -2
  674. package/dist/__tests__/visibility-extract.perf.test.d.ts.map +0 -1
  675. package/dist/__tests__/visibility-feature-dedup.integration.test.d.ts +0 -2
  676. package/dist/__tests__/visibility-feature-dedup.integration.test.d.ts.map +0 -1
  677. package/dist/__tests__/visibility-hidden-report.unit.test.d.ts +0 -2
  678. package/dist/__tests__/visibility-hidden-report.unit.test.d.ts.map +0 -1
  679. package/dist/__tests__/visibility-instances.integration.test.d.ts +0 -2
  680. package/dist/__tests__/visibility-instances.integration.test.d.ts.map +0 -1
  681. package/dist/__tests__/visibility-multi-world.integration.test.d.ts +0 -2
  682. package/dist/__tests__/visibility-multi-world.integration.test.d.ts.map +0 -1
  683. package/dist/__tests__/visibility-producer-matrix.integration.test.d.ts +0 -2
  684. package/dist/__tests__/visibility-producer-matrix.integration.test.d.ts.map +0 -1
  685. package/dist/__tests__/visibility-producer-matrix.unit.test.d.ts +0 -2
  686. package/dist/__tests__/visibility-producer-matrix.unit.test.d.ts.map +0 -1
  687. package/dist/__tests__/visibility-public-surface.test-d.d.ts +0 -2
  688. package/dist/__tests__/visibility-public-surface.test-d.d.ts.map +0 -1
  689. package/dist/__tests__/visibility-resolver.unit.test.d.ts +0 -2
  690. package/dist/__tests__/visibility-resolver.unit.test.d.ts.map +0 -1
  691. package/dist/__tests__/visibility-resource-short-circuit.unit.test.d.ts +0 -2
  692. package/dist/__tests__/visibility-resource-short-circuit.unit.test.d.ts.map +0 -1
  693. package/dist/__tests__/visibility-snapshot-contract.unit.test.d.ts +0 -2
  694. package/dist/__tests__/visibility-snapshot-contract.unit.test.d.ts.map +0 -1
  695. package/dist/__tests__/vocabulary.unit.test.d.ts +0 -2
  696. package/dist/__tests__/vocabulary.unit.test.d.ts.map +0 -1
  697. package/dist/chunk-CQXZC5XI.mjs.map +0 -1
  698. package/dist/chunk-QC4VW3XJ.mjs.map +0 -1
  699. package/dist/chunk-SCRSAK5B.mjs.map +0 -1
  700. package/dist/components/__tests__/schema-contract.unit.test.d.ts +0 -2
  701. package/dist/components/__tests__/schema-contract.unit.test.d.ts.map +0 -1
  702. package/dist/components/__tests__/schema-parity.unit.test.d.ts +0 -2
  703. package/dist/components/__tests__/schema-parity.unit.test.d.ts.map +0 -1
  704. package/dist/components/__tests__/visibility-schema.unit.test.d.ts +0 -2
  705. package/dist/components/__tests__/visibility-schema.unit.test.d.ts.map +0 -1
  706. package/dist/record/__tests__/material-bg-cache.unit.test.d.ts +0 -2
  707. package/dist/record/__tests__/material-bg-cache.unit.test.d.ts.map +0 -1
  708. package/dist/record/__tests__/material-slot-plan.unit.test.d.ts +0 -2
  709. package/dist/record/__tests__/material-slot-plan.unit.test.d.ts.map +0 -1
  710. package/dist/record/__tests__/typed-frame-graph-plan.unit.test.d.ts +0 -2
  711. package/dist/record/__tests__/typed-frame-graph-plan.unit.test.d.ts.map +0 -1
  712. package/dist/record/__tests__/video-texture-view.unit.test.d.ts +0 -2
  713. 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 {
@@ -153,9 +161,11 @@ import {
153
161
  type PipelineSpec,
154
162
  PipelineSpecError,
155
163
  } from '../pipeline-spec';
164
+ import { POINTS_LINES_MATERIAL_SHADER_ID } from '../points-lines/record';
156
165
  import type { FrameObservationOptions } from '../record/frame';
166
+ import type { BloomPersistentBundle } from '../record/render-context';
157
167
  import { SHADOW_CASTER_BUFFER_SIZE } from '../record/shadow-pass';
158
- import { VIEW_UNIFORM_BUFFER_SIZE } from '../record/view-ubo';
168
+ import { POINTS_LINES_VIEW_BUFFER_SIZE, VIEW_UNIFORM_BUFFER_SIZE } from '../record/view-ubo';
159
169
  import type {
160
170
  DrawOwnerOptions,
161
171
  FrameObservationRequest,
@@ -176,7 +186,7 @@ import type {
176
186
  RenderResult,
177
187
  RenderWorldLease,
178
188
  } from '../render-contract';
179
- import { STANDARD_TONEMAP_FEATURE_ID } from '../render-contract';
189
+ import { STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from '../render-contract';
180
190
  import {
181
191
  configureSurface,
182
192
  createRenderSystem,
@@ -193,6 +203,8 @@ import {
193
203
  import { tilemapChunkExtractSystem } from '../tilemap-chunk-extract-system';
194
204
  import { EngineEnvironmentError, type RhiBackendPack } from './backend-contract';
195
205
  import type { RendererAssemblyImplementation, RendererHostImplementation } from './host-contract';
206
+ import { projectRendererOutputInspection } from './renderer-inspection';
207
+ import { isRendererStateOperational } from './renderer-lifecycle';
196
208
 
197
209
  export { assembleMaterialProjection } from './material/assembly';
198
210
  export {
@@ -231,6 +243,9 @@ function loadRhiPack(
231
243
  const createShaderModule = mod.createShaderModule as
232
244
  | RhiBackendPack['createShaderModule']
233
245
  | undefined;
246
+ const createShaderModuleImmediate = mod.createShaderModuleImmediate as
247
+ | RhiBackendPack['createShaderModuleImmediate']
248
+ | undefined;
234
249
  const translateErrorEventToRhiError = mod.translateErrorEventToRhiError as
235
250
  | RhiBackendPack['translateErrorEventToRhiError']
236
251
  | undefined;
@@ -240,6 +255,7 @@ function loadRhiPack(
240
255
  return {
241
256
  rhi,
242
257
  ...(createShaderModule === undefined ? {} : { createShaderModule }),
258
+ ...(createShaderModuleImmediate === undefined ? {} : { createShaderModuleImmediate }),
243
259
  ...(translateErrorEventToRhiError === undefined ? {} : { translateErrorEventToRhiError }),
244
260
  ...(rawDeviceAccessor === undefined ? {} : { _internal_getRawDevice: rawDeviceAccessor }),
245
261
  ...(instrumentation === undefined ? {} : { instrumentation }),
@@ -476,20 +492,61 @@ export type LayoutKind = 'pbr' | 'pbr-skin' | 'hdrp-pbr' | 'sprite-urp' | 'unlit
476
492
  * Return every device-capability HDRP PBR variant that needs a lazy module
477
493
  * label seeded during renderer boot. Color availability is intentionally not
478
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.
479
497
  */
480
498
  export function selectHdrpPbrPrewarmVariants(
481
499
  manifestEntry: MaterialShaderManifestEntry | undefined,
482
500
  storageBufferCapable: boolean,
501
+ transmissionCapable: boolean,
483
502
  ): readonly MaterialShaderManifestEntry['variants'][number][] {
484
503
  return (
485
504
  manifestEntry?.variants.filter(
486
505
  (variant) =>
487
506
  variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
488
- variant.defines.CLUSTER_FORWARD_AVAILABLE === true,
507
+ variant.defines.CLUSTER_FORWARD_AVAILABLE === true &&
508
+ (transmissionCapable || variant.defines.TRANSMISSION_AVAILABLE !== true),
489
509
  ) ?? []
490
510
  );
491
511
  }
492
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
+
493
550
  /**
494
551
  * Select the PBR variant that matches a fallback mesh with no COLOR_0 input.
495
552
  * Unknown/custom material IDs may still use the legacy PBR fallback, so that
@@ -501,7 +558,9 @@ export function selectNoColorPbrVariant(
501
558
  variantSet: string | undefined,
502
559
  ): MaterialShaderManifestEntry['variants'][number] | undefined {
503
560
  const hdrpRequested =
504
- variantSet === '' || variantSet?.includes('CLUSTER_FORWARD_AVAILABLE=true') === true;
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'));
505
564
  return manifestEntry?.variants.find(
506
565
  (variant) =>
507
566
  variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
@@ -511,12 +570,7 @@ export function selectNoColorPbrVariant(
511
570
  );
512
571
  }
513
572
 
514
- export type MaterialShaderBindingContract =
515
- | 'group-0'
516
- | 'group-0-resource'
517
- | 'view-only'
518
- | 'view-and-scene-depth'
519
- | 'render-material';
573
+ export type MaterialShaderBindingContract = RenderFeatureMaterialShaderBindingContract;
520
574
  export type MaterialShaderVertexInputContract = 'none' | 'render-material';
521
575
 
522
576
  export function resolveMaterialShaderBindingContract(
@@ -631,6 +685,13 @@ export function selectPipelineLayoutForVariant(
631
685
  if (layoutKind === 'unlit-urp') {
632
686
  return state.pbrPipelineLayout;
633
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
+ }
634
695
  // HDRP variant matches when:
635
696
  // - layoutKind === 'hdrp-pbr' (caller-driven, future-proof), OR
636
697
  // - variantSet is the canonical all-true key '' (boot-time HDRP
@@ -684,7 +745,17 @@ export function resolveMaterialShaderVariantSet(
684
745
  ? backendKind === 'wgpu-webgl2'
685
746
  : axis === 'STORAGE_BUFFER_AVAILABLE'
686
747
  ? backendKind !== 'wgpu-webgl2' && storageBuffer
687
- : (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);
688
759
  }
689
760
  const sorted = Object.entries(filtered).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
690
761
  return sorted.every(([, value]) => value)
@@ -694,6 +765,21 @@ export function resolveMaterialShaderVariantSet(
694
765
  : sorted.map(([name, value]) => `${name}=${value}`).join('+');
695
766
  }
696
767
 
768
+ /**
769
+ * Keep a single-source material on its canonical module identity. A manifest
770
+ * entry with no variants cannot consume a synthesized capability or geometry
771
+ * axis, so carrying one into the pipeline cache only creates an uncached lazy
772
+ * compile beside the prewarmed source module.
773
+ */
774
+ export function normalizeMaterialShaderVariantSet(
775
+ requestedVariantSet: string | undefined,
776
+ manifestEntry: MaterialShaderManifestEntry | undefined,
777
+ ): string | undefined {
778
+ return manifestEntry !== undefined && manifestEntry.variants.length === 0
779
+ ? undefined
780
+ : requestedVariantSet;
781
+ }
782
+
697
783
  export async function createRenderer(
698
784
  canvas: unknown,
699
785
  options?: RendererOptions,
@@ -859,11 +945,32 @@ const COMPRESSION_FEATURES: GPUFeatureName[] = [
859
945
  'texture-compression-astc',
860
946
  ];
861
947
 
862
- function deviceOptionsForAdapter(
863
- adapter: Pick<RhiAdapter, 'features'>,
864
- ): { 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 {
865
960
  const requiredFeatures = COMPRESSION_FEATURES.filter((feature) => adapter.features.has(feature));
866
- 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
+ };
867
974
  }
868
975
 
869
976
  /**
@@ -1081,16 +1188,34 @@ async function makeWebGPURenderer(
1081
1188
  // compile across both paths (charter P4 consistent abstraction +
1082
1189
  // architecture-principles #6 idempotency).
1083
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;
1084
1196
  const getShaderModuleAdapter = (): ShaderDeviceAdapterInternal => {
1085
1197
  if (sharedShaderModuleAdapter === null) {
1086
1198
  sharedShaderModuleAdapter = makeShaderDeviceAdapter(
1087
1199
  internals.device,
1088
1200
  internals.errorRegistry,
1089
1201
  internals.pack.createShaderModule,
1202
+ internals.pack.createShaderModuleImmediate,
1090
1203
  );
1091
1204
  }
1092
1205
  return sharedShaderModuleAdapter;
1093
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
+ };
1094
1219
  const getShader = (): ShaderCatalog => {
1095
1220
  if (shaderInstance === null) {
1096
1221
  // feat-20260608-create-app-param-surface-trim / M2 / D-2 q5-A:
@@ -1310,13 +1435,13 @@ async function makeWebGPURenderer(
1310
1435
  >();
1311
1436
  const preparedMaterialPipelineLayoutCache = new Map<string, PipelineLayout>();
1312
1437
  // feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): per-shader material
1313
- // BGL + pipeline layout cache. A custom material shader declaring >3
1314
- // user-region textures (e.g. parallax `heightTexture`) needs a material BGL
1315
- // whose user-region + injection start differ from the shared built-in
1316
- // derived shared layout, plus a pipeline layout [view, perShaderMaterialBgl,
1317
- // meshArray, instances] that the PSO builds against. Built lazily on first
1318
- // request and cached by shaderId; 4-or-fewer-texture shaders never enter the cache
1319
- // (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.
1320
1445
  type PerShaderMaterialLayout = {
1321
1446
  materialBgl: BindGroupLayout;
1322
1447
  pipelineLayout: PipelineLayout;
@@ -1327,6 +1452,16 @@ async function makeWebGPURenderer(
1327
1452
  readonly layout: PerShaderMaterialLayout | null;
1328
1453
  };
1329
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';
1330
1465
  // Material binding contracts are derived from WGSL source, but the lookup
1331
1466
  // is also used by the per-submesh bind-group path. Cache the derived value
1332
1467
  // per renderer so a frame does not re-run the comment stripping and regex
@@ -1461,7 +1596,9 @@ async function makeWebGPURenderer(
1461
1596
  if (cached !== undefined) return cached;
1462
1597
  if (pipelineState === null) return null;
1463
1598
  const materialBgl =
1464
- getOrBuildPerShaderMaterialLayout(materialShaderId)?.materialBgl ??
1599
+ (isEngineOwnedMaterialShader(materialShaderId)
1600
+ ? undefined
1601
+ : getOrBuildPerShaderMaterialLayout(materialShaderId)?.materialBgl) ??
1465
1602
  pipelineState.materialBindGroupLayout;
1466
1603
  const result = internals.device.createPipelineLayout({
1467
1604
  label: `prepared-material-pl-${materialShaderId}`,
@@ -1474,15 +1611,15 @@ async function makeWebGPURenderer(
1474
1611
  preparedMaterialPipelineLayoutCache.set(materialShaderId, result.value);
1475
1612
  return result.value;
1476
1613
  };
1477
- // Returns the per-shader { materialBgl, pipelineLayout } for a custom shader
1478
- // whose user-region texture count exceeds the built-in 4, or null when the
1479
- // shader resolves to the shared built-in layout (4 textures) / is
1480
- // unregistered / pipelineState is not ready. Pure-ish: builds + caches on
1481
- // miss via the same buildBindGroupLayoutDescriptor SSOT (kind
1482
- // '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.
1483
1619
  const getOrBuildPerShaderMaterialLayout = (
1484
1620
  materialShaderId: string,
1485
1621
  ): PerShaderMaterialLayout | null => {
1622
+ if (isEngineOwnedMaterialShader(materialShaderId)) return null;
1486
1623
  const lookup = getShader().findMaterialArtifact(materialShaderId);
1487
1624
  if (!lookup.ok) return null;
1488
1625
  const cached = perShaderMaterialLayoutCache.get(materialShaderId);
@@ -1491,16 +1628,6 @@ async function makeWebGPURenderer(
1491
1628
  }
1492
1629
  if (pipelineState === null) return null;
1493
1630
  const paramSchema = lookup.value.paramSchema;
1494
- // 4-or-fewer user-region textures derive to the same shape as the shared
1495
- // built-in BGL -> reuse it (D-2; no per-shader entry).
1496
- if (derive(paramSchema).textureFieldNames.size <= 4) {
1497
- perShaderMaterialLayoutCache.set(materialShaderId, {
1498
- source: lookup.value.source,
1499
- paramSchema,
1500
- layout: null,
1501
- });
1502
- return null;
1503
- }
1504
1631
  const spec: PipelineSpec = {
1505
1632
  shader: { id: materialShaderId, passKind: 'forward', variantSet: undefined },
1506
1633
  attachments: { colorFormats: [], depthFormat: undefined, sampleCount: 1 },
@@ -1587,9 +1714,11 @@ async function makeWebGPURenderer(
1587
1714
  // `internals.device` / `internals.pack` are read live, so a rebuild after a
1588
1715
  // device swap compiles against the new device.
1589
1716
  let tonemapRegistered = false;
1590
- const buildPipeline = (): Promise<PipelineState> =>
1717
+ let temporalPostProcessesRegistered = false;
1718
+ const buildPipeline = (scope: DeviceScope = activeDeviceScope): Promise<PipelineState> =>
1591
1719
  buildReadyWebGPU(
1592
1720
  internals.device,
1721
+ scope,
1593
1722
  getShader,
1594
1723
  gpuStore,
1595
1724
  internals.pack.createShaderModule,
@@ -1628,13 +1757,31 @@ async function makeWebGPURenderer(
1628
1757
  // tonemap into this channel each frame (render-system-extract.ts w13).
1629
1758
  (source: string) => {
1630
1759
  if (tonemapRegistered) return;
1631
- renderSystem.registerBuiltinPostProcess(STANDARD_TONEMAP_FEATURE_ID, {
1760
+ renderSystem.registerBuiltinPostProcess(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, {
1632
1761
  source,
1633
1762
  params: { byteSize: 16, defaultValue: new Uint8Array(16) },
1634
1763
  reads: ['hdrColor'],
1635
1764
  });
1636
1765
  tonemapRegistered = true;
1637
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
+ },
1638
1785
  );
1639
1786
  const ready: Promise<Result<void, RhiError>> = buildPipeline().then(
1640
1787
  (state): Result<void, RhiError> => {
@@ -1714,6 +1861,8 @@ async function makeWebGPURenderer(
1714
1861
  meshAttributes?: VertexAttributeMap,
1715
1862
  vertexLayout?: string,
1716
1863
  vertexLayoutProjection?: VertexLayoutProjection,
1864
+ shaderModuleMode?: RenderFeatureShaderModuleMode,
1865
+ layoutKindOverride?: LayoutKind,
1717
1866
  ) => {
1718
1867
  // bug-20260611: derive LayoutKind from materialShaderId (caller-driven)
1719
1868
  // so the selector body stays free of literal shader-id strings (AC-09).
@@ -1725,17 +1874,17 @@ async function makeWebGPURenderer(
1725
1874
  // through the HDRP layout branch of the selector — sprite shaders have
1726
1875
  // no HDRP variant, plan-strategy R-1' guard.
1727
1876
  const layoutKind: LayoutKind | undefined =
1728
- materialShaderId === SKIN_MATERIAL_SHADER_ID
1877
+ layoutKindOverride ??
1878
+ (materialShaderId === SKIN_MATERIAL_SHADER_ID ||
1879
+ isSkinnedShadowCasterVariant(materialShaderId, variantSet)
1729
1880
  ? 'pbr-skin'
1730
1881
  : materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit'
1731
1882
  ? 'sprite-urp'
1732
1883
  : materialShaderId === 'forgeax::default-unlit'
1733
1884
  ? 'unlit-urp'
1734
- : undefined;
1885
+ : undefined);
1735
1886
  const usesSharedBootMaterialLayout =
1736
- materialShaderId === 'forgeax::default-standard-pbr' ||
1737
- materialShaderId === 'forgeax::default-unlit' ||
1738
- materialShaderId === SKIN_MATERIAL_SHADER_ID;
1887
+ materialShaderId !== undefined && isEngineOwnedMaterialShader(materialShaderId);
1739
1888
  // Built-in PBR/unlit/skin layouts are assembled at boot. Re-deriving a
1740
1889
  // per-shader layout from the expanded PBR schema would bypass the HDRP
1741
1890
  // group(2) selector and bind the URP mesh BGL to the cluster pipeline.
@@ -1743,12 +1892,11 @@ async function makeWebGPURenderer(
1743
1892
  !usesSharedBootMaterialLayout && layoutKind !== 'pbr-skin' && materialShaderId !== undefined
1744
1893
  ? getOrBuildPerShaderMaterialLayout(materialShaderId)
1745
1894
  : null;
1746
- // feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): a custom shader
1747
- // with >3 user-region textures owns a per-shader pipeline layout (its
1748
- // material BGL is wider than the shared built-in 18-entry). Skin / HDRP
1749
- // keep their dedicated layouts; everything else falls back to the shared
1750
- // URP/HDRP selector. 3-texture customs return null here -> shared layout
1751
- // (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.
1752
1900
  const bindingContract =
1753
1901
  materialShaderId === undefined
1754
1902
  ? 'render-material'
@@ -1884,9 +2032,13 @@ async function makeWebGPURenderer(
1884
2032
  ] satisfies readonly GPUVertexBufferLayout[]);
1885
2033
  return {
1886
2034
  device: internals.device,
1887
- shaderModuleFactory: getShaderModuleAdapter(),
2035
+ shaderModuleFactory:
2036
+ shaderModuleMode === 'immediate'
2037
+ ? getImmediateShaderModuleAdapter()
2038
+ : getShaderModuleAdapter(),
1888
2039
  pipelineLayout,
1889
2040
  vertexBuffers,
2041
+ ...(layoutKind === 'pbr-skin' ? { layoutKind } : {}),
1890
2042
  };
1891
2043
  };
1892
2044
  /**
@@ -1944,6 +2096,8 @@ async function makeWebGPURenderer(
1944
2096
  depthFormatOverride?: GPUTextureFormat | null,
1945
2097
  vertexLayout?: string,
1946
2098
  vertexLayoutProjection?: VertexLayoutProjection,
2099
+ shaderModuleMode?: RenderFeatureShaderModuleMode,
2100
+ layoutKindOverride?: LayoutKind,
1947
2101
  ): RenderPipeline | null => {
1948
2102
  const ctx = buildPipelineContext(
1949
2103
  variantSet,
@@ -1951,6 +2105,8 @@ async function makeWebGPURenderer(
1951
2105
  meshAttributes,
1952
2106
  vertexLayout,
1953
2107
  vertexLayoutProjection,
2108
+ shaderModuleMode,
2109
+ layoutKindOverride,
1954
2110
  );
1955
2111
  if (ctx === null) return null;
1956
2112
  // bug-20260612: route the LDR color format through pipelineState so the
@@ -1960,6 +2116,11 @@ async function makeWebGPURenderer(
1960
2116
  // the explicit null check).
1961
2117
  if (pipelineState === null) return null;
1962
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;
1963
2124
  const built = buildPipelineForMaterialShader(
1964
2125
  cacheKey,
1965
2126
  // The catalog entry is projected into the material-shader entry shape.
@@ -1993,7 +2154,7 @@ async function makeWebGPURenderer(
1993
2154
  }
1994
2155
  : undefined,
1995
2156
  undefined, // vertexEntry — default vs_main
1996
- undefined, // fragmentEntry — default fs_main
2157
+ fragmentEntry,
1997
2158
  undefined, // defines — none
1998
2159
  passKind,
1999
2160
  sampleCount,
@@ -2052,6 +2213,8 @@ async function makeWebGPURenderer(
2052
2213
  depthFormatOverride?: GPUTextureFormat | null,
2053
2214
  vertexLayout?: string,
2054
2215
  vertexLayoutProjection?: VertexLayoutProjection,
2216
+ shaderModuleMode: RenderFeatureShaderModuleMode = 'validated',
2217
+ layoutKindOverride?: LayoutKind,
2055
2218
  ): RenderPipeline | null => {
2056
2219
  // WebGL2 is a downlevel backend even when the record stage asks for the
2057
2220
  // native-capability variant string. Resolve the variant axes at this
@@ -2066,14 +2229,23 @@ async function makeWebGPURenderer(
2066
2229
  // (stored in materialShaderUvSetCounts during prepareMaterialShaders).
2067
2230
  const resolvedUvSetCount = shaderUvSetCount ?? materialShaderUvSetCounts.get(materialShaderId);
2068
2231
  const manifestEntry = findMaterialShaderManifestEntry(materialShaderId);
2069
- 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);
2070
2240
  // The record stage also composes engine capability axes for user shaders.
2071
2241
  // Keep only manifest-declared axes for non-engine material IDs: a plain
2072
2242
  // custom shader must not collapse that request to '' because '' is the
2073
2243
  // canonical HDRP variant key and would pair an HDRP PSO with the URP mesh
2074
2244
  // bind group. Builtin forgeax:: shaders keep the compatibility request
2075
2245
  // when a reduced test/legacy manifest omits variant metadata.
2076
- let effectiveVariantSet = resolvedVariantSet;
2246
+ // A single-source manifest entry must stay on its canonical module key;
2247
+ // otherwise a synthesized capability axis races the prewarmed module.
2248
+ let effectiveVariantSet = normalizeMaterialShaderVariantSet(resolvedVariantSet, manifestEntry);
2077
2249
  if (
2078
2250
  vertexLayoutProjection === undefined &&
2079
2251
  manifestEntry?.variants.some((variant) => 'VERTEX_COLOR_AVAILABLE' in variant.defines)
@@ -2209,6 +2381,7 @@ async function makeWebGPURenderer(
2209
2381
  depthFormatOverride,
2210
2382
  vertexLayout,
2211
2383
  vertexLayoutProjection,
2384
+ shaderModuleMode,
2212
2385
  );
2213
2386
  }
2214
2387
  const pbrEntry = findStandardPbrEntry(effectiveVariantSet);
@@ -2262,6 +2435,7 @@ async function makeWebGPURenderer(
2262
2435
  depthFormatOverride,
2263
2436
  vertexLayout,
2264
2437
  vertexLayoutProjection,
2438
+ shaderModuleMode,
2265
2439
  );
2266
2440
  }
2267
2441
  // feat-20260609 M4 / w31: resolve variant WGSL from manifest when
@@ -2352,6 +2526,8 @@ async function makeWebGPURenderer(
2352
2526
  depthFormatOverride,
2353
2527
  vertexLayout,
2354
2528
  vertexLayoutProjection,
2529
+ shaderModuleMode,
2530
+ layoutKindOverride,
2355
2531
  );
2356
2532
  };
2357
2533
  const getParamSchema = (materialShaderId: string) => {
@@ -2360,10 +2536,11 @@ async function makeWebGPURenderer(
2360
2536
  };
2361
2537
  // feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): expose the per-shader
2362
2538
  // material BGL so the record stage creates the material bind group against
2363
- // the matching layout (empty for group-0 shaders, wider for >3-texture
2364
- // custom shaders). Returns undefined for the ordinary 3-texture path so the
2365
- // 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.
2366
2542
  const getMaterialBindGroupLayout = (materialShaderId: string): BindGroupLayout | undefined => {
2543
+ if (isEngineOwnedMaterialShader(materialShaderId)) return undefined;
2367
2544
  const contract = getCachedMaterialShaderBindingContract(materialShaderId);
2368
2545
  if (contract === 'group-0') {
2369
2546
  return getOrBuildGroup0MaterialLayout()?.materialBgl;
@@ -2440,7 +2617,11 @@ async function makeWebGPURenderer(
2440
2617
  fragment: {
2441
2618
  module: moduleResult.value,
2442
2619
  entryPoint: 'fs_main',
2443
- 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 }],
2444
2625
  },
2445
2626
  primitive: { topology: 'triangle-list', cullMode: 'none', frontFace: 'ccw' },
2446
2627
  depthStencil: undefined,
@@ -2469,6 +2650,10 @@ async function makeWebGPURenderer(
2469
2650
  shaderModuleFactory: {
2470
2651
  createShaderModule: (descriptor) => getShaderModuleAdapter().createShaderModule(descriptor),
2471
2652
  },
2653
+ immediateShaderModuleFactory: {
2654
+ createShaderModule: (descriptor) =>
2655
+ getImmediateShaderModuleAdapter().createShaderModule(descriptor),
2656
+ },
2472
2657
  profiler: internals.options?.profiler,
2473
2658
  // feat-20260622-s5 M3 / w17: device + context read live off `internals` via
2474
2659
  // getters so the recover() rebuild (which swaps internals.device /
@@ -2481,6 +2666,9 @@ async function makeWebGPURenderer(
2481
2666
  get deviceScope() {
2482
2667
  return activeDeviceScope;
2483
2668
  },
2669
+ invalidateShaderModule: (label: string) => {
2670
+ sharedShaderModuleAdapter?.invalidateModule(label);
2671
+ },
2484
2672
  get context() {
2485
2673
  return internals.context;
2486
2674
  },
@@ -2518,6 +2706,18 @@ async function makeWebGPURenderer(
2518
2706
  return internals.meshSsboState;
2519
2707
  },
2520
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
+ }
2521
2721
  // The Standard pipeline is the sole built-in graph owner. Feature plans are
2522
2722
  // projected into that graph directly; no second post-process registry is
2523
2723
  // installed for fullscreen producers.
@@ -2578,6 +2778,22 @@ async function makeWebGPURenderer(
2578
2778
  : health.reason === 'internal-fault' || !readySettled
2579
2779
  ? 'faulted'
2580
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
+ });
2581
2797
  return Object.freeze({
2582
2798
  state,
2583
2799
  surface: surfaceReleased ? 'released' : 'available',
@@ -2590,9 +2806,22 @@ async function makeWebGPURenderer(
2590
2806
  features: Object.freeze(
2591
2807
  (internals.featureHost?.features ?? []).map((feature) => feature.identity),
2592
2808
  ),
2809
+ featureDiagnostics: internals.featureHost?.diagnostics() ?? Object.freeze([]),
2593
2810
  frustumStats: Object.freeze({ ...renderSystem.frustumStats }),
2594
2811
  visibilityStats: Object.freeze({ ...renderSystem.visibilityStats }),
2595
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 }),
2596
2825
  meshMaterialBindings: Object.freeze(
2597
2826
  renderSystem.meshMaterialBindings.map((observation) =>
2598
2827
  Object.freeze({
@@ -2611,6 +2840,22 @@ async function makeWebGPURenderer(
2611
2840
  createBindGroup: renderSystem.bindGroupCounts.createBindGroup,
2612
2841
  keys: Object.freeze([...renderSystem.bindGroupCounts.keys]),
2613
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
+ }),
2614
2859
  });
2615
2860
  },
2616
2861
  get device(): RhiDevice {
@@ -2701,6 +2946,12 @@ async function makeWebGPURenderer(
2701
2946
  observeCurrentFrame(options: FrameObservationOptions) {
2702
2947
  return renderSystem.observeCurrentFrame(options);
2703
2948
  },
2949
+ getCurrentGraphTarget(name: string) {
2950
+ return renderSystem.getCurrentGraphTarget(name);
2951
+ },
2952
+ requestGraphTargetCapture(request) {
2953
+ renderSystem.requestGraphTargetCapture(request);
2954
+ },
2704
2955
  get frustumStats() {
2705
2956
  return renderSystem.frustumStats;
2706
2957
  },
@@ -2972,7 +3223,12 @@ async function makeWebGPURenderer(
2972
3223
  // Configure context lazily on first draw (D-S1 single-point
2973
3224
  // exemption): GPUCanvasContext.configure({device}) needs a raw
2974
3225
  // The canvas context is configured through the RHI device scope.
2975
- ensureContextConfigured(internals, pipelineState, internals.errorRegistry);
3226
+ const contextConfigured = ensureContextConfigured(
3227
+ internals,
3228
+ pipelineState,
3229
+ internals.errorRegistry,
3230
+ );
3231
+ if (!contextConfigured.ok) return contextConfigured;
2976
3232
  // w24 — facade-level try/catch produces Result.err on unexpected throw
2977
3233
  // (D-P6 dual-channel preserved: per-stage RhiError continues to fan out
2978
3234
  // through onError separately; the facade Result is the synchronous
@@ -3140,13 +3396,6 @@ async function makeWebGPURenderer(
3140
3396
  if (disposed) return ok(undefined);
3141
3397
  disposed = true;
3142
3398
  const cleanupFailures: RendererOperationCause[] = [];
3143
- try {
3144
- activeDeviceScope.dispose();
3145
- } catch (cause) {
3146
- const error = wrapDisposeError(cause, 'activeDeviceScope.dispose');
3147
- cleanupFailures.push(error);
3148
- internals.errorRegistry.fire(error);
3149
- }
3150
3399
  for (const world of attachedWorlds) {
3151
3400
  try {
3152
3401
  const derivedSystemName = derivedSystemNames.get(world);
@@ -3228,6 +3477,18 @@ async function makeWebGPURenderer(
3228
3477
  cleanupFailures.push(error);
3229
3478
  internals.errorRegistry.fire(error);
3230
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
+ }
3231
3492
  // Step 5: the IBL cache is keyed by the retired DeviceScope generation.
3232
3493
  // It is no longer reachable through the active renderer scope, while
3233
3494
  // the device teardown owns the child GPU handles.
@@ -3307,6 +3568,7 @@ async function makeWebGPURenderer(
3307
3568
  // buffer (or crash the browser GPU process). Keep the ShaderCatalog's
3308
3569
  // CPU material/source catalogue, but rebuild this device-bound adapter.
3309
3570
  sharedShaderModuleAdapter = null;
3571
+ sharedImmediateShaderModuleAdapter = null;
3310
3572
  // The fullscreen post-process builder keeps one empty group-0 layout
3311
3573
  // outside RenderSystem's frame cache. It is also device-bound, so retain
3312
3574
  // the source/registration but force the next post-process pass to derive
@@ -3353,6 +3615,13 @@ async function makeWebGPURenderer(
3353
3615
  // swap is observed without reconstructing the RenderSystem.
3354
3616
  internals.device = device;
3355
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
+ );
3356
3625
  // Re-attach the device.lost fan-out to the new device's lost Promise,
3357
3626
  // reusing the SAME registries the host already subscribed to (SSOT
3358
3627
  // helper). When this device is lost again, health() flips to
@@ -3400,10 +3669,13 @@ async function makeWebGPURenderer(
3400
3669
  // the first post-recovery upload; unlike CPU asset payloads, these
3401
3670
  // handles cannot survive a device replacement.
3402
3671
  dynamicTextureStore.configureGpuDevice(adaptDynamicTextureDevice(internals.device));
3672
+ let candidatePipelineState: PipelineState | undefined;
3403
3673
  const abortDisposedRecovery = (): Result<void, RecoverFailure> | undefined => {
3404
3674
  if (!disposed) return undefined;
3405
3675
  gpuStore.destroyAll();
3676
+ candidatePipelineState?.perPassResources.drainBloomResources?.();
3406
3677
  renderSystem.disposeFrameState();
3678
+ recoveryScope.abandon();
3407
3679
  materialShaderPipelineCache.clear();
3408
3680
  perShaderMaterialLayoutCache.clear();
3409
3681
  group0ResourceLayouts.clear();
@@ -3422,7 +3694,7 @@ async function makeWebGPURenderer(
3422
3694
  );
3423
3695
  const abortedAfterShaders = abortDisposedRecovery();
3424
3696
  if (abortedAfterShaders !== undefined) return abortedAfterShaders;
3425
- pipelineState = await buildPipeline();
3697
+ candidatePipelineState = await buildPipeline(recoveryScope);
3426
3698
  const abortedAfterPipeline = abortDisposedRecovery();
3427
3699
  if (abortedAfterPipeline !== undefined) return abortedAfterPipeline;
3428
3700
  renderSystem.restorePostProcessResources();
@@ -3430,6 +3702,8 @@ async function makeWebGPURenderer(
3430
3702
  // Pipeline rebuild failed against the new device. Treat as a device
3431
3703
  // unavailability (the device was acquired but is not usable); health
3432
3704
  // stays `device-lost` so the host can retry.
3705
+ candidatePipelineState?.perPassResources.drainBloomResources?.();
3706
+ recoveryScope.abandon();
3433
3707
  const aborted = abortDisposedRecovery();
3434
3708
  if (aborted !== undefined) return aborted;
3435
3709
  return err(new RecoverError('recover-device-unavailable'));
@@ -3437,27 +3711,18 @@ async function makeWebGPURenderer(
3437
3711
  const abortedAfterPostProcess = abortDisposedRecovery();
3438
3712
  if (abortedAfterPostProcess !== undefined) return abortedAfterPostProcess;
3439
3713
 
3440
- const replacementResult = await activeDeviceScope.replace(
3441
- activeDeviceScope.generation + 1,
3442
- [],
3443
- );
3444
- if (!replacementResult.ok) {
3445
- const detail = replacementResult.error.primary.detail;
3446
- return err(
3447
- new RenderRecoveryError(
3448
- disposed ? 'recover-disposed-during-rebuild' : 'recover-lifecycle-failed',
3449
- {
3450
- owner: detail.owner,
3451
- generation: detail.generation,
3452
- recovery: disposed ? 'stop' : 'recover',
3453
- receipt: replacementResult.error.receipt,
3454
- cause: replacementResult.error,
3455
- },
3456
- ),
3457
- );
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'));
3458
3720
  }
3459
- activeDeviceScope = replacementResult.value;
3721
+ const previousDeviceScope = activeDeviceScope;
3722
+ activeDeviceScope = recoveryScope;
3723
+ pipelineState = candidatePipelineState;
3460
3724
  gpuStore.bindDeviceScope(activeDeviceScope);
3725
+ previousDeviceScope.retire();
3461
3726
 
3462
3727
  // Re-evaluate feature capability gates against the replacement device.
3463
3728
  // Feature recovery is isolated from renderer recovery: a feature that is
@@ -3555,7 +3820,7 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
3555
3820
  );
3556
3821
  },
3557
3822
  setProfile: (profile) => {
3558
- if (currentState !== 'alive') {
3823
+ if (!isRendererStateOperational(currentState)) {
3559
3824
  return err(
3560
3825
  new RendererOperationError('renderer-state-invalid', {
3561
3826
  operation: 'set-profile',
@@ -3569,6 +3834,13 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
3569
3834
  draw: (request) => {
3570
3835
  const result = renderer.drawFrame(request);
3571
3836
  if (!result.ok) return err(drawError(result.error));
3837
+ emit(
3838
+ Object.freeze({
3839
+ kind: 'frame-submitted',
3840
+ frameId: result.value.frameId,
3841
+ deviceGeneration: result.value.deviceGeneration,
3842
+ }),
3843
+ );
3572
3844
  return ok(result.value);
3573
3845
  },
3574
3846
  inspect: () => renderer.inspect(),
@@ -3621,7 +3893,7 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
3621
3893
  const error = new RendererOperationError('recovery-failed', {
3622
3894
  operation: 'recover',
3623
3895
  oldGeneration,
3624
- cause: result.error,
3896
+ cause: structuredRendererCause(result.error, 'recover'),
3625
3897
  });
3626
3898
  emit(Object.freeze({ kind: 'error', error }));
3627
3899
  return err(error);
@@ -3664,13 +3936,24 @@ function structuredRendererCause(cause: unknown, operation: string): RendererOpe
3664
3936
  typeof candidate.expected === 'string' &&
3665
3937
  typeof candidate.hint === 'string'
3666
3938
  ) {
3667
- 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
+ };
3668
3945
  }
3669
3946
  }
3670
- return new RendererContractFailureError(
3947
+ const contractFailure = new RendererContractFailureError(
3671
3948
  'draw',
3672
3949
  `${operation}: ${cause instanceof Error ? cause.message : String(cause)}`,
3673
3950
  );
3951
+ return {
3952
+ code: contractFailure.code,
3953
+ expected: contractFailure.expected,
3954
+ hint: contractFailure.hint,
3955
+ detail: contractFailure.detail,
3956
+ };
3674
3957
  }
3675
3958
 
3676
3959
  const RENDER_PROFILE_KEYS = new Set([
@@ -3681,10 +3964,6 @@ const RENDER_PROFILE_KEYS = new Set([
3681
3964
  'pbr',
3682
3965
  'ibl',
3683
3966
  'ssao',
3684
- 'bloom',
3685
- 'tone',
3686
- 'antialias',
3687
- 'sky',
3688
3967
  'fallback',
3689
3968
  'postStages',
3690
3969
  ]);
@@ -3705,32 +3984,13 @@ function validateRenderProfile(profile: RenderProfile): string | undefined {
3705
3984
  if (profile.shadows !== 'off' && profile.shadows !== 'hard' && profile.shadows !== 'filtered') {
3706
3985
  return "RenderProfile.shadows must be 'off', 'hard', or 'filtered'";
3707
3986
  }
3708
- if (
3709
- profile.tone !== 'none' &&
3710
- profile.tone !== 'aces-filmic' &&
3711
- profile.tone !== 'agx' &&
3712
- profile.tone !== 'cineon' &&
3713
- profile.tone !== 'linear' &&
3714
- profile.tone !== 'neutral' &&
3715
- profile.tone !== 'reinhard' &&
3716
- profile.tone !== 'reinhard-extended'
3717
- ) {
3718
- return 'RenderProfile.tone is not a supported Standard tone';
3719
- }
3720
- if (
3721
- profile.antialias !== 'none' &&
3722
- profile.antialias !== 'fxaa' &&
3723
- profile.antialias !== 'msaa'
3724
- ) {
3725
- return "RenderProfile.antialias must be 'none', 'fxaa', or 'msaa'";
3726
- }
3727
3987
  if (profile.fallback !== 'native' && profile.fallback !== 'cpu-webgl2') {
3728
3988
  return "RenderProfile.fallback must be 'native' or 'cpu-webgl2'";
3729
3989
  }
3730
- for (const field of ['pbr', 'ibl', 'ssao', 'bloom', 'sky'] as const) {
3990
+ for (const field of ['pbr', 'ibl', 'ssao'] as const) {
3731
3991
  if (typeof profile[field] !== 'boolean') return `RenderProfile.${field} must be boolean`;
3732
3992
  }
3733
- const stages = ['transparent-blend', 'bloom', 'tone', 'fxaa', 'output'] as const;
3993
+ const stages = STANDARD_POST_STAGE_NAMES;
3734
3994
  if (
3735
3995
  !Array.isArray(profile.postStages) ||
3736
3996
  profile.postStages.length !== stages.length ||
@@ -3781,8 +4041,11 @@ interface ShaderDeviceAdapterInternal extends ShaderCatalogDevice {
3781
4041
  * `module-forgeax::default-shadow-caster` returns OK on frame 1.
3782
4042
  */
3783
4043
  seedModule(label: string, module: ShaderModule): void;
4044
+ invalidateModule(label: string): void;
3784
4045
  }
3785
4046
 
4047
+ type ShaderModuleAdapterMode = 'validated' | 'immediate';
4048
+
3786
4049
  function makeShaderDeviceAdapter(
3787
4050
  rhiDevice: RhiDevice,
3788
4051
  _errorRegistry: RhiErrorListenerRegistry,
@@ -3792,10 +4055,18 @@ function makeShaderDeviceAdapter(
3792
4055
  desc: { code: string; label?: string | undefined },
3793
4056
  ) => Promise<Result<ShaderModule, RhiError>>)
3794
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',
3795
4065
  ): ShaderDeviceAdapterInternal {
3796
4066
  const moduleCache = new Map<string, ShaderModule>();
3797
4067
  const errorCache = new Map<string, RhiError>();
3798
- const pending = new Set<string>();
4068
+ const pending = new Map<string, number>();
4069
+ const epochs = new Map<string, number>();
3799
4070
 
3800
4071
  return {
3801
4072
  createShaderModule(desc): Result<ShaderModule, RhiError> {
@@ -3805,18 +4076,39 @@ function makeShaderDeviceAdapter(
3805
4076
  const cachedError = errorCache.get(key);
3806
4077
  if (cachedError !== undefined) return err(cachedError);
3807
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
+
3808
4098
  // Fire async creation; first sync call returns pending error (the AI consumer retry pattern).
3809
4099
  // M3 D-P4: rhi-webgpu supplies the async factory; rhi-wgpu (no top-level
3810
4100
  // async factory) falls back to the synchronous device.createShaderModule
3811
4101
  // entry which already returns Result<ShaderModule, RhiError>.
3812
4102
  if (!pending.has(key)) {
3813
- pending.add(key);
4103
+ const epoch = epochs.get(key) ?? 0;
4104
+ pending.set(key, epoch);
3814
4105
  const desc2: { code: string; label?: string | undefined } = { code: desc.code };
3815
4106
  if (desc.label !== undefined) desc2.label = desc.label;
3816
4107
  const asyncResult: Promise<Result<ShaderModule, RhiError>> = asyncCreateShaderModule
3817
4108
  ? asyncCreateShaderModule(rhiDevice, desc2)
3818
4109
  : invokeDeviceCreateShaderModule(rhiDevice, desc2);
3819
4110
  void asyncResult.then((result) => {
4111
+ if (pending.get(key) !== epoch || (epochs.get(key) ?? 0) !== epoch) return;
3820
4112
  if (result.ok) {
3821
4113
  moduleCache.set(key, result.value);
3822
4114
  } else {
@@ -3831,6 +4123,12 @@ function makeShaderDeviceAdapter(
3831
4123
  seedModule(label: string, module: ShaderModule): void {
3832
4124
  moduleCache.set(label, module);
3833
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
+ },
3834
4132
  };
3835
4133
  }
3836
4134
 
@@ -4076,7 +4374,11 @@ const HDR_COLOR_ATTACHMENT_FORMAT: GPUTextureFormat = 'rgba16float';
4076
4374
  // so it is intentionally absent here. A `mipmap:true` texture in an
4077
4375
  // un-prewarmed format surfaces a structured RhiError at record (never an
4078
4376
  // async stall) -- the falsifiable anchor for an incomplete prewarm list.
4079
- const MIPMAP_PREWARM_FORMATS: readonly GPUTextureFormat[] = ['rgba8unorm-srgb', 'rgba8unorm'];
4377
+ const MIPMAP_PREWARM_FORMATS: readonly GPUTextureFormat[] = [
4378
+ 'rgba8unorm-srgb',
4379
+ 'rgba8unorm',
4380
+ 'rgba16float',
4381
+ ];
4080
4382
 
4081
4383
  // feat-20260531-bloom-first-declarative-render-graph-pass / w13:
4082
4384
  // UBO sizes for bloom params (std140 layout).
@@ -4490,6 +4792,13 @@ async function prepareMaterialShaders(
4490
4792
  const variantDefines: Record<string, boolean> = {
4491
4793
  STORAGE_BUFFER_AVAILABLE: storageBufferCapable,
4492
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
+ }
4493
4802
  // Only include CLUSTER_FORWARD_AVAILABLE when the material shader declares
4494
4803
  // the axis (has a variant with this key), preserving backward compat for
4495
4804
  // entries without the pragma.
@@ -4499,6 +4808,12 @@ async function prepareMaterialShaders(
4499
4808
  if (msEntry.variants.some((v) => 'VERTEX_COLOR_AVAILABLE' in v.defines)) {
4500
4809
  variantDefines.VERTEX_COLOR_AVAILABLE = false;
4501
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
+ }
4502
4817
  // bug-20260708 M2 (a): PER_INSTANCE_REGION axis default = false at boot.
4503
4818
  // Per-entity spritePH path (`main-pass-sprite-draws.ts:511-513`) expects
4504
4819
  // `variantSet=undefined` to resolve to the PIR=false shader source (64B
@@ -4513,6 +4828,9 @@ async function prepareMaterialShaders(
4513
4828
  if (msEntry.variants.some((v) => 'PER_INSTANCE_REGION' in v.defines)) {
4514
4829
  variantDefines.PER_INSTANCE_REGION = false;
4515
4830
  }
4831
+ if (msEntry.variants.some((v) => 'SKINNING_DISABLED' in v.defines)) {
4832
+ variantDefines.SKINNING_DISABLED = true;
4833
+ }
4516
4834
  const sortedEntries = Object.entries(variantDefines).sort(([a], [b]) =>
4517
4835
  a < b ? -1 : a > b ? 1 : 0,
4518
4836
  );
@@ -4520,6 +4838,13 @@ async function prepareMaterialShaders(
4520
4838
  ? ''
4521
4839
  : sortedEntries.map(([k, v]) => `${k}=${v}`).join('+');
4522
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
+ }
4523
4848
  const wgsl = chosen?.composedWgsl ?? msEntry.composedWgsl;
4524
4849
  if (wgsl.length > 0) {
4525
4850
  const existing = registry.findMaterialArtifact(msEntry.identifier);
@@ -4601,6 +4926,7 @@ async function prepareMaterialShaders(
4601
4926
 
4602
4927
  async function buildReadyWebGPU(
4603
4928
  rhiDevice: RhiDevice,
4929
+ rendererScope: DeviceScope,
4604
4930
  getShader: () => ShaderCatalog,
4605
4931
  gpuStore: GpuResidencyCache,
4606
4932
  asyncCreateShaderModule:
@@ -4654,6 +4980,10 @@ async function buildReadyWebGPU(
4654
4980
  // tonemap source resolves inside this async function (after the manifest-load
4655
4981
  // await), by which point the synchronous `renderSystem` const is defined.
4656
4982
  registerBuiltinTonemap: (source: string) => void,
4983
+ registerBuiltinTemporalPostProcesses: (entries: {
4984
+ readonly motionBlur?: string;
4985
+ readonly taaResolve?: string;
4986
+ }) => void,
4657
4987
  ): Promise<PipelineState> {
4658
4988
  // bug-20260610: WebGL2 fallback path — read storage-buffer capability up
4659
4989
  // front so both Step 2 (shader-module compile, where we patch engine
@@ -4680,7 +5010,10 @@ async function buildReadyWebGPU(
4680
5010
  // pipeline target, configure() call, and color-attachment format —
4681
5011
  // single SSOT, no scattered branches. See selectSwapChainFormat
4682
5012
  // (above the SWAP_CHAIN_*_FORMAT historical constants).
4683
- const swapChainFormats = selectSwapChainFormat(storageBufferCapable);
5013
+ const surfaceViewFormats =
5014
+ (rhiDevice as unknown as { readonly surfaceViewFormats?: boolean }).surfaceViewFormats ??
5015
+ !webgl2Downlevel;
5016
+ const swapChainFormats = selectSwapChainFormat(storageBufferCapable, surfaceViewFormats);
4684
5017
  // The 'null' (headless RhiNull) backend has no UA preferred-canvas-format by
4685
5018
  // design; the rgba8unorm fallback is its intended steady state, not a
4686
5019
  // degraded one — firing 'rhi-not-available' there is territorially wrong
@@ -4785,6 +5118,7 @@ async function buildReadyWebGPU(
4785
5118
  let pbrEntry = findEngineManifestEntry('forgeax::default-standard-pbr');
4786
5119
  let unlitEntry = findEngineManifestEntry('forgeax::default-unlit');
4787
5120
  let tonemapEntry: ManifestEntry | undefined;
5121
+ let motionBlurEntry: ManifestEntry | undefined;
4788
5122
  let spriteEntry: ManifestEntry | undefined;
4789
5123
  // sprite-lit identification marker is the `spriteLitShadeAccum`
4790
5124
  // helper (defined in sprite-lit.wgsl, absent from sprite.wgsl). The
@@ -4794,6 +5128,7 @@ async function buildReadyWebGPU(
4794
5128
  // their paramSchema mirror) does not mis-classify sprite-lit as
4795
5129
  // sprite.
4796
5130
  let spriteLitEntry: ManifestEntry | undefined;
5131
+ let taaResolveEntry: ManifestEntry | undefined;
4797
5132
  let iblEquirectEntry: ManifestEntry | undefined;
4798
5133
  let iblIrradianceEntry: ManifestEntry | undefined;
4799
5134
  let iblPrefilterEntry: ManifestEntry | undefined;
@@ -4818,6 +5153,14 @@ async function buildReadyWebGPU(
4818
5153
  tonemapEntry ??= entry;
4819
5154
  continue;
4820
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
+ }
4821
5164
  if (entry.wgsl.includes('spriteLitShadeAccum')) {
4822
5165
  // sprite-lit identification — must precede the `pivotAndSize`
4823
5166
  // marker since sprite-lit also carries that field (paramSchema
@@ -4959,10 +5302,10 @@ async function buildReadyWebGPU(
4959
5302
  (!('CLUSTER_FORWARD_AVAILABLE' in v.defines) ||
4960
5303
  v.defines.CLUSTER_FORWARD_AVAILABLE === false) &&
4961
5304
  (!('VERTEX_COLOR_AVAILABLE' in v.defines) ||
4962
- v.defines.VERTEX_COLOR_AVAILABLE === false),
5305
+ v.defines.VERTEX_COLOR_AVAILABLE === false) &&
5306
+ (!('SKINNING_DISABLED' in v.defines) || v.defines.SKINNING_DISABLED === true),
4963
5307
  );
4964
- const v =
4965
- exact ?? ms.variants.find((cand) => cand.defines.STORAGE_BUFFER_AVAILABLE === false);
5308
+ const v = exact === undefined ? undefined : findVariantByKey(ms, exact.definesKey);
4966
5309
  if (v !== undefined) {
4967
5310
  idToVariantWgsl.set(ms.identifier, v.composedWgsl);
4968
5311
  }
@@ -5002,13 +5345,13 @@ async function buildReadyWebGPU(
5002
5345
  (!('CLUSTER_FORWARD_AVAILABLE' in variant.defines) ||
5003
5346
  variant.defines.CLUSTER_FORWARD_AVAILABLE === false) &&
5004
5347
  (!('VERTEX_COLOR_AVAILABLE' in variant.defines) ||
5005
- 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),
5006
5353
  );
5007
- const variant =
5008
- exact ??
5009
- ms.variants.find(
5010
- (candidate) => candidate.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable,
5011
- );
5354
+ const variant = exact === undefined ? undefined : findVariantByKey(ms, exact.definesKey);
5012
5355
  if (variant !== undefined) {
5013
5356
  idToRuntimeVariantWgsl.set(ms.identifier, variant.composedWgsl);
5014
5357
  }
@@ -5082,6 +5425,34 @@ async function buildReadyWebGPU(
5082
5425
  brdfLut: iblBrdfLutEntry.wgsl,
5083
5426
  });
5084
5427
  }
5428
+ // Points/Lines are an engine-owned record path rather than a producer
5429
+ // RenderFeature, so they cannot declare `requiredMaterialShaders`. Their
5430
+ // material pipeline still goes through the same lazy shader-module adapter
5431
+ // as producer features. Prewarm the optional manifest artifact here so a
5432
+ // first frame cannot race async module compilation and silently skip its
5433
+ // draw while the adapter is still pending. Older/minimal manifests may not
5434
+ // carry the optional entry; those hosts retain their existing behavior.
5435
+ const pointsLinesLookup = registry.findMaterialArtifact(POINTS_LINES_MATERIAL_SHADER_ID);
5436
+ if (pointsLinesLookup.ok) {
5437
+ const pointsLinesLabel = `module-${POINTS_LINES_MATERIAL_SHADER_ID}`;
5438
+ const pointsLinesShaderResult = await runShimStep(
5439
+ () =>
5440
+ asyncCreateShaderModule
5441
+ ? asyncCreateShaderModule(rhiDevice, {
5442
+ code: pointsLinesLookup.value.source,
5443
+ label: pointsLinesLabel,
5444
+ })
5445
+ : invokeDeviceCreateShaderModule(rhiDevice, {
5446
+ code: pointsLinesLookup.value.source,
5447
+ label: pointsLinesLabel,
5448
+ }),
5449
+ 'shader-compile-failed',
5450
+ `engine material shader '${POINTS_LINES_MATERIAL_SHADER_ID}' compiled`,
5451
+ `inspect the composed WGSL for '${POINTS_LINES_MATERIAL_SHADER_ID}' and check device.features`,
5452
+ );
5453
+ if (!pointsLinesShaderResult.ok) throw pointsLinesShaderResult.error;
5454
+ seedShaderModule(pointsLinesLabel, pointsLinesShaderResult.value);
5455
+ }
5085
5456
  // Producer-declared material shader modules use the same source and module
5086
5457
  // label as getMaterialShaderPipeline. Prewarming them here keeps the
5087
5458
  // renderer.initialization barrier meaningful for prepared graphics: the first draw
@@ -5136,6 +5507,12 @@ async function buildReadyWebGPU(
5136
5507
  if (pbrVariant !== undefined) {
5137
5508
  seedShaderModule(`module-forgeax::default-standard-pbr#${pbrVariant.definesKey}`, pbrModule);
5138
5509
  }
5510
+ const transmissionCapable =
5511
+ (rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
5512
+ STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
5513
+ const transmissionVariants = transmissionCapable
5514
+ ? selectStandardPbrTransmissionPrewarmVariants(pbrManifestEntry, storageBufferCapable)
5515
+ : [];
5139
5516
 
5140
5517
  // HDRP uses the canonical all-true variant key (`''`) and the lazy
5141
5518
  // material pipeline adapter therefore requests a distinct module label.
@@ -5143,9 +5520,35 @@ async function buildReadyWebGPU(
5143
5520
  // label. Color availability is part of the geometry-owned variant axis, so
5144
5521
  // both colored and no-color HDRP sources must be retained.
5145
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
+ }
5146
5548
  for (const hdrpVariant of selectHdrpPbrPrewarmVariants(
5147
5549
  pbrManifestEntry,
5148
5550
  storageBufferCapable,
5551
+ transmissionCapable,
5149
5552
  )) {
5150
5553
  const moduleLabel = `module-forgeax::default-standard-pbr#${hdrpVariant.definesKey}`;
5151
5554
  let hdrpModule = prewarmedHdrpModules.get(hdrpVariant.composedWgsl);
@@ -5281,6 +5684,49 @@ async function buildReadyWebGPU(
5281
5684
  // uses so the lazy build's createShaderModule call hits OK on
5282
5685
  // frame 1.
5283
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
+ }
5284
5730
  }
5285
5731
 
5286
5732
  // feat-20260621 M-A3 (D-5): register the built-in tonemap onto the unified
@@ -5300,27 +5746,62 @@ async function buildReadyWebGPU(
5300
5746
  // forever. Mirror the shadow_caster prewarm: await-compile the tonemap module
5301
5747
  // here and `seedShaderModule` it under the exact label
5302
5748
  // `buildPostProcessPipeline` requests (`post-process-${id}-module`), so the
5303
- // first `getPostProcessPipeline(STANDARD_TONEMAP_FEATURE_ID, …)` hits the module cache
5749
+ // first `getPostProcessPipeline(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, …)` hits the module cache
5304
5750
  // synchronously and builds the pipeline on frame 1.
5305
5751
  const tonemapPrewarm = await runShimStep(
5306
5752
  () =>
5307
5753
  asyncCreateShaderModule
5308
5754
  ? asyncCreateShaderModule(rhiDevice, {
5309
5755
  code: tonemapEntry.wgsl,
5310
- label: `post-process-${STANDARD_TONEMAP_FEATURE_ID}-module`,
5756
+ label: `post-process-${STANDARD_OUTPUT_TRANSFORM_FEATURE_ID}-module`,
5311
5757
  })
5312
5758
  : invokeDeviceCreateShaderModule(rhiDevice, {
5313
5759
  code: tonemapEntry.wgsl,
5314
- label: `post-process-${STANDARD_TONEMAP_FEATURE_ID}-module`,
5760
+ label: `post-process-${STANDARD_OUTPUT_TRANSFORM_FEATURE_ID}-module`,
5315
5761
  }),
5316
5762
  'shader-compile-failed',
5317
5763
  'tonemap shader module compiled (unified post-process prewarm)',
5318
5764
  'inspect manifest tonemap entry composed wgsl; check device.features',
5319
5765
  );
5320
5766
  if (!tonemapPrewarm.ok) throw tonemapPrewarm.error;
5321
- 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
+ );
5322
5771
  registerBuiltinTonemap(tonemapEntry.wgsl);
5323
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
+
5324
5805
  // feat-20260520-2d-sprite-layer-mvp M-3 / w24: sprite shader module
5325
5806
  // is optional in the 3-tuple legacy manifest (back-compat for apps
5326
5807
  // that locked their manifest URL before this feat). With the
@@ -5356,6 +5837,36 @@ async function buildReadyWebGPU(
5356
5837
  // shares the generic per-MaterialShader lazy build path, which now
5357
5838
  // needs the explicit seed to preserve frame-1 readiness.
5358
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
+ }
5359
5870
  }
5360
5871
 
5361
5872
  // feat-20260624-sprite-lit-shading-model-pure-2d-lighting M1' / t7:
@@ -5428,15 +5939,12 @@ async function buildReadyWebGPU(
5428
5939
  skyboxModule = skyboxShaderResult.value;
5429
5940
  }
5430
5941
 
5431
- // feat-20260531-bloom-first-declarative-render-graph-pass / w13:
5432
- // bloom shader modules are optional (apps with legacy manifests without
5433
- // bloom.wgsl continue to boot). Identified by the uniform-struct-name
5434
- // markers 'BloomBrightParams' / 'BloomBlurParams' / 'BloomCompositeParams'
5435
- // (see triage above; struct names survive naga_oil composition). When
5436
- // present, each module is compiled here; pipeline construction happens
5437
- // 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.
5438
5946
  if (bloomBrightEntry !== undefined) {
5439
- const brightShaderResult = await runShimStep(
5947
+ const result = await runShimStep(
5440
5948
  () =>
5441
5949
  asyncCreateShaderModule
5442
5950
  ? asyncCreateShaderModule(rhiDevice, {
@@ -5451,17 +5959,14 @@ async function buildReadyWebGPU(
5451
5959
  'bloom-bright shader module compiled',
5452
5960
  'inspect manifest bloom-bright entry composed wgsl; check device.features',
5453
5961
  );
5454
- if (!brightShaderResult.ok) throw brightShaderResult.error;
5455
- bloomBrightModule = brightShaderResult.value;
5962
+ if (!result.ok) throw result.error;
5963
+ bloomBrightModule = result.value;
5456
5964
  }
5457
5965
  if (bloomBlurEntry !== undefined) {
5458
- const blurShaderResult = await runShimStep(
5966
+ const result = await runShimStep(
5459
5967
  () =>
5460
5968
  asyncCreateShaderModule
5461
- ? asyncCreateShaderModule(rhiDevice, {
5462
- code: bloomBlurEntry.wgsl,
5463
- label: 'bloom-blur',
5464
- })
5969
+ ? asyncCreateShaderModule(rhiDevice, { code: bloomBlurEntry.wgsl, label: 'bloom-blur' })
5465
5970
  : invokeDeviceCreateShaderModule(rhiDevice, {
5466
5971
  code: bloomBlurEntry.wgsl,
5467
5972
  label: 'bloom-blur',
@@ -5470,11 +5975,11 @@ async function buildReadyWebGPU(
5470
5975
  'bloom-blur shader module compiled',
5471
5976
  'inspect manifest bloom-blur entry composed wgsl; check device.features',
5472
5977
  );
5473
- if (!blurShaderResult.ok) throw blurShaderResult.error;
5474
- bloomBlurModule = blurShaderResult.value;
5978
+ if (!result.ok) throw result.error;
5979
+ bloomBlurModule = result.value;
5475
5980
  }
5476
5981
  if (bloomCompositeEntry !== undefined) {
5477
- const compositeShaderResult = await runShimStep(
5982
+ const result = await runShimStep(
5478
5983
  () =>
5479
5984
  asyncCreateShaderModule
5480
5985
  ? asyncCreateShaderModule(rhiDevice, {
@@ -5489,8 +5994,8 @@ async function buildReadyWebGPU(
5489
5994
  'bloom-composite shader module compiled',
5490
5995
  'inspect manifest bloom-composite entry composed wgsl; check device.features',
5491
5996
  );
5492
- if (!compositeShaderResult.ok) throw compositeShaderResult.error;
5493
- bloomCompositeModule = compositeShaderResult.value;
5997
+ if (!result.ok) throw result.error;
5998
+ bloomCompositeModule = result.value;
5494
5999
  }
5495
6000
  // feat-20260612-hdrp-ssao M6 / w27: SSAO shader module compilation.
5496
6001
  // Optional manifest entry — absent on legacy manifests (zero-overhead
@@ -5522,11 +6027,11 @@ async function buildReadyWebGPU(
5522
6027
  // The factory builds 4 BindGroupLayouts (view + material + mesh-array +
5523
6028
  // instances) and the 4-slot pipeline layout in one call, throwing on each
5524
6029
  // device.createBindGroupLayout / device.createPipelineLayout failure.
5525
- // 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
5526
6031
  // mergeSkylightIntoMaterialBgl) -- pipeline layout itself stays at 4 slots,
5527
6032
  // no @group(4) allocated (D-5 round-4 fix for the round-2 maxBindGroups=4
5528
6033
  // BLOCKER). The unlit pipeline currently shares this 14-entry layout
5529
- // (carrying fallback identity at binding 7..13 from the record stage);
6034
+ // (carrying fallback identity at binding 17..23 from the record stage);
5530
6035
  // future split via `buildUnlitMaterialBgl` is exposed for tests.
5531
6036
  //
5532
6037
  // feat-20260520-directional-light-shadow-mapping merge: view BGL entries
@@ -5560,7 +6065,10 @@ async function buildReadyWebGPU(
5560
6065
  // (URP layout) so the manifest entry's URP variant WGSL still builds a
5561
6066
  // valid PSO instead of hard-disabling the entire HDRP-variant build path.
5562
6067
  let hdrpPbrPipelineLayoutHandle: PipelineLayout | null = null;
5563
- {
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) {
5564
6072
  const hdrpUnifiedBglDesc = createHdrpBindGroupLayoutDescriptor(storageBufferCapable);
5565
6073
  const hdrpUnifiedBglRes = rhiDevice.createBindGroupLayout(hdrpUnifiedBglDesc);
5566
6074
  if (hdrpUnifiedBglRes.ok) {
@@ -5899,6 +6407,20 @@ async function buildReadyWebGPU(
5899
6407
  );
5900
6408
  if (!viewUboResult.ok) throw viewUboResult.error;
5901
6409
 
6410
+ const pointsLinesViewBufferResult = runShimSyncStep(
6411
+ () =>
6412
+ rhiDevice.createBuffer({
6413
+ label: 'points-lines-view-ubo',
6414
+ size: POINTS_LINES_VIEW_BUFFER_SIZE,
6415
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
6416
+ mappedAtCreation: false,
6417
+ }),
6418
+ 'webgpu-runtime-error',
6419
+ 'createBuffer (points-lines view ubo) succeeded',
6420
+ 'check device.limits.maxUniformBufferBindingSize',
6421
+ );
6422
+ if (!pointsLinesViewBufferResult.ok) throw pointsLinesViewBufferResult.error;
6423
+
5902
6424
  // feat-20260613-csm-cascaded-shadow-maps M5 / w28: per-pass cascade-index
5903
6425
  // UBO consumed by shadow_caster.wgsl. Stable singleton; the record stage
5904
6426
  // queue.writeBuffer-overwrites the fields immediately before each pass's
@@ -6854,6 +7376,59 @@ async function buildReadyWebGPU(
6854
7376
  let bloomBlurHParamsBufferHandle: Buffer | null = null;
6855
7377
  let bloomBlurVParamsBufferHandle: Buffer | null = null;
6856
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
+ };
6857
7432
  // feat-20260612-hdrp-ssao M6 / w26 + w43: SSAO pipeline handles (D-A).
6858
7433
  // calc + blur RenderPipeline pair sharing a dedicated 6-entry BGL.
6859
7434
  // Optional — null when manifest lacks hdrp-ssao entry.
@@ -7154,361 +7729,560 @@ async function buildReadyWebGPU(
7154
7729
  // bloom-blur-v : same BGL, same module as blur-h, V-axis texelSize
7155
7730
  // bloom-composite: 2-tex + sampler + UBO@3 BGL, rgba16float target
7156
7731
 
7157
- // Shared bloom sampler: linear filter + clamp-to-edge (all 4 passes
7158
- // sample from textures using fullscreen triangle UVs).
7159
- if (bloomBrightModule !== null || bloomBlurModule !== null || bloomCompositeModule !== null) {
7160
- const bloomSamplerResult = runShimSyncStep(
7161
- () =>
7162
- rhiDevice.createSampler({
7163
- label: 'bloom-sampler',
7164
- magFilter: 'linear',
7165
- minFilter: 'linear',
7166
- mipmapFilter: 'linear',
7167
- addressModeU: 'clamp-to-edge',
7168
- addressModeV: 'clamp-to-edge',
7169
- }),
7170
- 'webgpu-runtime-error',
7171
- 'createSampler (bloom) succeeded',
7172
- 'check device.limits.maxSamplersPerShaderStage',
7173
- );
7174
- if (!bloomSamplerResult.ok) throw bloomSamplerResult.error;
7175
- bloomSamplerHandle = bloomSamplerResult.value;
7176
- }
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
+ }
7177
7768
 
7178
- // Bloom bright: 1-tex + sampler + UBO@2 BGL (D-4).
7179
- if (bloomBrightModule !== null && bloomSamplerHandle !== null) {
7180
- const brightBglResult = runShimSyncStep(
7181
- () =>
7182
- rhiDevice.createBindGroupLayout({
7183
- label: 'bloom-bright-bgl',
7184
- entries: [
7185
- {
7186
- binding: 0,
7187
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7188
- 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,
7189
7820
  },
7190
- {
7191
- binding: 1,
7192
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7193
- sampler: { type: 'filtering' },
7821
+ attachments: {
7822
+ colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
7823
+ depthFormat: undefined,
7824
+ sampleCount: 1,
7194
7825
  },
7195
- {
7196
- binding: 2,
7197
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7198
- buffer: { type: 'uniform' },
7826
+ geometry: {
7827
+ topology: 'triangle-list',
7828
+ stripIndexFormat: undefined,
7829
+ vertexLayout: {},
7199
7830
  },
7200
- ],
7201
- }),
7202
- 'webgpu-runtime-error',
7203
- 'createBindGroupLayout(bloom-bright) succeeded',
7204
- 'check device.limits.maxBindGroupsPerPipelineLayout',
7205
- );
7206
- if (!brightBglResult.ok) throw brightBglResult.error;
7207
- bloomBrightBglHandle = brightBglResult.value;
7208
-
7209
- const brightPlResult = runShimSyncStep(
7210
- () =>
7211
- rhiDevice.createPipelineLayout({
7212
- label: 'bloom-bright-pl',
7213
- bindGroupLayouts: [brightBglResult.value],
7214
- }),
7215
- 'webgpu-runtime-error',
7216
- 'createPipelineLayout(bloom-bright) succeeded',
7217
- 'verify bloom-bright BindGroupLayout matches shader @group(0) bindings',
7218
- );
7219
- 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
+ }
7220
7859
 
7221
- // M2-T4: bloom-bright pipeline via getOrBuildPipeline (lazy-build).
7222
- {
7223
- const brightSpec: PipelineSpec = {
7224
- shader: {
7225
- id: 'forgeax::post::bloom-bright',
7226
- passKind: 'post-process',
7227
- variantSet: undefined,
7228
- },
7229
- attachments: {
7230
- colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
7231
- depthFormat: undefined,
7232
- sampleCount: 1,
7233
- },
7234
- geometry: {
7235
- topology: 'triangle-list',
7236
- stripIndexFormat: undefined,
7237
- vertexLayout: {},
7238
- },
7239
- renderState: { cullMode: 'none' },
7240
- };
7241
- const modules = {
7242
- vertex: bloomBrightModule,
7243
- fragment: bloomBrightModule,
7244
- layout: brightPlResult.value,
7245
- };
7246
- try {
7247
- bloomBrightPipelineHandle = getOrBuildPipeline(
7248
- brightSpec,
7249
- pipelineDeviceProvider,
7250
- pipelineCache,
7251
- modules,
7252
- ) as RenderPipeline;
7253
- } catch (err) {
7254
- if (err instanceof PipelineSpecError) throw err;
7255
- throw new PipelineSpecError({
7256
- code: 'pipeline-build-failed',
7257
- detail: { cause: err },
7258
- 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);
7259
7876
  });
7260
7877
  }
7261
- }
7262
7878
 
7263
- // Bright params UBO: 16 B std140 (threshold f32 + 12 B pad).
7264
- const brightParamsResult = runShimSyncStep(
7265
- () =>
7266
- rhiDevice.createBuffer({
7267
- label: 'bloom-bright-params-ubo',
7268
- size: BRIGHT_PARAMS_BYTES,
7269
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
7270
- mappedAtCreation: false,
7271
- }),
7272
- 'webgpu-runtime-error',
7273
- 'createBuffer (bloom-bright params UBO) succeeded',
7274
- 'check device.limits.maxUniformBufferBindingSize',
7275
- );
7276
- if (!brightParamsResult.ok) throw brightParamsResult.error;
7277
- bloomBrightParamsBufferHandle = brightParamsResult.value;
7278
- }
7279
-
7280
- // Bloom blur H/V: same BGL (1-tex + sampler + UBO@2), same module,
7281
- // two separate pipelines with per-axis texelSize (D-1, D-4).
7282
- if (bloomBlurModule !== null && bloomSamplerHandle !== null) {
7283
- const blurBglResult = runShimSyncStep(
7284
- () =>
7285
- rhiDevice.createBindGroupLayout({
7286
- label: 'bloom-blur-bgl',
7287
- entries: [
7288
- {
7289
- binding: 0,
7290
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7291
- 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,
7292
7934
  },
7293
- {
7294
- binding: 1,
7295
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7296
- sampler: { type: 'filtering' },
7935
+ attachments: {
7936
+ colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
7937
+ depthFormat: undefined,
7938
+ sampleCount: 1,
7297
7939
  },
7298
- {
7299
- binding: 2,
7300
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7301
- buffer: { type: 'uniform' },
7940
+ geometry: {
7941
+ topology: 'triangle-list',
7942
+ stripIndexFormat: undefined,
7943
+ vertexLayout: {},
7302
7944
  },
7303
- ],
7304
- }),
7305
- 'webgpu-runtime-error',
7306
- 'createBindGroupLayout(bloom-blur) succeeded',
7307
- 'check device.limits.maxBindGroupsPerPipelineLayout',
7308
- );
7309
- if (!blurBglResult.ok) throw blurBglResult.error;
7310
- 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
+ }
7311
7976
 
7312
- const blurPlResult = runShimSyncStep(
7313
- () =>
7314
- rhiDevice.createPipelineLayout({
7315
- label: 'bloom-blur-pl',
7316
- bindGroupLayouts: [blurBglResult.value],
7317
- }),
7318
- 'webgpu-runtime-error',
7319
- 'createPipelineLayout(bloom-blur) succeeded',
7320
- 'verify bloom-blur BindGroupLayout matches shader @group(0) bindings',
7321
- );
7322
- 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
+ });
7323
7996
 
7324
- // M2-T4: bloom-blur H/V pipelines via getOrBuildPipeline (lazy-build).
7325
- // H and V share the same PSO descriptor — only per-axis texelSize UBO
7326
- // distinguishes them at record time. getOrBuildPipeline cache-hit on the
7327
- // second call returns the same handle (identical spec, identical PSO).
7328
- {
7329
- const blurSpec: PipelineSpec = {
7330
- shader: {
7331
- id: 'forgeax::post::bloom-blur',
7332
- passKind: 'post-process',
7333
- variantSet: undefined,
7334
- },
7335
- attachments: {
7336
- colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
7337
- depthFormat: undefined,
7338
- sampleCount: 1,
7339
- },
7340
- geometry: {
7341
- topology: 'triangle-list',
7342
- stripIndexFormat: undefined,
7343
- vertexLayout: {},
7344
- },
7345
- renderState: { cullMode: 'none' },
7346
- };
7347
- const modules = {
7348
- vertex: bloomBlurModule,
7349
- fragment: bloomBlurModule,
7350
- layout: blurPlResult.value,
7351
- label: 'bloom-blur-h-pipeline',
7352
- };
7353
- try {
7354
- bloomBlurHPipelineHandle = getOrBuildPipeline(
7355
- blurSpec,
7356
- pipelineDeviceProvider,
7357
- pipelineCache,
7358
- modules,
7359
- ) as RenderPipeline;
7360
- } catch (err) {
7361
- if (err instanceof PipelineSpecError) throw err;
7362
- throw new PipelineSpecError({
7363
- code: 'pipeline-build-failed',
7364
- detail: { cause: err },
7365
- 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);
7366
8012
  });
7367
8013
  }
7368
- // V is cache-hit on the same spec (identical PSO; per-axis UBO differentiates at record time).
7369
- bloomBlurVPipelineHandle = bloomBlurHPipelineHandle;
7370
- }
7371
-
7372
- // Blur params UBOs: 16 B std140 (texelSize.xy + radius + pad) each.
7373
- // bug-20260625: one per axis -- H and V must not share a buffer (the
7374
- // shared-buffer writeBuffer race made both passes blur vertically).
7375
- const blurHParamsResult = runShimSyncStep(
7376
- () =>
7377
- rhiDevice.createBuffer({
7378
- label: 'bloom-blur-h-params-ubo',
7379
- size: BLUR_PARAMS_BYTES,
7380
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
7381
- mappedAtCreation: false,
7382
- }),
7383
- 'webgpu-runtime-error',
7384
- 'createBuffer (bloom-blur-h params UBO) succeeded',
7385
- 'check device.limits.maxUniformBufferBindingSize',
7386
- );
7387
- if (!blurHParamsResult.ok) throw blurHParamsResult.error;
7388
- bloomBlurHParamsBufferHandle = blurHParamsResult.value;
7389
-
7390
- const blurVParamsResult = runShimSyncStep(
7391
- () =>
7392
- rhiDevice.createBuffer({
7393
- label: 'bloom-blur-v-params-ubo',
7394
- size: BLUR_PARAMS_BYTES,
7395
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
7396
- mappedAtCreation: false,
7397
- }),
7398
- 'webgpu-runtime-error',
7399
- 'createBuffer (bloom-blur-v params UBO) succeeded',
7400
- 'check device.limits.maxUniformBufferBindingSize',
7401
- );
7402
- if (!blurVParamsResult.ok) throw blurVParamsResult.error;
7403
- bloomBlurVParamsBufferHandle = blurVParamsResult.value;
7404
- }
7405
8014
 
7406
- // Bloom composite: 2-tex + sampler + UBO@3 BGL (D-4, D-5).
7407
- if (bloomCompositeModule !== null && bloomSamplerHandle !== null) {
7408
- const compositeBglResult = runShimSyncStep(
7409
- () =>
7410
- rhiDevice.createBindGroupLayout({
7411
- label: 'bloom-composite-bgl',
7412
- entries: [
7413
- {
7414
- binding: 0,
7415
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7416
- texture: { sampleType: 'float', viewDimension: '2d' },
7417
- },
7418
- {
7419
- binding: 1,
7420
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7421
- 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,
7422
8071
  },
7423
- {
7424
- binding: 2,
7425
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7426
- sampler: { type: 'filtering' },
8072
+ attachments: {
8073
+ colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
8074
+ depthFormat: undefined,
8075
+ sampleCount: 1,
7427
8076
  },
7428
- {
7429
- binding: 3,
7430
- visibility: GPU_SHADER_STAGE_FRAGMENT,
7431
- buffer: { type: 'uniform' },
8077
+ geometry: {
8078
+ topology: 'triangle-list',
8079
+ stripIndexFormat: undefined,
8080
+ vertexLayout: {},
7432
8081
  },
7433
- ],
7434
- }),
7435
- 'webgpu-runtime-error',
7436
- 'createBindGroupLayout(bloom-composite) succeeded',
7437
- 'check device.limits.maxBindGroupsPerPipelineLayout',
7438
- );
7439
- if (!compositeBglResult.ok) throw compositeBglResult.error;
7440
- 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
+ }
7441
8110
 
7442
- const compositePlResult = runShimSyncStep(
7443
- () =>
7444
- rhiDevice.createPipelineLayout({
7445
- label: 'bloom-composite-pl',
7446
- bindGroupLayouts: [compositeBglResult.value],
7447
- }),
7448
- 'webgpu-runtime-error',
7449
- 'createPipelineLayout(bloom-composite) succeeded',
7450
- 'verify bloom-composite BindGroupLayout matches shader @group(0) bindings',
7451
- );
7452
- 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
+ }
7453
8133
 
7454
- // M2-T4: bloom-composite pipeline via getOrBuildPipeline (lazy-build).
7455
- {
7456
- const compositeSpec: PipelineSpec = {
7457
- shader: {
7458
- id: 'forgeax::post::bloom-composite',
7459
- passKind: 'post-process',
7460
- variantSet: undefined,
7461
- },
7462
- attachments: {
7463
- colorFormats: [HDR_COLOR_ATTACHMENT_FORMAT],
7464
- depthFormat: undefined,
7465
- sampleCount: 1,
7466
- },
7467
- geometry: {
7468
- topology: 'triangle-list',
7469
- stripIndexFormat: undefined,
7470
- vertexLayout: {},
7471
- },
7472
- 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,
7473
8149
  };
7474
- const modules = {
7475
- vertex: bloomCompositeModule,
7476
- fragment: bloomCompositeModule,
7477
- 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
+ };
7478
8198
  };
7479
- try {
7480
- bloomCompositePipelineHandle = getOrBuildPipeline(
7481
- compositeSpec,
7482
- pipelineDeviceProvider,
7483
- pipelineCache,
7484
- modules,
7485
- ) as RenderPipeline;
7486
- } catch (err) {
7487
- if (err instanceof PipelineSpecError) throw err;
7488
- throw new PipelineSpecError({
7489
- code: 'pipeline-build-failed',
7490
- detail: { cause: err },
7491
- hint: 'createRenderPipeline (bloom-composite fullscreen) failed; inspect gpuMessage',
7492
- });
7493
- }
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;
7494
8284
  }
7495
-
7496
- // Composite params UBO: 16 B std140 (intensity f32 + 12 B pad).
7497
- const compositeParamsResult = runShimSyncStep(
7498
- () =>
7499
- rhiDevice.createBuffer({
7500
- label: 'bloom-composite-params-ubo',
7501
- size: COMPOSITE_PARAMS_BYTES,
7502
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
7503
- mappedAtCreation: false,
7504
- }),
7505
- 'webgpu-runtime-error',
7506
- 'createBuffer (bloom-composite params UBO) succeeded',
7507
- 'check device.limits.maxUniformBufferBindingSize',
7508
- );
7509
- if (!compositeParamsResult.ok) throw compositeParamsResult.error;
7510
- bloomCompositeParamsBufferHandle = compositeParamsResult.value;
7511
- }
8285
+ };
7512
8286
 
7513
8287
  // ── feat-20260612-hdrp-ssao M6 / w26 + w43 + M8 / w37 ───────────────────
7514
8288
  //
@@ -7730,7 +8504,7 @@ async function buildReadyWebGPU(
7730
8504
  if (!prewarmRes.ok) throw prewarmRes.error;
7731
8505
  }
7732
8506
 
7733
- return {
8507
+ const pipelineState: PipelineState = {
7734
8508
  // feat-20260518-pbr-direct-lighting-mvp M5 / w22.10 (AC-06 dual->triple
7735
8509
  // pipeline + D-2 + D-10): three distinct GPU render-pipeline handles
7736
8510
  // backed by 2 distinct shader modules (unlit + pbr) x 2 vertex stride
@@ -7813,10 +8587,12 @@ async function buildReadyWebGPU(
7813
8587
  meshes: meshHandles,
7814
8588
  format: swapChainFormats.storage,
7815
8589
  colorAttachmentFormat: swapChainFormats.view,
8590
+ surfaceProfile: surfaceViewFormats ? 'dual-view' : 'raw-only',
7816
8591
  viewBindGroupLayout: viewBglResult.value,
7817
8592
  materialBindGroupLayout: materialBglResult.value,
7818
8593
  meshBindGroupLayout: meshArrayBglResult.value,
7819
8594
  viewUniformBuffer: viewUboResult.value,
8595
+ pointsLinesViewBuffer: pointsLinesViewBufferResult.value,
7820
8596
  shadowCasterCascadeBuffer: shadowCasterCascadeUboResult.value,
7821
8597
  // feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M2 /
7822
8598
  // T-M2-05: PipelineState now references the wrapper objects owned by
@@ -7921,18 +8697,28 @@ async function buildReadyWebGPU(
7921
8697
  // (ensureLazyTexture, slot width/height tracking for size-drift rebuild).
7922
8698
  // BindGroup caches survive until the intermediate view is invalidated
7923
8699
  // by a resize (width/height drift forces null).
7924
- bloomBrightPipeline: bloomBrightPipelineHandle,
7925
- bloomBlurHPipeline: bloomBlurHPipelineHandle,
7926
- bloomBlurVPipeline: bloomBlurVPipelineHandle,
7927
- bloomCompositePipeline: bloomCompositePipelineHandle,
7928
- bloomBrightBindGroupLayout: bloomBrightBglHandle,
7929
- bloomBlurBindGroupLayout: bloomBlurBglHandle,
7930
- bloomCompositeBindGroupLayout: bloomCompositeBglHandle,
7931
- bloomSampler: bloomSamplerHandle,
7932
- bloomBrightParamsBuffer: bloomBrightParamsBufferHandle,
7933
- bloomBlurHParamsBuffer: bloomBlurHParamsBufferHandle,
7934
- bloomBlurVParamsBuffer: bloomBlurVParamsBufferHandle,
7935
- 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,
7936
8722
  bloomBrightTexture: null,
7937
8723
  bloomBrightView: null,
7938
8724
  bloomBrightWidth: 0,
@@ -7957,6 +8743,7 @@ async function buildReadyWebGPU(
7957
8743
  ssaoFallbackRawView: null,
7958
8744
  },
7959
8745
  };
8746
+ return pipelineState;
7960
8747
  }
7961
8748
 
7962
8749
  /**
@@ -7981,8 +8768,8 @@ function ensureContextConfigured(
7981
8768
  internals: WebGPURendererInternals,
7982
8769
  state: PipelineState,
7983
8770
  errorRegistry: RhiErrorListenerRegistry,
7984
- ): void {
7985
- if (state.perPassResources.configured) return;
8771
+ ): Result<void, RhiError> {
8772
+ if (state.perPassResources.configured) return ok(undefined);
7986
8773
  // M6 / w41 (feat-20260510-rhi-resource-creation): configure goes through
7987
8774
  // the forgeax `RhiCanvasContext` surface; the shim resolves the forgeax
7988
8775
  // RhiDevice back to the underlying raw GPUDevice via RAW_DEVICE_MAP
@@ -7995,7 +8782,7 @@ function ensureContextConfigured(
7995
8782
  const configuredDevice = internals.pack.instrumentation?.resolveSurfaceDevice?.(internals.device);
7996
8783
  if (configuredDevice !== undefined && !configuredDevice.ok) {
7997
8784
  errorRegistry.fire(configuredDevice.error);
7998
- return;
8785
+ return err(configuredDevice.error);
7999
8786
  }
8000
8787
  const cfgResult = configureSurface(
8001
8788
  internals.context,
@@ -8005,7 +8792,7 @@ function ensureContextConfigured(
8005
8792
  );
8006
8793
  if (!cfgResult.ok) {
8007
8794
  errorRegistry.fire(cfgResult.error);
8008
- return;
8795
+ return err(cfgResult.error);
8009
8796
  }
8010
8797
  state.perPassResources.configured = true;
8011
8798
  // bug-20260612: write the actually-configured swap-chain storage format
@@ -8018,6 +8805,7 @@ function ensureContextConfigured(
8018
8805
  // `${storage}-srgb` partner — derived, not duplicated.
8019
8806
  (globalThis as { __forgeaxSwapChainFormat?: GPUTextureFormat }).__forgeaxSwapChainFormat =
8020
8807
  state.format;
8808
+ return ok(undefined);
8021
8809
  }
8022
8810
 
8023
8811
  /**