@forgeax/engine-render 0.1.20 → 0.1.23

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 (645) hide show
  1. package/README.md +365 -29
  2. package/dist/assembly/backend-contract.d.ts +13 -0
  3. package/dist/assembly/backend-contract.d.ts.map +1 -1
  4. package/dist/assembly/device-feature-admission.d.ts +18 -0
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -0
  6. package/dist/assembly/directional-shadow-inspection.d.ts +38 -0
  7. package/dist/assembly/directional-shadow-inspection.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +30 -19
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/host-contract.d.ts +19 -0
  11. package/dist/assembly/host-contract.d.ts.map +1 -1
  12. package/dist/assembly/render-target-host.d.ts +46 -0
  13. package/dist/assembly/render-target-host.d.ts.map +1 -0
  14. package/dist/assets/render-asset-port.d.ts.map +1 -1
  15. package/dist/authoring.mjs +4 -1
  16. package/dist/authoring.mjs.map +1 -1
  17. package/dist/capture/cube-views.d.ts +21 -0
  18. package/dist/capture/cube-views.d.ts.map +1 -0
  19. package/dist/capture/scheduler.d.ts +54 -0
  20. package/dist/capture/scheduler.d.ts.map +1 -0
  21. package/dist/{chunk-PCHBPYML.mjs → chunk-4L3P6DT5.mjs} +2 -2
  22. package/dist/chunk-4L3P6DT5.mjs.map +1 -0
  23. package/dist/{chunk-VFPLTKCU.mjs → chunk-4NPUASZR.mjs} +15 -4
  24. package/dist/chunk-4NPUASZR.mjs.map +1 -0
  25. package/dist/chunk-DIYW3M5D.mjs +5085 -0
  26. package/dist/chunk-DIYW3M5D.mjs.map +1 -0
  27. package/dist/chunk-FBC4R44J.mjs +1003 -0
  28. package/dist/chunk-FBC4R44J.mjs.map +1 -0
  29. package/dist/{chunk-CDTWTHZR.mjs → chunk-FF7K3FSO.mjs} +474 -168
  30. package/dist/chunk-FF7K3FSO.mjs.map +1 -0
  31. package/dist/chunk-HZUPSYW7.mjs +1104 -0
  32. package/dist/chunk-HZUPSYW7.mjs.map +1 -0
  33. package/dist/{chunk-TBQJOK2L.mjs → chunk-QODA5UKU.mjs} +321 -269
  34. package/dist/chunk-QODA5UKU.mjs.map +1 -0
  35. package/dist/chunk-YALPS44A.mjs +296 -0
  36. package/dist/chunk-YALPS44A.mjs.map +1 -0
  37. package/dist/{chunk-4XINDLAY.mjs → chunk-ZUWKAZ2I.mjs} +1963 -2178
  38. package/dist/chunk-ZUWKAZ2I.mjs.map +1 -0
  39. package/dist/cluster-binner.d.ts +5 -0
  40. package/dist/cluster-binner.d.ts.map +1 -1
  41. package/dist/components/camera.d.ts +1 -0
  42. package/dist/components/camera.d.ts.map +1 -1
  43. package/dist/components/cube-camera.d.ts +20 -0
  44. package/dist/components/cube-camera.d.ts.map +1 -0
  45. package/dist/components/directional-light.d.ts +11 -2
  46. package/dist/components/directional-light.d.ts.map +1 -1
  47. package/dist/components/directional-shadow-filter.d.ts +25 -0
  48. package/dist/components/directional-shadow-filter.d.ts.map +1 -0
  49. package/dist/components/index.d.ts +5 -0
  50. package/dist/components/index.d.ts.map +1 -1
  51. package/dist/components/light-helpers.d.ts +4 -0
  52. package/dist/components/light-helpers.d.ts.map +1 -1
  53. package/dist/components/light-probe.d.ts +9 -0
  54. package/dist/components/light-probe.d.ts.map +1 -0
  55. package/dist/components/rect-area-light.d.ts +12 -0
  56. package/dist/components/rect-area-light.d.ts.map +1 -0
  57. package/dist/components/reflection-probe.d.ts +17 -0
  58. package/dist/components/reflection-probe.d.ts.map +1 -0
  59. package/dist/components/scene-instance.d.ts +4 -0
  60. package/dist/components/scene-instance.d.ts.map +1 -1
  61. package/dist/components/spot-light.d.ts +3 -0
  62. package/dist/components/spot-light.d.ts.map +1 -1
  63. package/dist/construct-renderer.mjs +14836 -9620
  64. package/dist/construct-renderer.mjs.map +1 -1
  65. package/dist/device/gpu-residency.d.ts +8 -1
  66. package/dist/device/gpu-residency.d.ts.map +1 -1
  67. package/dist/device/lifecycle-transaction.d.ts +2 -0
  68. package/dist/device/lifecycle-transaction.d.ts.map +1 -0
  69. package/dist/environment/frame.d.ts.map +1 -1
  70. package/dist/environment/lifecycle.d.ts +5 -0
  71. package/dist/environment/lifecycle.d.ts.map +1 -1
  72. package/dist/errors/gpu-driven.d.ts +26 -0
  73. package/dist/errors/gpu-driven.d.ts.map +1 -0
  74. package/dist/errors/index.d.ts +1 -0
  75. package/dist/errors/index.d.ts.map +1 -1
  76. package/dist/errors/recover.d.ts +9 -1
  77. package/dist/errors/recover.d.ts.map +1 -1
  78. package/dist/errors/render.d.ts +166 -93
  79. package/dist/errors/render.d.ts.map +1 -1
  80. package/dist/extract/directional-shadow-projection.d.ts +23 -0
  81. package/dist/extract/directional-shadow-projection.d.ts.map +1 -0
  82. package/dist/extract/environment.d.ts +14 -0
  83. package/dist/extract/environment.d.ts.map +1 -1
  84. package/dist/extract/gpu-driven.d.ts +50 -0
  85. package/dist/extract/gpu-driven.d.ts.map +1 -0
  86. package/dist/extract/visibility.d.ts.map +1 -1
  87. package/dist/features/fullscreen.d.ts.map +1 -1
  88. package/dist/features/targets.d.ts.map +1 -1
  89. package/dist/features/types.d.ts +10 -0
  90. package/dist/features/types.d.ts.map +1 -1
  91. package/dist/fullscreen-post-process-pass.d.ts +13 -2
  92. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  93. package/dist/gpu-driven/batch-topology.d.ts +34 -0
  94. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  95. package/dist/gpu-driven/prepared-draw.d.ts +63 -0
  96. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -0
  97. package/dist/gpu-driven/production-raster.d.ts +97 -2
  98. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  99. package/dist/gpu-driven/view-gpu.d.ts +64 -1
  100. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  101. package/dist/gpu-scene-schema.d.ts +14 -0
  102. package/dist/gpu-scene-schema.d.ts.map +1 -1
  103. package/dist/gpu-scene.d.ts +8 -1
  104. package/dist/gpu-scene.d.ts.map +1 -1
  105. package/dist/gpu-usage.d.ts +0 -1
  106. package/dist/gpu-usage.d.ts.map +1 -1
  107. package/dist/hdrp-buffers.d.ts +21 -19
  108. package/dist/hdrp-buffers.d.ts.map +1 -1
  109. package/dist/ibl/IblPipelineCache.d.ts +4 -0
  110. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  111. package/dist/ibl/kernel-cache.d.ts +36 -0
  112. package/dist/ibl/kernel-cache.d.ts.map +1 -0
  113. package/dist/index.d.ts +39 -3
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.mjs +91 -6
  116. package/dist/index.mjs.map +1 -1
  117. package/dist/inspection-types.d.ts +214 -1
  118. package/dist/inspection-types.d.ts.map +1 -1
  119. package/dist/instances-derived-bounds.d.ts +50 -0
  120. package/dist/instances-derived-bounds.d.ts.map +1 -0
  121. package/dist/internal.d.ts +9 -0
  122. package/dist/internal.d.ts.map +1 -1
  123. package/dist/internal.mjs +23 -2
  124. package/dist/internal.mjs.map +1 -1
  125. package/dist/light-buffer-layout.d.ts +58 -140
  126. package/dist/light-buffer-layout.d.ts.map +1 -1
  127. package/dist/materials.d.ts.map +1 -1
  128. package/dist/mesh-material-bindings.d.ts +54 -0
  129. package/dist/mesh-material-bindings.d.ts.map +1 -1
  130. package/dist/pbr-pipeline.d.ts +38 -14
  131. package/dist/pbr-pipeline.d.ts.map +1 -1
  132. package/dist/pipeline/standard-forward-lane.d.ts +2 -1
  133. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  134. package/dist/pipeline/standard-lighting/errors.d.ts +4 -0
  135. package/dist/pipeline/standard-lighting/errors.d.ts.map +1 -0
  136. package/dist/pipeline/standard-lighting/graph.d.ts +24 -0
  137. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -0
  138. package/dist/pipeline/standard-lighting/inspection.d.ts +38 -0
  139. package/dist/pipeline/standard-lighting/inspection.d.ts.map +1 -0
  140. package/dist/pipeline/standard-lighting/layout.d.ts +28 -0
  141. package/dist/pipeline/standard-lighting/layout.d.ts.map +1 -0
  142. package/dist/pipeline/standard-lighting/light-frame.d.ts +33 -0
  143. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -0
  144. package/dist/pipeline/standard-lighting/membership.d.ts +14 -0
  145. package/dist/pipeline/standard-lighting/membership.d.ts.map +1 -0
  146. package/dist/pipeline/standard-lighting/prepare.d.ts +16 -0
  147. package/dist/pipeline/standard-lighting/prepare.d.ts.map +1 -0
  148. package/dist/pipeline/standard-lighting/topology.d.ts +45 -0
  149. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -0
  150. package/dist/pipeline/standard-lighting/transport.d.ts +36 -0
  151. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -0
  152. package/dist/pipeline/standard-pipeline.d.ts +4 -13
  153. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  154. package/dist/pipeline/standard-post.d.ts.map +1 -1
  155. package/dist/pipeline/standard-profile.d.ts +5 -26
  156. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  157. package/dist/pipeline-builder.d.ts +2 -0
  158. package/dist/pipeline-builder.d.ts.map +1 -1
  159. package/dist/pipeline-spec.d.ts +40 -7
  160. package/dist/pipeline-spec.d.ts.map +1 -1
  161. package/dist/plugin.d.ts.map +1 -1
  162. package/dist/point-shadow-inspection.d.ts +21 -0
  163. package/dist/point-shadow-inspection.d.ts.map +1 -0
  164. package/dist/prepare/extended-lighting/ltc-resources.d.ts +31 -0
  165. package/dist/prepare/extended-lighting/ltc-resources.d.ts.map +1 -0
  166. package/dist/prepare/extended-lighting/resources.d.ts +72 -0
  167. package/dist/prepare/extended-lighting/resources.d.ts.map +1 -0
  168. package/dist/prepare/extended-lighting/spot-modifiers.d.ts +42 -0
  169. package/dist/prepare/extended-lighting/spot-modifiers.d.ts.map +1 -0
  170. package/dist/prepare/extended-lighting/state.d.ts +24 -0
  171. package/dist/prepare/extended-lighting/state.d.ts.map +1 -0
  172. package/dist/record/frame-lighting.d.ts +33 -36
  173. package/dist/record/frame-lighting.d.ts.map +1 -1
  174. package/dist/record/frame-snapshot.d.ts +78 -24
  175. package/dist/record/frame-snapshot.d.ts.map +1 -1
  176. package/dist/record/frame-targets.d.ts +5 -0
  177. package/dist/record/frame-targets.d.ts.map +1 -1
  178. package/dist/record/frame.d.ts +16 -6
  179. package/dist/record/frame.d.ts.map +1 -1
  180. package/dist/record/gpu-pass-timing/contract.d.ts +96 -0
  181. package/dist/record/gpu-pass-timing/contract.d.ts.map +1 -0
  182. package/dist/record/gpu-pass-timing/errors.d.ts +18 -0
  183. package/dist/record/gpu-pass-timing/errors.d.ts.map +1 -0
  184. package/dist/record/gpu-pass-timing/index.d.ts +8 -0
  185. package/dist/record/gpu-pass-timing/index.d.ts.map +1 -0
  186. package/dist/record/gpu-pass-timing/parser.d.ts +11 -0
  187. package/dist/record/gpu-pass-timing/parser.d.ts.map +1 -0
  188. package/dist/record/gpu-pass-timing/session.d.ts +43 -0
  189. package/dist/record/gpu-pass-timing/session.d.ts.map +1 -0
  190. package/dist/record/gpu-timing.d.ts +20 -3
  191. package/dist/record/gpu-timing.d.ts.map +1 -1
  192. package/dist/record/helpers.d.ts +0 -20
  193. package/dist/record/helpers.d.ts.map +1 -1
  194. package/dist/record/main-pass-geometry.d.ts +6 -1
  195. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  196. package/dist/record/main-pass-material.d.ts +25 -2
  197. package/dist/record/main-pass-material.d.ts.map +1 -1
  198. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  199. package/dist/record/main-pass.d.ts +2 -0
  200. package/dist/record/main-pass.d.ts.map +1 -1
  201. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  202. package/dist/record/render-context.d.ts +98 -6
  203. package/dist/record/render-context.d.ts.map +1 -1
  204. package/dist/record/shadow-pass.d.ts.map +1 -1
  205. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  206. package/dist/record/typed-frame-graph.d.ts +119 -4
  207. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  208. package/dist/record/view-ubo.d.ts +7 -5
  209. package/dist/record/view-ubo.d.ts.map +1 -1
  210. package/dist/reflection/filter.d.ts +22 -0
  211. package/dist/reflection/filter.d.ts.map +1 -0
  212. package/dist/reflection/gpu-table.d.ts +25 -0
  213. package/dist/reflection/gpu-table.d.ts.map +1 -0
  214. package/dist/reflection/inspection.d.ts +10 -0
  215. package/dist/reflection/inspection.d.ts.map +1 -0
  216. package/dist/reflection/projection.d.ts +140 -0
  217. package/dist/reflection/projection.d.ts.map +1 -0
  218. package/dist/reflection/record-owner.d.ts +60 -0
  219. package/dist/reflection/record-owner.d.ts.map +1 -0
  220. package/dist/render-contract.d.ts +62 -14
  221. package/dist/render-contract.d.ts.map +1 -1
  222. package/dist/render-graph-primitives.d.ts +1 -0
  223. package/dist/render-graph-primitives.d.ts.map +1 -1
  224. package/dist/render-pipeline.d.ts +49 -2
  225. package/dist/render-pipeline.d.ts.map +1 -1
  226. package/dist/render-system-extract.d.ts +160 -31
  227. package/dist/render-system-extract.d.ts.map +1 -1
  228. package/dist/render-system.d.ts +28 -2
  229. package/dist/render-system.d.ts.map +1 -1
  230. package/dist/renderer.d.ts +43 -0
  231. package/dist/renderer.d.ts.map +1 -0
  232. package/dist/scene/probe-blend-record.d.ts +22 -0
  233. package/dist/scene/probe-blend-record.d.ts.map +1 -0
  234. package/dist/scene/probe-blend.d.ts +142 -0
  235. package/dist/scene/probe-blend.d.ts.map +1 -0
  236. package/dist/scene/render-scene-types.d.ts +0 -6
  237. package/dist/scene/render-scene-types.d.ts.map +1 -1
  238. package/dist/scene/render-scene.d.ts +117 -9
  239. package/dist/scene/render-scene.d.ts.map +1 -1
  240. package/dist/scene/visibility/bounds.d.ts +20 -0
  241. package/dist/scene/visibility/bounds.d.ts.map +1 -0
  242. package/dist/scene/visibility/budget.d.ts +16 -0
  243. package/dist/scene/visibility/budget.d.ts.map +1 -0
  244. package/dist/scene/visibility/facet.d.ts +73 -0
  245. package/dist/scene/visibility/facet.d.ts.map +1 -0
  246. package/dist/scene/visibility/gpu-frame-timing.d.ts +41 -0
  247. package/dist/scene/visibility/gpu-frame-timing.d.ts.map +1 -0
  248. package/dist/scene/visibility/gpu-lod.d.ts +46 -0
  249. package/dist/scene/visibility/gpu-lod.d.ts.map +1 -0
  250. package/dist/scene/visibility/inspection.d.ts +132 -0
  251. package/dist/scene/visibility/inspection.d.ts.map +1 -0
  252. package/dist/scene/visibility/lod-selector.d.ts +25 -0
  253. package/dist/scene/visibility/lod-selector.d.ts.map +1 -0
  254. package/dist/scene/visibility/occlusion-confidence.d.ts +81 -0
  255. package/dist/scene/visibility/occlusion-confidence.d.ts.map +1 -0
  256. package/dist/scene/visibility/occlusion-pass.d.ts +57 -0
  257. package/dist/scene/visibility/occlusion-pass.d.ts.map +1 -0
  258. package/dist/scene/visibility/occlusion-query-pool.d.ts +57 -0
  259. package/dist/scene/visibility/occlusion-query-pool.d.ts.map +1 -0
  260. package/dist/scene/visibility/occlusion-runtime.d.ts +96 -0
  261. package/dist/scene/visibility/occlusion-runtime.d.ts.map +1 -0
  262. package/dist/scene/visibility/types.d.ts +34 -0
  263. package/dist/scene/visibility/types.d.ts.map +1 -0
  264. package/dist/shadow-atlas.d.ts +3 -3
  265. package/dist/shadow-atlas.d.ts.map +1 -1
  266. package/dist/ssao-buffers.d.ts +3 -0
  267. package/dist/ssao-buffers.d.ts.map +1 -1
  268. package/dist/ssr/admission.d.ts +122 -0
  269. package/dist/ssr/admission.d.ts.map +1 -0
  270. package/dist/ssr/composition.d.ts +32 -0
  271. package/dist/ssr/composition.d.ts.map +1 -0
  272. package/dist/ssr/errors.d.ts +39 -0
  273. package/dist/ssr/errors.d.ts.map +1 -0
  274. package/dist/ssr/identity.d.ts +13 -0
  275. package/dist/ssr/identity.d.ts.map +1 -0
  276. package/dist/systems/skin-palette-allocator.d.ts +1 -1
  277. package/dist/targets/contracts.d.ts +66 -0
  278. package/dist/targets/contracts.d.ts.map +1 -0
  279. package/dist/targets/graph-projection.d.ts +29 -0
  280. package/dist/targets/graph-projection.d.ts.map +1 -0
  281. package/dist/targets/material-source.d.ts +34 -0
  282. package/dist/targets/material-source.d.ts.map +1 -0
  283. package/dist/targets/owner.d.ts +32 -0
  284. package/dist/targets/owner.d.ts.map +1 -0
  285. package/dist/targets/physical.d.ts +18 -0
  286. package/dist/targets/physical.d.ts.map +1 -0
  287. package/dist/targets/readback.d.ts +39 -0
  288. package/dist/targets/readback.d.ts.map +1 -0
  289. package/dist/temporal/frame-coordinator.d.ts +13 -0
  290. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  291. package/dist/temporal/frame.d.ts +2 -0
  292. package/dist/temporal/frame.d.ts.map +1 -1
  293. package/dist/temporal/index.mjs +6 -5
  294. package/dist/temporal/view.d.ts +7 -0
  295. package/dist/temporal/view.d.ts.map +1 -1
  296. package/dist/tilemap-chunk-extract-system.d.ts +1 -1
  297. package/dist/typed-render-graph-primitives.d.ts +5 -1
  298. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  299. package/dist/typed-shadow-passes.d.ts +1 -1
  300. package/dist/typed-shadow-passes.d.ts.map +1 -1
  301. package/dist/volume/passes.d.ts +9 -5
  302. package/dist/volume/passes.d.ts.map +1 -1
  303. package/package.json +20 -20
  304. package/src/__tests__/bloom-exact-zero.unit.test.ts +48 -19
  305. package/src/__tests__/bloom-lifecycle.integration.test.ts +41 -13
  306. package/src/__tests__/camera-target-consumption.unit.test.ts +53 -0
  307. package/src/__tests__/cluster-binner.unit.test.ts +35 -0
  308. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +12 -2
  309. package/src/__tests__/cube-camera-scheduler.unit.test.ts +70 -0
  310. package/src/__tests__/cube-camera-single-submit.perf.test.ts +75 -0
  311. package/src/__tests__/cube-camera-view-matrices.unit.test.ts +41 -0
  312. package/src/__tests__/cube-camera-vocabulary.unit.test.ts +47 -0
  313. package/src/__tests__/cube-camera.integration.test.ts +42 -0
  314. package/src/__tests__/detached-inspection.integration.test.ts +6 -0
  315. package/src/__tests__/device-feature-admission.unit.test.ts +85 -0
  316. package/src/__tests__/direct-light-snapshot.unit.test.ts +62 -6
  317. package/src/__tests__/docs-gate-green.unit.test.ts +38 -0
  318. package/src/__tests__/environment-generation.integration.test.ts +16 -0
  319. package/src/__tests__/evidence-identity.ts +13 -0
  320. package/src/__tests__/evidence-identity.unit.test.ts +24 -0
  321. package/src/__tests__/extended-lighting-recovery.browser.test.ts +14 -0
  322. package/src/__tests__/extended-lighting-recovery.dawn.test.ts +14 -0
  323. package/src/__tests__/extended-lighting-recovery.integration.test.ts +81 -0
  324. package/src/__tests__/extended-lighting-resources.unit.test.ts +51 -0
  325. package/src/__tests__/extended-lighting-zero-feature.integration.test.ts +52 -0
  326. package/src/__tests__/factory-contract.integration.test.ts +288 -29
  327. package/src/__tests__/fog-component.unit.test.ts +67 -0
  328. package/src/__tests__/fog.integration.test.ts +86 -0
  329. package/src/__tests__/frame-observation.contract.test.ts +15 -0
  330. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +37 -0
  331. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +146 -0
  332. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +10 -8
  333. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +18 -1
  334. package/src/__tests__/gpu-driven-production.integration.test.ts +579 -17
  335. package/src/__tests__/gpu-driven-scaling.unit.test.ts +66 -2
  336. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +26 -1
  337. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +101 -6
  338. package/src/__tests__/gpu-driven-view.browser.test.ts +3 -3
  339. package/src/__tests__/gpu-driven-view.dawn.test.ts +3 -3
  340. package/src/__tests__/gpu-driven-view.unit.test.ts +50 -0
  341. package/src/__tests__/gpu-lod-evidence.ts +188 -0
  342. package/src/__tests__/gpu-lod-fallback.integration.test.ts +10 -0
  343. package/src/__tests__/gpu-lod-indirect.integration.test.ts +40 -0
  344. package/src/__tests__/gpu-lod-parity.dawn.test.ts +29 -0
  345. package/src/__tests__/gpu-lod-readiness.integration.test.ts +52 -0
  346. package/src/__tests__/gpu-lod-schema.unit.test.ts +66 -0
  347. package/src/__tests__/gpu-lod-selector.unit.test.ts +28 -0
  348. package/src/__tests__/gpu-lod-silhouette.browser.test.ts +13 -0
  349. package/src/__tests__/gpu-pass-timing-browser-runner.ts +281 -0
  350. package/src/__tests__/gpu-pass-timing-coexistence.integration.test.ts +80 -0
  351. package/src/__tests__/gpu-pass-timing-contract.unit.test.ts +243 -0
  352. package/src/__tests__/gpu-pass-timing-exact-zero.unit.test.ts +131 -0
  353. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +230 -0
  354. package/src/__tests__/gpu-pass-timing-observation.unit.test.ts +233 -0
  355. package/src/__tests__/gpu-pass-timing-session.unit.test.ts +438 -0
  356. package/src/__tests__/gpu-pass-timing.browser.test.ts +26 -0
  357. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +125 -0
  358. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +66 -1
  359. package/src/__tests__/gpu-scene.dawn.test.ts +8 -1
  360. package/src/__tests__/gpu-scene.unit.test.ts +49 -3
  361. package/src/__tests__/hdrp-group2-visibility.unit.test.ts +24 -0
  362. package/src/__tests__/instances-culling.integration.test.ts +94 -0
  363. package/src/__tests__/instances-derived-bounds.unit.test.ts +118 -0
  364. package/src/__tests__/light-buffer-layout.unit.test.ts +113 -3
  365. package/src/__tests__/light-inspection.unit.test.ts +55 -0
  366. package/src/__tests__/light-resource-error.unit.test.ts +43 -0
  367. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +109 -0
  368. package/src/__tests__/lod-bounds.unit.test.ts +42 -0
  369. package/src/__tests__/lod-cpu-lane.integration.test.ts +55 -0
  370. package/src/__tests__/lod-cpu-no-change.perf.test.ts +19 -0
  371. package/src/__tests__/lod-gpu-frame-timing.unit.test.ts +49 -0
  372. package/src/__tests__/lod-multi-view.integration.test.ts +63 -0
  373. package/src/__tests__/lod-occlusion-device-loss.integration.test.ts +27 -0
  374. package/src/__tests__/lod-occlusion-final-decision.integration.test.ts +47 -0
  375. package/src/__tests__/lod-occlusion-inspection.integration.test.ts +217 -0
  376. package/src/__tests__/lod-occlusion-inspection.schema.test-d.ts +14 -0
  377. package/src/__tests__/lod-occlusion-inspection.unit.test.ts +72 -0
  378. package/src/__tests__/lod-occlusion-visual-evidence.ts +91 -0
  379. package/src/__tests__/lod-occlusion.browser.test.ts +15 -0
  380. package/src/__tests__/lod-occlusion.dawn.test.ts +20 -0
  381. package/src/__tests__/lod-selector.profile.test.ts +35 -0
  382. package/src/__tests__/lod-selector.unit.test.ts +68 -0
  383. package/src/__tests__/lod-visibility-identity.unit.test.ts +198 -0
  384. package/src/__tests__/lod-visibility-lifecycle.integration.test.ts +132 -0
  385. package/src/__tests__/ltc-resources.unit.test.ts +32 -0
  386. package/src/__tests__/material-contract-inventory.unit.test.ts +17 -0
  387. package/src/__tests__/material-observation.integration.test.ts +101 -0
  388. package/src/__tests__/occlusion-confidence.unit.test.ts +168 -0
  389. package/src/__tests__/occlusion-invalidation.integration.test.ts +37 -0
  390. package/src/__tests__/occlusion-pass-routing.unit.test.ts +16 -0
  391. package/src/__tests__/occlusion-pass.dawn.test.ts +20 -0
  392. package/src/__tests__/occlusion-pass.integration.test.ts +43 -0
  393. package/src/__tests__/occlusion-pass.rhi-null.test.ts +22 -0
  394. package/src/__tests__/occlusion-query-pool.rhi-null.test.ts +21 -0
  395. package/src/__tests__/occlusion-query-pool.unit.test.ts +95 -0
  396. package/src/__tests__/occlusion-query-ticket.integration.test.ts +47 -0
  397. package/src/__tests__/occlusion-query.dawn.test.ts +16 -0
  398. package/src/__tests__/occlusion-query.evidence.ts +178 -0
  399. package/src/__tests__/occlusion-query.rhi-null.test.ts +31 -0
  400. package/src/__tests__/occlusion-runtime.unit.test.ts +120 -0
  401. package/src/__tests__/pbr-view-layout.unit.test.ts +16 -0
  402. package/src/__tests__/point-shadow-inspection.unit.test.ts +60 -0
  403. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +196 -0
  404. package/src/__tests__/probe-bindings.unit.test.ts +200 -0
  405. package/src/__tests__/probe-blend-numeric.unit.test.ts +67 -0
  406. package/src/__tests__/probe-blend-oracle.unit.test.ts +136 -0
  407. package/src/__tests__/probe-capacity.integration.test.ts +97 -0
  408. package/src/__tests__/probe-recovery.integration.test.ts +104 -0
  409. package/src/__tests__/probe-scene-dirty.integration.test.ts +211 -0
  410. package/src/__tests__/profiler-phase-catalog.test.ts +4 -1
  411. package/src/__tests__/profiler-phase-token.test.ts +1 -0
  412. package/src/__tests__/public-surface.test-d.ts +10 -0
  413. package/src/__tests__/rect-area-boundary.unit.test.ts +24 -0
  414. package/src/__tests__/reflection-fallback-graph.unit.test.ts +67 -0
  415. package/src/__tests__/reflection-fallback-inspection.unit.test.ts +77 -0
  416. package/src/__tests__/reflection-fallback-lifecycle.integration.test.ts +21 -0
  417. package/src/__tests__/reflection-fallback-projection.unit.test.ts +90 -0
  418. package/src/__tests__/reflection-fallback-source.unit.test.ts +32 -0
  419. package/src/__tests__/reflection-probe-authoring.unit.test.ts +71 -0
  420. package/src/__tests__/reflection-probe-filter.unit.test.ts +54 -0
  421. package/src/__tests__/reflection-probe-ibl-topology.unit.test.ts +45 -0
  422. package/src/__tests__/reflection-probe-owner-placement.unit.test.ts +39 -0
  423. package/src/__tests__/reflection-probe-pass-encode.integration.test.ts +101 -0
  424. package/src/__tests__/reflection-probe-performance.perf.test.ts +60 -0
  425. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +120 -0
  426. package/src/__tests__/reflection-probe-recovery.integration.test.ts +55 -0
  427. package/src/__tests__/reflection-probe-selection.unit.test.ts +46 -0
  428. package/src/__tests__/render-error-code-owner.test-d.ts +19 -3
  429. package/src/__tests__/render-feature-docs.unit.test.ts +12 -7
  430. package/src/__tests__/render-graph-fallback.integration.test.ts +24 -0
  431. package/src/__tests__/render-perf/extended-lighting.bench.test.ts +66 -0
  432. package/src/__tests__/render-scene-projection.unit.test.ts +447 -18
  433. package/src/__tests__/render-target-admission.unit.test.ts +80 -0
  434. package/src/__tests__/render-target-graph-projection.unit.test.ts +102 -0
  435. package/src/__tests__/render-target-inspection.unit.test.ts +52 -0
  436. package/src/__tests__/render-target-material-source.test-d.ts +30 -0
  437. package/src/__tests__/render-target-material-source.unit.test.ts +97 -0
  438. package/src/__tests__/render-target-owner-placement.unit.test.ts +203 -0
  439. package/src/__tests__/render-target-owner.unit.test.ts +122 -0
  440. package/src/__tests__/render-target-probe-soak.perf.test.ts +94 -0
  441. package/src/__tests__/render-target-public-schema.json +107 -0
  442. package/src/__tests__/render-target-public-schema.unit.test.ts +91 -0
  443. package/src/__tests__/render-target-public-surface.test-d.ts +83 -0
  444. package/src/__tests__/render-target-readback.integration.test.ts +90 -0
  445. package/src/__tests__/render-target-recovery.integration.test.ts +85 -0
  446. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +124 -0
  447. package/src/__tests__/renderer-lifecycle.integration.test.ts +14 -0
  448. package/src/__tests__/shader-stage-surface.unit.test.ts +19 -4
  449. package/src/__tests__/skin-motion-regression.unit.test.ts +154 -0
  450. package/src/__tests__/skinned-shadow-caster.test.ts +13 -4
  451. package/src/__tests__/spot-modifier-projection.unit.test.ts +70 -0
  452. package/src/__tests__/ssr-admission-matrix.integration.test.ts +206 -0
  453. package/src/__tests__/ssr-composition.unit.test.ts +46 -0
  454. package/src/__tests__/ssr-fallback-zero-work.bench.test.ts +94 -0
  455. package/src/__tests__/ssr-recovery-errors.unit.test.ts +58 -0
  456. package/src/__tests__/standard-cluster-capability.integration.test.ts +63 -0
  457. package/src/__tests__/standard-cluster-transport.unit.test.ts +121 -0
  458. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +299 -0
  459. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +15 -5
  460. package/src/__tests__/standard-lighting-inspection.integration.test.ts +138 -0
  461. package/src/__tests__/standard-lighting-ssot.unit.test.ts +18 -0
  462. package/src/__tests__/standard-lighting-write.unit.test.ts +441 -0
  463. package/src/__tests__/standard-lighting.integration.test.ts +67 -0
  464. package/src/__tests__/standard-pipeline.dawn.test.ts +206 -5
  465. package/src/__tests__/standard-pipeline.integration.test.ts +181 -150
  466. package/src/__tests__/standard-probe-binding.unit.test.ts +25 -0
  467. package/src/__tests__/standard-profile-oracle.unit.test.ts +23 -38
  468. package/src/__tests__/temporal-fallback-generation.integration.test.ts +16 -0
  469. package/src/__tests__/temporal-view.unit.test.ts +3 -3
  470. package/src/__tests__/transmission-prewarm.unit.test.ts +69 -0
  471. package/src/__tests__/typed-pipeline-topology.unit.test.ts +395 -14
  472. package/src/__tests__/urp-pipeline-targets.unit.test.ts +2 -2
  473. package/src/__tests__/visibility-budget.integration.test.ts +140 -0
  474. package/src/__tests__/visibility-budget.unit.test.ts +70 -0
  475. package/src/__tests__/visibility-extract.integration.test.ts +11 -6
  476. package/src/__tests__/visibility-resolver.unit.test.ts +15 -0
  477. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +6 -3
  478. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +31 -28
  479. package/src/assembly/backend-contract.ts +13 -0
  480. package/src/assembly/device-feature-admission.ts +80 -0
  481. package/src/assembly/directional-shadow-inspection.ts +122 -0
  482. package/src/assembly/factory.ts +1411 -366
  483. package/src/assembly/host-contract.ts +35 -0
  484. package/src/assembly/render-target-host.ts +847 -0
  485. package/src/assets/asset-decoders.ts +1 -1
  486. package/src/assets/render-asset-port.ts +7 -1
  487. package/src/capture/cube-views.ts +69 -0
  488. package/src/capture/scheduler.ts +265 -0
  489. package/src/cluster-binner.ts +31 -0
  490. package/src/components/__tests__/lighting-authoring.unit.test.ts +39 -0
  491. package/src/components/__tests__/rect-area-light.unit.test.ts +33 -0
  492. package/src/components/__tests__/schema-contract.unit.test.ts +12 -0
  493. package/src/components/__tests__/schema-parity.unit.test.ts +9 -0
  494. package/src/components/camera.ts +4 -0
  495. package/src/components/cube-camera.ts +52 -0
  496. package/src/components/directional-light.ts +22 -2
  497. package/src/components/directional-shadow-filter.ts +57 -0
  498. package/src/components/index.ts +5 -0
  499. package/src/components/light-helpers.ts +133 -8
  500. package/src/components/light-probe.ts +13 -0
  501. package/src/components/rect-area-light.ts +15 -0
  502. package/src/components/reflection-probe.ts +49 -0
  503. package/src/components/scene-instance.ts +6 -0
  504. package/src/components/spot-light.ts +3 -0
  505. package/src/device/__tests__/extended-lighting-recovery.unit.test.ts +24 -0
  506. package/src/device/gpu-residency.ts +140 -9
  507. package/src/device/lifecycle-transaction.ts +12 -0
  508. package/src/environment/frame.ts +6 -24
  509. package/src/environment/lifecycle.ts +20 -1
  510. package/src/errors/gpu-driven.ts +92 -0
  511. package/src/errors/index.ts +1 -0
  512. package/src/errors/recover.ts +32 -0
  513. package/src/errors/render.ts +341 -143
  514. package/src/extract/camera.ts +3 -3
  515. package/src/extract/directional-shadow-projection.ts +61 -0
  516. package/src/extract/environment.ts +57 -0
  517. package/src/extract/gpu-driven.ts +108 -0
  518. package/src/extract/visibility.ts +2 -1
  519. package/src/features/fullscreen.ts +3 -0
  520. package/src/features/targets.ts +4 -1
  521. package/src/features/types.ts +10 -0
  522. package/src/fullscreen-post-process-pass.ts +43 -2
  523. package/src/gpu-driven/batch-topology.ts +207 -28
  524. package/src/gpu-driven/prepared-draw.ts +208 -0
  525. package/src/gpu-driven/production-raster.ts +953 -43
  526. package/src/gpu-driven/view-gpu.ts +411 -29
  527. package/src/gpu-scene-schema.ts +13 -0
  528. package/src/gpu-scene.ts +140 -64
  529. package/src/gpu-usage.ts +0 -1
  530. package/src/hdrp-buffers.ts +169 -63
  531. package/src/ibl/IblPipelineCache.ts +11 -0
  532. package/src/ibl/kernel-cache.ts +88 -0
  533. package/src/index.ts +222 -0
  534. package/src/inspection-types.ts +276 -1
  535. package/src/instances-derived-bounds.ts +171 -0
  536. package/src/internal.ts +38 -0
  537. package/src/light-buffer-layout.ts +155 -310
  538. package/src/materials.ts +16 -2
  539. package/src/mesh-material-bindings.ts +132 -0
  540. package/src/pbr-pipeline.ts +255 -60
  541. package/src/pipeline/standard-forward-lane.ts +93 -16
  542. package/src/pipeline/standard-lighting/errors.ts +6 -0
  543. package/src/pipeline/standard-lighting/graph.ts +158 -0
  544. package/src/pipeline/standard-lighting/inspection.ts +88 -0
  545. package/src/pipeline/standard-lighting/layout.ts +31 -0
  546. package/src/pipeline/standard-lighting/light-frame.ts +35 -0
  547. package/src/pipeline/standard-lighting/membership.ts +43 -0
  548. package/src/pipeline/standard-lighting/prepare.ts +37 -0
  549. package/src/pipeline/standard-lighting/topology.ts +120 -0
  550. package/src/pipeline/standard-lighting/transport.ts +78 -0
  551. package/src/pipeline/standard-pipeline.ts +124 -193
  552. package/src/pipeline/standard-post.ts +26 -7
  553. package/src/pipeline/standard-profile.ts +17 -47
  554. package/src/pipeline-builder.ts +5 -2
  555. package/src/pipeline-spec.ts +95 -21
  556. package/src/plugin.ts +4 -0
  557. package/src/point-shadow-inspection.ts +55 -0
  558. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +56 -0
  559. package/src/prepare/extended-lighting/ltc-resources.ts +136 -0
  560. package/src/prepare/extended-lighting/resources.ts +389 -0
  561. package/src/prepare/extended-lighting/spot-modifiers.ts +194 -0
  562. package/src/prepare/extended-lighting/state.ts +92 -0
  563. package/src/record/__tests__/gpu-timing.unit.test.ts +177 -0
  564. package/src/record/__tests__/temporal-payload.unit.test.ts +64 -1
  565. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +73 -0
  566. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +20 -1
  567. package/src/record/__tests__/view-ubo-layout.unit.test.ts +40 -0
  568. package/src/record/frame-lighting.ts +591 -452
  569. package/src/record/frame-snapshot.ts +92 -39
  570. package/src/record/frame-targets.ts +17 -0
  571. package/src/record/frame.ts +662 -158
  572. package/src/record/gpu-pass-timing/contract.ts +180 -0
  573. package/src/record/gpu-pass-timing/errors.ts +34 -0
  574. package/src/record/gpu-pass-timing/index.ts +37 -0
  575. package/src/record/gpu-pass-timing/parser.ts +97 -0
  576. package/src/record/gpu-pass-timing/session.ts +938 -0
  577. package/src/record/gpu-timing.ts +112 -39
  578. package/src/record/helpers.ts +0 -58
  579. package/src/record/main-pass-geometry.ts +324 -119
  580. package/src/record/main-pass-material.ts +103 -10
  581. package/src/record/main-pass-sprite-draws.ts +99 -103
  582. package/src/record/main-pass.ts +71 -15
  583. package/src/record/mesh-ssbo.ts +47 -36
  584. package/src/record/render-context.ts +136 -3
  585. package/src/record/shadow-pass.ts +83 -68
  586. package/src/record/skybox-post-pass.ts +20 -4
  587. package/src/record/typed-frame-graph.ts +1034 -43
  588. package/src/record/view-ubo.ts +72 -47
  589. package/src/reflection/filter.ts +81 -0
  590. package/src/reflection/gpu-table.ts +44 -0
  591. package/src/reflection/inspection.ts +67 -0
  592. package/src/reflection/projection.ts +476 -0
  593. package/src/reflection/record-owner.ts +1283 -0
  594. package/src/render-contract.ts +93 -10
  595. package/src/render-graph-primitives.ts +9 -4
  596. package/src/render-pipeline.ts +122 -2
  597. package/src/render-system-extract.ts +1041 -225
  598. package/src/render-system.ts +1310 -85
  599. package/src/renderer.ts +222 -0
  600. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +36 -0
  601. package/src/scene/probe-blend-record.ts +54 -0
  602. package/src/scene/probe-blend.ts +813 -0
  603. package/src/scene/render-scene-types.ts +0 -7
  604. package/src/scene/render-scene.ts +942 -171
  605. package/src/scene/visibility/bounds.ts +57 -0
  606. package/src/scene/visibility/budget.ts +40 -0
  607. package/src/scene/visibility/facet.ts +400 -0
  608. package/src/scene/visibility/gpu-frame-timing.ts +101 -0
  609. package/src/scene/visibility/gpu-lod.ts +119 -0
  610. package/src/scene/visibility/inspection.ts +313 -0
  611. package/src/scene/visibility/lod-selector.ts +84 -0
  612. package/src/scene/visibility/occlusion-confidence.ts +478 -0
  613. package/src/scene/visibility/occlusion-pass.ts +165 -0
  614. package/src/scene/visibility/occlusion-query-pool.ts +178 -0
  615. package/src/scene/visibility/occlusion-runtime.ts +672 -0
  616. package/src/scene/visibility/types.ts +50 -0
  617. package/src/shadow-atlas.ts +5 -5
  618. package/src/ssao-buffers.ts +9 -2
  619. package/src/ssr/admission.ts +451 -0
  620. package/src/ssr/composition.ts +73 -0
  621. package/src/ssr/errors.ts +76 -0
  622. package/src/ssr/identity.ts +12 -0
  623. package/src/systems/skin-palette-allocator.ts +1 -1
  624. package/src/targets/contracts.ts +182 -0
  625. package/src/targets/graph-projection.ts +169 -0
  626. package/src/targets/material-source.ts +143 -0
  627. package/src/targets/owner.ts +214 -0
  628. package/src/targets/physical.ts +18 -0
  629. package/src/targets/readback.ts +153 -0
  630. package/src/temporal/frame-coordinator.ts +26 -0
  631. package/src/temporal/frame.ts +14 -1
  632. package/src/temporal/view.ts +11 -0
  633. package/src/tilemap-chunk-extract-system.ts +4 -4
  634. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +59 -20
  635. package/src/typed-render-graph-primitives.ts +53 -3
  636. package/src/typed-shadow-passes.ts +46 -40
  637. package/src/volume/passes.ts +61 -58
  638. package/dist/.tsbuildinfo +0 -1
  639. package/dist/chunk-4XINDLAY.mjs.map +0 -1
  640. package/dist/chunk-CDTWTHZR.mjs.map +0 -1
  641. package/dist/chunk-M2MP4ZOU.mjs +0 -73
  642. package/dist/chunk-M2MP4ZOU.mjs.map +0 -1
  643. package/dist/chunk-PCHBPYML.mjs.map +0 -1
  644. package/dist/chunk-TBQJOK2L.mjs.map +0 -1
  645. package/dist/chunk-VFPLTKCU.mjs.map +0 -1
@@ -50,17 +50,21 @@ import type {
50
50
  PipelineLayout,
51
51
  RenderPipeline,
52
52
  Result,
53
- RhiAdapter,
54
53
  RhiCanvasContext,
55
54
  RhiDevice,
56
55
  Sampler,
57
56
  ShaderModule,
57
+ TextureView,
58
58
  } from '@forgeax/engine-rhi';
59
59
  import { err, ok, RhiError, validateDrawArgs } from '@forgeax/engine-rhi';
60
60
  import { registerPropagateTransforms } from '@forgeax/engine-scene';
61
61
  import {
62
+ createStandardPbrArtifactReceipt,
62
63
  DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
63
64
  findVariantByKey,
65
+ LTC_TABLE_HEIGHT,
66
+ LTC_TABLE_WIDTH,
67
+ LTC_TABLES,
64
68
  type MaterialShaderEntry,
65
69
  type MaterialShaderManifestEntry,
66
70
  ShaderCatalog,
@@ -92,12 +96,16 @@ import {
92
96
  RendererOperationError,
93
97
  RenderFeatureCapabilityMissingError,
94
98
  RenderFeatureStageFailedError,
99
+ StandardProfileInvalidError,
95
100
  } from '../errors/render';
96
101
  import { createRenderFeatureHost, type RenderFeatureHost } from '../features/host';
97
102
  import type { RenderFeatureMaterialShaderBindingContract } from '../features/plan';
98
103
  import { RENDER_FEATURE_VERTEX_LAYOUTS } from '../features/prepared-graphics';
99
104
  import type { RenderFeature, RenderFeatureShaderModuleMode } from '../features/types';
100
- import type { PostProcessShaderEntry } from '../fullscreen-post-process-pass';
105
+ import {
106
+ type PostProcessShaderEntry,
107
+ postProcessShaderModuleLabel,
108
+ } from '../fullscreen-post-process-pass';
101
109
  import { glyphTextLayoutSystem } from '../glyph-text-layout-system';
102
110
  import { GpuBuffer } from '../gpu-resource';
103
111
  import { GPU_SHADER_STAGE_FRAGMENT, GPU_SHADER_STAGE_VERTEX } from '../gpu-stage';
@@ -127,18 +135,14 @@ import {
127
135
  LostListenerRegistry,
128
136
  RhiErrorListenerRegistry,
129
137
  } from '../lifecycle';
130
- import {
131
- assertStorageBufferCap,
132
- LIGHT_ARRAY_HEADER_BYTES,
133
- LIGHT_ARRAY_MAX_SLOTS,
134
- POINT_LIGHT_STD430_BYTES,
135
- SPOT_LIGHT_STD430_BYTES,
136
- } from '../light-buffer-layout';
138
+ import { assertStorageBufferCap } from '../light-buffer-layout';
137
139
  import {
138
140
  buildPbrMaterialUserRegionEntries,
139
141
  buildPbrPipelineLayouts,
140
142
  buildPbrSkinLayouts,
141
- isSkinnedShadowCasterVariant,
143
+ createHdrpSkinBindGroupLayoutDescriptor,
144
+ type PipelineGroup2Contract,
145
+ resolvePipelineGroup2Contract,
142
146
  SKIN_MATERIAL_SHADER_ID,
143
147
  } from '../pbr-pipeline';
144
148
  import { STANDARD_CLUSTER_MEMBERSHIP_WGSL, standardPipeline } from '../pipeline/standard-pipeline';
@@ -164,7 +168,31 @@ import {
164
168
  PipelineSpecError,
165
169
  } from '../pipeline-spec';
166
170
  import { POINTS_LINES_MATERIAL_SHADER_ID } from '../points-lines/record';
171
+ import type { ExtendedLightingResourceCandidate } from '../prepare/extended-lighting/resources';
172
+ import {
173
+ COOKIE_MATRIX_BYTES,
174
+ COOKIE_SLICE_SIZE,
175
+ deriveExtendedLightingCapability,
176
+ EXTENDED_LIGHTING_TOPOLOGY,
177
+ extendedLightingSampledTextureCapacityAvailable,
178
+ IES_SLICE_HEIGHT,
179
+ IES_SLICE_WIDTH,
180
+ } from '../prepare/extended-lighting/resources';
181
+ import { createCookieProjectionMatrixData } from '../prepare/extended-lighting/spot-modifiers';
182
+ import {
183
+ createExtendedLightingState,
184
+ projectExtendedLightingInspection,
185
+ promoteExtendedLightingCandidate,
186
+ } from '../prepare/extended-lighting/state';
167
187
  import type { FrameObservationOptions } from '../record/frame';
188
+ import type { GpuPassTimingReason } from '../record/gpu-pass-timing/errors.js';
189
+ import {
190
+ createGpuPassTimingSession,
191
+ DEFAULT_GPU_PASS_TIMING_OPTIONS,
192
+ type GpuPassTimingCapture,
193
+ type GpuPassTimingObservation,
194
+ type GpuPassTimingSession,
195
+ } from '../record/gpu-pass-timing/index.js';
168
196
  import { GpuTimingCapture, type VolumeTimingObservation } from '../record/gpu-timing';
169
197
  import type { BloomPersistentBundle } from '../record/render-context';
170
198
  import { SHADOW_CASTER_BUFFER_SIZE } from '../record/shadow-pass';
@@ -190,8 +218,10 @@ import type {
190
218
  RenderWorldLease,
191
219
  VolumetricFogShaderSources,
192
220
  } from '../render-contract';
193
- import { STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from '../render-contract';
221
+ import { FXAA_POST_PROCESS_ID, STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from '../render-contract';
222
+
194
223
  import {
224
+ attachGpuPassTimingSession,
195
225
  configureSurface,
196
226
  createRenderSystem,
197
227
  MATERIAL_PER_ENTITY_STRIDE,
@@ -199,6 +229,7 @@ import {
199
229
  type RenderSystem,
200
230
  selectSwapChainFormat,
201
231
  } from '../render-system';
232
+ import { createGpuPassTimingObservationStore, observeGpuPassTimingDisabled } from '../renderer.js';
202
233
  import { postSpawnResolveJoints } from '../scene-instances/post-spawn-resolve-joints';
203
234
  import {
204
235
  createSkinPaletteAllocator,
@@ -206,7 +237,12 @@ import {
206
237
  } from '../systems/skin-palette-allocator';
207
238
  import { tilemapChunkExtractSystem } from '../tilemap-chunk-extract-system';
208
239
  import { EngineEnvironmentError, type RhiBackendPack } from './backend-contract';
240
+ import {
241
+ deviceOptionsForAdapter,
242
+ STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
243
+ } from './device-feature-admission';
209
244
  import type { RendererAssemblyImplementation, RendererHostImplementation } from './host-contract';
245
+ import { createRenderTargetHost } from './render-target-host';
210
246
  import { projectRendererOutputInspection } from './renderer-inspection';
211
247
  import { isRendererStateOperational } from './renderer-lifecycle';
212
248
 
@@ -226,6 +262,19 @@ const GPU_ENVIRONMENT_ERROR_CODES = new Set([
226
262
  'oom',
227
263
  ]);
228
264
 
265
+ /**
266
+ * The boot artifact is only a registry/prewarm seed. It is deliberately not
267
+ * an active lighting decision: every frame derives its Standard variant from
268
+ * `RenderPipelineFrame.standardLighting.kind` at record time. Keeping this
269
+ * boot-only value named as such prevents the eager module from becoming a
270
+ * second topology authority.
271
+ */
272
+ const STANDARD_BOOT_TOPOLOGY_KIND: 'no-local-lights' | 'clustered' = 'no-local-lights';
273
+
274
+ function standardBootClusterAxis(): boolean {
275
+ return STANDARD_BOOT_TOPOLOGY_KIND === 'clustered';
276
+ }
277
+
229
278
  function classifyEnvErrorReason(
230
279
  base: string,
231
280
  primary: { code?: unknown; name?: unknown } | undefined,
@@ -275,8 +324,8 @@ function adaptMipmapShaderModuleFactory(
275
324
 
276
325
  // Re-export registerAdvanceAnimationPlayer so consumers can wire it.
277
326
  // Re-export registerPropagateTransforms so consumers can wire the
278
- // Transform.world mat4 derivation (audio listener sync + picking read the
279
- // derived Transform.world from scripts).
327
+ // GlobalTransform.world mat4 derivation (audio listener sync + picking read the
328
+ // derived GlobalTransform.world from scripts).
280
329
 
281
330
  // feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4): a single
282
331
  // shared empty ArrayBuffer used by `buildPipelineContext` to synthesize the
@@ -415,7 +464,7 @@ function toGpuVertexBufferLayouts(
415
464
  * Bundler-layer injection accepted by `createRenderer` (and proxied by
416
465
  * `createApp`) as the optional third argument.
417
466
  *
418
- * feat-20260608-create-app-param-surface-trim / M2 / D-3: aggregates the two
467
+ * feat-20260608-create-app-param-surface-trim / M2 / D-3: aggregates the three
419
468
  * host-injected build-tool channels:
420
469
  *
421
470
  * - shaderManifestUrl: the URL the host's vite-plugin-shader emit step
@@ -429,6 +478,9 @@ function toGpuVertexBufferLayouts(
429
478
  * AssetRegistry third ctor slot so DDC-miss assets can lazy-import.
430
479
  * Absent => shipped form (a DDC miss fails fast with `asset-not-imported`).
431
480
  *
481
+ * - build: exact checkout revision emitted by the build-tool adapter and
482
+ * carried into renderer-owned inspection identity.
483
+ *
432
484
  * The interface is duplicated structurally in `@forgeax/engine-app`
433
485
  * (BundlerOptions exported from packages/app); demos consume through the app
434
486
  * package, the engine accepts via this local minimal shape so packages/runtime
@@ -439,6 +491,8 @@ function toGpuVertexBufferLayouts(
439
491
  export interface BundlerOptions {
440
492
  readonly importTransport?: ImportTransport | undefined;
441
493
  readonly shaderManifestUrl?: string | undefined;
494
+ /** Exact checkout revision emitted by the build-tool adapter. */
495
+ readonly build?: string | undefined;
442
496
  }
443
497
 
444
498
  // ─── Backend pack — M3 auto-select internal shape ───────────────────────────
@@ -450,18 +504,14 @@ export interface BundlerOptions {
450
504
  /**
451
505
  * Per-variant PipelineLayout selector (M4.5 / D-10 option A).
452
506
  *
453
- * The HDRP variant (`CLUSTER_FORWARD_AVAILABLE=true` or canonical `''`
454
- * all-true when the shader declares the cluster axis) needs the 7-slot
455
- * group(2) BGL chain (`hdrpPbrPipelineLayout`); URP variants
456
- * (`CLUSTER_FORWARD_AVAILABLE=false` / undefined / no axis) keep the
457
- * 1-slot mesh-array BGL chain (`pbrPipelineLayout`).
507
+ * The resolved HDRP group(2) contract needs the 7-slot BGL chain
508
+ * (`hdrpPbrPipelineLayout`); ordinary material contracts keep the 1-slot
509
+ * mesh-array BGL chain (`pbrPipelineLayout`). Variant keys select source
510
+ * artifacts only and never override this contract.
458
511
  *
459
- * Returns `null` only when the URP fallback layout itself is null
460
- * (Camera-only / empty-manifest path). When the HDRP layout is null but
461
- * URP exists, the HDRP variant gracefully falls back to URP — the WGSL
462
- * resolved upstream by `findVariantByKey` is the URP variant in that
463
- * case (manifest entry with all-true definesKey === '' was registered
464
- * with isHdrpActive=false at boot when storage-buffer caps are absent).
512
+ * Returns `null` when the selected layout is unavailable. An HDRP variant
513
+ * never falls back to the URP layout: its clustered WGSL and group(2) ABI
514
+ * must be admitted together or the draw fails closed.
465
515
  *
466
516
  * Pure function — exported for unit-test access (M4.5 / w35).
467
517
  */
@@ -469,8 +519,9 @@ export interface BundlerOptions {
469
519
  * Closed enum of pipeline-layout kinds the selector can dispatch to.
470
520
  *
471
521
  * `pbr` — standard PBR (1-entry mesh-array BGL).
472
- * `pbr-skin` — bug-20260611-skin-pipeline-layout: 2-entry mesh-array BGL
473
- * (binding 0 meshes + binding 1 palette) for the `forgeax::pbr-skin`
522
+ * `pbr-skin` — bug-20260611-skin-pipeline-layout: 3-entry mesh-array BGL
523
+ * (binding 0 meshes + binding 1 current palette + binding 2 previous
524
+ * palette) for the `forgeax::pbr-skin`
474
525
  * material shader.
475
526
  * `hdrp-pbr` — feat-20260609 HDRP cluster-forward variant (7-slot group(2)
476
527
  * BGL substituted at slot 2).
@@ -490,7 +541,15 @@ export interface BundlerOptions {
490
541
  * material-shader layouts add a `LayoutKind` member + a PipelineState slot
491
542
  * + a switch arm here in one cut (charter P3 extensibility).
492
543
  */
493
- export type LayoutKind = 'pbr' | 'pbr-skin' | 'hdrp-pbr' | 'sprite-urp' | 'unlit-urp';
544
+ export type LayoutKind = 'pbr' | 'pbr-skin' | 'hdrp-pbr' | 'hdrp-skin' | 'sprite-urp' | 'unlit-urp';
545
+
546
+ /** The closed group(2) contract resolved from the selected WGSL artifact. */
547
+ export type { PipelineGroup2Contract } from '../pbr-pipeline';
548
+ export { resolvePipelineGroup2Contract } from '../pbr-pipeline';
549
+
550
+ function directionalPcssCapable(backendKind: RhiDevice['caps']['backendKind']): boolean {
551
+ return backendKind === 'webgpu' || backendKind === 'wgpu-native';
552
+ }
494
553
 
495
554
  /**
496
555
  * Return every device-capability HDRP PBR variant that needs a lazy module
@@ -502,17 +561,60 @@ export type LayoutKind = 'pbr' | 'pbr-skin' | 'hdrp-pbr' | 'sprite-urp' | 'unlit
502
561
  export function selectHdrpPbrPrewarmVariants(
503
562
  manifestEntry: MaterialShaderManifestEntry | undefined,
504
563
  storageBufferCapable: boolean,
505
- transmissionCapable: boolean,
564
+ extendedLightingShaderAvailableOrTransmissionCapable = true,
565
+ transmissionCapable = true,
566
+ directionalPcssAvailable = true,
506
567
  projectorAvailable: boolean = true,
507
568
  ): readonly MaterialShaderManifestEntry['variants'][number][] {
569
+ const variants = manifestEntry?.variants ?? [];
570
+ const hasExtendedLightingAxis = variants.some(
571
+ (variant) => 'EXTENDED_LIGHTING_AVAILABLE' in variant.defines,
572
+ );
573
+ const extendedLightingShaderAvailable = hasExtendedLightingAxis
574
+ ? extendedLightingShaderAvailableOrTransmissionCapable
575
+ : true;
576
+ const effectiveTransmissionCapable = hasExtendedLightingAxis
577
+ ? transmissionCapable
578
+ : extendedLightingShaderAvailableOrTransmissionCapable;
579
+ // HDRP's clustered group(2) ABI is storage-buffer backed. A downlevel
580
+ // WebGL2 device must stay on the URP variant; prewarming an HDRP source
581
+ // here would advertise an inactive route and can exceed its fragment
582
+ // uniform-buffer budget before the first frame.
583
+ if (!storageBufferCapable) return [];
508
584
  return (
509
- manifestEntry?.variants.filter(
585
+ variants.filter(
510
586
  (variant) =>
511
587
  variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
588
+ (!('EXTENDED_LIGHTING_AVAILABLE' in variant.defines) ||
589
+ variant.defines.EXTENDED_LIGHTING_AVAILABLE === extendedLightingShaderAvailable) &&
512
590
  variant.defines.CLUSTER_FORWARD_AVAILABLE === true &&
591
+ (!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
592
+ variant.defines.DIRECTIONAL_PCSS_AVAILABLE === directionalPcssAvailable) &&
513
593
  (!('PROJECTOR_AVAILABLE' in variant.defines) ||
514
594
  variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
515
- (transmissionCapable || variant.defines.TRANSMISSION_AVAILABLE !== true),
595
+ // ProbeBlend is an object-level opt-in. Boot/prewarm modules must
596
+ // stay on the no-probe ABI; a probe record selects its true variant
597
+ // later together with the probe-enabled slot-3 BGL.
598
+ variant.defines.PROBE_BLEND_AVAILABLE !== true &&
599
+ (effectiveTransmissionCapable || variant.defines.TRANSMISSION_AVAILABLE !== true),
600
+ ) ?? []
601
+ );
602
+ }
603
+
604
+ /** Return every active-capability pbr-skin variant used by URP and HDRP draws. */
605
+ export function selectSkinPrewarmVariants(
606
+ manifestEntry: MaterialShaderManifestEntry | undefined,
607
+ storageBufferCapable: boolean,
608
+ ): readonly MaterialShaderManifestEntry['variants'][number][] {
609
+ return (
610
+ manifestEntry?.variants.filter(
611
+ (variant) =>
612
+ variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
613
+ // The clustered skin ABI is only active on the storage-backed tier;
614
+ // storage=false must never prewarm or expose it as a usable variant.
615
+ (storageBufferCapable || variant.defines.CLUSTER_FORWARD_AVAILABLE !== true) &&
616
+ // ProbeBlend is selected per object, never by the boot skin module.
617
+ variant.defines.PROBE_BLEND_AVAILABLE !== true,
516
618
  ) ?? []
517
619
  );
518
620
  }
@@ -525,7 +627,9 @@ export function selectHdrpPbrPrewarmVariants(
525
627
  export function selectStandardPbrTransmissionPrewarmVariants(
526
628
  manifestEntry: MaterialShaderManifestEntry | undefined,
527
629
  storageBufferCapable: boolean,
630
+ directionalPcssAvailable = true,
528
631
  projectorAvailable: boolean = true,
632
+ extendedLightingShaderAvailable = true,
529
633
  ): readonly MaterialShaderManifestEntry['variants'][number][] {
530
634
  if (manifestEntry === undefined) {
531
635
  throw new Error('Standard material shader manifest row is missing');
@@ -535,10 +639,15 @@ export function selectStandardPbrTransmissionPrewarmVariants(
535
639
  const declared = manifestEntry.variants.find(
536
640
  (variant) =>
537
641
  variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
538
- variant.defines.CLUSTER_FORWARD_AVAILABLE === false &&
642
+ variant.defines.CLUSTER_FORWARD_AVAILABLE === standardBootClusterAxis() &&
539
643
  variant.defines.VERTEX_COLOR_AVAILABLE === false &&
644
+ (!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
645
+ variant.defines.DIRECTIONAL_PCSS_AVAILABLE === directionalPcssAvailable) &&
540
646
  (!('PROJECTOR_AVAILABLE' in variant.defines) ||
541
647
  variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
648
+ (!('EXTENDED_LIGHTING_AVAILABLE' in variant.defines) ||
649
+ variant.defines.EXTENDED_LIGHTING_AVAILABLE === extendedLightingShaderAvailable) &&
650
+ variant.defines.PROBE_BLEND_AVAILABLE !== true &&
542
651
  variant.defines.TRANSMISSION_AVAILABLE === transmissionAvailable,
543
652
  );
544
653
  if (declared === undefined) {
@@ -566,23 +675,39 @@ export function selectNoColorPbrVariant(
566
675
  manifestEntry: MaterialShaderManifestEntry | undefined,
567
676
  storageBufferCapable: boolean,
568
677
  variantSet: string | undefined,
678
+ extendedLightingShaderAvailable = true,
679
+ directionalPcssAvailable = true,
569
680
  projectorAvailable: boolean = true,
570
681
  ): MaterialShaderManifestEntry['variants'][number] | undefined {
571
- const hdrpRequested =
572
- variantSet === '' ||
573
- // biome-ignore lint/complexity/useOptionalChain: G-14 requires an explicit undefined guard so no variant request becomes false.
574
- (variantSet !== undefined && variantSet.includes('CLUSTER_FORWARD_AVAILABLE=true'));
682
+ const hdrpRequested = variantSet === '' || requestsClusterVariant(variantSet);
683
+ const probeBlendRequested = requestsProbeBlendVariant(variantSet);
575
684
  return manifestEntry?.variants.find(
576
685
  (variant) =>
577
686
  variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
687
+ (!('EXTENDED_LIGHTING_AVAILABLE' in variant.defines) ||
688
+ variant.defines.EXTENDED_LIGHTING_AVAILABLE === extendedLightingShaderAvailable) &&
578
689
  variant.defines.VERTEX_COLOR_AVAILABLE === false &&
690
+ (!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
691
+ variant.defines.DIRECTIONAL_PCSS_AVAILABLE === directionalPcssAvailable) &&
579
692
  (!('PROJECTOR_AVAILABLE' in variant.defines) ||
580
693
  variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
694
+ (!('PROBE_BLEND_AVAILABLE' in variant.defines) ||
695
+ variant.defines.PROBE_BLEND_AVAILABLE === probeBlendRequested) &&
581
696
  (!('CLUSTER_FORWARD_AVAILABLE' in variant.defines) ||
582
697
  variant.defines.CLUSTER_FORWARD_AVAILABLE === hdrpRequested),
583
698
  );
584
699
  }
585
700
 
701
+ function requestsClusterVariant(variantSet: string | undefined): boolean {
702
+ if (variantSet === undefined) return false;
703
+ return variantSet.includes('CLUSTER_FORWARD_AVAILABLE=true');
704
+ }
705
+
706
+ function requestsProbeBlendVariant(variantSet: string | undefined): boolean {
707
+ if (variantSet === undefined) return false;
708
+ return variantSet.includes('PROBE_BLEND_AVAILABLE=true');
709
+ }
710
+
586
711
  export type MaterialShaderBindingContract = RenderFeatureMaterialShaderBindingContract;
587
712
  export type MaterialShaderVertexInputContract = 'none' | 'render-material';
588
713
 
@@ -707,13 +832,18 @@ export function shouldDeferMissingPreparedMaterialShader(
707
832
  export function selectPipelineLayoutForVariant(
708
833
  state: {
709
834
  readonly pbrPipelineLayout: PipelineLayout | null;
835
+ readonly pbrProbePipelineLayout?: PipelineLayout | null;
710
836
  readonly hdrpPbrPipelineLayout: PipelineLayout | null;
837
+ readonly hdrpProbePbrPipelineLayout?: PipelineLayout | null;
711
838
  readonly pbrSkinPipelineLayout: PipelineLayout | null;
839
+ readonly pbrSkinProbePipelineLayout?: PipelineLayout | null;
840
+ readonly hdrpSkinPipelineLayout?: PipelineLayout | null;
712
841
  } | null,
713
842
  variantSet: string | undefined,
714
843
  layoutKind?: LayoutKind,
715
844
  ): PipelineLayout | null {
716
845
  if (state === null) return null;
846
+ const probeBlendVariant = variantSet?.includes('PROBE_BLEND_AVAILABLE=true') === true;
717
847
  // bug-20260611: skin layout selection takes precedence over HDRP variant
718
848
  // resolution. HDRP × skin is OOS-1 (plan-strategy R-2 — left for a
719
849
  // dedicated feat); when an HDRP-variant skin call ever lands, we fail
@@ -721,7 +851,12 @@ export function selectPipelineLayoutForVariant(
721
851
  // (charter P3 explicit failure, mirrors memory anchor
722
852
  // `hdrp-active-must-not-fallback-to-urp-pipeline`).
723
853
  if (layoutKind === 'pbr-skin') {
724
- return state.pbrSkinPipelineLayout;
854
+ return probeBlendVariant
855
+ ? (state.pbrSkinProbePipelineLayout ?? null)
856
+ : state.pbrSkinPipelineLayout;
857
+ }
858
+ if (layoutKind === 'hdrp-skin') {
859
+ return state.hdrpSkinPipelineLayout ?? null;
725
860
  }
726
861
  // bug-20260708 M2 (c): sprite / sprite-lit shaders reuse the URP 1-slot
727
862
  // mesh-array pipeline layout but their canonical all-true variant key
@@ -738,33 +873,24 @@ export function selectPipelineLayoutForVariant(
738
873
  if (layoutKind === 'unlit-urp') {
739
874
  return state.pbrPipelineLayout;
740
875
  }
741
- // Static PBR callers can use the canonical all-true variant key (`''`)
742
- // without becoming HDRP. Keep the shader variant identity independent from
743
- // the pipeline-layout identity; shadow-caster static draws use this explicit
744
- // kind so their ordinary mesh bind group remains compatible with the PSO.
876
+ // The resolved group(2) contract is authoritative for custom material
877
+ // shaders. A mesh-only composed WGSL uses the one-entry mesh BGL even when
878
+ // the renderer is otherwise running its HDRP lighting variant; allowing the
879
+ // variant key to re-select HDRP here recreates the mesh-vs-cluster mismatch.
745
880
  if (layoutKind === 'pbr') {
746
- return state.pbrPipelineLayout;
881
+ return probeBlendVariant ? (state.pbrProbePipelineLayout ?? null) : state.pbrPipelineLayout;
747
882
  }
748
- // HDRP variant matches when:
749
- // - layoutKind === 'hdrp-pbr' (caller-driven, future-proof), OR
750
- // - variantSet is the canonical all-true key '' (boot-time HDRP
751
- // registration produces '' when both axes resolve to true), OR
752
- // - variantSet contains `CLUSTER_FORWARD_AVAILABLE=true` substring
753
- // (record-stage caller emits the expanded form).
754
- // URP variants have CLUSTER_FORWARD_AVAILABLE=false explicitly, or omit
755
- // the axis altogether, or pass undefined (no variant routing).
756
- // G-14 (D-11): empty-string is the canonical all-true HDRP key, so we
757
- // explicitly distinguish '' vs undefined rather than using an optional-chain
758
- // (charter P3: explicit failure / no implicit empty-vs-missing collapse).
759
- const isHdrpVariant =
760
- layoutKind === 'hdrp-pbr' ||
761
- variantSet === '' ||
762
- // biome-ignore lint/complexity/useOptionalChain: G-14 grep gate forbids the optional-chain falsy collapse on variantSet.
763
- (variantSet !== undefined && variantSet.includes('CLUSTER_FORWARD_AVAILABLE=true'));
764
- if (isHdrpVariant && state.hdrpPbrPipelineLayout !== null) {
765
- return state.hdrpPbrPipelineLayout;
883
+ // Group(2) contract/layout kind is the sole layout authority. Variant keys
884
+ // identify WGSL artifacts, but must not independently select a bind-group
885
+ // layout; this prevents an empty/expanded key from bypassing the resolved
886
+ // storage contract.
887
+ const isHdrpVariant = layoutKind === 'hdrp-pbr';
888
+ if (isHdrpVariant) {
889
+ return probeBlendVariant
890
+ ? (state.hdrpProbePbrPipelineLayout ?? null)
891
+ : state.hdrpPbrPipelineLayout;
766
892
  }
767
- return state.pbrPipelineLayout;
893
+ return probeBlendVariant ? (state.pbrProbePipelineLayout ?? null) : state.pbrPipelineLayout;
768
894
  }
769
895
 
770
896
  /**
@@ -780,12 +906,14 @@ export function resolveMaterialShaderVariantSet(
780
906
  }>,
781
907
  backendKind: RhiDevice['caps']['backendKind'],
782
908
  storageBuffer: boolean,
783
- projectorAvailable: boolean = true,
909
+ maxSampledTexturesPerShaderStageOrProjectorAvailable?: number | boolean,
784
910
  ): string | undefined {
785
911
  const declaredAxes = new Set(variants.flatMap((variant) => Object.keys(variant.defines)));
786
912
  const declaresCapabilityVariants =
787
913
  declaredAxes.has('WEBGL2_COMPAT') ||
788
914
  declaredAxes.has('STORAGE_BUFFER_AVAILABLE') ||
915
+ declaredAxes.has('EXTENDED_LIGHTING_AVAILABLE') ||
916
+ declaredAxes.has('DIRECTIONAL_PCSS_AVAILABLE') ||
789
917
  declaredAxes.has('PROJECTOR_AVAILABLE');
790
918
  if (!declaresCapabilityVariants) return requestedVariantSet;
791
919
 
@@ -794,6 +922,22 @@ export function resolveMaterialShaderVariantSet(
794
922
  const separator = part.indexOf('=');
795
923
  if (separator > 0) requested[part.slice(0, separator)] = part.slice(separator + 1) === 'true';
796
924
  }
925
+ const sampledTextureLimit =
926
+ typeof maxSampledTexturesPerShaderStageOrProjectorAvailable === 'number'
927
+ ? maxSampledTexturesPerShaderStageOrProjectorAvailable
928
+ : undefined;
929
+ const extendedLightingAvailable =
930
+ sampledTextureLimit === undefined
931
+ ? true
932
+ : backendKind !== 'wgpu-webgl2' &&
933
+ storageBuffer &&
934
+ extendedLightingSampledTextureCapacityAvailable(sampledTextureLimit);
935
+ const projectorAvailable =
936
+ typeof maxSampledTexturesPerShaderStageOrProjectorAvailable === 'boolean'
937
+ ? maxSampledTexturesPerShaderStageOrProjectorAvailable
938
+ : sampledTextureLimit === undefined
939
+ ? true
940
+ : sampledTextureLimit >= STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
797
941
  const filtered: Record<string, boolean> = {};
798
942
  for (const axis of declaredAxes) {
799
943
  filtered[axis] =
@@ -804,22 +948,33 @@ export function resolveMaterialShaderVariantSet(
804
948
  : axis === 'SKINNING_DISABLED'
805
949
  ? (requested[axis] ?? true)
806
950
  : axis === 'CLUSTER_FORWARD_AVAILABLE'
807
- ? backendKind !== 'wgpu-webgl2' && (requested[axis] ?? false)
951
+ ? storageBuffer && backendKind !== 'wgpu-webgl2' && (requested[axis] ?? false)
808
952
  : axis === 'PER_INSTANCE_REGION'
809
953
  ? requestedVariantSet === '' || (requested[axis] ?? false)
810
954
  : axis === 'VERTEX_COLOR_AVAILABLE'
811
955
  ? requestedVariantSet === '' || (requested[axis] ?? false)
812
- : axis === 'TRANSMISSION_AVAILABLE'
813
- ? (requested[axis] ?? false)
814
- : axis === 'PROJECTOR_AVAILABLE'
815
- ? projectorAvailable
816
- : (requested[axis] ?? true);
956
+ : axis === 'EXTENDED_LIGHTING_AVAILABLE'
957
+ ? // This is a backend-owned ABI axis, just like projector
958
+ // and directional PCSS. An omitted caller request must
959
+ // inherit the device-admitted view layout; defaulting it
960
+ // to false pairs an extended view BGL with the projector
961
+ // WGSL and fails WebGPU validation at binding 11.
962
+ extendedLightingAvailable
963
+ : axis === 'PROBE_BLEND_AVAILABLE'
964
+ ? storageBuffer && (requested[axis] ?? false)
965
+ : axis === 'TRANSMISSION_AVAILABLE'
966
+ ? (requested[axis] ?? false)
967
+ : axis === 'DIRECTIONAL_PCSS_AVAILABLE'
968
+ ? directionalPcssCapable(backendKind)
969
+ : axis === 'PROJECTOR_AVAILABLE'
970
+ ? projectorAvailable
971
+ : (requested[axis] ?? false);
817
972
  }
818
973
  const sorted = Object.entries(filtered).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
819
974
  return sorted.every(([, value]) => value)
820
- ? requestedVariantSet === ''
821
- ? ''
822
- : undefined
975
+ ? requestedVariantSet === undefined
976
+ ? undefined
977
+ : ''
823
978
  : sorted.map(([name, value]) => `${name}=${value}`).join('+');
824
979
  }
825
980
 
@@ -843,8 +998,9 @@ export async function createRenderer(
843
998
  options?: RendererOptions,
844
999
  // feat-20260608-create-app-param-surface-trim / M2 / D-3: the third arg is
845
1000
  // now BundlerOptions -- the build-tool injection channel that aggregates
846
- // (a) shaderManifestUrl (vite-plugin-shader emit URL) and (b) importTransport
847
- // (dev-only AssetRegistry transport from feat-20260604). Both fields
1001
+ // (a) shaderManifestUrl (vite-plugin-shader emit URL), (b) importTransport,
1002
+ // (dev-only AssetRegistry transport from feat-20260604), and (c) build
1003
+ // identity for renderer-owned inspection evidence. All fields are
848
1004
  // optional so {} is a valid call shape. Absent => both shipped-form
849
1005
  // fallbacks fire: shaderManifestUrl falls back to '/shaders/manifest.json'
850
1006
  // (D-2 q5-A, ~30 lines below); importTransport stays undefined (AssetRegistry
@@ -883,6 +1039,28 @@ export async function createRenderer(
883
1039
  });
884
1040
  }
885
1041
 
1042
+ function markRendererBootstrapStage(
1043
+ name: string,
1044
+ detail?: Readonly<Record<string, unknown>>,
1045
+ ): void {
1046
+ const host = globalThis as {
1047
+ __forgeaxRendererBootstrap?: {
1048
+ readonly name: string;
1049
+ readonly startedAt: number;
1050
+ readonly previousElapsedMs: number;
1051
+ readonly detail?: Readonly<Record<string, unknown>>;
1052
+ };
1053
+ };
1054
+ const previous = host.__forgeaxRendererBootstrap;
1055
+ const now = Date.now();
1056
+ host.__forgeaxRendererBootstrap = {
1057
+ name,
1058
+ startedAt: now,
1059
+ previousElapsedMs: previous === undefined ? 0 : now - previous.startedAt,
1060
+ ...(detail === undefined ? {} : { detail }),
1061
+ };
1062
+ }
1063
+
886
1064
  // ─── WebGPU branch ──────────────────────────────────────────────────────────
887
1065
 
888
1066
  /**
@@ -909,7 +1087,11 @@ function attachDeviceLostFanout(
909
1087
  },
910
1088
  ): void {
911
1089
  const { lostRegistry, errorRegistry, healthRegistry, onDeviceLost } = registries;
912
- device.lost
1090
+ // Backend fixtures may compose a deterministic loss signal with the native
1091
+ // promise so the real recovery path can be exercised without manufacturing
1092
+ // a second renderer health state. Production packs leave this hook absent.
1093
+ const lost = pack.instrumentation?.deviceLost?.(device) ?? device.lost;
1094
+ lost
913
1095
  .then((info) => {
914
1096
  const safe = {
915
1097
  reason: info?.reason ?? 'unknown',
@@ -993,50 +1175,6 @@ type WebGPUOutcome =
993
1175
  | { kind: 'rhi-err'; error: RhiError }
994
1176
  | { kind: 'throw'; error: Error };
995
1177
 
996
- // Optional compression is exposed on a device only when it is requested from
997
- // the adapter. Keep this capability projection shared by initial construction
998
- // and device-loss recovery; otherwise recovery silently drops BC/ETC2/ASTC
999
- // and already-catalogued compressed textures fail on the first new frame.
1000
- const COMPRESSION_FEATURES: GPUFeatureName[] = [
1001
- 'texture-compression-bc',
1002
- 'texture-compression-etc2',
1003
- 'texture-compression-astc',
1004
- ];
1005
-
1006
- // The Standard transmission fragment ABI needs 17 sampled textures across its
1007
- // material, IBL, clustered-light, and SSAO bindings. Ordinary Standard draws
1008
- // use the non-transmission variant and remain valid on the WebGPU minimum of
1009
- // 16; only request the elevated limit when the adapter can provide it.
1010
- const STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES = 17;
1011
-
1012
- function deviceOptionsForAdapter(
1013
- adapter: Pick<RhiAdapter, 'features' | 'limits'>,
1014
- options?: Pick<RendererOptions, 'captureGpuTimings'>,
1015
- ):
1016
- | {
1017
- requiredFeatures?: GPUFeatureName[];
1018
- requiredLimits?: GPUDeviceDescriptor['requiredLimits'];
1019
- }
1020
- | undefined {
1021
- const requiredFeatures = COMPRESSION_FEATURES.filter((feature) => adapter.features.has(feature));
1022
- const supportsTransmission =
1023
- (adapter.limits.maxSampledTexturesPerShaderStage ?? 0) >=
1024
- STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
1025
- if (options?.captureGpuTimings === true && adapter.features.has('timestamp-query')) {
1026
- requiredFeatures.push('timestamp-query');
1027
- }
1028
- return {
1029
- ...(requiredFeatures.length > 0 ? { requiredFeatures } : {}),
1030
- ...(supportsTransmission
1031
- ? {
1032
- requiredLimits: {
1033
- maxSampledTexturesPerShaderStage: STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
1034
- },
1035
- }
1036
- : {}),
1037
- };
1038
- }
1039
-
1040
1178
  /**
1041
1179
  * Goes through the strict two-step path exposed by the injected backend pack:
1042
1180
  * `rhi.requestAdapter()` -> `adapter.requestDevice()`
@@ -1077,7 +1215,12 @@ async function tryCreateWebGPURenderer(
1077
1215
  // backend singleton (rhi-webgpu / rhi-wgpu / explicit escape hatch).
1078
1216
  // w19: thread canvas as compatibleSurface so the wgpu GL backend can
1079
1217
  // enumerate adapters against it (escape hatch, capability-gated).
1218
+ markRendererBootstrapStage('adapter-request-start');
1080
1219
  const adapterResult = await pack.rhi.requestAdapter(undefined, canvas);
1220
+ markRendererBootstrapStage('adapter-request-end', {
1221
+ ok: adapterResult.ok,
1222
+ ...(adapterResult.ok ? {} : { code: adapterResult.error.code }),
1223
+ });
1081
1224
  if (!adapterResult.ok) {
1082
1225
  return { kind: 'rhi-err', error: adapterResult.error };
1083
1226
  }
@@ -1085,11 +1228,19 @@ async function tryCreateWebGPURenderer(
1085
1228
  // M4 w28: filter compression features from adapter.features (AC-07). The
1086
1229
  // same helper is used by recover() below so both paths share one profile.
1087
1230
  const deviceOpts = deviceOptionsForAdapter(adapter, options);
1231
+ markRendererBootstrapStage('device-request-start');
1088
1232
  const result = await adapter.requestDevice(deviceOpts);
1233
+ markRendererBootstrapStage('device-request-end', {
1234
+ ok: result.ok,
1235
+ ...(result.ok ? {} : { code: result.error.code }),
1236
+ });
1089
1237
  if (!result.ok) {
1090
1238
  return { kind: 'rhi-err', error: result.error };
1091
1239
  }
1092
1240
  const device: RhiDevice = result.value;
1241
+ markRendererBootstrapStage('device-acquired', {
1242
+ backendKind: device.caps.backendKind,
1243
+ });
1093
1244
 
1094
1245
  // Acquire the canvas context through the backend pack (M3 / w16).
1095
1246
  // rhi-webgpu: internally calls canvas.getContext('webgpu') + branded wrap.
@@ -1172,10 +1323,8 @@ interface WebGPURendererInternals {
1172
1323
  options: RendererOptions | undefined;
1173
1324
  /**
1174
1325
  * feat-20260608-create-app-param-surface-trim / M2 / D-3: BundlerOptions
1175
- * forwarded verbatim from createRenderer's third arg. Carries
1176
- * shaderManifestUrl (host-injected vite-plugin-shader emit URL; absent =>
1177
- * createRenderer falls back to '/shaders/manifest.json') and the
1178
- * importTransport that previously rode a dedicated internal slot.
1326
+ * forwarded verbatim from createRenderer's third arg. Carries the
1327
+ * host-injected shaderManifestUrl, importTransport, and exact build identity.
1179
1328
  */
1180
1329
  bundler: BundlerOptions | undefined;
1181
1330
  lostRegistry: LostListenerRegistry;
@@ -1220,12 +1369,24 @@ interface WebGPURendererInternals {
1220
1369
  featureHost?: RenderFeatureHost;
1221
1370
  /** App-owned overlay capability read by the typed graph at frame time. */
1222
1371
  debugOverlay?: RenderDebugOverlay;
1372
+ gpuPassTimingSubmittedWork?: Promise<void> | undefined;
1223
1373
  }
1224
1374
 
1225
1375
  async function makeWebGPURenderer(
1226
1376
  internals: WebGPURendererInternals,
1227
1377
  ): Promise<RendererAssemblyImplementation> {
1228
1378
  let disposed = false;
1379
+ let renderSystemForTargetPromotion: RenderSystem | undefined;
1380
+ // The host keeps the zero-option createRenderTargetHost() seam compatible
1381
+ // for package-local lifecycle tests while this assembly supplies the live
1382
+ // DeviceScope generation callback.
1383
+ const renderTargetHost = createRenderTargetHost({
1384
+ rendererId: Symbol('renderer'),
1385
+ getGeneration: () => activeDeviceScope.generation,
1386
+ getDevice: () => internals.device,
1387
+ canPromoteTarget: (target) =>
1388
+ renderSystemForTargetPromotion?.isCubeCapturePending(target) !== true,
1389
+ });
1229
1390
  const featureHostResult = createRenderFeatureHost(internals.options?.features ?? []);
1230
1391
  if (!featureHostResult.ok) throw featureHostResult.error;
1231
1392
  internals.featureHost = featureHostResult.value;
@@ -1240,6 +1401,28 @@ async function makeWebGPURenderer(
1240
1401
  featureHostResult.value.features.flatMap((feature) => feature.requiredMaterialShaders ?? []),
1241
1402
  );
1242
1403
  let requiredMaterialShaders = Object.freeze([...requiredMaterialShaderSet]);
1404
+ const collectRequiredFullscreenPostProcesses = (
1405
+ features: readonly RenderFeature<unknown>[],
1406
+ ): readonly { readonly identity: string; readonly source: string }[] => {
1407
+ const entries = new Map<string, { readonly identity: string; readonly source: string }>();
1408
+ for (const feature of features) {
1409
+ for (const entry of feature.requiredFullscreenPostProcesses ?? []) {
1410
+ const existing = entries.get(entry.identity);
1411
+ if (existing !== undefined && existing.source !== entry.source) {
1412
+ throw new RhiError({
1413
+ code: 'internal-error',
1414
+ expected: `fullscreen identity '${entry.identity}' has one source across render features`,
1415
+ hint: 'rename the conflicting fullscreen identity or make its WGSL source identical',
1416
+ });
1417
+ }
1418
+ entries.set(entry.identity, entry);
1419
+ }
1420
+ }
1421
+ return Object.freeze([...entries.values()]);
1422
+ };
1423
+ let requiredFullscreenPostProcesses = collectRequiredFullscreenPostProcesses(
1424
+ featureHostResult.value.features,
1425
+ );
1243
1426
  // Lazy ShaderCatalog instance (plan-strategy section S-10 / D-R10 / OQ-5
1244
1427
  // close): constructed on first access; subsequent accesses return the
1245
1428
  // same instance.
@@ -1282,6 +1465,28 @@ async function makeWebGPURenderer(
1282
1465
  }
1283
1466
  return sharedImmediateShaderModuleAdapter;
1284
1467
  };
1468
+ const prewarmFullscreenFeatureModules = async (
1469
+ feature: RenderFeature<unknown>,
1470
+ ): Promise<Result<void, RhiError>> => {
1471
+ for (const postProcess of feature.requiredFullscreenPostProcesses ?? []) {
1472
+ const label = postProcessShaderModuleLabel(postProcess.identity, postProcess.source);
1473
+ const shaderResult = internals.pack.createShaderModule
1474
+ ? await internals.pack.createShaderModule(internals.device, {
1475
+ code: postProcess.source,
1476
+ label,
1477
+ })
1478
+ : await invokeDeviceCreateShaderModule(internals.device, {
1479
+ code: postProcess.source,
1480
+ label,
1481
+ });
1482
+ if (!shaderResult.ok) {
1483
+ internals.errorRegistry.fire(shaderResult.error);
1484
+ return err(shaderResult.error);
1485
+ }
1486
+ getShaderModuleAdapter().seedModule(label, shaderResult.value);
1487
+ }
1488
+ return ok(undefined);
1489
+ };
1285
1490
  const getShader = (): ShaderCatalog => {
1286
1491
  if (shaderInstance === null) {
1287
1492
  // feat-20260608-create-app-param-surface-trim / M2 / D-2 q5-A:
@@ -1346,6 +1551,57 @@ async function makeWebGPURenderer(
1346
1551
  // lives in a standalone store; `assets` keeps the CPU POD registry only.
1347
1552
  const gpuStore = new GpuResidencyCache();
1348
1553
  let activeDeviceScope = DeviceScope.create(0, 'renderer');
1554
+ let extendedLightingState = createExtendedLightingState(activeDeviceScope.generation);
1555
+ const gpuPassTimingOptions = internals.options?.gpuPassTiming;
1556
+ const timingRetentionFrames =
1557
+ gpuPassTimingOptions?.retentionFrames !== undefined &&
1558
+ Number.isInteger(gpuPassTimingOptions.retentionFrames) &&
1559
+ gpuPassTimingOptions.retentionFrames >= 1 &&
1560
+ gpuPassTimingOptions.retentionFrames <= 8
1561
+ ? gpuPassTimingOptions.retentionFrames
1562
+ : DEFAULT_GPU_PASS_TIMING_OPTIONS.retentionFrames;
1563
+ let gpuPassTimingSession: GpuPassTimingSession | undefined;
1564
+ let gpuPassTimingUnavailable: GpuPassTimingObservation | undefined;
1565
+ const timingObservationStore =
1566
+ gpuPassTimingOptions === undefined
1567
+ ? undefined
1568
+ : createGpuPassTimingObservationStore({
1569
+ retentionFrames: timingRetentionFrames,
1570
+ currentDeviceGeneration: () => activeDeviceScope.generation,
1571
+ });
1572
+ const retireGpuPassTimingSession = (): void => {
1573
+ const retired = gpuPassTimingSession;
1574
+ gpuPassTimingSession = undefined;
1575
+ retired?.dispose();
1576
+ };
1577
+ const failedGpuPassTimingObservation = (error: GpuPassTimingReason): GpuPassTimingObservation => {
1578
+ const latestKnownGood = timingObservationStore?.inspect().latestKnownGood;
1579
+ return {
1580
+ status: 'failed',
1581
+ error,
1582
+ ...(latestKnownGood === undefined ? {} : { latestKnownGood }),
1583
+ };
1584
+ };
1585
+ const establishGpuPassTimingSession = (): void => {
1586
+ retireGpuPassTimingSession();
1587
+ gpuPassTimingUnavailable = undefined;
1588
+ const timingOptions = gpuPassTimingOptions;
1589
+ if (timingOptions === undefined) return;
1590
+ const created = createGpuPassTimingSession(internals.device, timingOptions);
1591
+ if (created.ok) {
1592
+ gpuPassTimingSession = created.value;
1593
+ return;
1594
+ }
1595
+ gpuPassTimingUnavailable = {
1596
+ status: 'unavailable',
1597
+ reason: created.error,
1598
+ capability: {
1599
+ timestampQuery: internals.device.caps.timestampQuery,
1600
+ timestampPeriodNanoseconds: internals.device.caps.timestampPeriodNanoseconds,
1601
+ },
1602
+ };
1603
+ };
1604
+ establishGpuPassTimingSession();
1349
1605
  // feat-20260623-world-space-video-asset M4 / w16 (D-3): transient per-frame
1350
1606
  // video texture store, fully independent of gpuStore (AC-08). Configured with
1351
1607
  // the device alongside gpuStore below; threaded into the record stage via the
@@ -1479,7 +1735,10 @@ async function makeWebGPURenderer(
1479
1735
  // (the seeding closure captures this Map by reference; ordering is safe
1480
1736
  // because the prewarm step runs strictly before the first frame's
1481
1737
  // `getMaterialShaderPipeline` lookup that would consume it).
1482
- const materialShaderPipelineCache = new Map<string, RenderPipeline>();
1738
+ const materialShaderPipelineCache = new Map<
1739
+ string,
1740
+ { readonly pipeline: RenderPipeline; readonly group2Contract: PipelineGroup2Contract }
1741
+ >();
1483
1742
  const materialShaderManifestEntryCache = new Map<string, MaterialShaderManifestEntry>();
1484
1743
  // Variant resolution only depends on the requested axes, device capability
1485
1744
  // axes, and the manifest entry that owns the declarations. Cache it by the
@@ -1752,8 +2011,7 @@ async function makeWebGPURenderer(
1752
2011
  [],
1753
2012
  internals.device.caps.backendKind,
1754
2013
  internals.device.caps.storageBuffer,
1755
- (internals.device.limits.maxSampledTexturesPerShaderStage ?? 0) >=
1756
- STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
2014
+ internals.device.limits.maxSampledTexturesPerShaderStage,
1757
2015
  );
1758
2016
  }
1759
2017
  const variants = manifestEntry.variants;
@@ -1765,14 +2023,14 @@ async function makeWebGPURenderer(
1765
2023
  const projectorAvailable =
1766
2024
  (internals.device.limits.maxSampledTexturesPerShaderStage ?? 0) >=
1767
2025
  STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
1768
- const cacheKey = `${requestedVariantSet ?? '\u0000'}|${internals.device.caps.backendKind}|${internals.device.caps.storageBuffer ? '1' : '0'}|${projectorAvailable ? '1' : '0'}`;
2026
+ const cacheKey = `${requestedVariantSet ?? '\u0000'}|${internals.device.caps.backendKind}|${internals.device.caps.storageBuffer ? '1' : '0'}|${internals.device.limits.maxSampledTexturesPerShaderStage ?? 'unknown'}|${projectorAvailable ? '1' : '0'}`;
1769
2027
  if (byRequest.has(cacheKey)) return byRequest.get(cacheKey);
1770
2028
  const resolved = resolveMaterialShaderVariantSet(
1771
2029
  requestedVariantSet,
1772
2030
  variants,
1773
2031
  internals.device.caps.backendKind,
1774
2032
  internals.device.caps.storageBuffer,
1775
- projectorAvailable,
2033
+ internals.device.limits.maxSampledTexturesPerShaderStage,
1776
2034
  );
1777
2035
  byRequest.set(cacheKey, resolved);
1778
2036
  return resolved;
@@ -1798,6 +2056,44 @@ async function makeWebGPURenderer(
1798
2056
  // `internals.device` / `internals.pack` are read live, so a rebuild after a
1799
2057
  // device swap compiles against the new device.
1800
2058
  let tonemapRegistered = false;
2059
+ const promotePipelineExtendedLighting = (state: PipelineState, scope: DeviceScope): void => {
2060
+ if (
2061
+ state.extendedLightingAvailable !== true ||
2062
+ state.iesProfileTexture === undefined ||
2063
+ state.cookieTexture === undefined ||
2064
+ state.cookieMatrixBuffer === undefined ||
2065
+ state.iesProfileTextureView === undefined ||
2066
+ state.cookieTextureView === undefined ||
2067
+ state.ltcLambertTextureView === undefined ||
2068
+ state.ltcGgxTextureView === undefined
2069
+ ) {
2070
+ return;
2071
+ }
2072
+ // The pipeline builder owns these already-resident GPU handles. Project
2073
+ // that completed bundle into the inspection state so the public receipt
2074
+ // cannot report an empty candidate while the record path is consuming the
2075
+ // same IES/Cookie/LTC resources. The candidate is metadata-only here; no
2076
+ // second GPU allocation or lifecycle owner is introduced.
2077
+ const candidate: ExtendedLightingResourceCandidate = {
2078
+ topology: EXTENDED_LIGHTING_TOPOLOGY,
2079
+ generation: scope.generation,
2080
+ scope,
2081
+ iesSliceCount: 0,
2082
+ cookieSliceCount: 0,
2083
+ cookieMatrices: 0,
2084
+ sampler: state.defaultSampler,
2085
+ iesTexture: state.iesProfileTexture,
2086
+ cookieTexture: state.cookieTexture,
2087
+ cookieMatrixBuffer: state.cookieMatrixBuffer,
2088
+ descriptorBytes:
2089
+ IES_SLICE_WIDTH * IES_SLICE_HEIGHT * 2 * 32 +
2090
+ COOKIE_SLICE_SIZE * COOKIE_SLICE_SIZE * 4 * 32 +
2091
+ COOKIE_MATRIX_BYTES,
2092
+ uploadCount: 5,
2093
+ };
2094
+ extendedLightingState = promoteExtendedLightingCandidate(extendedLightingState, candidate);
2095
+ };
2096
+ let fxaaRegistered = false;
1801
2097
  let temporalPostProcessesRegistered = false;
1802
2098
  const buildPipeline = (scope: DeviceScope = activeDeviceScope): Promise<PipelineState> =>
1803
2099
  buildReadyWebGPU(
@@ -1808,6 +2104,7 @@ async function makeWebGPURenderer(
1808
2104
  internals.pack.createShaderModule,
1809
2105
  internals.errorRegistry,
1810
2106
  requiredMaterialShaders,
2107
+ requiredFullscreenPostProcesses,
1811
2108
  // feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M2 /
1812
2109
  // T-M2-05 + M3 / T-M3-04: callback set by buildReadyWebGPU once
1813
2110
  // meshSsboController is wired. Both the grow hook AND the read-only
@@ -1827,9 +2124,12 @@ async function makeWebGPURenderer(
1827
2124
  // SPEC_CONST prewarm. Closure captures the Map declared just above so
1828
2125
  // the URP-variant prewarmed PSOs become visible to the URP record path
1829
2126
  // immediately (no 1-frame async-compile skip-draw window).
1830
- (key, pso) => {
2127
+ (key, pso, group2Contract) => {
1831
2128
  if (!materialShaderPipelineCache.has(key)) {
1832
- materialShaderPipelineCache.set(key, pso);
2129
+ materialShaderPipelineCache.set(key, {
2130
+ pipeline: pso,
2131
+ group2Contract,
2132
+ });
1833
2133
  }
1834
2134
  },
1835
2135
  // feat-20260621 M-A3 (D-5): register the built-in tonemap on the unified
@@ -1848,6 +2148,19 @@ async function makeWebGPURenderer(
1848
2148
  });
1849
2149
  tonemapRegistered = true;
1850
2150
  },
2151
+ // FXAA is recorded by the special graph primitive, but its final-output
2152
+ // dither switch still uses the renderer-owned post-process params UBO.
2153
+ // Register the declaration through the same lifecycle so recovery
2154
+ // recreates the buffer on the replacement device.
2155
+ (source: string) => {
2156
+ if (fxaaRegistered) return;
2157
+ renderSystem.registerBuiltinPostProcess(FXAA_POST_PROCESS_ID, {
2158
+ source,
2159
+ params: { byteSize: 16, defaultValue: new Uint8Array(16) },
2160
+ reads: ['ldrColor'],
2161
+ });
2162
+ fxaaRegistered = true;
2163
+ },
1851
2164
  (entries: { readonly motionBlur?: string; readonly taaResolve?: string }) => {
1852
2165
  if (temporalPostProcessesRegistered) return;
1853
2166
  if (entries.motionBlur !== undefined) {
@@ -1870,14 +2183,20 @@ async function makeWebGPURenderer(
1870
2183
  internals.volumetricFogShaders = sources;
1871
2184
  },
1872
2185
  );
2186
+ markRendererBootstrapStage('renderer-initialization-start');
1873
2187
  const ready: Promise<Result<void, RhiError>> = buildPipeline().then(
1874
2188
  (state): Result<void, RhiError> => {
1875
2189
  pipelineState = state;
2190
+ promotePipelineExtendedLighting(state, activeDeviceScope);
1876
2191
  readySettled = true;
2192
+ markRendererBootstrapStage('renderer-initialized');
1877
2193
  return ok(undefined);
1878
2194
  },
1879
2195
  (e: unknown): Result<void, RhiError> => {
1880
2196
  readySettled = true;
2197
+ markRendererBootstrapStage('renderer-initialization-failed', {
2198
+ message: e instanceof Error ? e.message : String(e),
2199
+ });
1881
2200
  if (e instanceof RhiError) return err(e);
1882
2201
  const message = e instanceof Error ? e.message : String(e);
1883
2202
  return err(
@@ -1914,6 +2233,8 @@ async function makeWebGPURenderer(
1914
2233
  pbrManifestEntry,
1915
2234
  internals.device.caps.storageBuffer,
1916
2235
  variantSet,
2236
+ deriveExtendedLightingCapability(internals.device).admitted,
2237
+ directionalPcssCapable(internals.device.caps.backendKind),
1917
2238
  (internals.device.limits.maxSampledTexturesPerShaderStage ?? 0) >=
1918
2239
  STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
1919
2240
  );
@@ -1931,10 +2252,10 @@ async function makeWebGPURenderer(
1931
2252
  // paths (D-3). Null when manifest is empty (Camera-only path).
1932
2253
  //
1933
2254
  // feat-20260609-hdrp-cluster-fragment-ggx M4.5 / w37 (D-10 option A):
1934
- // `variantSet` parameter selects between URP `pbrPipelineLayout` (1-slot
1935
- // group(2) BGL) and HDRP `hdrpPbrPipelineLayout` (7-slot group(2) BGL)
1936
- // via `selectPipelineLayoutForVariant`. When undefined (legacy callers
1937
- // / fallback path) the URP layout is used preserving prior behaviour.
2255
+ // `variantSet` identifies the shader artifact, while the resolved group(2)
2256
+ // contract selects the matching Standard layout. A missing cluster layout
2257
+ // is an explicit unavailable result; it must not silently bind the mesh-only
2258
+ // layout to a clustered shader.
1938
2259
  const buildPipelineContext = (
1939
2260
  variantSet?: string,
1940
2261
  materialShaderId?: string,
@@ -1945,40 +2266,31 @@ async function makeWebGPURenderer(
1945
2266
  // consume. A caller passing a real `MeshAsset.attributes` produces the
1946
2267
  // exact 6-attribute / 72-byte layout the skin shader expects; passing
1947
2268
  // undefined falls into the synthetic 6-key sentinel below (the skin
1948
- // path is fully determined by layoutKind, so key-presence is the only
1949
- // signal `deriveVertexBufferLayout` reads — values never matter).
2269
+ // path is fully determined by the resolved group-2 contract, so
2270
+ // key-presence is the only signal `deriveVertexBufferLayout` reads).
1950
2271
  meshAttributes?: VertexAttributeMap,
1951
2272
  vertexLayout?: string,
1952
2273
  vertexLayoutProjection?: VertexLayoutProjection,
1953
2274
  shaderModuleMode?: RenderFeatureShaderModuleMode,
2275
+ group2Contract: PipelineGroup2Contract = 'mesh',
1954
2276
  layoutKindOverride?: LayoutKind,
1955
2277
  ) => {
1956
- // bug-20260611: derive LayoutKind from materialShaderId (caller-driven)
1957
- // so the selector body stays free of literal shader-id strings (AC-09).
1958
- // Skin shader gets its own 4-slot layout chain with 2-entry mesh-array
1959
- // BGL; everything else routes through the URP/HDRP variantSet logic.
1960
- // bug-20260708 M2 (c): sprite / sprite-lit map to 'sprite-urp' so the
1961
- // canonical all-true variant key `''` (used by SpriteInstances batches
1962
- // via SPRITE_PASS_PER_INSTANCE_REGION_VARIANT_SET) does NOT route
1963
- // through the HDRP layout branch of the selector — sprite shaders have
1964
- // no HDRP variant, plan-strategy R-1' guard.
1965
- const layoutKind: LayoutKind | undefined =
1966
- layoutKindOverride ??
1967
- (materialShaderId === SKIN_MATERIAL_SHADER_ID ||
1968
- isSkinnedShadowCasterVariant(materialShaderId, variantSet)
1969
- ? 'pbr-skin'
1970
- : materialShaderId === 'forgeax::sprite' || materialShaderId === 'forgeax::sprite-lit'
1971
- ? 'sprite-urp'
1972
- : materialShaderId === 'forgeax::default-unlit'
1973
- ? 'unlit-urp'
1974
- : undefined);
2278
+ const contractLayoutKind: LayoutKind =
2279
+ group2Contract === 'cluster'
2280
+ ? 'hdrp-pbr'
2281
+ : group2Contract === 'skin-cluster'
2282
+ ? 'hdrp-skin'
2283
+ : group2Contract === 'skin'
2284
+ ? 'pbr-skin'
2285
+ : 'pbr';
2286
+ const resolvedLayoutKind = layoutKindOverride ?? contractLayoutKind;
1975
2287
  const usesSharedBootMaterialLayout =
1976
2288
  materialShaderId !== undefined && isEngineOwnedMaterialShader(materialShaderId);
1977
2289
  // Built-in PBR/unlit/skin layouts are assembled at boot. Re-deriving a
1978
2290
  // per-shader layout from the expanded PBR schema would bypass the HDRP
1979
2291
  // group(2) selector and bind the URP mesh BGL to the cluster pipeline.
1980
2292
  const perShaderLayout =
1981
- !usesSharedBootMaterialLayout && layoutKind !== 'pbr-skin' && materialShaderId !== undefined
2293
+ !usesSharedBootMaterialLayout && materialShaderId !== undefined
1982
2294
  ? getOrBuildPerShaderMaterialLayout(materialShaderId)
1983
2295
  : null;
1984
2296
  // feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): an authored
@@ -2030,7 +2342,7 @@ async function makeWebGPURenderer(
2030
2342
  ? preparedMaterialLayout
2031
2343
  : perShaderLayout !== null
2032
2344
  ? perShaderLayout.pipelineLayout
2033
- : selectPipelineLayoutForVariant(pipelineState, variantSet, layoutKind);
2345
+ : selectPipelineLayoutForVariant(pipelineState, variantSet, resolvedLayoutKind);
2034
2346
  if (pipelineLayout === null) return null;
2035
2347
  // feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
2036
2348
  // pbr-skin -> deriveVertexBufferLayout (single SSOT for skin/non-skin
@@ -2081,7 +2393,7 @@ async function makeWebGPURenderer(
2081
2393
  : undefined,
2082
2394
  ),
2083
2395
  )
2084
- : layoutKind === 'pbr-skin'
2396
+ : materialShaderId === SKIN_MATERIAL_SHADER_ID
2085
2397
  ? toGpuVertexBufferLayouts(
2086
2398
  deriveVertexBufferLayout(
2087
2399
  meshAttributes ??
@@ -2127,7 +2439,7 @@ async function makeWebGPURenderer(
2127
2439
  : getShaderModuleAdapter(),
2128
2440
  pipelineLayout,
2129
2441
  vertexBuffers,
2130
- ...(layoutKind === 'pbr-skin' ? { layoutKind } : {}),
2442
+ ...(resolvedLayoutKind === 'pbr-skin' ? { layoutKind: 'pbr-skin' as const } : {}),
2131
2443
  };
2132
2444
  };
2133
2445
  /**
@@ -2151,7 +2463,7 @@ async function makeWebGPURenderer(
2151
2463
  passKind: PassKind = 'forward',
2152
2464
  // bug-20260611-skin-pipeline-layout: thread materialShaderId so
2153
2465
  // buildPipelineContext can derive `LayoutKind === 'pbr-skin'` and pick
2154
- // the 2-entry mesh-array BGL chain.
2466
+ // the 3-entry mesh-array BGL chain.
2155
2467
  materialShaderId?: string,
2156
2468
  // feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
2157
2469
  // pass the per-mesh `VertexAttributeMap` so the pbr-skin path's
@@ -2187,7 +2499,12 @@ async function makeWebGPURenderer(
2187
2499
  vertexLayoutProjection?: VertexLayoutProjection,
2188
2500
  shaderModuleMode?: RenderFeatureShaderModuleMode,
2189
2501
  layoutKindOverride?: LayoutKind,
2190
- ): RenderPipeline | null => {
2502
+ additionalColorFormats?: readonly GPUTextureFormat[],
2503
+ ): {
2504
+ readonly pipeline: RenderPipeline;
2505
+ readonly group2Contract: PipelineGroup2Contract;
2506
+ } | null => {
2507
+ const group2Contract = resolvePipelineGroup2Contract(entry.source);
2191
2508
  const ctx = buildPipelineContext(
2192
2509
  variantSet,
2193
2510
  materialShaderId,
@@ -2195,6 +2512,7 @@ async function makeWebGPURenderer(
2195
2512
  vertexLayout,
2196
2513
  vertexLayoutProjection,
2197
2514
  shaderModuleMode,
2515
+ group2Contract,
2198
2516
  layoutKindOverride,
2199
2517
  );
2200
2518
  if (ctx === null) return null;
@@ -2217,6 +2535,10 @@ async function makeWebGPURenderer(
2217
2535
  {
2218
2536
  ...ctx,
2219
2537
  colorFormat: isHdr ? HDR_COLOR_ATTACHMENT_FORMAT : ldrColorFormat,
2538
+ colorFormats:
2539
+ additionalColorFormats === undefined
2540
+ ? undefined
2541
+ : [isHdr ? HDR_COLOR_ATTACHMENT_FORMAT : ldrColorFormat, ...additionalColorFormats],
2220
2542
  depthFormat:
2221
2543
  depthFormatOverride === null ? undefined : (depthFormatOverride ?? DEPTH_TEXTURE_FORMAT),
2222
2544
  label,
@@ -2254,10 +2576,14 @@ async function makeWebGPURenderer(
2254
2576
  }
2255
2577
  return null;
2256
2578
  }
2257
- materialShaderPipelineCache.set(cacheKey, built.value);
2258
- return built.value;
2579
+ const pipelineEntry = {
2580
+ pipeline: built.value,
2581
+ group2Contract,
2582
+ } as const;
2583
+ materialShaderPipelineCache.set(cacheKey, pipelineEntry);
2584
+ return pipelineEntry;
2259
2585
  };
2260
- const getMaterialShaderPipeline = (
2586
+ const resolveMaterialShaderPipelineEntry = (
2261
2587
  materialShaderId: string,
2262
2588
  isHdr: boolean,
2263
2589
  renderState?: MaterialRenderState,
@@ -2304,7 +2630,11 @@ async function makeWebGPURenderer(
2304
2630
  vertexLayoutProjection?: VertexLayoutProjection,
2305
2631
  shaderModuleMode: RenderFeatureShaderModuleMode = 'validated',
2306
2632
  layoutKindOverride?: LayoutKind,
2307
- ): RenderPipeline | null => {
2633
+ additionalColorFormats?: readonly GPUTextureFormat[],
2634
+ ): {
2635
+ readonly pipeline: RenderPipeline;
2636
+ readonly group2Contract: PipelineGroup2Contract;
2637
+ } | null => {
2308
2638
  // WebGL2 is a downlevel backend even when the record stage asks for the
2309
2639
  // native-capability variant string. Resolve the variant axes at this
2310
2640
  // shared PSO seam so the shader source, BGL layout, and cache key agree on
@@ -2384,10 +2714,14 @@ async function makeWebGPURenderer(
2384
2714
  // getPreferredCanvasFormat returns rgba8unorm (dawn-node, lavapipe, wgpu-wasm GLES),
2385
2715
  // forcing a redundant second PSO build on first-frame URP record path.
2386
2716
  const colorFormat: GPUTextureFormat = isHdr ? 'rgba16float' : ldrColorFormat;
2717
+ const attachmentFormats =
2718
+ additionalColorFormats === undefined
2719
+ ? colorFormatsForPassKind(passKind, colorFormat)
2720
+ : [colorFormat, ...additionalColorFormats];
2387
2721
  const spec: PipelineSpec = {
2388
2722
  shader: { id: materialShaderId, passKind, variantSet: effectiveVariantSet },
2389
2723
  attachments: {
2390
- colorFormats: colorFormatsForPassKind(passKind, colorFormat),
2724
+ colorFormats: attachmentFormats,
2391
2725
  depthFormat:
2392
2726
  passKind === 'shadow-caster' || passKind === 'point-shadow-caster'
2393
2727
  ? 'depth32float'
@@ -2424,6 +2758,11 @@ async function makeWebGPURenderer(
2424
2758
  if (pipelineState === null) return null;
2425
2759
  const lookup = getShader().findMaterialArtifact(materialShaderId);
2426
2760
  if (!lookup.ok) {
2761
+ // An explicit variant request is an exact artifact contract. There is
2762
+ // no safe boot/PBR substitute: a substitute can pair the requested
2763
+ // source identity with a different group(2) ABI and only fail at GPU
2764
+ // validation. Let the caller retry after the manifest/artifact is fixed.
2765
+ if (effectiveVariantSet !== undefined) return null;
2427
2766
  if (shouldDeferMissingPreparedMaterialShader(vertexLayout)) {
2428
2767
  // Prepared VFX draws bind only their feature-owned groups. The
2429
2768
  // built-in PBR fallback declares mesh/instance groups 2/3 and would
@@ -2471,6 +2810,8 @@ async function makeWebGPURenderer(
2471
2810
  vertexLayout,
2472
2811
  vertexLayoutProjection,
2473
2812
  shaderModuleMode,
2813
+ layoutKindOverride,
2814
+ additionalColorFormats,
2474
2815
  );
2475
2816
  }
2476
2817
  const pbrEntry = findStandardPbrEntry(effectiveVariantSet);
@@ -2525,6 +2866,8 @@ async function makeWebGPURenderer(
2525
2866
  vertexLayout,
2526
2867
  vertexLayoutProjection,
2527
2868
  shaderModuleMode,
2869
+ layoutKindOverride,
2870
+ additionalColorFormats,
2528
2871
  );
2529
2872
  }
2530
2873
  // feat-20260609 M4 / w31: resolve variant WGSL from manifest when
@@ -2544,24 +2887,26 @@ async function makeWebGPURenderer(
2544
2887
  // Use `!== undefined` so the empty-string case enters the lookup.
2545
2888
  if (effectiveVariantSet !== undefined) {
2546
2889
  const registry = getShader();
2890
+ let matchedVariant = false;
2547
2891
  for (const msEntry of registry.materialShaderManifestEntries()) {
2548
2892
  if (msEntry.identifier === materialShaderId) {
2549
2893
  const variant = findVariantByKey(msEntry, effectiveVariantSet);
2550
- if (variant) {
2551
- // M3 / w12-w13: variant substitution carries the same source +
2552
- // paramSchema as the boot-registered entry; the binding layout
2553
- // is no longer carried on MaterialShaderEntry (deleted in
2554
- // w13) buildPbrPipelineLayouts is the BGL SSOT and reads
2555
- // derive(paramSchema).bglEntries on demand.
2556
- shaderEntry = {
2557
- source: variant.composedWgsl,
2558
- paramSchema: lookup.value.paramSchema,
2559
- paramSchemaProjection: lookup.value.paramSchemaProjection,
2560
- };
2561
- }
2894
+ if (variant === undefined) return null;
2895
+ matchedVariant = true;
2896
+ // M3 / w12-w13: variant substitution carries the same source +
2897
+ // paramSchema as the boot-registered entry; the binding layout
2898
+ // is no longer carried on MaterialShaderEntry (deleted in
2899
+ // w13) buildPbrPipelineLayouts is the BGL SSOT and reads
2900
+ // derive(paramSchema).bglEntries on demand.
2901
+ shaderEntry = {
2902
+ source: variant.composedWgsl,
2903
+ paramSchema: lookup.value.paramSchema,
2904
+ paramSchemaProjection: lookup.value.paramSchemaProjection,
2905
+ };
2562
2906
  break;
2563
2907
  }
2564
2908
  }
2909
+ if (!matchedVariant) return null;
2565
2910
  }
2566
2911
  // feat-20260609 M4 / R3-fixup: append `-${passKind}` to the PSO label so
2567
2912
  // GPU debug captures (and the shadow-caster branch in the builder)
@@ -2602,7 +2947,7 @@ async function makeWebGPURenderer(
2602
2947
  passKind,
2603
2948
  // bug-20260611-skin-pipeline-layout: thread the registered materialShaderId
2604
2949
  // so buildPipelineContext can resolve LayoutKind='pbr-skin' for the skin
2605
- // shader (2-entry mesh-array BGL).
2950
+ // shader (3-entry mesh-array BGL).
2606
2951
  materialShaderId,
2607
2952
  // feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4):
2608
2953
  // forward meshAttributes so the pbr-skin path's vertex buffer layout
@@ -2617,12 +2962,50 @@ async function makeWebGPURenderer(
2617
2962
  vertexLayoutProjection,
2618
2963
  shaderModuleMode,
2619
2964
  layoutKindOverride,
2965
+ additionalColorFormats,
2620
2966
  );
2621
2967
  };
2968
+ const getMaterialShaderPipelineEntry = (
2969
+ ...args: Parameters<typeof resolveMaterialShaderPipelineEntry>
2970
+ ) => resolveMaterialShaderPipelineEntry(...args);
2971
+ const getMaterialShaderPipeline = (
2972
+ ...args: Parameters<typeof resolveMaterialShaderPipelineEntry>
2973
+ ): RenderPipeline | null => getMaterialShaderPipelineEntry(...args)?.pipeline ?? null;
2622
2974
  const getParamSchema = (materialShaderId: string) => {
2623
2975
  const lookup = getShader().findMaterialArtifact(materialShaderId);
2624
2976
  return lookup.ok ? lookup.value.paramSchema : undefined;
2625
2977
  };
2978
+ const getMaterialShaderArtifact = (materialShaderId: string) => {
2979
+ if (
2980
+ materialShaderId !== 'forgeax::default-standard-pbr' &&
2981
+ materialShaderId !== 'forgeax::default-standard-pbr-skin'
2982
+ ) {
2983
+ return undefined;
2984
+ }
2985
+ const lookup = getShader().findMaterialArtifact(materialShaderId);
2986
+ if (!lookup.ok) return undefined;
2987
+ const receipt = createStandardPbrArtifactReceipt(
2988
+ materialShaderId === 'forgeax::default-standard-pbr-skin',
2989
+ );
2990
+ const uvSetCount = materialShaderUvSetCounts.get(materialShaderId);
2991
+ return {
2992
+ material: materialShaderId,
2993
+ pass: 'forward',
2994
+ wgsl: lookup.value.source,
2995
+ layoutIdentity: receipt.reflection.layoutIdentity,
2996
+ ...(uvSetCount === undefined ? {} : { uvSetCount }),
2997
+ bindings: [],
2998
+ deps: [],
2999
+ vertexInputs: receipt.vertexInputs.map(
3000
+ (input): Readonly<Record<string, unknown>> => ({
3001
+ semantic: input.semantic,
3002
+ location: input.location,
3003
+ format: input.format,
3004
+ }),
3005
+ ),
3006
+ receipt,
3007
+ };
3008
+ };
2626
3009
  // feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): expose the per-shader
2627
3010
  // material BGL so the record stage creates the material bind group against
2628
3011
  // the matching paramSchema-derived layout. Returns undefined only for
@@ -2727,6 +3110,13 @@ async function makeWebGPURenderer(
2727
3110
  );
2728
3111
  const renderSystem: RenderSystem = createRenderSystem({
2729
3112
  canvas: internals.canvas,
3113
+ // Keep inspection submits tied to the exact build-tool revision. This
3114
+ // bridge is independent of the SSR identity and is required by
3115
+ // renderer-owned LOD World attribution.
3116
+ get build() {
3117
+ return internals.bundler?.build;
3118
+ },
3119
+ ssrIdentity: internals.options?.ssrIdentity,
2730
3120
  get standardProfile() {
2731
3121
  return activeProfile;
2732
3122
  },
@@ -2739,6 +3129,10 @@ async function makeWebGPURenderer(
2739
3129
  shaderModuleFactory: {
2740
3130
  createShaderModule: (descriptor) => getShaderModuleAdapter().createShaderModule(descriptor),
2741
3131
  },
3132
+ createShaderModule: (device, descriptor) =>
3133
+ internals.pack.createShaderModule === undefined
3134
+ ? Promise.resolve(invokeDeviceCreateShaderModule(device, descriptor))
3135
+ : internals.pack.createShaderModule(device, descriptor),
2742
3136
  immediateShaderModuleFactory: {
2743
3137
  createShaderModule: (descriptor) =>
2744
3138
  getImmediateShaderModuleAdapter().createShaderModule(descriptor),
@@ -2752,6 +3146,9 @@ async function makeWebGPURenderer(
2752
3146
  get device() {
2753
3147
  return internals.device;
2754
3148
  },
3149
+ get beforeSubmit() {
3150
+ return internals.pack.instrumentation?.beforeSubmit;
3151
+ },
2755
3152
  get deviceScope() {
2756
3153
  return activeDeviceScope;
2757
3154
  },
@@ -2775,8 +3172,10 @@ async function makeWebGPURenderer(
2775
3172
  errorRegistry: internals.errorRegistry,
2776
3173
  healthRegistry: internals.healthRegistry,
2777
3174
  getMaterialShaderPipeline,
3175
+ getMaterialShaderPipelineEntry,
2778
3176
  getMaterialShaderBindingContract: getCachedMaterialShaderBindingContract,
2779
3177
  getParamSchema,
3178
+ getMaterialShaderArtifact,
2780
3179
  getMaterialBindGroupLayout,
2781
3180
  metrics,
2782
3181
  // feat-20260609 M4 / T-10-a: post-process pipeline factory (CONCERN-1 fix).
@@ -2794,10 +3193,93 @@ async function makeWebGPURenderer(
2794
3193
  get meshSsboState() {
2795
3194
  return internals.meshSsboState;
2796
3195
  },
3196
+ get gpuPassTimingSession() {
3197
+ return (
3198
+ internals as WebGPURendererInternals & {
3199
+ gpuPassTimingSession?: GpuPassTimingSession | undefined;
3200
+ }
3201
+ ).gpuPassTimingSession;
3202
+ },
3203
+ set gpuPassTimingSession(session: GpuPassTimingSession | undefined) {
3204
+ (
3205
+ internals as WebGPURendererInternals & {
3206
+ gpuPassTimingSession?: GpuPassTimingSession | undefined;
3207
+ }
3208
+ ).gpuPassTimingSession = session;
3209
+ },
3210
+ get gpuPassTimingCapture() {
3211
+ return (
3212
+ internals as WebGPURendererInternals & {
3213
+ gpuPassTimingCapture?: GpuPassTimingCapture | undefined;
3214
+ }
3215
+ ).gpuPassTimingCapture;
3216
+ },
3217
+ set gpuPassTimingCapture(capture: GpuPassTimingCapture | undefined) {
3218
+ (
3219
+ internals as WebGPURendererInternals & {
3220
+ gpuPassTimingCapture?: GpuPassTimingCapture | undefined;
3221
+ }
3222
+ ).gpuPassTimingCapture = capture;
3223
+ },
3224
+ get gpuPassTimingSubmittedWork() {
3225
+ return (
3226
+ internals as WebGPURendererInternals & {
3227
+ gpuPassTimingSubmittedWork?: Promise<void> | undefined;
3228
+ }
3229
+ ).gpuPassTimingSubmittedWork;
3230
+ },
3231
+ set gpuPassTimingSubmittedWork(completion: Promise<void> | undefined) {
3232
+ (
3233
+ internals as WebGPURendererInternals & {
3234
+ gpuPassTimingSubmittedWork?: Promise<void> | undefined;
3235
+ }
3236
+ ).gpuPassTimingSubmittedWork = completion;
3237
+ },
3238
+ get gpuPassTimingFrameIdentity() {
3239
+ return (
3240
+ internals as WebGPURendererInternals & {
3241
+ gpuPassTimingFrameIdentity?:
3242
+ | import('../record/gpu-pass-timing/session.js').GpuPassTimingFrameIdentity
3243
+ | undefined;
3244
+ }
3245
+ ).gpuPassTimingFrameIdentity;
3246
+ },
3247
+ set gpuPassTimingFrameIdentity(identity:
3248
+ | import('../record/gpu-pass-timing/session.js').GpuPassTimingFrameIdentity
3249
+ | undefined) {
3250
+ (
3251
+ internals as WebGPURendererInternals & {
3252
+ gpuPassTimingFrameIdentity?:
3253
+ | import('../record/gpu-pass-timing/session.js').GpuPassTimingFrameIdentity
3254
+ | undefined;
3255
+ }
3256
+ ).gpuPassTimingFrameIdentity = identity;
3257
+ },
3258
+ get gpuPassTimingBeginReason() {
3259
+ return (
3260
+ internals as WebGPURendererInternals & {
3261
+ gpuPassTimingBeginReason?: GpuPassTimingReason | undefined;
3262
+ }
3263
+ ).gpuPassTimingBeginReason;
3264
+ },
3265
+ set gpuPassTimingBeginReason(reason: GpuPassTimingReason | undefined) {
3266
+ (
3267
+ internals as WebGPURendererInternals & {
3268
+ gpuPassTimingBeginReason?: GpuPassTimingReason | undefined;
3269
+ }
3270
+ ).gpuPassTimingBeginReason = reason;
3271
+ },
3272
+ getRenderTargetPhysical: (target) => renderTargetHost.getPhysicalTarget(target),
3273
+ resolveRenderTargetTextureSource: (source) =>
3274
+ renderTargetHost.resolveRenderTargetTextureSource(source),
3275
+ encodeRenderTargetReadbacks: (encoder, faces) =>
3276
+ renderTargetHost.encodePendingReadbacks(encoder, faces),
2797
3277
  get volumetricFogShaders() {
2798
3278
  return internals.volumetricFogShaders;
2799
3279
  },
2800
3280
  });
3281
+ attachGpuPassTimingSession(internals, gpuPassTimingSession);
3282
+ renderSystemForTargetPromotion = renderSystem;
2801
3283
  if (
2802
3284
  internals.device.caps.backendKind === 'null' &&
2803
3285
  Array.from(getShader().entries()).length === 0
@@ -2822,6 +3304,10 @@ async function makeWebGPURenderer(
2822
3304
  const transformReleases = new Map<World, () => void>();
2823
3305
  const attachmentOwner = {};
2824
3306
  let frameId = 0;
3307
+ // Receipts remain valid for observation until their device generation is
3308
+ // retired. Keeping a renderer-owned registry avoids treating a concurrent
3309
+ // RAF submission as invalidation of an otherwise current receipt.
3310
+ const currentReceipts = new Set<FrameReceipt>();
2825
3311
  const issuedReceipts = new WeakSet<FrameReceipt>();
2826
3312
  const receiptTimings = new WeakMap<FrameReceipt, Promise<VolumeTimingObservation>>();
2827
3313
  let surfaceReleased = false;
@@ -2837,13 +3323,21 @@ async function makeWebGPURenderer(
2837
3323
  leasesByWorld.set(world, lease);
2838
3324
  return ok(lease);
2839
3325
  },
3326
+ createRenderTarget: (descriptor) => renderTargetHost.createRenderTarget(descriptor),
3327
+ resizeRenderTarget: (target, descriptor) =>
3328
+ renderTargetHost.resizeRenderTarget(target, descriptor),
3329
+ createRenderTargetTextureSource: (target, options) =>
3330
+ renderTargetHost.createRenderTargetTextureSource(target, options),
3331
+ requestTargetReadback: (target, request) =>
3332
+ renderTargetHost.requestTargetReadback(target, request),
3333
+ destroyRenderTarget: (target) => renderTargetHost.destroyRenderTarget(target),
2840
3334
  setProfile(profile: RenderProfile): RenderResult<void, RenderError> {
2841
3335
  const invalid = validateRenderProfile(profile);
2842
3336
  if (invalid !== undefined) {
2843
3337
  return err(
2844
3338
  new RendererOperationError('frame-input-invalid', {
2845
3339
  operation: 'set-profile',
2846
- cause: new RendererContractFailureError('draw', invalid),
3340
+ cause: new StandardProfileInvalidError(invalid),
2847
3341
  }),
2848
3342
  );
2849
3343
  }
@@ -2862,6 +3356,12 @@ async function makeWebGPURenderer(
2862
3356
  );
2863
3357
  }
2864
3358
  },
3359
+ inspectLodOcclusion() {
3360
+ return {
3361
+ lodOcclusion: renderSystem.lodOcclusionInspection,
3362
+ gpuDriven: renderSystem.gpuDrivenInspection,
3363
+ };
3364
+ },
2865
3365
  inspect(): RenderInspection {
2866
3366
  const health = internals.healthRegistry.getLastSnapshot();
2867
3367
  const state = disposed
@@ -2888,6 +3388,7 @@ async function makeWebGPURenderer(
2888
3388
  : { presentationProof: internals.context.presentationProof }),
2889
3389
  });
2890
3390
  const volumetricFog = renderSystem.volumetricFog;
3391
+ const directionalShadow = renderSystem.directionalShadow;
2891
3392
  return Object.freeze({
2892
3393
  state,
2893
3394
  surface: surfaceReleased ? 'released' : 'available',
@@ -2905,18 +3406,31 @@ async function makeWebGPURenderer(
2905
3406
  frustumStats: Object.freeze({ ...renderSystem.frustumStats }),
2906
3407
  visibilityStats: Object.freeze({ ...renderSystem.visibilityStats }),
2907
3408
  renderScene: renderSystem.renderScene,
3409
+ reflectionProbes: renderSystem.reflectionProbes,
3410
+ ssrDependencies: renderSystem.ssrDependencies,
2908
3411
  environment: renderSystem.environment,
2909
3412
  temporal: renderSystem.temporal,
2910
3413
  bloom: renderSystem.bloom,
3414
+ extendedLighting: projectExtendedLightingInspection(extendedLightingState),
3415
+ directionalShadow,
2911
3416
  ...(renderSystem.temporalTargetInspection === undefined
2912
3417
  ? {}
2913
3418
  : { temporalTarget: Object.freeze(renderSystem.temporalTargetInspection) }),
2914
3419
  ...(renderSystem.motionBlurInspection === undefined
2915
3420
  ? {}
2916
3421
  : { motionBlur: Object.freeze(renderSystem.motionBlurInspection) }),
3422
+ ...(renderSystem.lodOcclusionInspection === undefined
3423
+ ? {}
3424
+ : { lodOcclusion: Object.freeze(renderSystem.lodOcclusionInspection) }),
2917
3425
  ...(renderSystem.transmission === undefined
2918
3426
  ? {}
2919
3427
  : { transmission: renderSystem.transmission }),
3428
+ ...(renderSystem.standardLightingInspection === undefined
3429
+ ? {}
3430
+ : { standardLighting: Object.freeze({ ...renderSystem.standardLightingInspection }) }),
3431
+ ...(renderSystem.pointShadowInspection === undefined
3432
+ ? {}
3433
+ : { pointShadow: Object.freeze({ ...renderSystem.pointShadowInspection }) }),
2920
3434
  meshMaterialBindings: Object.freeze(
2921
3435
  renderSystem.meshMaterialBindings.map((observation) =>
2922
3436
  Object.freeze({
@@ -3065,6 +3579,9 @@ async function makeWebGPURenderer(
3065
3579
  get bindGroupCounts() {
3066
3580
  return renderSystem.bindGroupCounts;
3067
3581
  },
3582
+ configureStandard(config) {
3583
+ renderSystem.configureStandard(config);
3584
+ },
3068
3585
  renderFeatureDiagnostics() {
3069
3586
  return internals.featureHost?.diagnostics() ?? [];
3070
3587
  },
@@ -3090,6 +3607,35 @@ async function makeWebGPURenderer(
3090
3607
  new RenderFeatureCapabilityMissingError(feature.identity, host.size, missingCapability),
3091
3608
  );
3092
3609
  }
3610
+ let nextRequiredFullscreenPostProcesses: readonly {
3611
+ readonly identity: string;
3612
+ readonly source: string;
3613
+ }[];
3614
+ try {
3615
+ nextRequiredFullscreenPostProcesses = collectRequiredFullscreenPostProcesses([
3616
+ ...host.features,
3617
+ feature,
3618
+ ]);
3619
+ } catch (cause) {
3620
+ const conflict =
3621
+ cause instanceof RhiError
3622
+ ? cause
3623
+ : new RhiError({
3624
+ code: 'internal-error',
3625
+ expected: 'fullscreen feature declarations are internally consistent',
3626
+ hint: 'repair the conflicting fullscreen identity/source declarations',
3627
+ });
3628
+ internals.errorRegistry.fire(conflict);
3629
+ return err(
3630
+ new RenderFeatureStageFailedError(feature.identity, -1, 'prepare', 'registration'),
3631
+ );
3632
+ }
3633
+ const fullscreenPrewarm = await prewarmFullscreenFeatureModules(feature);
3634
+ if (!fullscreenPrewarm.ok) {
3635
+ return err(
3636
+ new RenderFeatureStageFailedError(feature.identity, -1, 'prepare', 'registration'),
3637
+ );
3638
+ }
3093
3639
  for (const materialShaderId of feature.requiredMaterialShaders ?? []) {
3094
3640
  const lookup = getShader().findMaterialArtifact(materialShaderId);
3095
3641
  if (!lookup.ok) {
@@ -3128,6 +3674,7 @@ async function makeWebGPURenderer(
3128
3674
  requiredMaterialShaderSet.add(materialShaderId);
3129
3675
  }
3130
3676
  requiredMaterialShaders = Object.freeze([...requiredMaterialShaderSet]);
3677
+ requiredFullscreenPostProcesses = nextRequiredFullscreenPostProcesses;
3131
3678
  }
3132
3679
  return installed;
3133
3680
  },
@@ -3156,6 +3703,7 @@ async function makeWebGPURenderer(
3156
3703
  }
3157
3704
  }
3158
3705
  requiredMaterialShaders = Object.freeze([...requiredMaterialShaderSet]);
3706
+ requiredFullscreenPostProcesses = collectRequiredFullscreenPostProcesses(host.features);
3159
3707
  }
3160
3708
  return removed;
3161
3709
  },
@@ -3329,23 +3877,52 @@ async function makeWebGPURenderer(
3329
3877
  // through onError separately; the facade Result is the synchronous
3330
3878
  // summary AI users can ignore or branch on).
3331
3879
  try {
3880
+ renderTargetHost.beginFrame();
3881
+ const timingHost = internals as WebGPURendererInternals & {
3882
+ gpuPassTimingFrameIdentity?:
3883
+ | {
3884
+ readonly frameId: number;
3885
+ readonly deviceGeneration: number;
3886
+ readonly graphGeneration: number;
3887
+ }
3888
+ | undefined;
3889
+ gpuPassTimingCapture?:
3890
+ | import('../record/gpu-pass-timing/session.js').GpuPassTimingCapture
3891
+ | undefined;
3892
+ gpuPassTimingBeginReason?: GpuPassTimingReason | undefined;
3893
+ };
3332
3894
  const timingRequested =
3333
3895
  isFrameRequest &&
3334
3896
  (frameRequest?.profileFrame !== undefined ||
3335
3897
  internals.options?.captureGpuTimings === true);
3336
- let timingCapture: GpuTimingCapture | undefined;
3337
- let timingUnavailableReason = timingRequested
3898
+ let volumeTimingCapture: GpuTimingCapture | undefined;
3899
+ const volumeTimingUnavailableReason = timingRequested
3338
3900
  ? 'timestamp capture is unavailable on this device'
3339
3901
  : 'timestamp capture was not requested for this frame';
3340
- if (timingRequested) {
3902
+ // The pass-timing session is authoritative when the feature is
3903
+ // enabled. Keep the volume-specific capture for its existing
3904
+ // benchmark path, but do not allocate two timestamp readbacks for a
3905
+ // single receipt when both options are present.
3906
+ if (timingRequested && gpuPassTimingOptions === undefined) {
3341
3907
  const createdTiming = GpuTimingCapture.create(internals.device);
3342
- if (createdTiming.ok) timingCapture = createdTiming.value;
3343
- else
3344
- timingUnavailableReason = `timestamp capture setup failed: ${createdTiming.error.code}`;
3908
+ if (createdTiming.ok) volumeTimingCapture = createdTiming.value;
3909
+ }
3910
+ if (isFrameRequest) {
3911
+ timingHost.gpuPassTimingFrameIdentity = {
3912
+ frameId: frameId + 1,
3913
+ deviceGeneration: activeDeviceScope.generation,
3914
+ graphGeneration: 0,
3915
+ };
3345
3916
  }
3346
- const submitted = renderSystem.draw(worlds, drawOptions, renderReadLeases, timingCapture);
3917
+ const submitted = renderSystem.draw(
3918
+ worlds,
3919
+ drawOptions,
3920
+ renderReadLeases,
3921
+ volumeTimingCapture,
3922
+ );
3923
+ timingHost.gpuPassTimingFrameIdentity = undefined;
3347
3924
  if (!submitted) {
3348
- timingCapture?.discard();
3925
+ volumeTimingCapture?.discard();
3349
3926
  return err(
3350
3927
  new RendererContractFailureError(
3351
3928
  'draw',
@@ -3353,36 +3930,80 @@ async function makeWebGPURenderer(
3353
3930
  ),
3354
3931
  );
3355
3932
  }
3933
+ const timingCompletion = timingHost.gpuPassTimingSubmittedWork;
3934
+ timingHost.gpuPassTimingSubmittedWork = undefined;
3935
+ const reflectionFallbackCompletion = renderSystem.reflectionFallbackCompletion;
3356
3936
  internals.pack.instrumentation?.onFrameBoundary?.();
3937
+ const receiptFrameId = ++frameId;
3938
+ const receiptGeneration = activeDeviceScope.generation;
3939
+ const queueCompletion = timingCompletion ?? internals.device.queue.onSubmittedWorkDone();
3940
+ const completed: FrameReceipt['completed'] = Promise.all([
3941
+ queueCompletion,
3942
+ reflectionFallbackCompletion ?? Promise.resolve(),
3943
+ ])
3944
+ .then(() => ok(undefined))
3945
+ .catch((cause: unknown) =>
3946
+ err(
3947
+ new RendererOperationError('device-operation-failed', {
3948
+ operation: 'complete-frame',
3949
+ frameId: receiptFrameId,
3950
+ deviceGeneration: receiptGeneration,
3951
+ cause: structuredRendererCause(cause, 'complete-frame'),
3952
+ }),
3953
+ ),
3954
+ );
3955
+ renderTargetHost.onFrameSubmitted(completed);
3357
3956
  if (isFrameRequest) {
3358
- const receiptFrameId = ++frameId;
3359
- const receiptGeneration = activeDeviceScope.generation;
3360
- const completed: FrameReceipt['completed'] = internals.device.queue
3361
- .onSubmittedWorkDone()
3362
- .then(() => ok(undefined))
3363
- .catch((cause: unknown) =>
3364
- err(
3365
- new RendererOperationError('device-operation-failed', {
3366
- operation: 'complete-frame',
3367
- frameId: receiptFrameId,
3368
- deviceGeneration: receiptGeneration,
3369
- cause: structuredRendererCause(cause, 'complete-frame'),
3370
- }),
3371
- ),
3372
- );
3373
3957
  const receipt = Object.freeze({
3374
3958
  frameId: receiptFrameId,
3375
3959
  deviceGeneration: receiptGeneration,
3376
3960
  completed,
3377
3961
  });
3378
- receiptTimings.set(
3379
- receipt,
3380
- timingCapture?.observation() ??
3381
- Promise.resolve({
3382
- status: 'unavailable',
3383
- reason: timingUnavailableReason,
3384
- }),
3385
- );
3962
+ const passTimingCapture = timingHost.gpuPassTimingCapture;
3963
+ const timingBeginReason = timingHost.gpuPassTimingBeginReason;
3964
+ const timingSource =
3965
+ passTimingCapture === undefined
3966
+ ? gpuPassTimingUnavailable === undefined && timingBeginReason === undefined
3967
+ ? undefined
3968
+ : async (): Promise<GpuPassTimingObservation> =>
3969
+ gpuPassTimingUnavailable ??
3970
+ failedGpuPassTimingObservation(timingBeginReason as GpuPassTimingReason)
3971
+ : async (): Promise<GpuPassTimingObservation> => {
3972
+ const observed = await passTimingCapture.observe();
3973
+ if (!observed.ok) {
3974
+ return failedGpuPassTimingObservation(observed.error);
3975
+ }
3976
+ const frame = observed.value;
3977
+ const unmeasured = frame.passes.find((pass) => pass.status === 'unmeasured');
3978
+ if (unmeasured !== undefined) {
3979
+ return { status: 'partial', frame, reason: unmeasured.reason };
3980
+ }
3981
+ if (frame.droppedPassCount > 0) {
3982
+ return {
3983
+ status: 'partial',
3984
+ frame,
3985
+ reason: {
3986
+ code: 'query-budget-exceeded',
3987
+ expected: 'all executed passes fit within the bounded query budget',
3988
+ hint: 'increase maxPassesPerFrame or simplify the pass graph',
3989
+ detail: { droppedPassCount: frame.droppedPassCount },
3990
+ },
3991
+ };
3992
+ }
3993
+ return { status: 'complete', frame };
3994
+ };
3995
+ timingObservationStore?.register(receipt, timingSource);
3996
+ currentReceipts.add(receipt);
3997
+ if (timingRequested && gpuPassTimingOptions === undefined) {
3998
+ receiptTimings.set(
3999
+ receipt,
4000
+ volumeTimingCapture?.observation() ??
4001
+ Promise.resolve({
4002
+ status: 'unavailable',
4003
+ reason: volumeTimingUnavailableReason,
4004
+ }),
4005
+ );
4006
+ }
3386
4007
  issuedReceipts.add(receipt);
3387
4008
  return ok(receipt);
3388
4009
  }
@@ -3407,7 +4028,16 @@ async function makeWebGPURenderer(
3407
4028
  receipt: FrameReceipt,
3408
4029
  request: FrameObservationRequest,
3409
4030
  ): Promise<RenderResult<FrameReceiptObservation, RenderError>> {
4031
+ // The bounded pass contract owns `timings`; the volume timing producer
4032
+ // owns the separate `volumeTimings` field. Keeping the fields distinct
4033
+ // prevents consumers from having to discriminate an untagged union.
4034
+ if (gpuPassTimingOptions !== undefined) {
4035
+ return timingObservationStore === undefined
4036
+ ? observeGpuPassTimingDisabled(receipt, request, () => activeDeviceScope.generation)
4037
+ : timingObservationStore.observe(receipt, request);
4038
+ }
3410
4039
  if (
4040
+ !currentReceipts.has(receipt) ||
3411
4041
  !issuedReceipts.has(receipt) ||
3412
4042
  receipt.deviceGeneration !== activeDeviceScope.generation
3413
4043
  ) {
@@ -3432,11 +4062,17 @@ async function makeWebGPURenderer(
3432
4062
  }),
3433
4063
  );
3434
4064
  }
3435
- const timings = request.include.includes('timings')
4065
+ await renderSystem.observeLodOcclusion();
4066
+ const targetReadbacks =
4067
+ request.targetReadbacks === undefined || request.targetReadbacks.length === 0
4068
+ ? undefined
4069
+ : await renderTargetHost.observeTargetReadbacks(receipt, request.targetReadbacks);
4070
+ if (targetReadbacks !== undefined && !targetReadbacks.ok) return targetReadbacks;
4071
+ const volumeTimings = request.include.includes('timings')
3436
4072
  ? await (receiptTimings.get(receipt) ??
3437
- Promise.resolve({
4073
+ Promise.resolve<VolumeTimingObservation>({
3438
4074
  status: 'unavailable',
3439
- reason: 'timing receipt was not retained',
4075
+ reason: 'timestamp capture was not requested for this frame',
3440
4076
  }))
3441
4077
  : undefined;
3442
4078
  return ok(
@@ -3444,7 +4080,10 @@ async function makeWebGPURenderer(
3444
4080
  frameId: receipt.frameId,
3445
4081
  deviceGeneration: receipt.deviceGeneration,
3446
4082
  include: Object.freeze([...request.include]),
3447
- ...(timings === undefined ? {} : { timings }),
4083
+ ...(targetReadbacks === undefined
4084
+ ? {}
4085
+ : { targetReadbacks: Object.freeze(targetReadbacks.value) }),
4086
+ ...(volumeTimings === undefined ? {} : { volumeTimings }),
3448
4087
  }),
3449
4088
  );
3450
4089
  },
@@ -3519,6 +4158,8 @@ async function makeWebGPURenderer(
3519
4158
  dispose(): RenderResult<void, RenderError> {
3520
4159
  if (disposed) return ok(undefined);
3521
4160
  disposed = true;
4161
+ currentReceipts.clear();
4162
+ renderTargetHost.dispose();
3522
4163
  const cleanupFailures: RendererOperationCause[] = [];
3523
4164
  for (const world of attachedWorlds) {
3524
4165
  try {
@@ -3699,6 +4340,8 @@ async function makeWebGPURenderer(
3699
4340
  // a layout from the replacement device.
3700
4341
  emptyPostProcessBgl = null;
3701
4342
  internals.context.unconfigure();
4343
+ retireGpuPassTimingSession();
4344
+ attachGpuPassTimingSession(internals, undefined);
3702
4345
 
3703
4346
  // Step (b) / B-2 / B-AC-02: shed device-bound RenderSystem state minted by
3704
4347
  // the lost device — the render-graph pendingDestroy queue (its PooledTextures
@@ -3845,8 +4488,13 @@ async function makeWebGPURenderer(
3845
4488
  const previousDeviceScope = activeDeviceScope;
3846
4489
  activeDeviceScope = recoveryScope;
3847
4490
  pipelineState = candidatePipelineState;
4491
+ promotePipelineExtendedLighting(candidatePipelineState, activeDeviceScope);
4492
+ currentReceipts.clear();
3848
4493
  gpuStore.bindDeviceScope(activeDeviceScope);
4494
+ renderTargetHost.recover();
3849
4495
  previousDeviceScope.retire();
4496
+ establishGpuPassTimingSession();
4497
+ attachGpuPassTimingSession(internals, gpuPassTimingSession);
3850
4498
 
3851
4499
  // Re-evaluate feature capability gates against the replacement device.
3852
4500
  // Feature recovery is isolated from renderer recovery: a feature that is
@@ -3932,7 +4580,7 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
3932
4580
  });
3933
4581
 
3934
4582
  return {
3935
- attach: (world) => {
4583
+ attach: (world: World) => {
3936
4584
  const result = renderer.attach(world);
3937
4585
  return result.ok
3938
4586
  ? result
@@ -3943,6 +4591,12 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
3943
4591
  }),
3944
4592
  );
3945
4593
  },
4594
+ createRenderTarget: (descriptor) => renderer.createRenderTarget(descriptor),
4595
+ resizeRenderTarget: (target, descriptor) => renderer.resizeRenderTarget(target, descriptor),
4596
+ createRenderTargetTextureSource: (target, options) =>
4597
+ renderer.createRenderTargetTextureSource(target, options),
4598
+ requestTargetReadback: (target, request) => renderer.requestTargetReadback(target, request),
4599
+ destroyRenderTarget: (target) => renderer.destroyRenderTarget(target),
3946
4600
  setProfile: (profile) => {
3947
4601
  if (!isRendererStateOperational(currentState)) {
3948
4602
  return err(
@@ -3955,7 +4609,7 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
3955
4609
  return renderer.setProfile(profile);
3956
4610
  },
3957
4611
  state: () => currentState,
3958
- draw: (request) => {
4612
+ draw: (request: RenderFrameInput) => {
3959
4613
  const result = renderer.drawFrame(request);
3960
4614
  if (!result.ok) return err(drawError(result.error));
3961
4615
  emit(
@@ -3968,8 +4622,9 @@ export function exposeRenderer(renderer: RendererHostImplementation): Renderer {
3968
4622
  return ok(result.value);
3969
4623
  },
3970
4624
  inspect: () => renderer.inspect(),
3971
- observe: (receipt, request) => renderer.observe(receipt, request),
3972
- subscribe: (listener) => {
4625
+ observe: (receipt: FrameReceipt, request: FrameObservationRequest) =>
4626
+ renderer.observe(receipt, request),
4627
+ subscribe: (listener: RendererEventListener) => {
3973
4628
  if (disposed) return () => undefined;
3974
4629
  listeners.add(listener);
3975
4630
  return () => listeners.delete(listener);
@@ -4083,17 +4738,11 @@ function structuredRendererCause(cause: unknown, operation: string): RendererOpe
4083
4738
  const RENDER_PROFILE_KEYS = new Set([
4084
4739
  'pipelineId',
4085
4740
  'lightCount',
4086
- 'lighting',
4741
+ 'renderPath',
4087
4742
  'shadows',
4088
4743
  'pbr',
4089
4744
  'ibl',
4090
4745
  'ssao',
4091
- 'bloom',
4092
- 'tone',
4093
- 'antialias',
4094
- 'sky',
4095
- 'volumetricFog',
4096
- 'fallback',
4097
4746
  'postStages',
4098
4747
  ]);
4099
4748
 
@@ -4107,36 +4756,15 @@ function validateRenderProfile(profile: RenderProfile): string | undefined {
4107
4756
  if (!STANDARD_LIGHT_COUNTS.some((count) => count === profile.lightCount)) {
4108
4757
  return 'RenderProfile.lightCount must be 1, 32, or 256';
4109
4758
  }
4110
- if (profile.lighting !== 'direct' && profile.lighting !== 'clustered') {
4111
- return "RenderProfile.lighting must be 'direct' or 'clustered'";
4759
+ if (profile.renderPath !== 'forward' && profile.renderPath !== 'deferred') {
4760
+ return "RenderProfile.renderPath must be 'forward' or 'deferred'";
4112
4761
  }
4113
4762
  if (profile.shadows !== 'off' && profile.shadows !== 'hard' && profile.shadows !== 'filtered') {
4114
4763
  return "RenderProfile.shadows must be 'off', 'hard', or 'filtered'";
4115
4764
  }
4116
- if (profile.fallback !== 'native' && profile.fallback !== 'cpu-webgl2') {
4117
- return "RenderProfile.fallback must be 'native' or 'cpu-webgl2'";
4118
- }
4119
4765
  for (const field of ['pbr', 'ibl', 'ssao'] as const) {
4120
4766
  if (typeof profile[field] !== 'boolean') return `RenderProfile.${field} must be boolean`;
4121
4767
  }
4122
- if (profile.volumetricFog !== undefined) {
4123
- if (profile.volumetricFog.quality !== 'low' && profile.volumetricFog.quality !== 'high')
4124
- return "RenderProfile.volumetricFog.quality must be 'low' or 'high'";
4125
- if (profile.volumetricFog.depth !== 48 && profile.volumetricFog.depth !== 64) {
4126
- return 'RenderProfile.volumetricFog.depth must be 48 or 64';
4127
- }
4128
- if (profile.volumetricFog.tileSize !== 4 && profile.volumetricFog.tileSize !== 16) {
4129
- return 'RenderProfile.volumetricFog.tileSize must be 4 or 16';
4130
- }
4131
- if (profile.volumetricFog.quality === 'low' && profile.volumetricFog.depth !== 48)
4132
- return 'RenderProfile.volumetricFog.low must use depth 48';
4133
- if (profile.volumetricFog.quality === 'high' && profile.volumetricFog.depth !== 64)
4134
- return 'RenderProfile.volumetricFog.high must use depth 64';
4135
- if (profile.volumetricFog.quality === 'low' && profile.volumetricFog.tileSize !== 16)
4136
- return 'RenderProfile.volumetricFog.low must use tileSize 16';
4137
- if (profile.volumetricFog.quality === 'high' && profile.volumetricFog.tileSize !== 4)
4138
- return 'RenderProfile.volumetricFog.high must use tileSize 4';
4139
- }
4140
4768
  const stages = STANDARD_POST_STAGE_NAMES;
4141
4769
  if (
4142
4770
  !Array.isArray(profile.postStages) ||
@@ -4409,23 +5037,20 @@ function makeRhiNotAvailableError(): RhiError {
4409
5037
  // inverseViewProj mat4x4<f32> (64 B) appended at tail (offset 176, align 16,
4410
5038
  // total 176 + 64 = 240 B). Field added to common.wgsl View struct; host
4411
5039
  // writes the tail via queue.writeBuffer in render-system-record.
4412
- // feat-20260613-csm-cascaded-shadow-maps M4 / w16: extended to 592 B.
5040
+ // feat-20260613-csm-cascaded-shadow-maps M4 / w16: extended to 960 B.
4413
5041
  // lightSpaceMatrix replaced by lightViewProj_A at offset 112; inverseViewProj
4414
5042
  // stays at offset 176; lightViewProj_B..D + splitPlanes[4] + cascadeCount +
4415
- // cascadeBlend + tail padding appended after inverseViewProj. Total 148 f32
4416
- // = 592 B, fixed independent of cascadeCount (AC-08).
4417
- // feat-20260621-merge-directionallightshadow-into-directionallight M3 / m3-t3:
4418
- // depthBias / normalBias / pcfKernelSize from the merged DirectionalLight
4419
- // append at the formerly-free tail pad (bytes 504/508/512, floats 126/127/128).
4420
- // VIEW_UBO_BYTES stays 592 (the host tail pad shrinks 88 B -> 64 B); the WGSL
4421
- // View struct in common.wgsl carries the matching 3 f32 (SSOT comment synced).
5043
+ // cascadeBlend + tail padding appended after inverseViewProj. The directional
5044
+ // payload is 240 f32 = 960 B, placed in a 1024 B dynamic-uniform slot (AC-08).
5045
+ // M1 reserves [126]/[127] for depthBias/normalBias and [128..131] for the
5046
+ // directional filter carrier; the Spot matrix tail begins at float 132.
4422
5047
  // feat-20260625-spot-light-shadow-mapping w25 (scope-amend webkit-fallback):
4423
- // grew 592 -> 784. The per-spot fragment-read perspective lightViewProj matrices
5048
+ // grew from the directional 960 B payload to the 1024 B slot. The per-spot fragment-read perspective lightViewProj matrices
4424
5049
  // (`array<mat4x4<f32>, 4>` = 256 B) fold from a standalone @group(0) binding 9
4425
5050
  // uniform buffer into the View UBO tail (bytes 528..784, 16 B-aligned after
4426
- // pcfKernelSize at byte 512). This removes the 12th fragment-stage uniform
5051
+ // the directional filter carrier at bytes 512..528). This removes the 12th fragment-stage uniform
4427
5052
  // buffer that overflowed GLES 3.0's max_uniform_buffers_per_shader_stage = 11 on
4428
- // the WebGL2 fallback path. render-system-record writes the full 196 f32 payload;
5053
+ // the WebGL2 fallback path. render-system-record writes the full 240 f32 payload;
4429
5054
  // common.wgsl `View.spotLightViewProj` is the matching SSOT.
4430
5055
  const VIEW_UBO_BYTES = VIEW_UNIFORM_BUFFER_SIZE;
4431
5056
 
@@ -4914,6 +5539,7 @@ async function prepareMaterialShaders(
4914
5539
  if (!capCheck.ok) throw capCheck.error;
4915
5540
  storageBufferCapable = !webgl2Downlevel && capCheck.value;
4916
5541
  }
5542
+ const extendedLightingShaderAvailable = deriveExtendedLightingCapability(rhiDevice).admitted;
4917
5543
  // The shared PBR view/material ABI consumes the WebGPU minimum of 16
4918
5544
  // fragment sampled textures. The authored SpotLight projector is the
4919
5545
  // optional 17th texture; select its shader/BGL pair from the same limit so
@@ -4922,7 +5548,6 @@ async function prepareMaterialShaders(
4922
5548
  const projectorAvailable =
4923
5549
  (rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
4924
5550
  STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
4925
-
4926
5551
  // ── Step 1: manifest load ─────────────────────────────────────────────────
4927
5552
  const registry = getShader();
4928
5553
  const loaded = await registry.loadManifest();
@@ -4931,12 +5556,11 @@ async function prepareMaterialShaders(
4931
5556
  }
4932
5557
 
4933
5558
  // ── Step 1b: material shader variant resolution + registration ───────────
4934
- // feat-20260609-hdrp-cluster-fragment-ggx M1 / w7: dual-axis variant
4935
- // resolution. At boot time isHdrpActive is always false (URP default),
4936
- // so CLUSTER_FORWARD_AVAILABLE=false variant is selected. HDRP variant
4937
- // (CLUSTER_FORWARD_AVAILABLE=true) is resolved later when
4938
- // clustered Standard profile activation selects the clustered variant.
4939
- const isHdrpActive = false; // M4: wire to actual renderSystem.frameState.isHdrpActive
5559
+ // Install the non-clustered artifact as the eager registry seed. This value
5560
+ // only makes the pre-frame registry usable; record-time Standard topology is
5561
+ // derived from the frame's prepared `standardLighting.kind` and never reads
5562
+ // this boot seed as an active lighting decision.
5563
+ const bootClusterAxis = standardBootClusterAxis();
4940
5564
  for (const msEntry of registry.materialShaderManifestEntries()) {
4941
5565
  // bug-20260610: skip synthetic non-material engine entries piggy-backing
4942
5566
  // on the materialShaders channel for variant surfacing (shadow_caster).
@@ -4947,6 +5571,11 @@ async function prepareMaterialShaders(
4947
5571
  const variantDefines: Record<string, boolean> = {
4948
5572
  STORAGE_BUFFER_AVAILABLE: storageBufferCapable,
4949
5573
  };
5574
+ if (msEntry.variants.some((v) => 'DIRECTIONAL_PCSS_AVAILABLE' in v.defines)) {
5575
+ variantDefines.DIRECTIONAL_PCSS_AVAILABLE = directionalPcssCapable(
5576
+ rhiDevice.caps.backendKind,
5577
+ );
5578
+ }
4950
5579
  // User material manifests may declare WEBGL2_COMPAT alongside the shared
4951
5580
  // mesh-storage axis. The fallback backend must select that authored
4952
5581
  // branch at boot; otherwise it asks for the impossible bare
@@ -4958,11 +5587,20 @@ async function prepareMaterialShaders(
4958
5587
  // the axis (has a variant with this key), preserving backward compat for
4959
5588
  // entries without the pragma.
4960
5589
  if (msEntry.variants.some((v) => 'CLUSTER_FORWARD_AVAILABLE' in v.defines)) {
4961
- variantDefines.CLUSTER_FORWARD_AVAILABLE = isHdrpActive;
5590
+ variantDefines.CLUSTER_FORWARD_AVAILABLE = bootClusterAxis;
4962
5591
  }
4963
5592
  if (msEntry.variants.some((v) => 'VERTEX_COLOR_AVAILABLE' in v.defines)) {
4964
5593
  variantDefines.VERTEX_COLOR_AVAILABLE = false;
4965
5594
  }
5595
+ if (msEntry.variants.some((v) => 'PROBE_BLEND_AVAILABLE' in v.defines)) {
5596
+ // ProbeBlendRecord is an object-level opt-in, not a device capability.
5597
+ // The boot/default module must stay on the no-probe ABI; record-stage
5598
+ // entries with a retained record request the explicit true variant.
5599
+ variantDefines.PROBE_BLEND_AVAILABLE = false;
5600
+ }
5601
+ if (msEntry.variants.some((v) => 'EXTENDED_LIGHTING_AVAILABLE' in v.defines)) {
5602
+ variantDefines.EXTENDED_LIGHTING_AVAILABLE = extendedLightingShaderAvailable;
5603
+ }
4966
5604
  if (
4967
5605
  msEntry.identifier === 'forgeax::default-standard-pbr' &&
4968
5606
  msEntry.variants.some((v) => 'PROJECTOR_AVAILABLE' in v.defines)
@@ -4975,6 +5613,17 @@ async function prepareMaterialShaders(
4975
5613
  ) {
4976
5614
  variantDefines.TRANSMISSION_AVAILABLE = false;
4977
5615
  }
5616
+ // The fallback MRT is a record-time opt-in. Boot registration must select
5617
+ // the ordinary single-target Standard artifact when the manifest declares
5618
+ // the axis; record-time topology derives the true branch only after the
5619
+ // fallback attachment has been admitted. Omitting this false value asks
5620
+ // the closed manifest for a non-existent partial key.
5621
+ if (
5622
+ msEntry.identifier === 'forgeax::default-standard-pbr' &&
5623
+ msEntry.variants.some((v) => 'REFLECTION_FALLBACK_AVAILABLE' in v.defines)
5624
+ ) {
5625
+ variantDefines.REFLECTION_FALLBACK_AVAILABLE = false;
5626
+ }
4978
5627
  // bug-20260708 M2 (a): PER_INSTANCE_REGION axis default = false at boot.
4979
5628
  // Per-entity spritePH path (`main-pass-sprite-draws.ts:511-513`) expects
4980
5629
  // `variantSet=undefined` to resolve to the PIR=false shader source (64B
@@ -5103,6 +5752,11 @@ async function buildReadyWebGPU(
5103
5752
  * frame while the shared async shader adapter is still warming up.
5104
5753
  */
5105
5754
  requiredMaterialShaders: readonly string[],
5755
+ /** Fullscreen feature modules compiled before the first synchronous frame. */
5756
+ requiredFullscreenPostProcesses: readonly {
5757
+ readonly identity: string;
5758
+ readonly source: string;
5759
+ }[],
5106
5760
  // feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M2 /
5107
5761
  // T-M2-05 + M3 / T-M3-04: surface for setting `internals.growMeshSsbo`
5108
5762
  // + `internals.meshSsboState` so the record stage's
@@ -5131,7 +5785,11 @@ async function buildReadyWebGPU(
5131
5785
  // `getMaterialShaderPipeline` lookup hits a live PSO instead of falling
5132
5786
  // into a 1-frame async-compile skip-draw window. Idempotent: caller
5133
5787
  // guards against re-seeding when a key already exists.
5134
- seedMaterialShaderPipelineCache: (key: string, pso: RenderPipeline) => void,
5788
+ seedMaterialShaderPipelineCache: (
5789
+ key: string,
5790
+ pso: RenderPipeline,
5791
+ group2Contract: PipelineGroup2Contract,
5792
+ ) => void,
5135
5793
  // feat-20260621 M-A3 (D-5): register the engine built-in tonemap onto the
5136
5794
  // unified post-process channel once the tonemap manifest entry's composed
5137
5795
  // WGSL is resolved. Invoked with the tonemap WGSL source string; the outer
@@ -5141,6 +5799,8 @@ async function buildReadyWebGPU(
5141
5799
  // tonemap source resolves inside this async function (after the manifest-load
5142
5800
  // await), by which point the synchronous `renderSystem` const is defined.
5143
5801
  registerBuiltinTonemap: (source: string) => void,
5802
+ /** Register the special FXAA pass' final-output policy params UBO. */
5803
+ registerBuiltinFxaa: (source: string) => void,
5144
5804
  registerBuiltinTemporalPostProcesses: (entries: {
5145
5805
  readonly motionBlur?: string;
5146
5806
  readonly taaResolve?: string;
@@ -5164,10 +5824,10 @@ async function buildReadyWebGPU(
5164
5824
  const capCheck = assertStorageBufferCap(capValue);
5165
5825
  storageBufferCapable = capCheck.ok ? !webgl2Downlevel && capCheck.value : !webgl2Downlevel;
5166
5826
  }
5827
+ const extendedLightingShaderAvailable = deriveExtendedLightingCapability(rhiDevice).admitted;
5167
5828
  const projectorAvailable =
5168
5829
  (rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
5169
5830
  STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
5170
-
5171
5831
  // bug-20260612: choose swap-chain storage / view formats by backend.
5172
5832
  // Channel 2 (native WebGPU) follows navigator.gpu.getPreferredCanvasFormat();
5173
5833
  // Channel 3 (wgpu-wasm GLES, storageBufferCapable=false) hard-codes
@@ -5238,6 +5898,10 @@ async function buildReadyWebGPU(
5238
5898
  let unlitModule: ShaderModule | null = null;
5239
5899
  let spriteModule: ShaderModule | null = null;
5240
5900
  let spriteLitModule: ShaderModule | null = null;
5901
+ let pbrGroup2Contract: PipelineGroup2Contract | undefined;
5902
+ let unlitGroup2Contract: PipelineGroup2Contract | undefined;
5903
+ let spriteGroup2Contract: PipelineGroup2Contract | undefined;
5904
+ let spriteLitGroup2Contract: PipelineGroup2Contract | undefined;
5241
5905
  let fxaaModule: ShaderModule | null = null;
5242
5906
  let skyboxModule: ShaderModule | null = null;
5243
5907
  let bloomBrightModule: ShaderModule | null = null;
@@ -5432,7 +6096,7 @@ async function buildReadyWebGPU(
5432
6096
  // bug-20260610: WebGL2 fallback path — when storageBufferCapable=false
5433
6097
  // the variant-aware Step 1b registers material shaders with the
5434
6098
  // STORAGE_BUFFER_AVAILABLE=false WGSL (uniform fallback for meshes /
5435
- // pointLights / instances), but Step 2 above triaged pbr/unlit/sprite/
6099
+ // meshes / instances), but Step 2 above triaged pbr/unlit/sprite/
5436
6100
  // pbr-skin/msdf-text from the FLAT `registry.entries()` map which only
5437
6101
  // contains the default (storage) variant. Compiling those with
5438
6102
  // STORAGE_BUFFER_AVAILABLE=true while the pipeline layout is built
@@ -5456,22 +6120,28 @@ async function buildReadyWebGPU(
5456
6120
  // CLUSTER_FORWARD_AVAILABLE in PR #344) only emits compound keys like
5457
6121
  // `CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=false`, so
5458
6122
  // findVariantByKey('STORAGE_BUFFER_AVAILABLE=false') silently misses.
5459
- // The fallback path forces isHdrpActive=false (Step 1b above), so the
5460
- // matching PBR variant is the (CFA=false, SBA=false) one pick by
5461
- // defines fields and prefer CLUSTER_FORWARD_AVAILABLE=false when present;
5462
- // otherwise each STORAGE_BUFFER_AVAILABLE=false variant.
6123
+ // The eager registry seed is the non-clustered boot artifact (the active
6124
+ // frame topology is not known here). Pick by defines fields and prefer
6125
+ // the boot cluster axis when present; otherwise use the storage axis.
5463
6126
  const idToVariantWgsl = new Map<string, string>();
5464
6127
  for (const ms of registry.materialShaderManifestEntries()) {
5465
6128
  const exact = ms.variants.find(
5466
- (v) =>
5467
- v.defines.STORAGE_BUFFER_AVAILABLE === false &&
5468
- (!('CLUSTER_FORWARD_AVAILABLE' in v.defines) ||
5469
- v.defines.CLUSTER_FORWARD_AVAILABLE === false) &&
5470
- (!('VERTEX_COLOR_AVAILABLE' in v.defines) ||
5471
- v.defines.VERTEX_COLOR_AVAILABLE === false) &&
5472
- (!('PROJECTOR_AVAILABLE' in v.defines) ||
5473
- v.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
5474
- (!('SKINNING_DISABLED' in v.defines) || v.defines.SKINNING_DISABLED === true),
6129
+ (variant) =>
6130
+ variant.defines.STORAGE_BUFFER_AVAILABLE === false &&
6131
+ (!('CLUSTER_FORWARD_AVAILABLE' in variant.defines) ||
6132
+ variant.defines.CLUSTER_FORWARD_AVAILABLE === standardBootClusterAxis()) &&
6133
+ (!('VERTEX_COLOR_AVAILABLE' in variant.defines) ||
6134
+ variant.defines.VERTEX_COLOR_AVAILABLE === false) &&
6135
+ (!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
6136
+ variant.defines.DIRECTIONAL_PCSS_AVAILABLE ===
6137
+ directionalPcssCapable(rhiDevice.caps.backendKind)) &&
6138
+ (!('PROJECTOR_AVAILABLE' in variant.defines) ||
6139
+ variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
6140
+ (!('REFLECTION_FALLBACK_AVAILABLE' in variant.defines) ||
6141
+ variant.defines.REFLECTION_FALLBACK_AVAILABLE === false) &&
6142
+ variant.defines.PROBE_BLEND_AVAILABLE !== true &&
6143
+ (!('SKINNING_DISABLED' in variant.defines) ||
6144
+ variant.defines.SKINNING_DISABLED === true),
5475
6145
  );
5476
6146
  const v = exact === undefined ? undefined : findVariantByKey(ms, exact.definesKey);
5477
6147
  if (v !== undefined) {
@@ -5500,8 +6170,8 @@ async function buildReadyWebGPU(
5500
6170
  }
5501
6171
  }
5502
6172
  // The flat `registry.entries()` map carries the canonical composed source,
5503
- // while the boot layout is the URP/device-capability contract. Select the
5504
- // matching runtime variant for every eager engine module, including native
6173
+ // while this eager module uses the boot layout contract. Select the
6174
+ // matching boot variant for every eager engine module, including native
5505
6175
  // WebGPU. Without this step the canonical PBR/Unlit source declares the
5506
6176
  // optional COLOR_0 vertex input while the boot SPEC_CONST layouts for
5507
6177
  // ordinary meshes do not provide location 13.
@@ -5510,16 +6180,24 @@ async function buildReadyWebGPU(
5510
6180
  const exact = ms.variants.find(
5511
6181
  (variant) =>
5512
6182
  variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
6183
+ (!('EXTENDED_LIGHTING_AVAILABLE' in variant.defines) ||
6184
+ variant.defines.EXTENDED_LIGHTING_AVAILABLE === extendedLightingShaderAvailable) &&
5513
6185
  (!('CLUSTER_FORWARD_AVAILABLE' in variant.defines) ||
5514
- variant.defines.CLUSTER_FORWARD_AVAILABLE === false) &&
6186
+ variant.defines.CLUSTER_FORWARD_AVAILABLE === standardBootClusterAxis()) &&
5515
6187
  (!('VERTEX_COLOR_AVAILABLE' in variant.defines) ||
5516
6188
  variant.defines.VERTEX_COLOR_AVAILABLE === false) &&
5517
6189
  (!('PROJECTOR_AVAILABLE' in variant.defines) ||
5518
6190
  variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
6191
+ variant.defines.PROBE_BLEND_AVAILABLE !== true &&
5519
6192
  (!('SKINNING_DISABLED' in variant.defines) ||
5520
6193
  variant.defines.SKINNING_DISABLED === true) &&
6194
+ (!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
6195
+ variant.defines.DIRECTIONAL_PCSS_AVAILABLE ===
6196
+ directionalPcssCapable(rhiDevice.caps.backendKind)) &&
5521
6197
  (!('TRANSMISSION_AVAILABLE' in variant.defines) ||
5522
- variant.defines.TRANSMISSION_AVAILABLE === false),
6198
+ variant.defines.TRANSMISSION_AVAILABLE === false) &&
6199
+ (!('REFLECTION_FALLBACK_AVAILABLE' in variant.defines) ||
6200
+ variant.defines.REFLECTION_FALLBACK_AVAILABLE === false),
5523
6201
  );
5524
6202
  const variant = exact === undefined ? undefined : findVariantByKey(ms, exact.definesKey);
5525
6203
  if (variant !== undefined) {
@@ -5650,6 +6328,27 @@ async function buildReadyWebGPU(
5650
6328
  seedShaderModule(label, shaderResult.value);
5651
6329
  }
5652
6330
 
6331
+ // A boot-installed fullscreen feature has the same synchronous record
6332
+ // contract as the built-in output transform: its module must be seeded
6333
+ // before the caller can drive a tight frame loop. Otherwise the typed
6334
+ // graph's first post-effect admission is correctly held back, but a
6335
+ // synchronous consumer would only see the base image until it yields.
6336
+ // Late-installed features use the matching registration path below.
6337
+ for (const postProcess of requiredFullscreenPostProcesses) {
6338
+ const label = postProcessShaderModuleLabel(postProcess.identity, postProcess.source);
6339
+ const shaderResult = await runShimStep(
6340
+ () =>
6341
+ asyncCreateShaderModule
6342
+ ? asyncCreateShaderModule(rhiDevice, { code: postProcess.source, label })
6343
+ : invokeDeviceCreateShaderModule(rhiDevice, { code: postProcess.source, label }),
6344
+ 'shader-compile-failed',
6345
+ `declared fullscreen render feature '${postProcess.identity}' compiled`,
6346
+ `inspect the composed WGSL for '${postProcess.identity}' and check device.features`,
6347
+ );
6348
+ if (!shaderResult.ok) throw shaderResult.error;
6349
+ seedShaderModule(label, shaderResult.value);
6350
+ }
6351
+
5653
6352
  // The volume graph is authored from ECS state and therefore cannot be
5654
6353
  // compiled while the static material manifest is assembled. Its pass
5655
6354
  // encoder nevertheless uses the same shared lazy shader adapter, so the
@@ -5735,6 +6434,7 @@ async function buildReadyWebGPU(
5735
6434
  );
5736
6435
  if (!pbrShaderResult.ok) throw pbrShaderResult.error;
5737
6436
  pbrModule = pbrShaderResult.value;
6437
+ pbrGroup2Contract = resolvePipelineGroup2Contract(pbrEntry.wgsl);
5738
6438
  // feat-20260629-multi-uv-set-support: a real extra-UV mesh creates a
5739
6439
  // layout-specific material PSO after the boot-time standard-layout PSO
5740
6440
  // has been pre-warmed. Seed the shared shader-module adapter under the
@@ -5758,7 +6458,9 @@ async function buildReadyWebGPU(
5758
6458
  ? selectStandardPbrTransmissionPrewarmVariants(
5759
6459
  pbrManifestEntry,
5760
6460
  storageBufferCapable,
6461
+ directionalPcssCapable(rhiDevice.caps.backendKind),
5761
6462
  projectorAvailable,
6463
+ extendedLightingShaderAvailable,
5762
6464
  )
5763
6465
  : [];
5764
6466
 
@@ -5796,7 +6498,9 @@ async function buildReadyWebGPU(
5796
6498
  for (const hdrpVariant of selectHdrpPbrPrewarmVariants(
5797
6499
  pbrManifestEntry,
5798
6500
  storageBufferCapable,
6501
+ extendedLightingShaderAvailable,
5799
6502
  transmissionCapable,
6503
+ directionalPcssCapable(rhiDevice.caps.backendKind),
5800
6504
  projectorAvailable,
5801
6505
  )) {
5802
6506
  const moduleLabel = `module-forgeax::default-standard-pbr#${hdrpVariant.definesKey}`;
@@ -5824,6 +6528,43 @@ async function buildReadyWebGPU(
5824
6528
  seedShaderModule(moduleLabel, hdrpModule);
5825
6529
  }
5826
6530
 
6531
+ // Skin geometry uses the same lazy material-pipeline adapter as standard
6532
+ // PBR, but its dedicated module was previously never warmed. A record
6533
+ // frame therefore received a pending module result, skipped the skin PSO,
6534
+ // and silently emitted no draw. Compile and seed every active-capability
6535
+ // skin variant so both URP and HDRP resolve their exact PSO consumer on
6536
+ // frame one (including the geometry-owned COLOR_0=false variant).
6537
+ const pbrSkinManifestEntry = [...registry.materialShaderManifestEntries()].find(
6538
+ (entry) => entry.identifier === SKIN_MATERIAL_SHADER_ID,
6539
+ );
6540
+ const activeSkinVariants = selectSkinPrewarmVariants(
6541
+ pbrSkinManifestEntry,
6542
+ storageBufferCapable,
6543
+ );
6544
+ for (const skinVariant of activeSkinVariants) {
6545
+ const moduleLabel = `module-${SKIN_MATERIAL_SHADER_ID}#${skinVariant.definesKey}`;
6546
+ const skinModuleResult = await runShimStep(
6547
+ () =>
6548
+ asyncCreateShaderModule
6549
+ ? asyncCreateShaderModule(rhiDevice, {
6550
+ code: skinVariant.composedWgsl,
6551
+ label: moduleLabel,
6552
+ })
6553
+ : invokeDeviceCreateShaderModule(rhiDevice, {
6554
+ code: skinVariant.composedWgsl,
6555
+ label: moduleLabel,
6556
+ }),
6557
+ 'shader-compile-failed',
6558
+ `pbr-skin variant ${skinVariant.definesKey || '<default>'} compiled`,
6559
+ 'inspect the selected pbr-skin storage/cluster/color variant and device.features',
6560
+ );
6561
+ if (!skinModuleResult.ok) throw skinModuleResult.error;
6562
+ seedShaderModule(moduleLabel, skinModuleResult.value);
6563
+ if (skinVariant.definesKey === '') {
6564
+ seedShaderModule(`module-${SKIN_MATERIAL_SHADER_ID}`, skinModuleResult.value);
6565
+ }
6566
+ }
6567
+
5827
6568
  // M2-07-c: Materials.standard with an explicit renderState reaches the
5828
6569
  // lazy material pipeline path, whose module label includes the requested
5829
6570
  // variant set. Reuse the boot-compiled PBR module only for manifest
@@ -5848,6 +6589,7 @@ async function buildReadyWebGPU(
5848
6589
  );
5849
6590
  if (!unlitShaderResult.ok) throw unlitShaderResult.error;
5850
6591
  unlitModule = unlitShaderResult.value;
6592
+ unlitGroup2Contract = resolvePipelineGroup2Contract(unlitEntry.wgsl);
5851
6593
  // Seed the shared lazy adapter with the eagerly compiled module so
5852
6594
  // prepared color-only PSOs build synchronously on their first frame.
5853
6595
  seedShaderModule('unlit', unlitModule);
@@ -5994,7 +6736,8 @@ async function buildReadyWebGPU(
5994
6736
  // frames (e.g. the hello-tonemap dawn smoke) would stall on a black frame
5995
6737
  // forever. Mirror the shadow_caster prewarm: await-compile the tonemap module
5996
6738
  // here and `seedShaderModule` it under the exact label
5997
- // `buildPostProcessPipeline` requests (`post-process-${id}-module`), so the
6739
+ // `buildPostProcessPipeline` requests the content-addressed module label,
6740
+ // so the
5998
6741
  // first `getPostProcessPipeline(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, …)` hits the module cache
5999
6742
  // synchronously and builds the pipeline on frame 1.
6000
6743
  const tonemapPrewarm = await runShimStep(
@@ -6002,11 +6745,17 @@ async function buildReadyWebGPU(
6002
6745
  asyncCreateShaderModule
6003
6746
  ? asyncCreateShaderModule(rhiDevice, {
6004
6747
  code: tonemapEntry.wgsl,
6005
- label: `post-process-${STANDARD_OUTPUT_TRANSFORM_FEATURE_ID}-module`,
6748
+ label: postProcessShaderModuleLabel(
6749
+ STANDARD_OUTPUT_TRANSFORM_FEATURE_ID,
6750
+ tonemapEntry.wgsl,
6751
+ ),
6006
6752
  })
6007
6753
  : invokeDeviceCreateShaderModule(rhiDevice, {
6008
6754
  code: tonemapEntry.wgsl,
6009
- label: `post-process-${STANDARD_OUTPUT_TRANSFORM_FEATURE_ID}-module`,
6755
+ label: postProcessShaderModuleLabel(
6756
+ STANDARD_OUTPUT_TRANSFORM_FEATURE_ID,
6757
+ tonemapEntry.wgsl,
6758
+ ),
6010
6759
  }),
6011
6760
  'shader-compile-failed',
6012
6761
  'tonemap shader module compiled (unified post-process prewarm)',
@@ -6014,7 +6763,7 @@ async function buildReadyWebGPU(
6014
6763
  );
6015
6764
  if (!tonemapPrewarm.ok) throw tonemapPrewarm.error;
6016
6765
  seedShaderModule(
6017
- `post-process-${STANDARD_OUTPUT_TRANSFORM_FEATURE_ID}-module`,
6766
+ postProcessShaderModuleLabel(STANDARD_OUTPUT_TRANSFORM_FEATURE_ID, tonemapEntry.wgsl),
6018
6767
  tonemapPrewarm.value,
6019
6768
  );
6020
6769
  registerBuiltinTonemap(tonemapEntry.wgsl);
@@ -6031,18 +6780,18 @@ async function buildReadyWebGPU(
6031
6780
  asyncCreateShaderModule
6032
6781
  ? asyncCreateShaderModule(rhiDevice, {
6033
6782
  code: entry.wgsl,
6034
- label: `post-process-${id}-module`,
6783
+ label: postProcessShaderModuleLabel(id, entry.wgsl),
6035
6784
  })
6036
6785
  : invokeDeviceCreateShaderModule(rhiDevice, {
6037
6786
  code: entry.wgsl,
6038
- label: `post-process-${id}-module`,
6787
+ label: postProcessShaderModuleLabel(id, entry.wgsl),
6039
6788
  }),
6040
6789
  'shader-compile-failed',
6041
6790
  `${id} shader module compiled (unified post-process prewarm)`,
6042
6791
  `inspect manifest ${id} entry composed wgsl; check device.features`,
6043
6792
  );
6044
6793
  if (!prewarm.ok) throw prewarm.error;
6045
- seedShaderModule(`post-process-${id}-module`, prewarm.value);
6794
+ seedShaderModule(postProcessShaderModuleLabel(id, entry.wgsl), prewarm.value);
6046
6795
  };
6047
6796
  await prewarmTemporal(motionBlurEntry, 'forgeax.motion-blur');
6048
6797
  await prewarmTemporal(taaResolveEntry, 'forgeax.taa-resolve');
@@ -6071,6 +6820,7 @@ async function buildReadyWebGPU(
6071
6820
  );
6072
6821
  if (!spriteShaderResult.ok) throw spriteShaderResult.error;
6073
6822
  spriteModule = spriteShaderResult.value;
6823
+ spriteGroup2Contract = resolvePipelineGroup2Contract(spriteEntry.wgsl);
6074
6824
  // feat-20260625-refactor-sprite-as-transparent-mesh CI-fix: seed the
6075
6825
  // lazy adapter cache under the same label (`module-forgeax::sprite`)
6076
6826
  // that `getMaterialShaderPipeline -> buildAndCachePipeline ->
@@ -6141,7 +6891,44 @@ async function buildReadyWebGPU(
6141
6891
  );
6142
6892
  if (!spriteLitShaderResult.ok) throw spriteLitShaderResult.error;
6143
6893
  spriteLitModule = spriteLitShaderResult.value;
6894
+ spriteLitGroup2Contract = resolvePipelineGroup2Contract(spriteLitEntryConst.wgsl);
6144
6895
  seedShaderModule('module-forgeax::sprite-lit', spriteLitShaderResult.value);
6896
+ // Sprite-lit participates in the same Standard capability split as
6897
+ // PBR. Record requests the explicit URP/HDRP variant per frame, so seed
6898
+ // every device-matched artifact during readiness; a tight dawn/browser
6899
+ // frame loop cannot yield to the async module adapter between draws.
6900
+ const spriteLitManifestEntry = Array.from(registry.materialShaderManifestEntries()).find(
6901
+ (entry) => entry.identifier === 'forgeax::sprite-lit',
6902
+ );
6903
+ const prewarmedSpriteLitModules = new Map<string, ShaderModule>([
6904
+ [spriteLitEntryConst.wgsl, spriteLitShaderResult.value],
6905
+ ]);
6906
+ for (const variant of spriteLitManifestEntry?.variants ?? []) {
6907
+ if (variant.defines.STORAGE_BUFFER_AVAILABLE !== storageBufferCapable) continue;
6908
+ const moduleLabel = `module-forgeax::sprite-lit#${variant.definesKey}`;
6909
+ let variantModule = prewarmedSpriteLitModules.get(variant.composedWgsl);
6910
+ if (variantModule === undefined) {
6911
+ const variantResult = await runShimStep(
6912
+ () =>
6913
+ asyncCreateShaderModule
6914
+ ? asyncCreateShaderModule(rhiDevice, {
6915
+ code: variant.composedWgsl,
6916
+ label: moduleLabel,
6917
+ })
6918
+ : invokeDeviceCreateShaderModule(rhiDevice, {
6919
+ code: variant.composedWgsl,
6920
+ label: moduleLabel,
6921
+ }),
6922
+ 'shader-compile-failed',
6923
+ `sprite-lit variant ${variant.definesKey || '<default>'} compiled`,
6924
+ 'inspect the selected sprite-lit Standard capability variant and device.features',
6925
+ );
6926
+ if (!variantResult.ok) throw variantResult.error;
6927
+ variantModule = variantResult.value;
6928
+ prewarmedSpriteLitModules.set(variant.composedWgsl, variantModule);
6929
+ }
6930
+ seedShaderModule(moduleLabel, variantModule);
6931
+ }
6145
6932
  }
6146
6933
 
6147
6934
  // feat-20260528-fxaa-post-processing: fxaa shader module is optional
@@ -6164,6 +6951,7 @@ async function buildReadyWebGPU(
6164
6951
  );
6165
6952
  if (!fxaaShaderResult.ok) throw fxaaShaderResult.error;
6166
6953
  fxaaModule = fxaaShaderResult.value;
6954
+ registerBuiltinFxaa(fxaaEntry.wgsl);
6167
6955
  }
6168
6956
 
6169
6957
  // feat-20260531-skybox-env-background M3 / w15: skybox shader module.
@@ -6295,6 +7083,7 @@ async function buildReadyWebGPU(
6295
7083
  ok(
6296
7084
  buildPbrPipelineLayouts(rhiDevice, {
6297
7085
  storageBuffer: storageBufferCapable,
7086
+ extendedLighting: extendedLightingShaderAvailable,
6298
7087
  projectorAvailable,
6299
7088
  }),
6300
7089
  ),
@@ -6307,6 +7096,7 @@ async function buildReadyWebGPU(
6307
7096
  const materialBglResult = ok(pbrLayouts.value.materialBgl);
6308
7097
  const meshArrayBglResult = ok(pbrLayouts.value.meshArrayBgl);
6309
7098
  const instancesBglResult = ok(pbrLayouts.value.instancesBgl);
7099
+ const probeInstancesBglResult = ok(pbrLayouts.value.probeInstancesBgl);
6310
7100
  const pipelineLayoutResult = ok(pbrLayouts.value.pipelineLayout);
6311
7101
 
6312
7102
  // feat-20260609-hdrp-cluster-fragment-ggx M4.5 / w36 (D-10 option A):
@@ -6316,15 +7106,15 @@ async function buildReadyWebGPU(
6316
7106
  // BGL at group(2) so an HDRP-variant PSO validates against the 7-slot
6317
7107
  // group(2) bindGroup that the record stage sets via hdrpClusterBindGroup.
6318
7108
  // Stays null when createBindGroupLayout / createPipelineLayout fails;
6319
- // selectPipelineLayoutForVariant gracefully falls back to pbrPipelineLayout
6320
- // (URP layout) so the manifest entry's URP variant WGSL still builds a
6321
- // valid PSO instead of hard-disabling the entire HDRP-variant build path.
7109
+ // selectPipelineLayoutForVariant propagates that null so the clustered
7110
+ // material remains unavailable instead of binding an incompatible layout.
6322
7111
  let hdrpPbrPipelineLayoutHandle: PipelineLayout | null = null;
7112
+ let hdrpProbePbrPipelineLayoutHandle: PipelineLayout | null = null;
6323
7113
  // The cluster layout is storage-buffer backed. On the WebGL2/fallback
6324
7114
  // capability tier its uniform-buffer projection exceeds the backend's
6325
7115
  // fragment-stage limit, and HDRP is not a valid route there anyway.
6326
7116
  if (storageBufferCapable) {
6327
- const hdrpUnifiedBglDesc = createHdrpBindGroupLayoutDescriptor(storageBufferCapable);
7117
+ const hdrpUnifiedBglDesc = createHdrpBindGroupLayoutDescriptor();
6328
7118
  const hdrpUnifiedBglRes = rhiDevice.createBindGroupLayout(hdrpUnifiedBglDesc);
6329
7119
  if (hdrpUnifiedBglRes.ok) {
6330
7120
  const hdrpPlRes = rhiDevice.createPipelineLayout({
@@ -6339,6 +7129,23 @@ async function buildReadyWebGPU(
6339
7129
  if (hdrpPlRes.ok) {
6340
7130
  hdrpPbrPipelineLayoutHandle = hdrpPlRes.value;
6341
7131
  }
7132
+ // ProbeBlendRecord adds a fragment-visible storage binding and is not a
7133
+ // legal downlevel extension of the uniform-fallback/WebGL2 layout. Keep
7134
+ // the HDRP probe PSO on the same capability gate as the standard probe
7135
+ // PSO; otherwise layout construction itself exceeds GLES' uniform
7136
+ // buffer limit before a probe draw can be rejected cleanly.
7137
+ if (storageBufferCapable) {
7138
+ const hdrpProbePlRes = rhiDevice.createPipelineLayout({
7139
+ label: 'hdrp-probe-pbr-pl',
7140
+ bindGroupLayouts: [
7141
+ viewBglResult.value,
7142
+ materialBglResult.value,
7143
+ hdrpUnifiedBglRes.value,
7144
+ probeInstancesBglResult.value,
7145
+ ],
7146
+ });
7147
+ if (hdrpProbePlRes.ok) hdrpProbePbrPipelineLayoutHandle = hdrpProbePlRes.value;
7148
+ }
6342
7149
  }
6343
7150
  }
6344
7151
 
@@ -6395,23 +7202,27 @@ async function buildReadyWebGPU(
6395
7202
  // bug-20260611-skin-pipeline-layout-mesh-array-bgl-2bindings: boot-time
6396
7203
  // build of the skin-variant PipelineLayout. Mirrors the HDRP block above
6397
7204
  // (D-2 / D-3 in plan-strategy): reuse view / material / instances BGLs
6398
- // from `pbrLayouts` and only create a 2-entry mesh-array BGL (binding 0
6399
- // meshes + binding 1 palette) so the `forgeax::pbr-skin` shader's
7205
+ // from `pbrLayouts` and only create a 3-entry mesh-array BGL (binding 0
7206
+ // meshes + binding 1 current palette + binding 2 previous palette) so the
7207
+ // `forgeax::pbr-skin` shader's
6400
7208
  // `@group(2) @binding(1) palette` declaration validates. Stays null when
6401
7209
  // createBindGroupLayout / createPipelineLayout fails;
6402
7210
  // selectPipelineLayoutForVariant returns null in that case (charter P3
6403
7211
  // explicit failure -- no silent fallback to URP layout, mirroring memory
6404
7212
  // anchor `hdrp-active-must-not-fallback-to-urp-pipeline`).
6405
7213
  let pbrSkinPipelineLayoutHandle: PipelineLayout | null = null;
6406
- // feat-20260611 R2 / M8 / w28: capture the 2-binding skin mesh-array BGL
7214
+ let pbrSkinProbePipelineLayoutHandle: PipelineLayout | null = null;
7215
+ // feat-20260611 R2 / M8 / w28: capture the 3-binding skin mesh-array BGL
6407
7216
  // produced by `buildPbrSkinLayouts` so the record stage can build a BG
6408
- // matching `pbr-skin-pl` (pipeline-layout BGL[2] is this 2-entry skin
7217
+ // matching `pbr-skin-pl` (pipeline-layout BGL[2] is this 3-entry skin
6409
7218
  // BGL, NOT the 1-entry `pbr-mesh-array-bgl`). The BGL handle stays null
6410
7219
  // when the skin pipeline layout itself failed to build, keeping the skin
6411
7220
  // path explicitly disabled (charter P3 — record-stage falls back to URP
6412
7221
  // path which is correct for non-skin entries; skin entries hit the
6413
7222
  // explicit-failure branch).
6414
7223
  let pbrSkinMeshBindGroupLayoutHandle: BindGroupLayout | null = null;
7224
+ let hdrpSkinPipelineLayoutHandle: PipelineLayout | null = null;
7225
+ let hdrpSkinMeshBindGroupLayoutHandle: BindGroupLayout | null = null;
6415
7226
  {
6416
7227
  const skinLayoutsResult = runShimSyncStep(
6417
7228
  () =>
@@ -6424,9 +7235,33 @@ async function buildReadyWebGPU(
6424
7235
  );
6425
7236
  if (skinLayoutsResult.ok) {
6426
7237
  pbrSkinPipelineLayoutHandle = skinLayoutsResult.value.pipelineLayout;
7238
+ pbrSkinProbePipelineLayoutHandle = skinLayoutsResult.value.probePipelineLayout;
6427
7239
  pbrSkinMeshBindGroupLayoutHandle = skinLayoutsResult.value.meshArrayBgl;
6428
7240
  }
6429
7241
  }
7242
+ // Keep HDRP skin construction on the same storage-capability gate as the
7243
+ // ordinary Standard cluster layout above. There is no uniform-backed
7244
+ // clustered skin route when storage buffers are unavailable.
7245
+ if (storageBufferCapable) {
7246
+ const skinBglResult = rhiDevice.createBindGroupLayout(
7247
+ createHdrpSkinBindGroupLayoutDescriptor(),
7248
+ );
7249
+ if (skinBglResult.ok) {
7250
+ const skinPlResult = rhiDevice.createPipelineLayout({
7251
+ label: 'hdrp-skin-pl',
7252
+ bindGroupLayouts: [
7253
+ viewBglResult.value,
7254
+ materialBglResult.value,
7255
+ skinBglResult.value,
7256
+ instancesBglResult.value,
7257
+ ],
7258
+ });
7259
+ if (skinPlResult.ok) {
7260
+ hdrpSkinPipelineLayoutHandle = skinPlResult.value;
7261
+ hdrpSkinMeshBindGroupLayoutHandle = skinBglResult.value;
7262
+ }
7263
+ }
7264
+ }
6430
7265
 
6431
7266
  // feat-20260612-skin-palette-per-frame-upload M1 / m1-2: skin palette
6432
7267
  // allocator. Replaces the prior 16320 B identity-seeded UBO stub (PR #353,
@@ -6767,54 +7602,6 @@ async function buildReadyWebGPU(
6767
7602
  // count >= 2 in createRenderer.ts — definition + this exposure).
6768
7603
  setGrowMeshSsboHook(meshSsboController.growMeshSsbo, meshSsboController.state);
6769
7604
 
6770
- // feat-20260519-light-casters-point-spot-pbr M3 / w20 (D-S1 + D-S2):
6771
- // PointLight + SpotLight std430 storage buffers. Header 16 B (count u32 +
6772
- // 12 B pad) + 4-slot first-slice cap (32 B / 48 B per slot). Sized
6773
- // generously so that the buffer.size never changes once allocated; per-
6774
- // frame writeBuffer overwrites the entire range (D-S6 full rewrite +
6775
- // charter P3 explicit failure: stable buffer.size means no orphan slot
6776
- // reuse).
6777
- const POINT_LIGHTS_BUFFER_BYTES =
6778
- LIGHT_ARRAY_HEADER_BYTES + POINT_LIGHT_STD430_BYTES * LIGHT_ARRAY_MAX_SLOTS;
6779
- const SPOT_LIGHTS_BUFFER_BYTES =
6780
- LIGHT_ARRAY_HEADER_BYTES + SPOT_LIGHT_STD430_BYTES * LIGHT_ARRAY_MAX_SLOTS;
6781
- // bug-20260610: WebGL2 fallback — pointLights / spotLights buffers must
6782
- // be UNIFORM-usage (the storage variant of the shader is gated by
6783
- // STORAGE_BUFFER_AVAILABLE; the uniform-fallback path expects uniform
6784
- // buffers). Without this swap, the buffer-creation succeeds (driver still
6785
- // accepts STORAGE flag), but `createBindGroup` validation rejects the
6786
- // mismatch: `Usage flags BufferUsages(COPY_DST | STORAGE) ... do not
6787
- // contain required usage flags BufferUsages(UNIFORM)`.
6788
- const LIGHTS_BUFFER_USAGE = storageBufferCapable
6789
- ? GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST
6790
- : GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST;
6791
- const pointLightsBufferResult = runShimSyncStep(
6792
- () =>
6793
- rhiDevice.createBuffer({
6794
- label: 'pbr-point-lights-ssbo',
6795
- size: POINT_LIGHTS_BUFFER_BYTES,
6796
- usage: LIGHTS_BUFFER_USAGE,
6797
- mappedAtCreation: false,
6798
- }),
6799
- 'webgpu-runtime-error',
6800
- 'createBuffer (point-lights ssbo) succeeded',
6801
- 'check device.limits.maxStorageBufferBindingSize',
6802
- );
6803
- if (!pointLightsBufferResult.ok) throw pointLightsBufferResult.error;
6804
- const spotLightsBufferResult = runShimSyncStep(
6805
- () =>
6806
- rhiDevice.createBuffer({
6807
- label: 'pbr-spot-lights-ssbo',
6808
- size: SPOT_LIGHTS_BUFFER_BYTES,
6809
- usage: LIGHTS_BUFFER_USAGE,
6810
- mappedAtCreation: false,
6811
- }),
6812
- 'webgpu-runtime-error',
6813
- 'createBuffer (spot-lights ssbo) succeeded',
6814
- 'check device.limits.maxStorageBufferBindingSize',
6815
- );
6816
- if (!spotLightsBufferResult.ok) throw spotLightsBufferResult.error;
6817
-
6818
7605
  // feat-20260513-instanced-mesh M3 (T-M3-2): identity-mat4 fallback
6819
7606
  // storage buffer. Single 64-byte storage buffer carrying one identity
6820
7607
  // mat4 column-major. Renderables without an `Instances` component bind
@@ -6994,6 +7781,225 @@ async function buildReadyWebGPU(
6994
7781
  );
6995
7782
  if (!fallbackTextureViewResult.ok) throw fallbackTextureViewResult.error;
6996
7783
 
7784
+ let iesModifierFallbackTexture: import('@forgeax/engine-rhi').Texture | undefined;
7785
+ let cookieModifierFallbackTexture: import('@forgeax/engine-rhi').Texture | undefined;
7786
+ let cookieModifierFallbackMatrixBuffer: Buffer | undefined;
7787
+ let iesModifierFallbackView: TextureView | undefined;
7788
+ let cookieModifierFallbackView: TextureView | undefined;
7789
+ let residentLtcLambertView: TextureView | undefined;
7790
+ let residentLtcGgxView: TextureView | undefined;
7791
+ const requireExtendedResource = <T>(value: T | undefined, name: string): T => {
7792
+ if (value === undefined) {
7793
+ throw new Error(`extended-lighting resource missing: ${name}`);
7794
+ }
7795
+ return value;
7796
+ };
7797
+
7798
+ if (extendedLightingShaderAvailable) {
7799
+ // The direct-light modifier ABI is a 32-layer array. It is created only
7800
+ // for the matching extended-lighting topology; the no-extension variant
7801
+ // has no IES/Cookie/LTC declarations or sampled resources at all.
7802
+ const modifierArrayLayers = 32;
7803
+ const iesModifierBytesPerRow = IES_SLICE_WIDTH * 2;
7804
+ const cookieModifierBytesPerRow = COOKIE_SLICE_SIZE * 4;
7805
+ const iesModifierFallbackData = new Uint8Array(
7806
+ iesModifierBytesPerRow * IES_SLICE_HEIGHT * modifierArrayLayers,
7807
+ );
7808
+ for (let offset = 0; offset < iesModifierFallbackData.length; offset += 2) {
7809
+ iesModifierFallbackData[offset] = 0;
7810
+ iesModifierFallbackData[offset + 1] = 0x3c;
7811
+ }
7812
+ const cookieModifierFallbackData = new Uint8Array(
7813
+ cookieModifierBytesPerRow * COOKIE_SLICE_SIZE * modifierArrayLayers,
7814
+ );
7815
+ cookieModifierFallbackData.fill(0xff);
7816
+ const iesModifierFallbackResult = runShimSyncStep(
7817
+ () =>
7818
+ rhiDevice.createTexture({
7819
+ label: 'fallback-ies-profile-array',
7820
+ size: {
7821
+ width: IES_SLICE_WIDTH,
7822
+ height: IES_SLICE_HEIGHT,
7823
+ depthOrArrayLayers: modifierArrayLayers,
7824
+ },
7825
+ mipLevelCount: 1,
7826
+ sampleCount: 1,
7827
+ dimension: '2d',
7828
+ format: 'r16float',
7829
+ usage: GPU_TEXTURE_USAGE_TEXTURE_BINDING | GPU_TEXTURE_USAGE_COPY_DST,
7830
+ viewFormats: [],
7831
+ textureBindingViewDimension: undefined,
7832
+ }),
7833
+ 'webgpu-runtime-error',
7834
+ 'createTexture (fallback IES array) succeeded',
7835
+ 'check array-layer and texture-binding limits',
7836
+ );
7837
+ if (!iesModifierFallbackResult.ok) throw iesModifierFallbackResult.error;
7838
+ const cookieModifierFallbackResult = runShimSyncStep(
7839
+ () =>
7840
+ rhiDevice.createTexture({
7841
+ label: 'fallback-cookie-array',
7842
+ size: {
7843
+ width: COOKIE_SLICE_SIZE,
7844
+ height: COOKIE_SLICE_SIZE,
7845
+ depthOrArrayLayers: modifierArrayLayers,
7846
+ },
7847
+ mipLevelCount: 1,
7848
+ sampleCount: 1,
7849
+ dimension: '2d',
7850
+ format: 'rgba8unorm',
7851
+ usage: GPU_TEXTURE_USAGE_TEXTURE_BINDING | GPU_TEXTURE_USAGE_COPY_DST,
7852
+ viewFormats: [],
7853
+ textureBindingViewDimension: undefined,
7854
+ }),
7855
+ 'webgpu-runtime-error',
7856
+ 'createTexture (fallback Cookie array) succeeded',
7857
+ 'check array-layer and texture-binding limits',
7858
+ );
7859
+ if (!cookieModifierFallbackResult.ok) throw cookieModifierFallbackResult.error;
7860
+ const iesWrite = runShimSyncStep(
7861
+ () =>
7862
+ queue.writeTexture(
7863
+ { texture: iesModifierFallbackResult.value, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
7864
+ iesModifierFallbackData,
7865
+ { offset: 0, bytesPerRow: iesModifierBytesPerRow, rowsPerImage: IES_SLICE_HEIGHT },
7866
+ {
7867
+ width: IES_SLICE_WIDTH,
7868
+ height: IES_SLICE_HEIGHT,
7869
+ depthOrArrayLayers: modifierArrayLayers,
7870
+ },
7871
+ ),
7872
+ 'queue-write-buffer-out-of-bounds',
7873
+ 'queue.writeTexture (fallback IES array) succeeded',
7874
+ 'verify modifier IES array row alignment',
7875
+ );
7876
+ if (!iesWrite.ok) throw iesWrite.error;
7877
+ const cookieWrite = runShimSyncStep(
7878
+ () =>
7879
+ queue.writeTexture(
7880
+ {
7881
+ texture: cookieModifierFallbackResult.value,
7882
+ mipLevel: 0,
7883
+ origin: { x: 0, y: 0, z: 0 },
7884
+ },
7885
+ cookieModifierFallbackData,
7886
+ { offset: 0, bytesPerRow: cookieModifierBytesPerRow, rowsPerImage: COOKIE_SLICE_SIZE },
7887
+ {
7888
+ width: COOKIE_SLICE_SIZE,
7889
+ height: COOKIE_SLICE_SIZE,
7890
+ depthOrArrayLayers: modifierArrayLayers,
7891
+ },
7892
+ ),
7893
+ 'queue-write-buffer-out-of-bounds',
7894
+ 'queue.writeTexture (fallback Cookie array) succeeded',
7895
+ 'verify modifier Cookie array row alignment',
7896
+ );
7897
+ if (!cookieWrite.ok) throw cookieWrite.error;
7898
+ const iesModifierFallbackViewResult = runShimSyncStep(
7899
+ () =>
7900
+ rhiDevice.createTextureView(iesModifierFallbackResult.value, {
7901
+ label: 'fallback-ies-profile-array-view',
7902
+ dimension: '2d-array',
7903
+ arrayLayerCount: modifierArrayLayers,
7904
+ }),
7905
+ 'webgpu-runtime-error',
7906
+ 'createTextureView (fallback IES array) succeeded',
7907
+ 'verify 2d-array view matches modifier BGL',
7908
+ );
7909
+ if (!iesModifierFallbackViewResult.ok) throw iesModifierFallbackViewResult.error;
7910
+ iesModifierFallbackTexture = iesModifierFallbackResult.value;
7911
+ iesModifierFallbackView = iesModifierFallbackViewResult.value;
7912
+ const cookieModifierFallbackViewResult = runShimSyncStep(
7913
+ () =>
7914
+ rhiDevice.createTextureView(cookieModifierFallbackResult.value, {
7915
+ label: 'fallback-cookie-array-view',
7916
+ dimension: '2d-array',
7917
+ arrayLayerCount: modifierArrayLayers,
7918
+ }),
7919
+ 'webgpu-runtime-error',
7920
+ 'createTextureView (fallback Cookie array) succeeded',
7921
+ 'verify 2d-array view matches modifier BGL',
7922
+ );
7923
+ if (!cookieModifierFallbackViewResult.ok) throw cookieModifierFallbackViewResult.error;
7924
+ cookieModifierFallbackTexture = cookieModifierFallbackResult.value;
7925
+ cookieModifierFallbackView = cookieModifierFallbackViewResult.value;
7926
+
7927
+ const cookieMatrixFallbackData = createCookieProjectionMatrixData(modifierArrayLayers);
7928
+ const cookieMatrixBufferResult = runShimSyncStep(
7929
+ () =>
7930
+ rhiDevice.createBuffer({
7931
+ label: 'fallback-cookie-matrices',
7932
+ size: COOKIE_MATRIX_BYTES,
7933
+ usage: GPU_BUFFER_USAGE_COPY_DST | GPU_BUFFER_USAGE_UNIFORM,
7934
+ mappedAtCreation: false,
7935
+ }),
7936
+ 'webgpu-runtime-error',
7937
+ 'createBuffer (fallback Cookie matrices) succeeded',
7938
+ 'check uniform-buffer size and usage support',
7939
+ );
7940
+ if (!cookieMatrixBufferResult.ok) throw cookieMatrixBufferResult.error;
7941
+ const cookieMatrixWrite = runShimSyncStep(
7942
+ () =>
7943
+ rhiDevice.queue.writeBuffer(cookieMatrixBufferResult.value, 0, cookieMatrixFallbackData),
7944
+ 'queue-write-buffer-out-of-bounds',
7945
+ 'queue.writeBuffer (fallback Cookie matrices) succeeded',
7946
+ 'verify Cookie matrix payload alignment',
7947
+ );
7948
+ if (!cookieMatrixWrite.ok) throw cookieMatrixWrite.error;
7949
+ cookieModifierFallbackMatrixBuffer = cookieMatrixBufferResult.value;
7950
+
7951
+ const createResidentLtcView = (label: string, table: Uint16Array): TextureView => {
7952
+ const textureResult = runShimSyncStep(
7953
+ () =>
7954
+ rhiDevice.createTexture({
7955
+ label,
7956
+ size: { width: LTC_TABLE_WIDTH, height: LTC_TABLE_HEIGHT, depthOrArrayLayers: 1 },
7957
+ mipLevelCount: 1,
7958
+ sampleCount: 1,
7959
+ dimension: '2d',
7960
+ format: 'rgba16float',
7961
+ usage: GPU_TEXTURE_USAGE_TEXTURE_BINDING | GPU_TEXTURE_USAGE_COPY_DST,
7962
+ viewFormats: [],
7963
+ textureBindingViewDimension: undefined,
7964
+ }),
7965
+ 'webgpu-runtime-error',
7966
+ `createTexture (${label}) succeeded`,
7967
+ 'check resident LTC table texture capabilities',
7968
+ );
7969
+ if (!textureResult.ok) throw textureResult.error;
7970
+ const uploadResult = runShimSyncStep(
7971
+ () =>
7972
+ queue.writeTexture(
7973
+ { texture: textureResult.value, mipLevel: 0, origin: { x: 0, y: 0, z: 0 } },
7974
+ new Uint8Array(table.buffer, table.byteOffset, table.byteLength),
7975
+ { offset: 0, bytesPerRow: LTC_TABLE_WIDTH * 8, rowsPerImage: LTC_TABLE_HEIGHT },
7976
+ { width: LTC_TABLE_WIDTH, height: LTC_TABLE_HEIGHT, depthOrArrayLayers: 1 },
7977
+ ),
7978
+ 'queue-write-buffer-out-of-bounds',
7979
+ `queue.writeTexture (${label}) succeeded`,
7980
+ 'verify resident LTC table row alignment',
7981
+ );
7982
+ if (!uploadResult.ok) throw uploadResult.error;
7983
+ const viewResult = runShimSyncStep(
7984
+ () =>
7985
+ rhiDevice.createTextureView(textureResult.value, {
7986
+ label: `${label}-view`,
7987
+ dimension: '2d',
7988
+ }),
7989
+ 'webgpu-runtime-error',
7990
+ `createTextureView (${label}) succeeded`,
7991
+ 'verify resident LTC view matches shader binding',
7992
+ );
7993
+ if (!viewResult.ok) throw viewResult.error;
7994
+ return viewResult.value;
7995
+ };
7996
+ residentLtcLambertView = createResidentLtcView(
7997
+ 'extended-lighting-ltc-lambert',
7998
+ LTC_TABLES.lambert,
7999
+ );
8000
+ residentLtcGgxView = createResidentLtcView('extended-lighting-ltc-ggx', LTC_TABLES.ggx);
8001
+ }
8002
+
6997
8003
  // Normal-slot fallback: 1x1 RGBA8 (128, 128, 255, 255). pbr.wgsl decodes
6998
8004
  // sample.rg * 2 - 1 + z = sqrt(1 - x^2 - y^2), so RG=(128,128)=0.5 maps
6999
8005
  // to tangent (0, 0, 1) -- zero perturbation when normalTexture is absent.
@@ -7303,17 +8309,20 @@ async function buildReadyWebGPU(
7303
8309
  fragmentEntryPoint?: string;
7304
8310
  layout?: unknown;
7305
8311
  label?: string;
8312
+ group2Contract?: PipelineGroup2Contract;
7306
8313
  }
7307
8314
  >();
7308
8315
  if (unlitModule !== null)
7309
8316
  shaderModuleMap.set('forgeax::default-unlit', {
7310
8317
  vertex: unlitModule,
7311
8318
  fragment: unlitModule,
8319
+ ...(unlitGroup2Contract === undefined ? {} : { group2Contract: unlitGroup2Contract }),
7312
8320
  });
7313
8321
  if (pbrModule !== null)
7314
8322
  shaderModuleMap.set('forgeax::default-standard-pbr', {
7315
8323
  vertex: pbrModule,
7316
8324
  fragment: pbrModule,
8325
+ ...(pbrGroup2Contract === undefined ? {} : { group2Contract: pbrGroup2Contract }),
7317
8326
  });
7318
8327
  if (spriteModule !== null)
7319
8328
  // feat-20260625-refactor-sprite-as-transparent-mesh M3 / w14 (D-7):
@@ -7325,6 +8334,7 @@ async function buildReadyWebGPU(
7325
8334
  shaderModuleMap.set('forgeax::sprite', {
7326
8335
  vertex: spriteModule,
7327
8336
  fragment: spriteModule,
8337
+ ...(spriteGroup2Contract === undefined ? {} : { group2Contract: spriteGroup2Contract }),
7328
8338
  });
7329
8339
  // feat-20260624-sprite-lit-shading-model-pure-2d-lighting M1' / t7:
7330
8340
  // sprite-lit shader registers under canonical `forgeax::sprite-lit` —
@@ -7333,6 +8343,7 @@ async function buildReadyWebGPU(
7333
8343
  shaderModuleMap.set('forgeax::sprite-lit', {
7334
8344
  vertex: spriteLitModule,
7335
8345
  fragment: spriteLitModule,
8346
+ ...(spriteLitGroup2Contract === undefined ? {} : { group2Contract: spriteLitGroup2Contract }),
7336
8347
  });
7337
8348
  // M2-T4: fullscreen-post shader modules (tonemap + skybox) are registered
7338
8349
  // in their respective blocks after pipeline layout creation — layout is
@@ -7434,7 +8445,6 @@ async function buildReadyWebGPU(
7434
8445
  ...runtimeSpecConstTable,
7435
8446
  ...runtimeLinearLdrMaterialSpecTable,
7436
8447
  ];
7437
-
7438
8448
  // Boot-time pre-warm: build SPEC_CONST entries whose shader modules are
7439
8449
  // compiled. Entries referencing a missing module are silently skipped
7440
8450
  // (the empty-manifest path — D-3). Each build failure for an available
@@ -7467,7 +8477,7 @@ async function buildReadyWebGPU(
7467
8477
  // `makeWebGPURenderer` scope) from the prewarmed `pipelineCache` for
7468
8478
  // SPEC_CONST entries whose `variantSet !== undefined`. The URP record
7469
8479
  // path queries `getMaterialShaderPipeline(...)` keyed off
7470
- // `cacheKeyOf(spec)` with `variantSet=URP_PBR_VARIANT_SET`; both caches
8480
+ // `cacheKeyOf(spec)` with the non-clustered boot variant; both caches
7471
8481
  // generate keys via the same `cacheKeyOf` so the lookup hits the
7472
8482
  // boot-time prewarmed PSO instead of triggering a 1-frame async-compile
7473
8483
  // skip-draw. Seeding only variantSet-bearing entries keeps the
@@ -7477,11 +8487,12 @@ async function buildReadyWebGPU(
7477
8487
  // explicit instead of collapsing into one map (charter P3).
7478
8488
  for (const spec of runtimeSpecConstTable) {
7479
8489
  if (spec.shader.variantSet === undefined) continue;
7480
- if (shaderModuleMap.get(spec.shader.id) === undefined) continue;
8490
+ const modules = shaderModuleMap.get(spec.shader.id);
8491
+ if (modules === undefined) continue;
7481
8492
  const key = cacheKeyOf(spec);
7482
8493
  const built = pipelineCache.get(key);
7483
- if (built !== undefined) {
7484
- seedMaterialShaderPipelineCache(key, built as RenderPipeline);
8494
+ if (built !== undefined && modules.group2Contract !== undefined) {
8495
+ seedMaterialShaderPipelineCache(key, built as RenderPipeline, modules.group2Contract);
7485
8496
  }
7486
8497
  }
7487
8498
 
@@ -7489,11 +8500,12 @@ async function buildReadyWebGPU(
7489
8500
  // swap-chain view target. Seed every material entry, including no-variant
7490
8501
  // unlit, because tonemap=none reaches this cache directly on frame one.
7491
8502
  for (const spec of runtimeLinearLdrMaterialSpecTable) {
7492
- if (shaderModuleMap.get(spec.shader.id) === undefined) continue;
8503
+ const modules = shaderModuleMap.get(spec.shader.id);
8504
+ if (modules === undefined) continue;
7493
8505
  const key = cacheKeyOf(spec);
7494
8506
  const built = pipelineCache.get(key);
7495
- if (built !== undefined) {
7496
- seedMaterialShaderPipelineCache(key, built as RenderPipeline);
8507
+ if (built !== undefined && modules.group2Contract !== undefined) {
8508
+ seedMaterialShaderPipelineCache(key, built as RenderPipeline, modules.group2Contract);
7497
8509
  }
7498
8510
  }
7499
8511
  }
@@ -7701,15 +8713,15 @@ async function buildReadyWebGPU(
7701
8713
 
7702
8714
  // feat-20260528-fxaa-post-processing M2 / w10: FXAA pipeline prebuilt.
7703
8715
  // When the manifest contains the fxaa entry (rgb2luma marker, D-5),
7704
- // construct the 2-entry BGL (texture + sampler, no UBO per D-2),
8716
+ // construct the 3-entry BGL (texture + sampler + output-policy UBO),
7705
8717
  // pipeline layout, fullscreen render pipeline (vertex = fullscreen
7706
8718
  // triangle from fxaa.wgsl, fragment = FXAA 3.11 algorithm), and
7707
8719
  // linear clamp-to-edge sampler. Mirrors the tonemap pipeline
7708
8720
  // construction pattern directly above.
7709
8721
  if (fxaaModule !== null) {
7710
- // FXAA BindGroupLayout: 2 entries (texture + sampler, no UBO).
7711
- // D-2: the fxaa.wgsl fragment stage declares @binding(0) texture_2d<f32>
7712
- // + @binding(1) sampler.
8722
+ // FXAA BindGroupLayout: 3 entries (texture + sampler + output-policy UBO).
8723
+ // The fxaa.wgsl fragment stage declares @binding(0) texture_2d<f32>,
8724
+ // @binding(1) sampler, and @binding(2) FxaaParams.
7713
8725
  const fxaaBglResult = runShimSyncStep(
7714
8726
  () =>
7715
8727
  rhiDevice.createBindGroupLayout({
@@ -7725,6 +8737,11 @@ async function buildReadyWebGPU(
7725
8737
  visibility: GPU_SHADER_STAGE_FRAGMENT,
7726
8738
  sampler: { type: 'filtering' },
7727
8739
  },
8740
+ {
8741
+ binding: 2,
8742
+ visibility: GPU_SHADER_STAGE_FRAGMENT,
8743
+ buffer: { type: 'uniform' },
8744
+ },
7728
8745
  ],
7729
8746
  }),
7730
8747
  'webgpu-runtime-error',
@@ -8760,6 +9777,7 @@ async function buildReadyWebGPU(
8760
9777
  }
8761
9778
 
8762
9779
  const pipelineState: PipelineState = {
9780
+ device: rhiDevice,
8763
9781
  // feat-20260518-pbr-direct-lighting-mvp M5 / w22.10 (AC-06 dual->triple
8764
9782
  // pipeline + D-2 + D-10): three distinct GPU render-pipeline handles
8765
9783
  // backed by 2 distinct shader modules (unlit + pbr) x 2 vertex stride
@@ -8802,6 +9820,8 @@ async function buildReadyWebGPU(
8802
9820
  // standard pipeline fields above).
8803
9821
  pbrPipelineLayout:
8804
9822
  unlitModule !== null && pbrModule !== null ? pipelineLayoutResult.value : null,
9823
+ pbrProbePipelineLayout:
9824
+ unlitModule !== null && pbrModule !== null ? pbrLayouts.value.probePipelineLayout : null,
8805
9825
  // feat-20260609-hdrp-cluster-fragment-ggx M4.5 / w36 (D-10 option A):
8806
9826
  // HDRP-variant PipelineLayout (4-BGL chain with HDRP unified 7-slot
8807
9827
  // group(2) BGL substituted for the URP 1-slot mesh-array BGL). Built
@@ -8811,10 +9831,12 @@ async function buildReadyWebGPU(
8811
9831
  // `selectPipelineLayoutForVariant` based on the caller's variantSet.
8812
9832
  hdrpPbrPipelineLayout:
8813
9833
  unlitModule !== null && pbrModule !== null ? hdrpPbrPipelineLayoutHandle : null,
9834
+ hdrpProbePbrPipelineLayout:
9835
+ unlitModule !== null && pbrModule !== null ? hdrpProbePbrPipelineLayoutHandle : null,
8814
9836
  hdrpClusterMembershipPipeline,
8815
9837
  hdrpClusterMembershipBindGroupLayout,
8816
9838
  // bug-20260611-skin-pipeline-layout: skin-variant PipelineLayout (4-BGL
8817
- // chain with 2-entry mesh-array BGL substituted for the standard 1-entry
9839
+ // chain with 3-entry mesh-array BGL substituted for the standard 1-entry
8818
9840
  // mesh-array BGL). Built at boot above (see pbrSkinPipelineLayoutHandle);
8819
9841
  // gated on the same unlit/pbr module presence so Camera-only and empty-
8820
9842
  // manifest paths see null. Selected per call by
@@ -8822,17 +9844,23 @@ async function buildReadyWebGPU(
8822
9844
  // `LayoutKind = 'pbr-skin'`.
8823
9845
  pbrSkinPipelineLayout:
8824
9846
  unlitModule !== null && pbrModule !== null ? pbrSkinPipelineLayoutHandle : null,
9847
+ pbrSkinProbePipelineLayout:
9848
+ unlitModule !== null && pbrModule !== null ? pbrSkinProbePipelineLayoutHandle : null,
8825
9849
  // feat-20260611 R2 / M8 / w28 (IS-14): record-stage skin BG plumbing.
8826
9850
  // Mirror the gating used by `pbrSkinPipelineLayout` -- both fields
8827
9851
  // come from the same `buildPbrSkinLayouts` call so they live or die
8828
9852
  // together. Surfaced on PipelineState so render-system-record can
8829
- // build a 2-binding BG matching `pbr-skin-pl` (was: 1-binding
9853
+ // build a 3-binding BG matching `pbr-skin-pl` (was: 1-binding
8830
9854
  // `pbr-mesh-bg` + `pbr-mesh-array-bgl`, which the device rejected
8831
9855
  // every frame with `Bind group layout pbr-skin-mesh-array-bgl ... does
8832
9856
  // not match layout pbr-mesh-array-bgl` + Invalid CommandBuffer +
8833
9857
  // queue-submit-failed).
8834
9858
  pbrSkinMeshBindGroupLayout:
8835
9859
  unlitModule !== null && pbrModule !== null ? pbrSkinMeshBindGroupLayoutHandle : null,
9860
+ hdrpSkinPipelineLayout:
9861
+ unlitModule !== null && pbrModule !== null ? hdrpSkinPipelineLayoutHandle : null,
9862
+ hdrpSkinMeshBindGroupLayout:
9863
+ unlitModule !== null && pbrModule !== null ? hdrpSkinMeshBindGroupLayoutHandle : null,
8836
9864
  // feat-20260612-skin-palette-per-frame-upload M1 / m1-2: animator-ready
8837
9865
  // skin-palette allocator (replaces the prior identity-buffer stub).
8838
9866
  // Same gating as `pbrSkinPipelineLayout` -- `null` when the skin
@@ -8844,6 +9872,7 @@ async function buildReadyWebGPU(
8844
9872
  colorAttachmentFormat: swapChainFormats.view,
8845
9873
  surfaceProfile: surfaceViewFormats ? 'dual-view' : 'raw-only',
8846
9874
  viewBindGroupLayout: viewBglResult.value,
9875
+ extendedLightingAvailable: extendedLightingShaderAvailable,
8847
9876
  projectorAvailable,
8848
9877
  materialBindGroupLayout: materialBglResult.value,
8849
9878
  meshBindGroupLayout: meshArrayBglResult.value,
@@ -8859,9 +9888,8 @@ async function buildReadyWebGPU(
8859
9888
  // `{ buffer: Buffer; sizeInBytes: number }` in T-M2-06 to match.
8860
9889
  materialUniformBuffer: meshSsboState.material,
8861
9890
  meshStorageBuffer: meshSsboState.mesh,
8862
- pointLightsBuffer: pointLightsBufferResult.value,
8863
- spotLightsBuffer: spotLightsBufferResult.value,
8864
9891
  instancesBindGroupLayout: instancesBglResult.value,
9892
+ probeInstancesBindGroupLayout: probeInstancesBglResult.value,
8865
9893
  identityInstanceBuffer: identityInstanceResult.value,
8866
9894
  defaultSampler: defaultSamplerResult.value,
8867
9895
  nearestSampler: nearestSamplerResult.value,
@@ -8884,6 +9912,23 @@ async function buildReadyWebGPU(
8884
9912
  // feat-20260520-directional-light-shadow-mapping M2 / w14 (D-1):
8885
9913
  // 1x1 depth32float fallback bound at viewBindGroup binding(3).
8886
9914
  shadowFallbackTextureView: shadowFallbackViewResult.value,
9915
+ ...(extendedLightingShaderAvailable
9916
+ ? {
9917
+ iesProfileTexture: requireExtendedResource(iesModifierFallbackTexture, 'ies texture'),
9918
+ cookieTexture: requireExtendedResource(cookieModifierFallbackTexture, 'cookie texture'),
9919
+ cookieMatrixBuffer: requireExtendedResource(
9920
+ cookieModifierFallbackMatrixBuffer,
9921
+ 'cookie matrix buffer',
9922
+ ),
9923
+ iesProfileTextureView: requireExtendedResource(iesModifierFallbackView, 'ies view'),
9924
+ cookieTextureView: requireExtendedResource(cookieModifierFallbackView, 'cookie view'),
9925
+ ltcLambertTextureView: requireExtendedResource(
9926
+ residentLtcLambertView,
9927
+ 'ltc lambert view',
9928
+ ),
9929
+ ltcGgxTextureView: requireExtendedResource(residentLtcGgxView, 'ltc ggx view'),
9930
+ }
9931
+ : {}),
8887
9932
  // feat-20260612-point-light-shadows-urp-hdrp Round-2 F-1: 1x1x6
8888
9933
  // depth32float cube_array fallback bound at viewBindGroup binding(5)
8889
9934
  // when no PointLightShadow snapshots are active. Always-present