@forgeax/engine-render 0.1.21 → 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 (426) hide show
  1. package/README.md +150 -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.map +1 -1
  5. package/dist/assembly/factory.d.ts +14 -6
  6. package/dist/assembly/factory.d.ts.map +1 -1
  7. package/dist/assembly/host-contract.d.ts +10 -0
  8. package/dist/assembly/host-contract.d.ts.map +1 -1
  9. package/dist/authoring.mjs +1 -1
  10. package/dist/{chunk-PCHBPYML.mjs → chunk-4L3P6DT5.mjs} +2 -2
  11. package/dist/chunk-4L3P6DT5.mjs.map +1 -0
  12. package/dist/{chunk-VFPLTKCU.mjs → chunk-4NPUASZR.mjs} +15 -4
  13. package/dist/chunk-4NPUASZR.mjs.map +1 -0
  14. package/dist/chunk-DIYW3M5D.mjs +5085 -0
  15. package/dist/chunk-DIYW3M5D.mjs.map +1 -0
  16. package/dist/chunk-FBC4R44J.mjs +1003 -0
  17. package/dist/chunk-FBC4R44J.mjs.map +1 -0
  18. package/dist/{chunk-FS2KNUQH.mjs → chunk-FF7K3FSO.mjs} +253 -489
  19. package/dist/chunk-FF7K3FSO.mjs.map +1 -0
  20. package/dist/chunk-HZUPSYW7.mjs +1104 -0
  21. package/dist/chunk-HZUPSYW7.mjs.map +1 -0
  22. package/dist/{chunk-6OPXDQ2E.mjs → chunk-QODA5UKU.mjs} +33 -294
  23. package/dist/chunk-QODA5UKU.mjs.map +1 -0
  24. package/dist/chunk-YALPS44A.mjs +296 -0
  25. package/dist/chunk-YALPS44A.mjs.map +1 -0
  26. package/dist/{chunk-5YADUCA7.mjs → chunk-ZUWKAZ2I.mjs} +585 -991
  27. package/dist/chunk-ZUWKAZ2I.mjs.map +1 -0
  28. package/dist/cluster-binner.d.ts +5 -0
  29. package/dist/cluster-binner.d.ts.map +1 -1
  30. package/dist/components/index.d.ts +3 -0
  31. package/dist/components/index.d.ts.map +1 -1
  32. package/dist/components/light-helpers.d.ts +4 -0
  33. package/dist/components/light-helpers.d.ts.map +1 -1
  34. package/dist/components/light-probe.d.ts +9 -0
  35. package/dist/components/light-probe.d.ts.map +1 -0
  36. package/dist/components/rect-area-light.d.ts +12 -0
  37. package/dist/components/rect-area-light.d.ts.map +1 -0
  38. package/dist/components/scene-instance.d.ts +4 -0
  39. package/dist/components/scene-instance.d.ts.map +1 -1
  40. package/dist/components/spot-light.d.ts +3 -0
  41. package/dist/components/spot-light.d.ts.map +1 -1
  42. package/dist/construct-renderer.mjs +10437 -10044
  43. package/dist/construct-renderer.mjs.map +1 -1
  44. package/dist/device/gpu-residency.d.ts +8 -1
  45. package/dist/device/gpu-residency.d.ts.map +1 -1
  46. package/dist/device/lifecycle-transaction.d.ts +2 -0
  47. package/dist/device/lifecycle-transaction.d.ts.map +1 -0
  48. package/dist/environment/lifecycle.d.ts.map +1 -1
  49. package/dist/errors/gpu-driven.d.ts +26 -0
  50. package/dist/errors/gpu-driven.d.ts.map +1 -0
  51. package/dist/errors/index.d.ts +1 -0
  52. package/dist/errors/index.d.ts.map +1 -1
  53. package/dist/errors/recover.d.ts +9 -1
  54. package/dist/errors/recover.d.ts.map +1 -1
  55. package/dist/errors/render.d.ts +26 -1
  56. package/dist/errors/render.d.ts.map +1 -1
  57. package/dist/extract/gpu-driven.d.ts +50 -0
  58. package/dist/extract/gpu-driven.d.ts.map +1 -0
  59. package/dist/extract/visibility.d.ts.map +1 -1
  60. package/dist/gpu-driven/batch-topology.d.ts +34 -0
  61. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  62. package/dist/gpu-driven/prepared-draw.d.ts +63 -0
  63. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -0
  64. package/dist/gpu-driven/production-raster.d.ts +98 -2
  65. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  66. package/dist/gpu-driven/view-gpu.d.ts +64 -1
  67. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  68. package/dist/gpu-scene-schema.d.ts +14 -0
  69. package/dist/gpu-scene-schema.d.ts.map +1 -1
  70. package/dist/gpu-scene.d.ts +8 -1
  71. package/dist/gpu-scene.d.ts.map +1 -1
  72. package/dist/gpu-usage.d.ts +0 -1
  73. package/dist/gpu-usage.d.ts.map +1 -1
  74. package/dist/hdrp-buffers.d.ts +3 -3
  75. package/dist/hdrp-buffers.d.ts.map +1 -1
  76. package/dist/index.d.ts +12 -1
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.mjs +90 -5
  79. package/dist/index.mjs.map +1 -1
  80. package/dist/inspection-types.d.ts +163 -1
  81. package/dist/inspection-types.d.ts.map +1 -1
  82. package/dist/internal.d.ts +9 -0
  83. package/dist/internal.d.ts.map +1 -1
  84. package/dist/internal.mjs +23 -2
  85. package/dist/internal.mjs.map +1 -1
  86. package/dist/light-buffer-layout.d.ts +58 -149
  87. package/dist/light-buffer-layout.d.ts.map +1 -1
  88. package/dist/materials.d.ts.map +1 -1
  89. package/dist/pbr-pipeline.d.ts +26 -12
  90. package/dist/pbr-pipeline.d.ts.map +1 -1
  91. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  92. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -1
  93. package/dist/pipeline/standard-lighting/light-frame.d.ts +3 -3
  94. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -1
  95. package/dist/pipeline/standard-lighting/topology.d.ts +1 -1
  96. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -1
  97. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -1
  98. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  99. package/dist/pipeline-builder.d.ts +2 -0
  100. package/dist/pipeline-builder.d.ts.map +1 -1
  101. package/dist/pipeline-spec.d.ts +1 -1
  102. package/dist/pipeline-spec.d.ts.map +1 -1
  103. package/dist/plugin.d.ts.map +1 -1
  104. package/dist/prepare/extended-lighting/ltc-resources.d.ts +31 -0
  105. package/dist/prepare/extended-lighting/ltc-resources.d.ts.map +1 -0
  106. package/dist/prepare/extended-lighting/resources.d.ts +72 -0
  107. package/dist/prepare/extended-lighting/resources.d.ts.map +1 -0
  108. package/dist/prepare/extended-lighting/spot-modifiers.d.ts +42 -0
  109. package/dist/prepare/extended-lighting/spot-modifiers.d.ts.map +1 -0
  110. package/dist/prepare/extended-lighting/state.d.ts +24 -0
  111. package/dist/prepare/extended-lighting/state.d.ts.map +1 -0
  112. package/dist/record/frame-lighting.d.ts +8 -6
  113. package/dist/record/frame-lighting.d.ts.map +1 -1
  114. package/dist/record/frame-snapshot.d.ts +50 -10
  115. package/dist/record/frame-snapshot.d.ts.map +1 -1
  116. package/dist/record/frame.d.ts +7 -3
  117. package/dist/record/frame.d.ts.map +1 -1
  118. package/dist/record/gpu-timing.d.ts +19 -7
  119. package/dist/record/gpu-timing.d.ts.map +1 -1
  120. package/dist/record/helpers.d.ts +0 -20
  121. package/dist/record/helpers.d.ts.map +1 -1
  122. package/dist/record/main-pass-geometry.d.ts +6 -1
  123. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  124. package/dist/record/main-pass-material.d.ts +3 -1
  125. package/dist/record/main-pass-material.d.ts.map +1 -1
  126. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  127. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  128. package/dist/record/render-context.d.ts +57 -6
  129. package/dist/record/render-context.d.ts.map +1 -1
  130. package/dist/record/shadow-pass.d.ts.map +1 -1
  131. package/dist/record/typed-frame-graph.d.ts +49 -2
  132. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  133. package/dist/record/view-ubo.d.ts +1 -2
  134. package/dist/record/view-ubo.d.ts.map +1 -1
  135. package/dist/reflection/inspection.d.ts +10 -0
  136. package/dist/reflection/inspection.d.ts.map +1 -0
  137. package/dist/reflection/projection.d.ts +49 -0
  138. package/dist/reflection/projection.d.ts.map +1 -1
  139. package/dist/reflection/record-owner.d.ts +60 -0
  140. package/dist/reflection/record-owner.d.ts.map +1 -0
  141. package/dist/render-contract.d.ts +14 -8
  142. package/dist/render-contract.d.ts.map +1 -1
  143. package/dist/render-pipeline.d.ts +7 -0
  144. package/dist/render-pipeline.d.ts.map +1 -1
  145. package/dist/render-system-extract.d.ts +104 -16
  146. package/dist/render-system-extract.d.ts.map +1 -1
  147. package/dist/render-system.d.ts +12 -1
  148. package/dist/render-system.d.ts.map +1 -1
  149. package/dist/scene/probe-blend-record.d.ts +22 -0
  150. package/dist/scene/probe-blend-record.d.ts.map +1 -0
  151. package/dist/scene/probe-blend.d.ts +142 -0
  152. package/dist/scene/probe-blend.d.ts.map +1 -0
  153. package/dist/scene/render-scene-types.d.ts +0 -6
  154. package/dist/scene/render-scene-types.d.ts.map +1 -1
  155. package/dist/scene/render-scene.d.ts +103 -9
  156. package/dist/scene/render-scene.d.ts.map +1 -1
  157. package/dist/scene/visibility/bounds.d.ts +20 -0
  158. package/dist/scene/visibility/bounds.d.ts.map +1 -0
  159. package/dist/scene/visibility/budget.d.ts +16 -0
  160. package/dist/scene/visibility/budget.d.ts.map +1 -0
  161. package/dist/scene/visibility/facet.d.ts +73 -0
  162. package/dist/scene/visibility/facet.d.ts.map +1 -0
  163. package/dist/scene/visibility/gpu-frame-timing.d.ts +41 -0
  164. package/dist/scene/visibility/gpu-frame-timing.d.ts.map +1 -0
  165. package/dist/scene/visibility/gpu-lod.d.ts +46 -0
  166. package/dist/scene/visibility/gpu-lod.d.ts.map +1 -0
  167. package/dist/scene/visibility/inspection.d.ts +132 -0
  168. package/dist/scene/visibility/inspection.d.ts.map +1 -0
  169. package/dist/scene/visibility/lod-selector.d.ts +25 -0
  170. package/dist/scene/visibility/lod-selector.d.ts.map +1 -0
  171. package/dist/scene/visibility/occlusion-confidence.d.ts +81 -0
  172. package/dist/scene/visibility/occlusion-confidence.d.ts.map +1 -0
  173. package/dist/scene/visibility/occlusion-pass.d.ts +57 -0
  174. package/dist/scene/visibility/occlusion-pass.d.ts.map +1 -0
  175. package/dist/scene/visibility/occlusion-query-pool.d.ts +57 -0
  176. package/dist/scene/visibility/occlusion-query-pool.d.ts.map +1 -0
  177. package/dist/scene/visibility/occlusion-runtime.d.ts +96 -0
  178. package/dist/scene/visibility/occlusion-runtime.d.ts.map +1 -0
  179. package/dist/scene/visibility/types.d.ts +34 -0
  180. package/dist/scene/visibility/types.d.ts.map +1 -0
  181. package/dist/ssr/admission.d.ts +122 -0
  182. package/dist/ssr/admission.d.ts.map +1 -0
  183. package/dist/ssr/composition.d.ts +32 -0
  184. package/dist/ssr/composition.d.ts.map +1 -0
  185. package/dist/ssr/errors.d.ts +39 -0
  186. package/dist/ssr/errors.d.ts.map +1 -0
  187. package/dist/ssr/identity.d.ts +13 -0
  188. package/dist/ssr/identity.d.ts.map +1 -0
  189. package/dist/systems/skin-palette-allocator.d.ts +1 -1
  190. package/dist/temporal/frame-coordinator.d.ts +13 -0
  191. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  192. package/dist/temporal/frame.d.ts +2 -0
  193. package/dist/temporal/frame.d.ts.map +1 -1
  194. package/dist/temporal/index.mjs +6 -5
  195. package/dist/temporal/view.d.ts +7 -0
  196. package/dist/temporal/view.d.ts.map +1 -1
  197. package/dist/tilemap-chunk-extract-system.d.ts +1 -1
  198. package/dist/typed-render-graph-primitives.d.ts +4 -0
  199. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  200. package/dist/volume/passes.d.ts +8 -4
  201. package/dist/volume/passes.d.ts.map +1 -1
  202. package/package.json +20 -20
  203. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +5 -0
  204. package/src/__tests__/detached-inspection.integration.test.ts +6 -0
  205. package/src/__tests__/device-feature-admission.unit.test.ts +13 -1
  206. package/src/__tests__/direct-light-snapshot.unit.test.ts +59 -3
  207. package/src/__tests__/environment-generation.integration.test.ts +16 -0
  208. package/src/__tests__/evidence-identity.ts +13 -0
  209. package/src/__tests__/evidence-identity.unit.test.ts +24 -0
  210. package/src/__tests__/extended-lighting-recovery.browser.test.ts +14 -0
  211. package/src/__tests__/extended-lighting-recovery.dawn.test.ts +14 -0
  212. package/src/__tests__/extended-lighting-recovery.integration.test.ts +81 -0
  213. package/src/__tests__/extended-lighting-resources.unit.test.ts +51 -0
  214. package/src/__tests__/extended-lighting-zero-feature.integration.test.ts +52 -0
  215. package/src/__tests__/factory-contract.integration.test.ts +33 -0
  216. package/src/__tests__/frame-observation.contract.test.ts +15 -0
  217. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +146 -0
  218. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +1 -0
  219. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +1 -0
  220. package/src/__tests__/gpu-driven-production.integration.test.ts +572 -9
  221. package/src/__tests__/gpu-driven-scaling.unit.test.ts +66 -2
  222. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +26 -1
  223. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +89 -2
  224. package/src/__tests__/gpu-driven-view.browser.test.ts +3 -3
  225. package/src/__tests__/gpu-driven-view.dawn.test.ts +3 -3
  226. package/src/__tests__/gpu-driven-view.unit.test.ts +50 -0
  227. package/src/__tests__/gpu-lod-evidence.ts +188 -0
  228. package/src/__tests__/gpu-lod-fallback.integration.test.ts +10 -0
  229. package/src/__tests__/gpu-lod-indirect.integration.test.ts +40 -0
  230. package/src/__tests__/gpu-lod-parity.dawn.test.ts +29 -0
  231. package/src/__tests__/gpu-lod-readiness.integration.test.ts +52 -0
  232. package/src/__tests__/gpu-lod-schema.unit.test.ts +66 -0
  233. package/src/__tests__/gpu-lod-selector.unit.test.ts +28 -0
  234. package/src/__tests__/gpu-lod-silhouette.browser.test.ts +13 -0
  235. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +66 -1
  236. package/src/__tests__/gpu-scene.dawn.test.ts +8 -1
  237. package/src/__tests__/gpu-scene.unit.test.ts +49 -3
  238. package/src/__tests__/light-buffer-layout.unit.test.ts +113 -3
  239. package/src/__tests__/light-inspection.unit.test.ts +55 -0
  240. package/src/__tests__/light-resource-error.unit.test.ts +43 -0
  241. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +109 -0
  242. package/src/__tests__/lod-bounds.unit.test.ts +42 -0
  243. package/src/__tests__/lod-cpu-lane.integration.test.ts +55 -0
  244. package/src/__tests__/lod-cpu-no-change.perf.test.ts +19 -0
  245. package/src/__tests__/lod-gpu-frame-timing.unit.test.ts +49 -0
  246. package/src/__tests__/lod-multi-view.integration.test.ts +63 -0
  247. package/src/__tests__/lod-occlusion-device-loss.integration.test.ts +27 -0
  248. package/src/__tests__/lod-occlusion-final-decision.integration.test.ts +47 -0
  249. package/src/__tests__/lod-occlusion-inspection.integration.test.ts +217 -0
  250. package/src/__tests__/lod-occlusion-inspection.schema.test-d.ts +14 -0
  251. package/src/__tests__/lod-occlusion-inspection.unit.test.ts +72 -0
  252. package/src/__tests__/lod-occlusion-visual-evidence.ts +91 -0
  253. package/src/__tests__/lod-occlusion.browser.test.ts +15 -0
  254. package/src/__tests__/lod-occlusion.dawn.test.ts +20 -0
  255. package/src/__tests__/lod-selector.profile.test.ts +35 -0
  256. package/src/__tests__/lod-selector.unit.test.ts +68 -0
  257. package/src/__tests__/lod-visibility-identity.unit.test.ts +198 -0
  258. package/src/__tests__/lod-visibility-lifecycle.integration.test.ts +132 -0
  259. package/src/__tests__/ltc-resources.unit.test.ts +32 -0
  260. package/src/__tests__/material-contract-inventory.unit.test.ts +17 -0
  261. package/src/__tests__/occlusion-confidence.unit.test.ts +168 -0
  262. package/src/__tests__/occlusion-invalidation.integration.test.ts +37 -0
  263. package/src/__tests__/occlusion-pass-routing.unit.test.ts +16 -0
  264. package/src/__tests__/occlusion-pass.dawn.test.ts +20 -0
  265. package/src/__tests__/occlusion-pass.integration.test.ts +43 -0
  266. package/src/__tests__/occlusion-pass.rhi-null.test.ts +22 -0
  267. package/src/__tests__/occlusion-query-pool.rhi-null.test.ts +21 -0
  268. package/src/__tests__/occlusion-query-pool.unit.test.ts +95 -0
  269. package/src/__tests__/occlusion-query-ticket.integration.test.ts +47 -0
  270. package/src/__tests__/occlusion-query.dawn.test.ts +16 -0
  271. package/src/__tests__/occlusion-query.evidence.ts +178 -0
  272. package/src/__tests__/occlusion-query.rhi-null.test.ts +31 -0
  273. package/src/__tests__/occlusion-runtime.unit.test.ts +120 -0
  274. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +196 -0
  275. package/src/__tests__/probe-bindings.unit.test.ts +200 -0
  276. package/src/__tests__/probe-blend-numeric.unit.test.ts +67 -0
  277. package/src/__tests__/probe-blend-oracle.unit.test.ts +136 -0
  278. package/src/__tests__/probe-capacity.integration.test.ts +97 -0
  279. package/src/__tests__/probe-recovery.integration.test.ts +104 -0
  280. package/src/__tests__/probe-scene-dirty.integration.test.ts +211 -0
  281. package/src/__tests__/profiler-phase-catalog.test.ts +4 -1
  282. package/src/__tests__/profiler-phase-token.test.ts +1 -0
  283. package/src/__tests__/rect-area-boundary.unit.test.ts +24 -0
  284. package/src/__tests__/reflection-fallback-graph.unit.test.ts +67 -0
  285. package/src/__tests__/reflection-fallback-inspection.unit.test.ts +77 -0
  286. package/src/__tests__/reflection-fallback-lifecycle.integration.test.ts +21 -0
  287. package/src/__tests__/reflection-fallback-projection.unit.test.ts +90 -0
  288. package/src/__tests__/reflection-fallback-source.unit.test.ts +32 -0
  289. package/src/__tests__/reflection-probe-owner-placement.unit.test.ts +39 -0
  290. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +120 -0
  291. package/src/__tests__/render-error-code-owner.test-d.ts +1 -0
  292. package/src/__tests__/render-feature-docs.unit.test.ts +12 -7
  293. package/src/__tests__/render-graph-fallback.integration.test.ts +24 -0
  294. package/src/__tests__/render-perf/extended-lighting.bench.test.ts +66 -0
  295. package/src/__tests__/render-scene-projection.unit.test.ts +447 -18
  296. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +76 -0
  297. package/src/__tests__/skin-motion-regression.unit.test.ts +22 -0
  298. package/src/__tests__/skinned-shadow-caster.test.ts +2 -2
  299. package/src/__tests__/spot-modifier-projection.unit.test.ts +70 -0
  300. package/src/__tests__/ssr-admission-matrix.integration.test.ts +206 -0
  301. package/src/__tests__/ssr-composition.unit.test.ts +46 -0
  302. package/src/__tests__/ssr-fallback-zero-work.bench.test.ts +94 -0
  303. package/src/__tests__/ssr-recovery-errors.unit.test.ts +58 -0
  304. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +67 -0
  305. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +7 -0
  306. package/src/__tests__/standard-lighting-inspection.integration.test.ts +1 -1
  307. package/src/__tests__/standard-lighting-write.unit.test.ts +66 -2
  308. package/src/__tests__/temporal-fallback-generation.integration.test.ts +16 -0
  309. package/src/__tests__/temporal-view.unit.test.ts +3 -3
  310. package/src/__tests__/transmission-prewarm.unit.test.ts +69 -0
  311. package/src/__tests__/visibility-budget.integration.test.ts +140 -0
  312. package/src/__tests__/visibility-budget.unit.test.ts +70 -0
  313. package/src/__tests__/visibility-extract.integration.test.ts +11 -6
  314. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +6 -3
  315. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +31 -28
  316. package/src/assembly/backend-contract.ts +13 -0
  317. package/src/assembly/device-feature-admission.ts +16 -9
  318. package/src/assembly/factory.ts +547 -93
  319. package/src/assembly/host-contract.ts +10 -0
  320. package/src/cluster-binner.ts +31 -0
  321. package/src/components/__tests__/lighting-authoring.unit.test.ts +39 -0
  322. package/src/components/__tests__/rect-area-light.unit.test.ts +33 -0
  323. package/src/components/index.ts +3 -0
  324. package/src/components/light-helpers.ts +52 -1
  325. package/src/components/light-probe.ts +13 -0
  326. package/src/components/rect-area-light.ts +15 -0
  327. package/src/components/scene-instance.ts +6 -0
  328. package/src/components/spot-light.ts +3 -0
  329. package/src/device/__tests__/extended-lighting-recovery.unit.test.ts +24 -0
  330. package/src/device/gpu-residency.ts +140 -9
  331. package/src/device/lifecycle-transaction.ts +12 -0
  332. package/src/environment/lifecycle.ts +8 -1
  333. package/src/errors/gpu-driven.ts +92 -0
  334. package/src/errors/index.ts +1 -0
  335. package/src/errors/recover.ts +32 -0
  336. package/src/errors/render.ts +55 -1
  337. package/src/extract/camera.ts +3 -3
  338. package/src/extract/gpu-driven.ts +108 -0
  339. package/src/extract/visibility.ts +0 -23
  340. package/src/gpu-driven/batch-topology.ts +206 -28
  341. package/src/gpu-driven/prepared-draw.ts +208 -0
  342. package/src/gpu-driven/production-raster.ts +953 -41
  343. package/src/gpu-driven/view-gpu.ts +411 -29
  344. package/src/gpu-scene-schema.ts +13 -0
  345. package/src/gpu-scene.ts +134 -65
  346. package/src/gpu-usage.ts +0 -1
  347. package/src/hdrp-buffers.ts +6 -6
  348. package/src/index.ts +74 -0
  349. package/src/inspection-types.ts +220 -1
  350. package/src/internal.ts +38 -0
  351. package/src/light-buffer-layout.ts +154 -324
  352. package/src/materials.ts +16 -2
  353. package/src/pbr-pipeline.ts +151 -45
  354. package/src/pipeline/standard-forward-lane.ts +47 -0
  355. package/src/pipeline/standard-lighting/graph.ts +2 -1
  356. package/src/pipeline/standard-lighting/light-frame.ts +3 -2
  357. package/src/pipeline/standard-lighting/topology.ts +1 -1
  358. package/src/pipeline/standard-lighting/transport.ts +2 -1
  359. package/src/pipeline/standard-pipeline.ts +2 -0
  360. package/src/pipeline-builder.ts +5 -2
  361. package/src/pipeline-spec.ts +12 -1
  362. package/src/plugin.ts +4 -0
  363. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +56 -0
  364. package/src/prepare/extended-lighting/ltc-resources.ts +136 -0
  365. package/src/prepare/extended-lighting/resources.ts +389 -0
  366. package/src/prepare/extended-lighting/spot-modifiers.ts +194 -0
  367. package/src/prepare/extended-lighting/state.ts +92 -0
  368. package/src/record/__tests__/gpu-timing.unit.test.ts +177 -0
  369. package/src/record/__tests__/temporal-payload.unit.test.ts +23 -1
  370. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +20 -1
  371. package/src/record/frame-lighting.ts +252 -53
  372. package/src/record/frame-snapshot.ts +57 -12
  373. package/src/record/frame.ts +410 -93
  374. package/src/record/gpu-timing.ts +108 -44
  375. package/src/record/helpers.ts +0 -58
  376. package/src/record/main-pass-geometry.ts +204 -39
  377. package/src/record/main-pass-material.ts +8 -1
  378. package/src/record/main-pass-sprite-draws.ts +52 -85
  379. package/src/record/mesh-ssbo.ts +47 -36
  380. package/src/record/render-context.ts +65 -4
  381. package/src/record/shadow-pass.ts +66 -40
  382. package/src/record/typed-frame-graph.ts +225 -83
  383. package/src/record/view-ubo.ts +14 -24
  384. package/src/reflection/inspection.ts +67 -0
  385. package/src/reflection/projection.ts +196 -1
  386. package/src/reflection/record-owner.ts +1283 -0
  387. package/src/render-contract.ts +17 -5
  388. package/src/render-pipeline.ts +5 -0
  389. package/src/render-system-extract.ts +493 -134
  390. package/src/render-system.ts +811 -615
  391. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +36 -0
  392. package/src/scene/probe-blend-record.ts +54 -0
  393. package/src/scene/probe-blend.ts +813 -0
  394. package/src/scene/render-scene-types.ts +0 -7
  395. package/src/scene/render-scene.ts +848 -183
  396. package/src/scene/visibility/bounds.ts +57 -0
  397. package/src/scene/visibility/budget.ts +40 -0
  398. package/src/scene/visibility/facet.ts +400 -0
  399. package/src/scene/visibility/gpu-frame-timing.ts +101 -0
  400. package/src/scene/visibility/gpu-lod.ts +119 -0
  401. package/src/scene/visibility/inspection.ts +313 -0
  402. package/src/scene/visibility/lod-selector.ts +84 -0
  403. package/src/scene/visibility/occlusion-confidence.ts +478 -0
  404. package/src/scene/visibility/occlusion-pass.ts +165 -0
  405. package/src/scene/visibility/occlusion-query-pool.ts +178 -0
  406. package/src/scene/visibility/occlusion-runtime.ts +672 -0
  407. package/src/scene/visibility/types.ts +50 -0
  408. package/src/ssr/admission.ts +451 -0
  409. package/src/ssr/composition.ts +73 -0
  410. package/src/ssr/errors.ts +76 -0
  411. package/src/ssr/identity.ts +12 -0
  412. package/src/systems/skin-palette-allocator.ts +1 -1
  413. package/src/temporal/frame-coordinator.ts +26 -0
  414. package/src/temporal/frame.ts +14 -1
  415. package/src/temporal/view.ts +11 -0
  416. package/src/tilemap-chunk-extract-system.ts +4 -4
  417. package/src/typed-render-graph-primitives.ts +48 -0
  418. package/src/volume/passes.ts +46 -49
  419. package/dist/.tsbuildinfo +0 -1
  420. package/dist/chunk-5YADUCA7.mjs.map +0 -1
  421. package/dist/chunk-6OPXDQ2E.mjs.map +0 -1
  422. package/dist/chunk-BXBQQKFQ.mjs +0 -67
  423. package/dist/chunk-BXBQQKFQ.mjs.map +0 -1
  424. package/dist/chunk-FS2KNUQH.mjs.map +0 -1
  425. package/dist/chunk-PCHBPYML.mjs.map +0 -1
  426. package/dist/chunk-VFPLTKCU.mjs.map +0 -1
@@ -54,12 +54,17 @@ import type {
54
54
  RhiDevice,
55
55
  Sampler,
56
56
  ShaderModule,
57
+ TextureView,
57
58
  } from '@forgeax/engine-rhi';
58
59
  import { err, ok, RhiError, validateDrawArgs } from '@forgeax/engine-rhi';
59
60
  import { registerPropagateTransforms } from '@forgeax/engine-scene';
60
61
  import {
62
+ createStandardPbrArtifactReceipt,
61
63
  DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
62
64
  findVariantByKey,
65
+ LTC_TABLE_HEIGHT,
66
+ LTC_TABLE_WIDTH,
67
+ LTC_TABLES,
63
68
  type MaterialShaderEntry,
64
69
  type MaterialShaderManifestEntry,
65
70
  ShaderCatalog,
@@ -130,13 +135,7 @@ import {
130
135
  LostListenerRegistry,
131
136
  RhiErrorListenerRegistry,
132
137
  } from '../lifecycle';
133
- import {
134
- assertStorageBufferCap,
135
- LIGHT_ARRAY_HEADER_BYTES,
136
- LIGHT_ARRAY_MAX_SLOTS,
137
- POINT_LIGHT_STD430_BYTES,
138
- SPOT_LIGHT_STD430_BYTES,
139
- } from '../light-buffer-layout';
138
+ import { assertStorageBufferCap } from '../light-buffer-layout';
140
139
  import {
141
140
  buildPbrMaterialUserRegionEntries,
142
141
  buildPbrPipelineLayouts,
@@ -169,6 +168,22 @@ import {
169
168
  PipelineSpecError,
170
169
  } from '../pipeline-spec';
171
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';
172
187
  import type { FrameObservationOptions } from '../record/frame';
173
188
  import type { GpuPassTimingReason } from '../record/gpu-pass-timing/errors.js';
174
189
  import {
@@ -309,8 +324,8 @@ function adaptMipmapShaderModuleFactory(
309
324
 
310
325
  // Re-export registerAdvanceAnimationPlayer so consumers can wire it.
311
326
  // Re-export registerPropagateTransforms so consumers can wire the
312
- // Transform.world mat4 derivation (audio listener sync + picking read the
313
- // derived Transform.world from scripts).
327
+ // GlobalTransform.world mat4 derivation (audio listener sync + picking read the
328
+ // derived GlobalTransform.world from scripts).
314
329
 
315
330
  // feat-20260611-fox-skinning-vertex-attribute-chain M4 / w16 (D-4): a single
316
331
  // shared empty ArrayBuffer used by `buildPipelineContext` to synthesize the
@@ -449,7 +464,7 @@ function toGpuVertexBufferLayouts(
449
464
  * Bundler-layer injection accepted by `createRenderer` (and proxied by
450
465
  * `createApp`) as the optional third argument.
451
466
  *
452
- * 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
453
468
  * host-injected build-tool channels:
454
469
  *
455
470
  * - shaderManifestUrl: the URL the host's vite-plugin-shader emit step
@@ -463,6 +478,9 @@ function toGpuVertexBufferLayouts(
463
478
  * AssetRegistry third ctor slot so DDC-miss assets can lazy-import.
464
479
  * Absent => shipped form (a DDC miss fails fast with `asset-not-imported`).
465
480
  *
481
+ * - build: exact checkout revision emitted by the build-tool adapter and
482
+ * carried into renderer-owned inspection identity.
483
+ *
466
484
  * The interface is duplicated structurally in `@forgeax/engine-app`
467
485
  * (BundlerOptions exported from packages/app); demos consume through the app
468
486
  * package, the engine accepts via this local minimal shape so packages/runtime
@@ -473,6 +491,8 @@ function toGpuVertexBufferLayouts(
473
491
  export interface BundlerOptions {
474
492
  readonly importTransport?: ImportTransport | undefined;
475
493
  readonly shaderManifestUrl?: string | undefined;
494
+ /** Exact checkout revision emitted by the build-tool adapter. */
495
+ readonly build?: string | undefined;
476
496
  }
477
497
 
478
498
  // ─── Backend pack — M3 auto-select internal shape ───────────────────────────
@@ -541,25 +561,42 @@ function directionalPcssCapable(backendKind: RhiDevice['caps']['backendKind']):
541
561
  export function selectHdrpPbrPrewarmVariants(
542
562
  manifestEntry: MaterialShaderManifestEntry | undefined,
543
563
  storageBufferCapable: boolean,
544
- transmissionCapable: boolean,
564
+ extendedLightingShaderAvailableOrTransmissionCapable = true,
565
+ transmissionCapable = true,
545
566
  directionalPcssAvailable = true,
546
567
  projectorAvailable: boolean = true,
547
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;
548
579
  // HDRP's clustered group(2) ABI is storage-buffer backed. A downlevel
549
580
  // WebGL2 device must stay on the URP variant; prewarming an HDRP source
550
581
  // here would advertise an inactive route and can exceed its fragment
551
582
  // uniform-buffer budget before the first frame.
552
583
  if (!storageBufferCapable) return [];
553
584
  return (
554
- manifestEntry?.variants.filter(
585
+ variants.filter(
555
586
  (variant) =>
556
587
  variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
588
+ (!('EXTENDED_LIGHTING_AVAILABLE' in variant.defines) ||
589
+ variant.defines.EXTENDED_LIGHTING_AVAILABLE === extendedLightingShaderAvailable) &&
557
590
  variant.defines.CLUSTER_FORWARD_AVAILABLE === true &&
558
591
  (!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
559
592
  variant.defines.DIRECTIONAL_PCSS_AVAILABLE === directionalPcssAvailable) &&
560
593
  (!('PROJECTOR_AVAILABLE' in variant.defines) ||
561
594
  variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
562
- (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),
563
600
  ) ?? []
564
601
  );
565
602
  }
@@ -575,7 +612,9 @@ export function selectSkinPrewarmVariants(
575
612
  variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
576
613
  // The clustered skin ABI is only active on the storage-backed tier;
577
614
  // storage=false must never prewarm or expose it as a usable variant.
578
- (storageBufferCapable || variant.defines.CLUSTER_FORWARD_AVAILABLE !== true),
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,
579
618
  ) ?? []
580
619
  );
581
620
  }
@@ -590,6 +629,7 @@ export function selectStandardPbrTransmissionPrewarmVariants(
590
629
  storageBufferCapable: boolean,
591
630
  directionalPcssAvailable = true,
592
631
  projectorAvailable: boolean = true,
632
+ extendedLightingShaderAvailable = true,
593
633
  ): readonly MaterialShaderManifestEntry['variants'][number][] {
594
634
  if (manifestEntry === undefined) {
595
635
  throw new Error('Standard material shader manifest row is missing');
@@ -605,6 +645,9 @@ export function selectStandardPbrTransmissionPrewarmVariants(
605
645
  variant.defines.DIRECTIONAL_PCSS_AVAILABLE === directionalPcssAvailable) &&
606
646
  (!('PROJECTOR_AVAILABLE' in variant.defines) ||
607
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 &&
608
651
  variant.defines.TRANSMISSION_AVAILABLE === transmissionAvailable,
609
652
  );
610
653
  if (declared === undefined) {
@@ -632,18 +675,24 @@ export function selectNoColorPbrVariant(
632
675
  manifestEntry: MaterialShaderManifestEntry | undefined,
633
676
  storageBufferCapable: boolean,
634
677
  variantSet: string | undefined,
678
+ extendedLightingShaderAvailable = true,
635
679
  directionalPcssAvailable = true,
636
680
  projectorAvailable: boolean = true,
637
681
  ): MaterialShaderManifestEntry['variants'][number] | undefined {
638
682
  const hdrpRequested = variantSet === '' || requestsClusterVariant(variantSet);
683
+ const probeBlendRequested = requestsProbeBlendVariant(variantSet);
639
684
  return manifestEntry?.variants.find(
640
685
  (variant) =>
641
686
  variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
687
+ (!('EXTENDED_LIGHTING_AVAILABLE' in variant.defines) ||
688
+ variant.defines.EXTENDED_LIGHTING_AVAILABLE === extendedLightingShaderAvailable) &&
642
689
  variant.defines.VERTEX_COLOR_AVAILABLE === false &&
643
690
  (!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
644
691
  variant.defines.DIRECTIONAL_PCSS_AVAILABLE === directionalPcssAvailable) &&
645
692
  (!('PROJECTOR_AVAILABLE' in variant.defines) ||
646
693
  variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
694
+ (!('PROBE_BLEND_AVAILABLE' in variant.defines) ||
695
+ variant.defines.PROBE_BLEND_AVAILABLE === probeBlendRequested) &&
647
696
  (!('CLUSTER_FORWARD_AVAILABLE' in variant.defines) ||
648
697
  variant.defines.CLUSTER_FORWARD_AVAILABLE === hdrpRequested),
649
698
  );
@@ -654,6 +703,11 @@ function requestsClusterVariant(variantSet: string | undefined): boolean {
654
703
  return variantSet.includes('CLUSTER_FORWARD_AVAILABLE=true');
655
704
  }
656
705
 
706
+ function requestsProbeBlendVariant(variantSet: string | undefined): boolean {
707
+ if (variantSet === undefined) return false;
708
+ return variantSet.includes('PROBE_BLEND_AVAILABLE=true');
709
+ }
710
+
657
711
  export type MaterialShaderBindingContract = RenderFeatureMaterialShaderBindingContract;
658
712
  export type MaterialShaderVertexInputContract = 'none' | 'render-material';
659
713
 
@@ -778,14 +832,18 @@ export function shouldDeferMissingPreparedMaterialShader(
778
832
  export function selectPipelineLayoutForVariant(
779
833
  state: {
780
834
  readonly pbrPipelineLayout: PipelineLayout | null;
835
+ readonly pbrProbePipelineLayout?: PipelineLayout | null;
781
836
  readonly hdrpPbrPipelineLayout: PipelineLayout | null;
837
+ readonly hdrpProbePbrPipelineLayout?: PipelineLayout | null;
782
838
  readonly pbrSkinPipelineLayout: PipelineLayout | null;
839
+ readonly pbrSkinProbePipelineLayout?: PipelineLayout | null;
783
840
  readonly hdrpSkinPipelineLayout?: PipelineLayout | null;
784
841
  } | null,
785
- _variantSet: string | undefined,
842
+ variantSet: string | undefined,
786
843
  layoutKind?: LayoutKind,
787
844
  ): PipelineLayout | null {
788
845
  if (state === null) return null;
846
+ const probeBlendVariant = variantSet?.includes('PROBE_BLEND_AVAILABLE=true') === true;
789
847
  // bug-20260611: skin layout selection takes precedence over HDRP variant
790
848
  // resolution. HDRP × skin is OOS-1 (plan-strategy R-2 — left for a
791
849
  // dedicated feat); when an HDRP-variant skin call ever lands, we fail
@@ -793,7 +851,9 @@ export function selectPipelineLayoutForVariant(
793
851
  // (charter P3 explicit failure, mirrors memory anchor
794
852
  // `hdrp-active-must-not-fallback-to-urp-pipeline`).
795
853
  if (layoutKind === 'pbr-skin') {
796
- return state.pbrSkinPipelineLayout;
854
+ return probeBlendVariant
855
+ ? (state.pbrSkinProbePipelineLayout ?? null)
856
+ : state.pbrSkinPipelineLayout;
797
857
  }
798
858
  if (layoutKind === 'hdrp-skin') {
799
859
  return state.hdrpSkinPipelineLayout ?? null;
@@ -818,14 +878,19 @@ export function selectPipelineLayoutForVariant(
818
878
  // the renderer is otherwise running its HDRP lighting variant; allowing the
819
879
  // variant key to re-select HDRP here recreates the mesh-vs-cluster mismatch.
820
880
  if (layoutKind === 'pbr') {
821
- return state.pbrPipelineLayout;
881
+ return probeBlendVariant ? (state.pbrProbePipelineLayout ?? null) : state.pbrPipelineLayout;
822
882
  }
823
883
  // Group(2) contract/layout kind is the sole layout authority. Variant keys
824
884
  // identify WGSL artifacts, but must not independently select a bind-group
825
885
  // layout; this prevents an empty/expanded key from bypassing the resolved
826
886
  // storage contract.
827
887
  const isHdrpVariant = layoutKind === 'hdrp-pbr';
828
- return isHdrpVariant ? state.hdrpPbrPipelineLayout : state.pbrPipelineLayout;
888
+ if (isHdrpVariant) {
889
+ return probeBlendVariant
890
+ ? (state.hdrpProbePbrPipelineLayout ?? null)
891
+ : state.hdrpPbrPipelineLayout;
892
+ }
893
+ return probeBlendVariant ? (state.pbrProbePipelineLayout ?? null) : state.pbrPipelineLayout;
829
894
  }
830
895
 
831
896
  /**
@@ -841,12 +906,13 @@ export function resolveMaterialShaderVariantSet(
841
906
  }>,
842
907
  backendKind: RhiDevice['caps']['backendKind'],
843
908
  storageBuffer: boolean,
844
- projectorAvailable: boolean = true,
909
+ maxSampledTexturesPerShaderStageOrProjectorAvailable?: number | boolean,
845
910
  ): string | undefined {
846
911
  const declaredAxes = new Set(variants.flatMap((variant) => Object.keys(variant.defines)));
847
912
  const declaresCapabilityVariants =
848
913
  declaredAxes.has('WEBGL2_COMPAT') ||
849
914
  declaredAxes.has('STORAGE_BUFFER_AVAILABLE') ||
915
+ declaredAxes.has('EXTENDED_LIGHTING_AVAILABLE') ||
850
916
  declaredAxes.has('DIRECTIONAL_PCSS_AVAILABLE') ||
851
917
  declaredAxes.has('PROJECTOR_AVAILABLE');
852
918
  if (!declaresCapabilityVariants) return requestedVariantSet;
@@ -856,6 +922,22 @@ export function resolveMaterialShaderVariantSet(
856
922
  const separator = part.indexOf('=');
857
923
  if (separator > 0) requested[part.slice(0, separator)] = part.slice(separator + 1) === 'true';
858
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;
859
941
  const filtered: Record<string, boolean> = {};
860
942
  for (const axis of declaredAxes) {
861
943
  filtered[axis] =
@@ -871,13 +953,22 @@ export function resolveMaterialShaderVariantSet(
871
953
  ? requestedVariantSet === '' || (requested[axis] ?? false)
872
954
  : axis === 'VERTEX_COLOR_AVAILABLE'
873
955
  ? requestedVariantSet === '' || (requested[axis] ?? false)
874
- : axis === 'TRANSMISSION_AVAILABLE'
875
- ? (requested[axis] ?? false)
876
- : axis === 'DIRECTIONAL_PCSS_AVAILABLE'
877
- ? directionalPcssCapable(backendKind)
878
- : axis === 'PROJECTOR_AVAILABLE'
879
- ? projectorAvailable
880
- : (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);
881
972
  }
882
973
  const sorted = Object.entries(filtered).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
883
974
  return sorted.every(([, value]) => value)
@@ -907,8 +998,9 @@ export async function createRenderer(
907
998
  options?: RendererOptions,
908
999
  // feat-20260608-create-app-param-surface-trim / M2 / D-3: the third arg is
909
1000
  // now BundlerOptions -- the build-tool injection channel that aggregates
910
- // (a) shaderManifestUrl (vite-plugin-shader emit URL) and (b) importTransport
911
- // (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
912
1004
  // optional so {} is a valid call shape. Absent => both shipped-form
913
1005
  // fallbacks fire: shaderManifestUrl falls back to '/shaders/manifest.json'
914
1006
  // (D-2 q5-A, ~30 lines below); importTransport stays undefined (AssetRegistry
@@ -995,7 +1087,11 @@ function attachDeviceLostFanout(
995
1087
  },
996
1088
  ): void {
997
1089
  const { lostRegistry, errorRegistry, healthRegistry, onDeviceLost } = registries;
998
- 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
999
1095
  .then((info) => {
1000
1096
  const safe = {
1001
1097
  reason: info?.reason ?? 'unknown',
@@ -1227,10 +1323,8 @@ interface WebGPURendererInternals {
1227
1323
  options: RendererOptions | undefined;
1228
1324
  /**
1229
1325
  * feat-20260608-create-app-param-surface-trim / M2 / D-3: BundlerOptions
1230
- * forwarded verbatim from createRenderer's third arg. Carries
1231
- * shaderManifestUrl (host-injected vite-plugin-shader emit URL; absent =>
1232
- * createRenderer falls back to '/shaders/manifest.json') and the
1233
- * 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.
1234
1328
  */
1235
1329
  bundler: BundlerOptions | undefined;
1236
1330
  lostRegistry: LostListenerRegistry;
@@ -1457,6 +1551,7 @@ async function makeWebGPURenderer(
1457
1551
  // lives in a standalone store; `assets` keeps the CPU POD registry only.
1458
1552
  const gpuStore = new GpuResidencyCache();
1459
1553
  let activeDeviceScope = DeviceScope.create(0, 'renderer');
1554
+ let extendedLightingState = createExtendedLightingState(activeDeviceScope.generation);
1460
1555
  const gpuPassTimingOptions = internals.options?.gpuPassTiming;
1461
1556
  const timingRetentionFrames =
1462
1557
  gpuPassTimingOptions?.retentionFrames !== undefined &&
@@ -1916,8 +2011,7 @@ async function makeWebGPURenderer(
1916
2011
  [],
1917
2012
  internals.device.caps.backendKind,
1918
2013
  internals.device.caps.storageBuffer,
1919
- (internals.device.limits.maxSampledTexturesPerShaderStage ?? 0) >=
1920
- STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
2014
+ internals.device.limits.maxSampledTexturesPerShaderStage,
1921
2015
  );
1922
2016
  }
1923
2017
  const variants = manifestEntry.variants;
@@ -1929,14 +2023,14 @@ async function makeWebGPURenderer(
1929
2023
  const projectorAvailable =
1930
2024
  (internals.device.limits.maxSampledTexturesPerShaderStage ?? 0) >=
1931
2025
  STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
1932
- 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'}`;
1933
2027
  if (byRequest.has(cacheKey)) return byRequest.get(cacheKey);
1934
2028
  const resolved = resolveMaterialShaderVariantSet(
1935
2029
  requestedVariantSet,
1936
2030
  variants,
1937
2031
  internals.device.caps.backendKind,
1938
2032
  internals.device.caps.storageBuffer,
1939
- projectorAvailable,
2033
+ internals.device.limits.maxSampledTexturesPerShaderStage,
1940
2034
  );
1941
2035
  byRequest.set(cacheKey, resolved);
1942
2036
  return resolved;
@@ -1962,6 +2056,43 @@ async function makeWebGPURenderer(
1962
2056
  // `internals.device` / `internals.pack` are read live, so a rebuild after a
1963
2057
  // device swap compiles against the new device.
1964
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
+ };
1965
2096
  let fxaaRegistered = false;
1966
2097
  let temporalPostProcessesRegistered = false;
1967
2098
  const buildPipeline = (scope: DeviceScope = activeDeviceScope): Promise<PipelineState> =>
@@ -2056,6 +2187,7 @@ async function makeWebGPURenderer(
2056
2187
  const ready: Promise<Result<void, RhiError>> = buildPipeline().then(
2057
2188
  (state): Result<void, RhiError> => {
2058
2189
  pipelineState = state;
2190
+ promotePipelineExtendedLighting(state, activeDeviceScope);
2059
2191
  readySettled = true;
2060
2192
  markRendererBootstrapStage('renderer-initialized');
2061
2193
  return ok(undefined);
@@ -2101,6 +2233,7 @@ async function makeWebGPURenderer(
2101
2233
  pbrManifestEntry,
2102
2234
  internals.device.caps.storageBuffer,
2103
2235
  variantSet,
2236
+ deriveExtendedLightingCapability(internals.device).admitted,
2104
2237
  directionalPcssCapable(internals.device.caps.backendKind),
2105
2238
  (internals.device.limits.maxSampledTexturesPerShaderStage ?? 0) >=
2106
2239
  STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES,
@@ -2366,6 +2499,7 @@ async function makeWebGPURenderer(
2366
2499
  vertexLayoutProjection?: VertexLayoutProjection,
2367
2500
  shaderModuleMode?: RenderFeatureShaderModuleMode,
2368
2501
  layoutKindOverride?: LayoutKind,
2502
+ additionalColorFormats?: readonly GPUTextureFormat[],
2369
2503
  ): {
2370
2504
  readonly pipeline: RenderPipeline;
2371
2505
  readonly group2Contract: PipelineGroup2Contract;
@@ -2401,6 +2535,10 @@ async function makeWebGPURenderer(
2401
2535
  {
2402
2536
  ...ctx,
2403
2537
  colorFormat: isHdr ? HDR_COLOR_ATTACHMENT_FORMAT : ldrColorFormat,
2538
+ colorFormats:
2539
+ additionalColorFormats === undefined
2540
+ ? undefined
2541
+ : [isHdr ? HDR_COLOR_ATTACHMENT_FORMAT : ldrColorFormat, ...additionalColorFormats],
2404
2542
  depthFormat:
2405
2543
  depthFormatOverride === null ? undefined : (depthFormatOverride ?? DEPTH_TEXTURE_FORMAT),
2406
2544
  label,
@@ -2492,6 +2630,7 @@ async function makeWebGPURenderer(
2492
2630
  vertexLayoutProjection?: VertexLayoutProjection,
2493
2631
  shaderModuleMode: RenderFeatureShaderModuleMode = 'validated',
2494
2632
  layoutKindOverride?: LayoutKind,
2633
+ additionalColorFormats?: readonly GPUTextureFormat[],
2495
2634
  ): {
2496
2635
  readonly pipeline: RenderPipeline;
2497
2636
  readonly group2Contract: PipelineGroup2Contract;
@@ -2575,10 +2714,14 @@ async function makeWebGPURenderer(
2575
2714
  // getPreferredCanvasFormat returns rgba8unorm (dawn-node, lavapipe, wgpu-wasm GLES),
2576
2715
  // forcing a redundant second PSO build on first-frame URP record path.
2577
2716
  const colorFormat: GPUTextureFormat = isHdr ? 'rgba16float' : ldrColorFormat;
2717
+ const attachmentFormats =
2718
+ additionalColorFormats === undefined
2719
+ ? colorFormatsForPassKind(passKind, colorFormat)
2720
+ : [colorFormat, ...additionalColorFormats];
2578
2721
  const spec: PipelineSpec = {
2579
2722
  shader: { id: materialShaderId, passKind, variantSet: effectiveVariantSet },
2580
2723
  attachments: {
2581
- colorFormats: colorFormatsForPassKind(passKind, colorFormat),
2724
+ colorFormats: attachmentFormats,
2582
2725
  depthFormat:
2583
2726
  passKind === 'shadow-caster' || passKind === 'point-shadow-caster'
2584
2727
  ? 'depth32float'
@@ -2667,6 +2810,8 @@ async function makeWebGPURenderer(
2667
2810
  vertexLayout,
2668
2811
  vertexLayoutProjection,
2669
2812
  shaderModuleMode,
2813
+ layoutKindOverride,
2814
+ additionalColorFormats,
2670
2815
  );
2671
2816
  }
2672
2817
  const pbrEntry = findStandardPbrEntry(effectiveVariantSet);
@@ -2721,6 +2866,8 @@ async function makeWebGPURenderer(
2721
2866
  vertexLayout,
2722
2867
  vertexLayoutProjection,
2723
2868
  shaderModuleMode,
2869
+ layoutKindOverride,
2870
+ additionalColorFormats,
2724
2871
  );
2725
2872
  }
2726
2873
  // feat-20260609 M4 / w31: resolve variant WGSL from manifest when
@@ -2815,6 +2962,7 @@ async function makeWebGPURenderer(
2815
2962
  vertexLayoutProjection,
2816
2963
  shaderModuleMode,
2817
2964
  layoutKindOverride,
2965
+ additionalColorFormats,
2818
2966
  );
2819
2967
  };
2820
2968
  const getMaterialShaderPipelineEntry = (
@@ -2827,6 +2975,37 @@ async function makeWebGPURenderer(
2827
2975
  const lookup = getShader().findMaterialArtifact(materialShaderId);
2828
2976
  return lookup.ok ? lookup.value.paramSchema : undefined;
2829
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
+ };
2830
3009
  // feat-20260621-learn-render-5-5-parallax M2 / w6 (D-1): expose the per-shader
2831
3010
  // material BGL so the record stage creates the material bind group against
2832
3011
  // the matching paramSchema-derived layout. Returns undefined only for
@@ -2931,6 +3110,13 @@ async function makeWebGPURenderer(
2931
3110
  );
2932
3111
  const renderSystem: RenderSystem = createRenderSystem({
2933
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,
2934
3120
  get standardProfile() {
2935
3121
  return activeProfile;
2936
3122
  },
@@ -2960,6 +3146,9 @@ async function makeWebGPURenderer(
2960
3146
  get device() {
2961
3147
  return internals.device;
2962
3148
  },
3149
+ get beforeSubmit() {
3150
+ return internals.pack.instrumentation?.beforeSubmit;
3151
+ },
2963
3152
  get deviceScope() {
2964
3153
  return activeDeviceScope;
2965
3154
  },
@@ -2986,6 +3175,7 @@ async function makeWebGPURenderer(
2986
3175
  getMaterialShaderPipelineEntry,
2987
3176
  getMaterialShaderBindingContract: getCachedMaterialShaderBindingContract,
2988
3177
  getParamSchema,
3178
+ getMaterialShaderArtifact,
2989
3179
  getMaterialBindGroupLayout,
2990
3180
  metrics,
2991
3181
  // feat-20260609 M4 / T-10-a: post-process pipeline factory (CONCERN-1 fix).
@@ -3166,6 +3356,12 @@ async function makeWebGPURenderer(
3166
3356
  );
3167
3357
  }
3168
3358
  },
3359
+ inspectLodOcclusion() {
3360
+ return {
3361
+ lodOcclusion: renderSystem.lodOcclusionInspection,
3362
+ gpuDriven: renderSystem.gpuDrivenInspection,
3363
+ };
3364
+ },
3169
3365
  inspect(): RenderInspection {
3170
3366
  const health = internals.healthRegistry.getLastSnapshot();
3171
3367
  const state = disposed
@@ -3211,9 +3407,11 @@ async function makeWebGPURenderer(
3211
3407
  visibilityStats: Object.freeze({ ...renderSystem.visibilityStats }),
3212
3408
  renderScene: renderSystem.renderScene,
3213
3409
  reflectionProbes: renderSystem.reflectionProbes,
3410
+ ssrDependencies: renderSystem.ssrDependencies,
3214
3411
  environment: renderSystem.environment,
3215
3412
  temporal: renderSystem.temporal,
3216
3413
  bloom: renderSystem.bloom,
3414
+ extendedLighting: projectExtendedLightingInspection(extendedLightingState),
3217
3415
  directionalShadow,
3218
3416
  ...(renderSystem.temporalTargetInspection === undefined
3219
3417
  ? {}
@@ -3221,6 +3419,9 @@ async function makeWebGPURenderer(
3221
3419
  ...(renderSystem.motionBlurInspection === undefined
3222
3420
  ? {}
3223
3421
  : { motionBlur: Object.freeze(renderSystem.motionBlurInspection) }),
3422
+ ...(renderSystem.lodOcclusionInspection === undefined
3423
+ ? {}
3424
+ : { lodOcclusion: Object.freeze(renderSystem.lodOcclusionInspection) }),
3224
3425
  ...(renderSystem.transmission === undefined
3225
3426
  ? {}
3226
3427
  : { transmission: renderSystem.transmission }),
@@ -3731,12 +3932,15 @@ async function makeWebGPURenderer(
3731
3932
  }
3732
3933
  const timingCompletion = timingHost.gpuPassTimingSubmittedWork;
3733
3934
  timingHost.gpuPassTimingSubmittedWork = undefined;
3935
+ const reflectionFallbackCompletion = renderSystem.reflectionFallbackCompletion;
3734
3936
  internals.pack.instrumentation?.onFrameBoundary?.();
3735
3937
  const receiptFrameId = ++frameId;
3736
3938
  const receiptGeneration = activeDeviceScope.generation;
3737
- const completed: FrameReceipt['completed'] = (
3738
- timingCompletion ?? internals.device.queue.onSubmittedWorkDone()
3739
- )
3939
+ const queueCompletion = timingCompletion ?? internals.device.queue.onSubmittedWorkDone();
3940
+ const completed: FrameReceipt['completed'] = Promise.all([
3941
+ queueCompletion,
3942
+ reflectionFallbackCompletion ?? Promise.resolve(),
3943
+ ])
3740
3944
  .then(() => ok(undefined))
3741
3945
  .catch((cause: unknown) =>
3742
3946
  err(
@@ -3858,6 +4062,7 @@ async function makeWebGPURenderer(
3858
4062
  }),
3859
4063
  );
3860
4064
  }
4065
+ await renderSystem.observeLodOcclusion();
3861
4066
  const targetReadbacks =
3862
4067
  request.targetReadbacks === undefined || request.targetReadbacks.length === 0
3863
4068
  ? undefined
@@ -4283,6 +4488,7 @@ async function makeWebGPURenderer(
4283
4488
  const previousDeviceScope = activeDeviceScope;
4284
4489
  activeDeviceScope = recoveryScope;
4285
4490
  pipelineState = candidatePipelineState;
4491
+ promotePipelineExtendedLighting(candidatePipelineState, activeDeviceScope);
4286
4492
  currentReceipts.clear();
4287
4493
  gpuStore.bindDeviceScope(activeDeviceScope);
4288
4494
  renderTargetHost.recover();
@@ -5333,6 +5539,7 @@ async function prepareMaterialShaders(
5333
5539
  if (!capCheck.ok) throw capCheck.error;
5334
5540
  storageBufferCapable = !webgl2Downlevel && capCheck.value;
5335
5541
  }
5542
+ const extendedLightingShaderAvailable = deriveExtendedLightingCapability(rhiDevice).admitted;
5336
5543
  // The shared PBR view/material ABI consumes the WebGPU minimum of 16
5337
5544
  // fragment sampled textures. The authored SpotLight projector is the
5338
5545
  // optional 17th texture; select its shader/BGL pair from the same limit so
@@ -5385,6 +5592,15 @@ async function prepareMaterialShaders(
5385
5592
  if (msEntry.variants.some((v) => 'VERTEX_COLOR_AVAILABLE' in v.defines)) {
5386
5593
  variantDefines.VERTEX_COLOR_AVAILABLE = false;
5387
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
+ }
5388
5604
  if (
5389
5605
  msEntry.identifier === 'forgeax::default-standard-pbr' &&
5390
5606
  msEntry.variants.some((v) => 'PROJECTOR_AVAILABLE' in v.defines)
@@ -5397,6 +5613,17 @@ async function prepareMaterialShaders(
5397
5613
  ) {
5398
5614
  variantDefines.TRANSMISSION_AVAILABLE = false;
5399
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
+ }
5400
5627
  // bug-20260708 M2 (a): PER_INSTANCE_REGION axis default = false at boot.
5401
5628
  // Per-entity spritePH path (`main-pass-sprite-draws.ts:511-513`) expects
5402
5629
  // `variantSet=undefined` to resolve to the PIR=false shader source (64B
@@ -5597,6 +5824,7 @@ async function buildReadyWebGPU(
5597
5824
  const capCheck = assertStorageBufferCap(capValue);
5598
5825
  storageBufferCapable = capCheck.ok ? !webgl2Downlevel && capCheck.value : !webgl2Downlevel;
5599
5826
  }
5827
+ const extendedLightingShaderAvailable = deriveExtendedLightingCapability(rhiDevice).admitted;
5600
5828
  const projectorAvailable =
5601
5829
  (rhiDevice.limits.maxSampledTexturesPerShaderStage ?? 0) >=
5602
5830
  STANDARD_PBR_REQUIRED_SAMPLED_TEXTURES;
@@ -5868,7 +6096,7 @@ async function buildReadyWebGPU(
5868
6096
  // bug-20260610: WebGL2 fallback path — when storageBufferCapable=false
5869
6097
  // the variant-aware Step 1b registers material shaders with the
5870
6098
  // STORAGE_BUFFER_AVAILABLE=false WGSL (uniform fallback for meshes /
5871
- // pointLights / instances), but Step 2 above triaged pbr/unlit/sprite/
6099
+ // meshes / instances), but Step 2 above triaged pbr/unlit/sprite/
5872
6100
  // pbr-skin/msdf-text from the FLAT `registry.entries()` map which only
5873
6101
  // contains the default (storage) variant. Compiling those with
5874
6102
  // STORAGE_BUFFER_AVAILABLE=true while the pipeline layout is built
@@ -5909,6 +6137,9 @@ async function buildReadyWebGPU(
5909
6137
  directionalPcssCapable(rhiDevice.caps.backendKind)) &&
5910
6138
  (!('PROJECTOR_AVAILABLE' in variant.defines) ||
5911
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 &&
5912
6143
  (!('SKINNING_DISABLED' in variant.defines) ||
5913
6144
  variant.defines.SKINNING_DISABLED === true),
5914
6145
  );
@@ -5949,19 +6180,24 @@ async function buildReadyWebGPU(
5949
6180
  const exact = ms.variants.find(
5950
6181
  (variant) =>
5951
6182
  variant.defines.STORAGE_BUFFER_AVAILABLE === storageBufferCapable &&
6183
+ (!('EXTENDED_LIGHTING_AVAILABLE' in variant.defines) ||
6184
+ variant.defines.EXTENDED_LIGHTING_AVAILABLE === extendedLightingShaderAvailable) &&
5952
6185
  (!('CLUSTER_FORWARD_AVAILABLE' in variant.defines) ||
5953
6186
  variant.defines.CLUSTER_FORWARD_AVAILABLE === standardBootClusterAxis()) &&
5954
6187
  (!('VERTEX_COLOR_AVAILABLE' in variant.defines) ||
5955
6188
  variant.defines.VERTEX_COLOR_AVAILABLE === false) &&
5956
6189
  (!('PROJECTOR_AVAILABLE' in variant.defines) ||
5957
6190
  variant.defines.PROJECTOR_AVAILABLE === projectorAvailable) &&
6191
+ variant.defines.PROBE_BLEND_AVAILABLE !== true &&
5958
6192
  (!('SKINNING_DISABLED' in variant.defines) ||
5959
6193
  variant.defines.SKINNING_DISABLED === true) &&
5960
6194
  (!('DIRECTIONAL_PCSS_AVAILABLE' in variant.defines) ||
5961
6195
  variant.defines.DIRECTIONAL_PCSS_AVAILABLE ===
5962
6196
  directionalPcssCapable(rhiDevice.caps.backendKind)) &&
5963
6197
  (!('TRANSMISSION_AVAILABLE' in variant.defines) ||
5964
- 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),
5965
6201
  );
5966
6202
  const variant = exact === undefined ? undefined : findVariantByKey(ms, exact.definesKey);
5967
6203
  if (variant !== undefined) {
@@ -6224,6 +6460,7 @@ async function buildReadyWebGPU(
6224
6460
  storageBufferCapable,
6225
6461
  directionalPcssCapable(rhiDevice.caps.backendKind),
6226
6462
  projectorAvailable,
6463
+ extendedLightingShaderAvailable,
6227
6464
  )
6228
6465
  : [];
6229
6466
 
@@ -6261,6 +6498,7 @@ async function buildReadyWebGPU(
6261
6498
  for (const hdrpVariant of selectHdrpPbrPrewarmVariants(
6262
6499
  pbrManifestEntry,
6263
6500
  storageBufferCapable,
6501
+ extendedLightingShaderAvailable,
6264
6502
  transmissionCapable,
6265
6503
  directionalPcssCapable(rhiDevice.caps.backendKind),
6266
6504
  projectorAvailable,
@@ -6845,6 +7083,7 @@ async function buildReadyWebGPU(
6845
7083
  ok(
6846
7084
  buildPbrPipelineLayouts(rhiDevice, {
6847
7085
  storageBuffer: storageBufferCapable,
7086
+ extendedLighting: extendedLightingShaderAvailable,
6848
7087
  projectorAvailable,
6849
7088
  }),
6850
7089
  ),
@@ -6857,6 +7096,7 @@ async function buildReadyWebGPU(
6857
7096
  const materialBglResult = ok(pbrLayouts.value.materialBgl);
6858
7097
  const meshArrayBglResult = ok(pbrLayouts.value.meshArrayBgl);
6859
7098
  const instancesBglResult = ok(pbrLayouts.value.instancesBgl);
7099
+ const probeInstancesBglResult = ok(pbrLayouts.value.probeInstancesBgl);
6860
7100
  const pipelineLayoutResult = ok(pbrLayouts.value.pipelineLayout);
6861
7101
 
6862
7102
  // feat-20260609-hdrp-cluster-fragment-ggx M4.5 / w36 (D-10 option A):
@@ -6869,6 +7109,7 @@ async function buildReadyWebGPU(
6869
7109
  // selectPipelineLayoutForVariant propagates that null so the clustered
6870
7110
  // material remains unavailable instead of binding an incompatible layout.
6871
7111
  let hdrpPbrPipelineLayoutHandle: PipelineLayout | null = null;
7112
+ let hdrpProbePbrPipelineLayoutHandle: PipelineLayout | null = null;
6872
7113
  // The cluster layout is storage-buffer backed. On the WebGL2/fallback
6873
7114
  // capability tier its uniform-buffer projection exceeds the backend's
6874
7115
  // fragment-stage limit, and HDRP is not a valid route there anyway.
@@ -6888,6 +7129,23 @@ async function buildReadyWebGPU(
6888
7129
  if (hdrpPlRes.ok) {
6889
7130
  hdrpPbrPipelineLayoutHandle = hdrpPlRes.value;
6890
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
+ }
6891
7149
  }
6892
7150
  }
6893
7151
 
@@ -6953,6 +7211,7 @@ async function buildReadyWebGPU(
6953
7211
  // explicit failure -- no silent fallback to URP layout, mirroring memory
6954
7212
  // anchor `hdrp-active-must-not-fallback-to-urp-pipeline`).
6955
7213
  let pbrSkinPipelineLayoutHandle: PipelineLayout | null = null;
7214
+ let pbrSkinProbePipelineLayoutHandle: PipelineLayout | null = null;
6956
7215
  // feat-20260611 R2 / M8 / w28: capture the 3-binding skin mesh-array BGL
6957
7216
  // produced by `buildPbrSkinLayouts` so the record stage can build a BG
6958
7217
  // matching `pbr-skin-pl` (pipeline-layout BGL[2] is this 3-entry skin
@@ -6976,6 +7235,7 @@ async function buildReadyWebGPU(
6976
7235
  );
6977
7236
  if (skinLayoutsResult.ok) {
6978
7237
  pbrSkinPipelineLayoutHandle = skinLayoutsResult.value.pipelineLayout;
7238
+ pbrSkinProbePipelineLayoutHandle = skinLayoutsResult.value.probePipelineLayout;
6979
7239
  pbrSkinMeshBindGroupLayoutHandle = skinLayoutsResult.value.meshArrayBgl;
6980
7240
  }
6981
7241
  }
@@ -7342,54 +7602,6 @@ async function buildReadyWebGPU(
7342
7602
  // count >= 2 in createRenderer.ts — definition + this exposure).
7343
7603
  setGrowMeshSsboHook(meshSsboController.growMeshSsbo, meshSsboController.state);
7344
7604
 
7345
- // feat-20260519-light-casters-point-spot-pbr M3 / w20 (D-S1 + D-S2):
7346
- // PointLight + SpotLight std430 storage buffers. Header 16 B (count u32 +
7347
- // 12 B pad) + 4-slot first-slice cap (32 B / 48 B per slot). Sized
7348
- // generously so that the buffer.size never changes once allocated; per-
7349
- // frame writeBuffer overwrites the entire range (D-S6 full rewrite +
7350
- // charter P3 explicit failure: stable buffer.size means no orphan slot
7351
- // reuse).
7352
- const POINT_LIGHTS_BUFFER_BYTES =
7353
- LIGHT_ARRAY_HEADER_BYTES + POINT_LIGHT_STD430_BYTES * LIGHT_ARRAY_MAX_SLOTS;
7354
- const SPOT_LIGHTS_BUFFER_BYTES =
7355
- LIGHT_ARRAY_HEADER_BYTES + SPOT_LIGHT_STD430_BYTES * LIGHT_ARRAY_MAX_SLOTS;
7356
- // bug-20260610: WebGL2 fallback — pointLights / spotLights buffers must
7357
- // be UNIFORM-usage (the storage variant of the shader is gated by
7358
- // STORAGE_BUFFER_AVAILABLE; the uniform-fallback path expects uniform
7359
- // buffers). Without this swap, the buffer-creation succeeds (driver still
7360
- // accepts STORAGE flag), but `createBindGroup` validation rejects the
7361
- // mismatch: `Usage flags BufferUsages(COPY_DST | STORAGE) ... do not
7362
- // contain required usage flags BufferUsages(UNIFORM)`.
7363
- const LIGHTS_BUFFER_USAGE = storageBufferCapable
7364
- ? GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST
7365
- : GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST;
7366
- const pointLightsBufferResult = runShimSyncStep(
7367
- () =>
7368
- rhiDevice.createBuffer({
7369
- label: 'pbr-point-lights-ssbo',
7370
- size: POINT_LIGHTS_BUFFER_BYTES,
7371
- usage: LIGHTS_BUFFER_USAGE,
7372
- mappedAtCreation: false,
7373
- }),
7374
- 'webgpu-runtime-error',
7375
- 'createBuffer (point-lights ssbo) succeeded',
7376
- 'check device.limits.maxStorageBufferBindingSize',
7377
- );
7378
- if (!pointLightsBufferResult.ok) throw pointLightsBufferResult.error;
7379
- const spotLightsBufferResult = runShimSyncStep(
7380
- () =>
7381
- rhiDevice.createBuffer({
7382
- label: 'pbr-spot-lights-ssbo',
7383
- size: SPOT_LIGHTS_BUFFER_BYTES,
7384
- usage: LIGHTS_BUFFER_USAGE,
7385
- mappedAtCreation: false,
7386
- }),
7387
- 'webgpu-runtime-error',
7388
- 'createBuffer (spot-lights ssbo) succeeded',
7389
- 'check device.limits.maxStorageBufferBindingSize',
7390
- );
7391
- if (!spotLightsBufferResult.ok) throw spotLightsBufferResult.error;
7392
-
7393
7605
  // feat-20260513-instanced-mesh M3 (T-M3-2): identity-mat4 fallback
7394
7606
  // storage buffer. Single 64-byte storage buffer carrying one identity
7395
7607
  // mat4 column-major. Renderables without an `Instances` component bind
@@ -7569,6 +7781,225 @@ async function buildReadyWebGPU(
7569
7781
  );
7570
7782
  if (!fallbackTextureViewResult.ok) throw fallbackTextureViewResult.error;
7571
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
+
7572
8003
  // Normal-slot fallback: 1x1 RGBA8 (128, 128, 255, 255). pbr.wgsl decodes
7573
8004
  // sample.rg * 2 - 1 + z = sqrt(1 - x^2 - y^2), so RG=(128,128)=0.5 maps
7574
8005
  // to tangent (0, 0, 1) -- zero perturbation when normalTexture is absent.
@@ -9389,6 +9820,8 @@ async function buildReadyWebGPU(
9389
9820
  // standard pipeline fields above).
9390
9821
  pbrPipelineLayout:
9391
9822
  unlitModule !== null && pbrModule !== null ? pipelineLayoutResult.value : null,
9823
+ pbrProbePipelineLayout:
9824
+ unlitModule !== null && pbrModule !== null ? pbrLayouts.value.probePipelineLayout : null,
9392
9825
  // feat-20260609-hdrp-cluster-fragment-ggx M4.5 / w36 (D-10 option A):
9393
9826
  // HDRP-variant PipelineLayout (4-BGL chain with HDRP unified 7-slot
9394
9827
  // group(2) BGL substituted for the URP 1-slot mesh-array BGL). Built
@@ -9398,6 +9831,8 @@ async function buildReadyWebGPU(
9398
9831
  // `selectPipelineLayoutForVariant` based on the caller's variantSet.
9399
9832
  hdrpPbrPipelineLayout:
9400
9833
  unlitModule !== null && pbrModule !== null ? hdrpPbrPipelineLayoutHandle : null,
9834
+ hdrpProbePbrPipelineLayout:
9835
+ unlitModule !== null && pbrModule !== null ? hdrpProbePbrPipelineLayoutHandle : null,
9401
9836
  hdrpClusterMembershipPipeline,
9402
9837
  hdrpClusterMembershipBindGroupLayout,
9403
9838
  // bug-20260611-skin-pipeline-layout: skin-variant PipelineLayout (4-BGL
@@ -9409,6 +9844,8 @@ async function buildReadyWebGPU(
9409
9844
  // `LayoutKind = 'pbr-skin'`.
9410
9845
  pbrSkinPipelineLayout:
9411
9846
  unlitModule !== null && pbrModule !== null ? pbrSkinPipelineLayoutHandle : null,
9847
+ pbrSkinProbePipelineLayout:
9848
+ unlitModule !== null && pbrModule !== null ? pbrSkinProbePipelineLayoutHandle : null,
9412
9849
  // feat-20260611 R2 / M8 / w28 (IS-14): record-stage skin BG plumbing.
9413
9850
  // Mirror the gating used by `pbrSkinPipelineLayout` -- both fields
9414
9851
  // come from the same `buildPbrSkinLayouts` call so they live or die
@@ -9435,6 +9872,7 @@ async function buildReadyWebGPU(
9435
9872
  colorAttachmentFormat: swapChainFormats.view,
9436
9873
  surfaceProfile: surfaceViewFormats ? 'dual-view' : 'raw-only',
9437
9874
  viewBindGroupLayout: viewBglResult.value,
9875
+ extendedLightingAvailable: extendedLightingShaderAvailable,
9438
9876
  projectorAvailable,
9439
9877
  materialBindGroupLayout: materialBglResult.value,
9440
9878
  meshBindGroupLayout: meshArrayBglResult.value,
@@ -9450,9 +9888,8 @@ async function buildReadyWebGPU(
9450
9888
  // `{ buffer: Buffer; sizeInBytes: number }` in T-M2-06 to match.
9451
9889
  materialUniformBuffer: meshSsboState.material,
9452
9890
  meshStorageBuffer: meshSsboState.mesh,
9453
- pointLightsBuffer: pointLightsBufferResult.value,
9454
- spotLightsBuffer: spotLightsBufferResult.value,
9455
9891
  instancesBindGroupLayout: instancesBglResult.value,
9892
+ probeInstancesBindGroupLayout: probeInstancesBglResult.value,
9456
9893
  identityInstanceBuffer: identityInstanceResult.value,
9457
9894
  defaultSampler: defaultSamplerResult.value,
9458
9895
  nearestSampler: nearestSamplerResult.value,
@@ -9475,6 +9912,23 @@ async function buildReadyWebGPU(
9475
9912
  // feat-20260520-directional-light-shadow-mapping M2 / w14 (D-1):
9476
9913
  // 1x1 depth32float fallback bound at viewBindGroup binding(3).
9477
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
+ : {}),
9478
9932
  // feat-20260612-point-light-shadows-urp-hdrp Round-2 F-1: 1x1x6
9479
9933
  // depth32float cube_array fallback bound at viewBindGroup binding(5)
9480
9934
  // when no PointLightShadow snapshots are active. Always-present