@forgeax/engine-render 0.1.19 → 0.1.21

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 (452) hide show
  1. package/README.md +215 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/assembly/device-feature-admission.d.ts +18 -0
  4. package/dist/assembly/device-feature-admission.d.ts.map +1 -0
  5. package/dist/assembly/directional-shadow-inspection.d.ts +38 -0
  6. package/dist/assembly/directional-shadow-inspection.d.ts.map +1 -0
  7. package/dist/assembly/factory.d.ts +34 -20
  8. package/dist/assembly/factory.d.ts.map +1 -1
  9. package/dist/assembly/host-contract.d.ts +9 -0
  10. package/dist/assembly/host-contract.d.ts.map +1 -1
  11. package/dist/assembly/render-target-host.d.ts +46 -0
  12. package/dist/assembly/render-target-host.d.ts.map +1 -0
  13. package/dist/assets/render-asset-port.d.ts.map +1 -1
  14. package/dist/authoring.mjs +3 -0
  15. package/dist/authoring.mjs.map +1 -1
  16. package/dist/capture/cube-views.d.ts +21 -0
  17. package/dist/capture/cube-views.d.ts.map +1 -0
  18. package/dist/capture/scheduler.d.ts +54 -0
  19. package/dist/capture/scheduler.d.ts.map +1 -0
  20. package/dist/{chunk-RJRUS55L.mjs → chunk-5YADUCA7.mjs} +1896 -1624
  21. package/dist/chunk-5YADUCA7.mjs.map +1 -0
  22. package/dist/{chunk-LOSO4CTV.mjs → chunk-6OPXDQ2E.mjs} +423 -46
  23. package/dist/chunk-6OPXDQ2E.mjs.map +1 -0
  24. package/dist/{chunk-IPXCABEP.mjs → chunk-BXBQQKFQ.mjs} +3 -9
  25. package/dist/chunk-BXBQQKFQ.mjs.map +1 -0
  26. package/dist/chunk-FS2KNUQH.mjs +1294 -0
  27. package/dist/chunk-FS2KNUQH.mjs.map +1 -0
  28. package/dist/components/camera.d.ts +1 -0
  29. package/dist/components/camera.d.ts.map +1 -1
  30. package/dist/components/cube-camera.d.ts +20 -0
  31. package/dist/components/cube-camera.d.ts.map +1 -0
  32. package/dist/components/directional-light.d.ts +11 -2
  33. package/dist/components/directional-light.d.ts.map +1 -1
  34. package/dist/components/directional-shadow-filter.d.ts +25 -0
  35. package/dist/components/directional-shadow-filter.d.ts.map +1 -0
  36. package/dist/components/index.d.ts +2 -0
  37. package/dist/components/index.d.ts.map +1 -1
  38. package/dist/components/light-helpers.d.ts.map +1 -1
  39. package/dist/components/reflection-probe.d.ts +17 -0
  40. package/dist/components/reflection-probe.d.ts.map +1 -0
  41. package/dist/components/spot-light.d.ts +12 -0
  42. package/dist/components/spot-light.d.ts.map +1 -1
  43. package/dist/construct-renderer.mjs +8552 -1612
  44. package/dist/construct-renderer.mjs.map +1 -1
  45. package/dist/device/gpu-residency.d.ts +19 -2
  46. package/dist/device/gpu-residency.d.ts.map +1 -1
  47. package/dist/environment/frame.d.ts.map +1 -1
  48. package/dist/environment/lifecycle.d.ts +5 -0
  49. package/dist/environment/lifecycle.d.ts.map +1 -1
  50. package/dist/errors/render.d.ts +209 -93
  51. package/dist/errors/render.d.ts.map +1 -1
  52. package/dist/extract/directional-shadow-projection.d.ts +23 -0
  53. package/dist/extract/directional-shadow-projection.d.ts.map +1 -0
  54. package/dist/extract/environment.d.ts +14 -0
  55. package/dist/extract/environment.d.ts.map +1 -1
  56. package/dist/extract/visibility.d.ts.map +1 -1
  57. package/dist/features/fullscreen.d.ts.map +1 -1
  58. package/dist/features/targets.d.ts.map +1 -1
  59. package/dist/features/types.d.ts +10 -0
  60. package/dist/features/types.d.ts.map +1 -1
  61. package/dist/fullscreen-post-process-pass.d.ts +13 -2
  62. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  63. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  64. package/dist/gpu-driven/production-raster.d.ts +0 -1
  65. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  66. package/dist/gpu-scene.d.ts.map +1 -1
  67. package/dist/gpu-usage.d.ts +2 -0
  68. package/dist/gpu-usage.d.ts.map +1 -1
  69. package/dist/hdrp-buffers.d.ts +19 -17
  70. package/dist/hdrp-buffers.d.ts.map +1 -1
  71. package/dist/ibl/IblPipelineCache.d.ts +4 -0
  72. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  73. package/dist/ibl/kernel-cache.d.ts +36 -0
  74. package/dist/ibl/kernel-cache.d.ts.map +1 -0
  75. package/dist/index.d.ts +35 -4
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.mjs +4 -4
  78. package/dist/index.mjs.map +1 -1
  79. package/dist/inspection-types.d.ts +51 -0
  80. package/dist/inspection-types.d.ts.map +1 -1
  81. package/dist/instances-derived-bounds.d.ts +50 -0
  82. package/dist/instances-derived-bounds.d.ts.map +1 -0
  83. package/dist/internal.mjs +1 -1
  84. package/dist/light-buffer-layout.d.ts +30 -13
  85. package/dist/light-buffer-layout.d.ts.map +1 -1
  86. package/dist/mesh-material-bindings.d.ts +54 -0
  87. package/dist/mesh-material-bindings.d.ts.map +1 -1
  88. package/dist/pbr-pipeline.d.ts +20 -7
  89. package/dist/pbr-pipeline.d.ts.map +1 -1
  90. package/dist/pipeline/standard-forward-lane.d.ts +2 -1
  91. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  92. package/dist/pipeline/standard-lighting/errors.d.ts +4 -0
  93. package/dist/pipeline/standard-lighting/errors.d.ts.map +1 -0
  94. package/dist/pipeline/standard-lighting/graph.d.ts +24 -0
  95. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -0
  96. package/dist/pipeline/standard-lighting/inspection.d.ts +38 -0
  97. package/dist/pipeline/standard-lighting/inspection.d.ts.map +1 -0
  98. package/dist/pipeline/standard-lighting/layout.d.ts +28 -0
  99. package/dist/pipeline/standard-lighting/layout.d.ts.map +1 -0
  100. package/dist/pipeline/standard-lighting/light-frame.d.ts +33 -0
  101. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -0
  102. package/dist/pipeline/standard-lighting/membership.d.ts +14 -0
  103. package/dist/pipeline/standard-lighting/membership.d.ts.map +1 -0
  104. package/dist/pipeline/standard-lighting/prepare.d.ts +16 -0
  105. package/dist/pipeline/standard-lighting/prepare.d.ts.map +1 -0
  106. package/dist/pipeline/standard-lighting/topology.d.ts +45 -0
  107. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -0
  108. package/dist/pipeline/standard-lighting/transport.d.ts +36 -0
  109. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -0
  110. package/dist/pipeline/standard-pipeline.d.ts +4 -13
  111. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  112. package/dist/pipeline/standard-post.d.ts.map +1 -1
  113. package/dist/pipeline/standard-profile.d.ts +12 -20
  114. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  115. package/dist/pipeline-spec.d.ts +40 -7
  116. package/dist/pipeline-spec.d.ts.map +1 -1
  117. package/dist/point-shadow-inspection.d.ts +21 -0
  118. package/dist/point-shadow-inspection.d.ts.map +1 -0
  119. package/dist/record/frame-lighting.d.ts +29 -32
  120. package/dist/record/frame-lighting.d.ts.map +1 -1
  121. package/dist/record/frame-snapshot.d.ts +65 -15
  122. package/dist/record/frame-snapshot.d.ts.map +1 -1
  123. package/dist/record/frame-targets.d.ts +5 -0
  124. package/dist/record/frame-targets.d.ts.map +1 -1
  125. package/dist/record/frame.d.ts +12 -5
  126. package/dist/record/frame.d.ts.map +1 -1
  127. package/dist/record/gpu-pass-timing/contract.d.ts +96 -0
  128. package/dist/record/gpu-pass-timing/contract.d.ts.map +1 -0
  129. package/dist/record/gpu-pass-timing/errors.d.ts +18 -0
  130. package/dist/record/gpu-pass-timing/errors.d.ts.map +1 -0
  131. package/dist/record/gpu-pass-timing/index.d.ts +8 -0
  132. package/dist/record/gpu-pass-timing/index.d.ts.map +1 -0
  133. package/dist/record/gpu-pass-timing/parser.d.ts +11 -0
  134. package/dist/record/gpu-pass-timing/parser.d.ts.map +1 -0
  135. package/dist/record/gpu-pass-timing/session.d.ts +43 -0
  136. package/dist/record/gpu-pass-timing/session.d.ts.map +1 -0
  137. package/dist/record/gpu-timing.d.ts +37 -0
  138. package/dist/record/gpu-timing.d.ts.map +1 -0
  139. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  140. package/dist/record/main-pass-material.d.ts +23 -2
  141. package/dist/record/main-pass-material.d.ts.map +1 -1
  142. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  143. package/dist/record/main-pass.d.ts +2 -0
  144. package/dist/record/main-pass.d.ts.map +1 -1
  145. package/dist/record/render-context.d.ts +48 -3
  146. package/dist/record/render-context.d.ts.map +1 -1
  147. package/dist/record/shadow-pass.d.ts.map +1 -1
  148. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  149. package/dist/record/typed-frame-graph.d.ts +81 -5
  150. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  151. package/dist/record/view-ubo.d.ts +7 -4
  152. package/dist/record/view-ubo.d.ts.map +1 -1
  153. package/dist/reflection/filter.d.ts +22 -0
  154. package/dist/reflection/filter.d.ts.map +1 -0
  155. package/dist/reflection/gpu-table.d.ts +25 -0
  156. package/dist/reflection/gpu-table.d.ts.map +1 -0
  157. package/dist/reflection/projection.d.ts +91 -0
  158. package/dist/reflection/projection.d.ts.map +1 -0
  159. package/dist/render-contract.d.ts +114 -7
  160. package/dist/render-contract.d.ts.map +1 -1
  161. package/dist/render-data.d.ts +10 -6
  162. package/dist/render-data.d.ts.map +1 -1
  163. package/dist/render-graph-primitives.d.ts +1 -0
  164. package/dist/render-graph-primitives.d.ts.map +1 -1
  165. package/dist/render-pipeline.d.ts +78 -2
  166. package/dist/render-pipeline.d.ts.map +1 -1
  167. package/dist/render-system-extract.d.ts +113 -20
  168. package/dist/render-system-extract.d.ts.map +1 -1
  169. package/dist/render-system.d.ts +24 -3
  170. package/dist/render-system.d.ts.map +1 -1
  171. package/dist/renderer-inspect.d.ts +29 -0
  172. package/dist/renderer-inspect.d.ts.map +1 -0
  173. package/dist/renderer.d.ts +43 -0
  174. package/dist/renderer.d.ts.map +1 -0
  175. package/dist/scene/render-scene.d.ts +14 -0
  176. package/dist/scene/render-scene.d.ts.map +1 -1
  177. package/dist/shadow-atlas.d.ts +3 -3
  178. package/dist/shadow-atlas.d.ts.map +1 -1
  179. package/dist/ssao-buffers.d.ts +3 -0
  180. package/dist/ssao-buffers.d.ts.map +1 -1
  181. package/dist/targets/contracts.d.ts +66 -0
  182. package/dist/targets/contracts.d.ts.map +1 -0
  183. package/dist/targets/graph-projection.d.ts +29 -0
  184. package/dist/targets/graph-projection.d.ts.map +1 -0
  185. package/dist/targets/material-source.d.ts +34 -0
  186. package/dist/targets/material-source.d.ts.map +1 -0
  187. package/dist/targets/owner.d.ts +32 -0
  188. package/dist/targets/owner.d.ts.map +1 -0
  189. package/dist/targets/physical.d.ts +18 -0
  190. package/dist/targets/physical.d.ts.map +1 -0
  191. package/dist/targets/readback.d.ts +39 -0
  192. package/dist/targets/readback.d.ts.map +1 -0
  193. package/dist/temporal/frame.d.ts +44 -0
  194. package/dist/temporal/frame.d.ts.map +1 -0
  195. package/dist/temporal/index.mjs +3 -3
  196. package/dist/typed-render-graph-primitives.d.ts +6 -1
  197. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  198. package/dist/typed-shadow-atlas.d.ts +10 -0
  199. package/dist/typed-shadow-atlas.d.ts.map +1 -0
  200. package/dist/typed-shadow-passes.d.ts +1 -1
  201. package/dist/typed-shadow-passes.d.ts.map +1 -1
  202. package/dist/volume/capability.d.ts +57 -0
  203. package/dist/volume/capability.d.ts.map +1 -0
  204. package/dist/volume/component.d.ts +61 -0
  205. package/dist/volume/component.d.ts.map +1 -0
  206. package/dist/volume/extract.d.ts +11 -0
  207. package/dist/volume/extract.d.ts.map +1 -0
  208. package/dist/volume/inspection.d.ts +38 -0
  209. package/dist/volume/inspection.d.ts.map +1 -0
  210. package/dist/volume/passes.d.ts +38 -0
  211. package/dist/volume/passes.d.ts.map +1 -0
  212. package/dist/volume/recovery.d.ts +38 -0
  213. package/dist/volume/recovery.d.ts.map +1 -0
  214. package/dist/volume/resources.d.ts +57 -0
  215. package/dist/volume/resources.d.ts.map +1 -0
  216. package/dist/volume/temporal.d.ts +35 -0
  217. package/dist/volume/temporal.d.ts.map +1 -0
  218. package/package.json +20 -20
  219. package/src/__tests__/bloom-exact-zero.unit.test.ts +48 -19
  220. package/src/__tests__/bloom-lifecycle.integration.test.ts +41 -13
  221. package/src/__tests__/camera-target-consumption.unit.test.ts +53 -0
  222. package/src/__tests__/cluster-binner.unit.test.ts +35 -0
  223. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +7 -2
  224. package/src/__tests__/cube-camera-scheduler.unit.test.ts +70 -0
  225. package/src/__tests__/cube-camera-single-submit.perf.test.ts +75 -0
  226. package/src/__tests__/cube-camera-view-matrices.unit.test.ts +41 -0
  227. package/src/__tests__/cube-camera-vocabulary.unit.test.ts +47 -0
  228. package/src/__tests__/cube-camera.integration.test.ts +42 -0
  229. package/src/__tests__/device-feature-admission.unit.test.ts +73 -0
  230. package/src/__tests__/direct-light-snapshot.unit.test.ts +3 -3
  231. package/src/__tests__/docs-gate-green.unit.test.ts +38 -0
  232. package/src/__tests__/factory-contract.integration.test.ts +255 -29
  233. package/src/__tests__/fog-component.unit.test.ts +67 -0
  234. package/src/__tests__/fog.integration.test.ts +86 -0
  235. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +37 -0
  236. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +10 -9
  237. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +17 -1
  238. package/src/__tests__/gpu-driven-production.integration.test.ts +16 -17
  239. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +12 -4
  240. package/src/__tests__/gpu-pass-timing-browser-runner.ts +281 -0
  241. package/src/__tests__/gpu-pass-timing-coexistence.integration.test.ts +80 -0
  242. package/src/__tests__/gpu-pass-timing-contract.unit.test.ts +243 -0
  243. package/src/__tests__/gpu-pass-timing-exact-zero.unit.test.ts +131 -0
  244. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +230 -0
  245. package/src/__tests__/gpu-pass-timing-observation.unit.test.ts +233 -0
  246. package/src/__tests__/gpu-pass-timing-session.unit.test.ts +438 -0
  247. package/src/__tests__/gpu-pass-timing.browser.test.ts +26 -0
  248. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +125 -0
  249. package/src/__tests__/hdrp-group2-visibility.unit.test.ts +24 -0
  250. package/src/__tests__/instances-culling.integration.test.ts +94 -0
  251. package/src/__tests__/instances-derived-bounds.unit.test.ts +118 -0
  252. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +14 -14
  253. package/src/__tests__/material-observation.integration.test.ts +101 -0
  254. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
  255. package/src/__tests__/pbr-view-layout.unit.test.ts +16 -0
  256. package/src/__tests__/point-shadow-inspection.unit.test.ts +60 -0
  257. package/src/__tests__/public-surface.test-d.ts +10 -0
  258. package/src/__tests__/reflection-probe-authoring.unit.test.ts +71 -0
  259. package/src/__tests__/reflection-probe-filter.unit.test.ts +54 -0
  260. package/src/__tests__/reflection-probe-ibl-topology.unit.test.ts +45 -0
  261. package/src/__tests__/reflection-probe-pass-encode.integration.test.ts +101 -0
  262. package/src/__tests__/reflection-probe-performance.perf.test.ts +60 -0
  263. package/src/__tests__/reflection-probe-recovery.integration.test.ts +55 -0
  264. package/src/__tests__/reflection-probe-selection.unit.test.ts +46 -0
  265. package/src/__tests__/render-error-code-owner.test-d.ts +23 -3
  266. package/src/__tests__/render-feature-docs.unit.test.ts +1 -1
  267. package/src/__tests__/render-target-admission.unit.test.ts +80 -0
  268. package/src/__tests__/render-target-graph-projection.unit.test.ts +102 -0
  269. package/src/__tests__/render-target-inspection.unit.test.ts +52 -0
  270. package/src/__tests__/render-target-material-source.test-d.ts +30 -0
  271. package/src/__tests__/render-target-material-source.unit.test.ts +97 -0
  272. package/src/__tests__/render-target-owner-placement.unit.test.ts +203 -0
  273. package/src/__tests__/render-target-owner.unit.test.ts +122 -0
  274. package/src/__tests__/render-target-probe-soak.perf.test.ts +94 -0
  275. package/src/__tests__/render-target-public-schema.json +107 -0
  276. package/src/__tests__/render-target-public-schema.unit.test.ts +91 -0
  277. package/src/__tests__/render-target-public-surface.test-d.ts +83 -0
  278. package/src/__tests__/render-target-readback.integration.test.ts +90 -0
  279. package/src/__tests__/render-target-recovery.integration.test.ts +85 -0
  280. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +76 -0
  281. package/src/__tests__/renderer-frame-transaction.integration.test.ts +1 -1
  282. package/src/__tests__/renderer-lifecycle.integration.test.ts +14 -0
  283. package/src/__tests__/shader-stage-surface.unit.test.ts +19 -4
  284. package/src/__tests__/skin-motion-regression.unit.test.ts +132 -0
  285. package/src/__tests__/skinned-shadow-caster.test.ts +32 -2
  286. package/src/__tests__/standard-cluster-capability.integration.test.ts +63 -0
  287. package/src/__tests__/standard-cluster-transport.unit.test.ts +121 -0
  288. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +232 -0
  289. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +8 -5
  290. package/src/__tests__/standard-lighting-inspection.integration.test.ts +138 -0
  291. package/src/__tests__/standard-lighting-ssot.unit.test.ts +18 -0
  292. package/src/__tests__/standard-lighting-write.unit.test.ts +377 -0
  293. package/src/__tests__/standard-lighting.integration.test.ts +67 -0
  294. package/src/__tests__/standard-pipeline.dawn.test.ts +206 -5
  295. package/src/__tests__/standard-pipeline.integration.test.ts +181 -150
  296. package/src/__tests__/standard-probe-binding.unit.test.ts +25 -0
  297. package/src/__tests__/standard-profile-oracle.unit.test.ts +23 -36
  298. package/src/__tests__/typed-pipeline-topology.unit.test.ts +395 -14
  299. package/src/__tests__/urp-pipeline-targets.unit.test.ts +2 -2
  300. package/src/__tests__/visibility-resolver.unit.test.ts +15 -0
  301. package/src/__tests__/volume-optics-oracle.unit.test.ts +85 -0
  302. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +655 -0
  303. package/src/__tests__/volumetric-fog-component.unit.test.ts +71 -0
  304. package/src/__tests__/volumetric-fog-composite-stage-readback.dawn.test.ts +376 -0
  305. package/src/__tests__/volumetric-fog-errors.unit.test.ts +51 -0
  306. package/src/__tests__/volumetric-fog-inspection.unit.test.ts +77 -0
  307. package/src/__tests__/volumetric-fog-light-capability.unit.test.ts +19 -0
  308. package/src/__tests__/volumetric-fog-light-consumers.integration.test.ts +18 -0
  309. package/src/__tests__/volumetric-fog-light-errors.unit.test.ts +17 -0
  310. package/src/__tests__/volumetric-fog-light-topology.unit.test.ts +21 -0
  311. package/src/__tests__/volumetric-fog-light-transaction.integration.test.ts +53 -0
  312. package/src/__tests__/volumetric-fog-m9-watcher.integration.test.ts +27 -0
  313. package/src/__tests__/volumetric-fog-point-light-owner.integration.test.ts +44 -0
  314. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +49 -0
  315. package/src/__tests__/volumetric-fog-point-spot-contract.unit.test.ts +28 -0
  316. package/src/__tests__/volumetric-fog-point-spot-lkg.integration.test.ts +65 -0
  317. package/src/__tests__/volumetric-fog-point-spot-selection.integration.test.ts +49 -0
  318. package/src/__tests__/volumetric-fog-point-spot-temporal.unit.test.ts +55 -0
  319. package/src/__tests__/volumetric-fog-projector-contract.unit.test.ts +22 -0
  320. package/src/__tests__/volumetric-fog-projector-inspection.unit.test.ts +12 -0
  321. package/src/__tests__/volumetric-fog-projector-surface-volume.integration.test.ts +11 -0
  322. package/src/__tests__/volumetric-fog-projector-transaction.integration.test.ts +26 -0
  323. package/src/__tests__/volumetric-fog-recovery.integration.test.ts +47 -0
  324. package/src/__tests__/volumetric-fog-selected-light-resolution.integration.test.ts +63 -0
  325. package/src/__tests__/volumetric-fog-spot-closure.integration.test.ts +26 -0
  326. package/src/__tests__/volumetric-fog-spot-contract.test-d.ts +21 -0
  327. package/src/__tests__/volumetric-fog-spot-contract.unit.test.ts +27 -0
  328. package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +14 -0
  329. package/src/__tests__/volumetric-fog-spot-optics.integration.test.ts +29 -0
  330. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +550 -0
  331. package/src/__tests__/volumetric-fog-temporal-selected-light.unit.test.ts +33 -0
  332. package/src/__tests__/volumetric-fog-temporal.unit.test.ts +45 -0
  333. package/src/__tests__/volumetric-fog-topology.unit.test.ts +237 -0
  334. package/src/__tests__/volumetric-fog-transparent.integration.test.ts +27 -0
  335. package/src/assembly/device-feature-admission.ts +73 -0
  336. package/src/assembly/directional-shadow-inspection.ts +122 -0
  337. package/src/assembly/factory.ts +1093 -246
  338. package/src/assembly/host-contract.ts +25 -0
  339. package/src/assembly/render-target-host.ts +847 -0
  340. package/src/assets/asset-decoders.ts +1 -1
  341. package/src/assets/render-asset-port.ts +7 -1
  342. package/src/capture/cube-views.ts +69 -0
  343. package/src/capture/scheduler.ts +265 -0
  344. package/src/components/__tests__/schema-contract.unit.test.ts +12 -0
  345. package/src/components/__tests__/schema-parity.unit.test.ts +9 -0
  346. package/src/components/camera.ts +4 -0
  347. package/src/components/cube-camera.ts +52 -0
  348. package/src/components/directional-light.ts +22 -2
  349. package/src/components/directional-shadow-filter.ts +57 -0
  350. package/src/components/index.ts +2 -0
  351. package/src/components/light-helpers.ts +85 -7
  352. package/src/components/reflection-probe.ts +49 -0
  353. package/src/components/spot-light.ts +22 -3
  354. package/src/device/gpu-residency.ts +168 -58
  355. package/src/environment/frame.ts +6 -24
  356. package/src/environment/lifecycle.ts +12 -0
  357. package/src/errors/render.ts +405 -143
  358. package/src/extract/directional-shadow-projection.ts +61 -0
  359. package/src/extract/environment.ts +57 -0
  360. package/src/extract/visibility.ts +25 -1
  361. package/src/features/fullscreen.ts +3 -0
  362. package/src/features/targets.ts +4 -1
  363. package/src/features/types.ts +10 -0
  364. package/src/fullscreen-post-process-pass.ts +43 -2
  365. package/src/gpu-driven/batch-topology.ts +1 -0
  366. package/src/gpu-driven/production-raster.ts +1 -3
  367. package/src/gpu-scene.ts +8 -1
  368. package/src/gpu-usage.ts +2 -0
  369. package/src/hdrp-buffers.ts +165 -59
  370. package/src/ibl/IblPipelineCache.ts +11 -0
  371. package/src/ibl/kernel-cache.ts +88 -0
  372. package/src/index.ts +186 -1
  373. package/src/inspection-types.ts +56 -0
  374. package/src/instances-derived-bounds.ts +171 -0
  375. package/src/light-buffer-layout.ts +74 -26
  376. package/src/mesh-material-bindings.ts +132 -0
  377. package/src/pbr-pipeline.ts +139 -27
  378. package/src/pipeline/standard-forward-lane.ts +65 -15
  379. package/src/pipeline/standard-lighting/errors.ts +6 -0
  380. package/src/pipeline/standard-lighting/graph.ts +157 -0
  381. package/src/pipeline/standard-lighting/inspection.ts +88 -0
  382. package/src/pipeline/standard-lighting/layout.ts +31 -0
  383. package/src/pipeline/standard-lighting/light-frame.ts +34 -0
  384. package/src/pipeline/standard-lighting/membership.ts +43 -0
  385. package/src/pipeline/standard-lighting/prepare.ts +37 -0
  386. package/src/pipeline/standard-lighting/topology.ts +120 -0
  387. package/src/pipeline/standard-lighting/transport.ts +77 -0
  388. package/src/pipeline/standard-pipeline.ts +141 -192
  389. package/src/pipeline/standard-post.ts +26 -7
  390. package/src/pipeline/standard-profile.ts +23 -30
  391. package/src/pipeline-spec.ts +84 -21
  392. package/src/point-shadow-inspection.ts +55 -0
  393. package/src/record/__tests__/temporal-payload.unit.test.ts +42 -1
  394. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +73 -0
  395. package/src/record/__tests__/view-ubo-layout.unit.test.ts +40 -0
  396. package/src/record/frame-lighting.ts +470 -516
  397. package/src/record/frame-snapshot.ts +76 -28
  398. package/src/record/frame-targets.ts +23 -2
  399. package/src/record/frame.ts +1169 -79
  400. package/src/record/gpu-pass-timing/contract.ts +180 -0
  401. package/src/record/gpu-pass-timing/errors.ts +34 -0
  402. package/src/record/gpu-pass-timing/index.ts +37 -0
  403. package/src/record/gpu-pass-timing/parser.ts +97 -0
  404. package/src/record/gpu-pass-timing/session.ts +938 -0
  405. package/src/record/gpu-timing.ts +233 -0
  406. package/src/record/main-pass-geometry.ts +120 -80
  407. package/src/record/main-pass-material.ts +101 -11
  408. package/src/record/main-pass-sprite-draws.ts +54 -25
  409. package/src/record/main-pass.ts +71 -15
  410. package/src/record/render-context.ts +77 -0
  411. package/src/record/shadow-pass.ts +104 -66
  412. package/src/record/skybox-post-pass.ts +20 -4
  413. package/src/record/typed-frame-graph.ts +1016 -32
  414. package/src/record/view-ubo.ts +63 -28
  415. package/src/reflection/filter.ts +81 -0
  416. package/src/reflection/gpu-table.ts +44 -0
  417. package/src/reflection/projection.ts +281 -0
  418. package/src/render-contract.ts +147 -4
  419. package/src/render-data.ts +50 -22
  420. package/src/render-graph-primitives.ts +9 -4
  421. package/src/render-pipeline.ts +161 -2
  422. package/src/render-system-extract.ts +886 -103
  423. package/src/render-system.ts +1174 -66
  424. package/src/renderer-inspect.ts +66 -0
  425. package/src/renderer.ts +222 -0
  426. package/src/scene/render-scene.ts +117 -9
  427. package/src/shadow-atlas.ts +5 -5
  428. package/src/ssao-buffers.ts +9 -2
  429. package/src/targets/contracts.ts +182 -0
  430. package/src/targets/graph-projection.ts +169 -0
  431. package/src/targets/material-source.ts +143 -0
  432. package/src/targets/owner.ts +214 -0
  433. package/src/targets/physical.ts +18 -0
  434. package/src/targets/readback.ts +153 -0
  435. package/src/temporal/frame.ts +128 -0
  436. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +61 -23
  437. package/src/typed-render-graph-primitives.ts +34 -2
  438. package/src/typed-shadow-atlas.ts +17 -0
  439. package/src/typed-shadow-passes.ts +47 -40
  440. package/src/volume/capability.ts +173 -0
  441. package/src/volume/component.ts +110 -0
  442. package/src/volume/extract.ts +21 -0
  443. package/src/volume/inspection.ts +78 -0
  444. package/src/volume/passes.ts +686 -0
  445. package/src/volume/recovery.ts +86 -0
  446. package/src/volume/resources.ts +236 -0
  447. package/src/volume/temporal.ts +89 -0
  448. package/dist/chunk-IPXCABEP.mjs.map +0 -1
  449. package/dist/chunk-LOSO4CTV.mjs.map +0 -1
  450. package/dist/chunk-RJRUS55L.mjs.map +0 -1
  451. package/dist/chunk-YTQ5C7YQ.mjs +0 -584
  452. package/dist/chunk-YTQ5C7YQ.mjs.map +0 -1
@@ -2,39 +2,42 @@
2
2
  // feat-20260704 M5/w31: further-split from frame.ts (AC-05 <=1500 lines/file).
3
3
  // Pure leaf helpers invoked once each from recordFrame; behavior verbatim.
4
4
 
5
- import { mat4, type Vec3, vec3 } from '@forgeax/engine-math';
5
+ import { mat4, vec3 } from '@forgeax/engine-math';
6
6
  import {
7
7
  type BindGroup,
8
+ type BindGroupEntry,
8
9
  type BindGroupLayout,
10
+ type RhiDevice,
9
11
  RhiError,
10
12
  type TextureView,
11
13
  } from '@forgeax/engine-rhi';
12
- import { toShared } from '@forgeax/engine-types';
13
- import { bin, type ClusterBinProfilePhase, type ClusterBinProfileRunner } from '../cluster-binner';
14
+ import { err, ok, type Result, toShared } from '@forgeax/engine-types';
14
15
  import {
15
- HdrpIndexListOverflowError,
16
- HdrpLightBudgetExceededError,
17
16
  PointShadowAtlasBoundsViolationError,
18
17
  PointShadowAtlasUninitializedError,
18
+ type RenderError,
19
+ RendererOperationError,
20
+ StandardClusterTransportUnavailableError,
19
21
  } from '../errors/render';
20
22
  import {
21
23
  createHdrpClusterMembershipBindGroup,
22
24
  createHdrpUnifiedBindGroup,
23
25
  getOrCreateHdrpBuffers,
24
- HDRP_UNIFORM_LIGHT_CAPACITY,
25
26
  packClusterUniform,
26
27
  } from '../hdrp-buffers';
28
+ import { packLightSlot } from '../light-buffer-layout';
27
29
  import {
28
- LIGHT_ARRAY_HEADER_BYTES,
29
- LIGHT_ARRAY_MAX_SLOTS,
30
- POINT_LIGHT_STD430_BYTES,
31
- packLightArrayHeader,
32
- packLightSlot,
33
- packPointLight,
34
- packSpotLight,
35
- SPOT_LIGHT_STD430_BYTES,
36
- } from '../light-buffer-layout';
37
- import { DEFAULT_CLUSTER_GRID, LIGHT_INDEX_LIST_CAPACITY } from '../pipeline/standard-profile';
30
+ type PreparedStandardLighting,
31
+ prepareStandardLighting,
32
+ } from '../pipeline/standard-lighting/prepare';
33
+ import type { StandardTopologyInputValue } from '../pipeline/standard-lighting/topology';
34
+ import {
35
+ type StandardClusterTransport,
36
+ selectStandardClusterTransport,
37
+ } from '../pipeline/standard-lighting/transport';
38
+ import { DEFAULT_CLUSTER_GRID, DEFAULT_STANDARD_PROFILE } from '../pipeline/standard-profile';
39
+ import type { ReflectionProbeTable } from '../reflection/gpu-table';
40
+ import type { ReflectionProbeSelectionResult } from '../reflection/projection';
38
41
  import type { CameraSnapshot, RenderHdrpClusterPhase } from '../render-contract';
39
42
  import type {
40
43
  DirectionalLightSnapshot,
@@ -43,7 +46,11 @@ import type {
43
46
  SkyboxSnapshot,
44
47
  SkylightSnapshot,
45
48
  } from '../render-system-extract';
46
- import { ShadowAtlas } from '../shadow-atlas';
49
+ import {
50
+ SHADOW_ATLAS_DEFAULT_FACE_SIZE,
51
+ SHADOW_ATLAS_DEFAULT_LAYERS,
52
+ ShadowAtlas,
53
+ } from '../shadow-atlas';
47
54
  import { getOrCreateSsaoBuffers } from '../ssao-buffers';
48
55
  import { getSsaoParameters } from '../ssao-config';
49
56
  import type { BindGroupCounts, RenderFrameState } from './frame-snapshot';
@@ -52,13 +59,43 @@ import {
52
59
  computeViewMatrix,
53
60
  isLitMaterialSnapshot,
54
61
  warnMultiLightDirectional,
55
- warnMultiLightPoint,
56
- warnMultiLightSpot,
57
62
  } from './helpers';
58
63
  import { getOrCreateFromChain, MESH_SSBO_BYTES, MESH_UBO_FULL_ARRAY_BYTES } from './mesh-ssbo';
59
64
  import type { PipelineState, RecordProfileRunner, RenderSystemInternals } from './render-context';
60
65
  import { POINTS_LINES_VIEW_BYTES, VIEW_UNIFORM_BYTES } from './view-ubo';
61
66
 
67
+ export interface StandardReflectionProbeBinding {
68
+ readonly probeIndex: number | undefined;
69
+ readonly useSkylight: boolean;
70
+ }
71
+
72
+ export function resolveReflectionProbeBinding(
73
+ selection: ReflectionProbeSelectionResult,
74
+ table?: ReflectionProbeTable,
75
+ ): StandardReflectionProbeBinding {
76
+ if (selection.kind !== 'probe') return { probeIndex: undefined, useSkylight: true };
77
+ const primitiveKey = `${selection.worldId}:${selection.entityKey}`;
78
+ const tableIndex = table?.cpuIndexByPrimitive(primitiveKey);
79
+ return { probeIndex: tableIndex ?? selection.entityKey, useSkylight: false };
80
+ }
81
+
82
+ export function projectDeviceOwnedBindGroup<T>(
83
+ state:
84
+ | {
85
+ readonly device: RhiDevice;
86
+ readonly layout: BindGroupLayout;
87
+ readonly bindGroup: T;
88
+ }
89
+ | null
90
+ | undefined,
91
+ device: RhiDevice,
92
+ layout: BindGroupLayout | null,
93
+ ): T | null {
94
+ return state !== null && state !== undefined && state.device === device && state.layout === layout
95
+ ? state.bindGroup
96
+ : null;
97
+ }
98
+
62
99
  function runHdrpClusterProfilePhase<T>(
63
100
  runner: RecordProfileRunner | undefined,
64
101
  phase: RenderHdrpClusterPhase,
@@ -67,6 +104,26 @@ function runHdrpClusterProfilePhase<T>(
67
104
  return runner === undefined ? action() : runner(phase, action);
68
105
  }
69
106
 
107
+ function mapClusterUploadFailure(
108
+ resource: string,
109
+ error: {
110
+ readonly code: string;
111
+ readonly expected: string;
112
+ readonly hint: string;
113
+ readonly detail?: unknown;
114
+ },
115
+ ): RendererOperationError<'device-operation-failed'> {
116
+ return new RendererOperationError('device-operation-failed', {
117
+ operation: 'draw',
118
+ cause: {
119
+ code: error.code,
120
+ expected: `queue.writeBuffer succeeds for Standard Cluster ${resource}`,
121
+ hint: `repair the device write for Standard Cluster ${resource} and retry the frame`,
122
+ detail: error,
123
+ },
124
+ });
125
+ }
126
+
70
127
  /**
71
128
  * feat-20260704 M3/w18: per-frame bind-group cache resolution, extracted
72
129
  * verbatim from `recordFrame`. Walks the handle-identity WeakMap caches
@@ -87,8 +144,11 @@ export function buildPerFrameBindGroups(
87
144
  graphTargets?: {
88
145
  readonly directionalShadow?: TextureView | undefined;
89
146
  readonly spotShadow?: TextureView | undefined;
147
+ readonly projector?: TextureView | undefined;
148
+ readonly projectorSampler?: import('@forgeax/engine-rhi').Sampler | undefined;
90
149
  },
91
150
  includeView = true,
151
+ standardLighting?: StandardTopologyInputValue,
92
152
  ): {
93
153
  viewBindGroup: BindGroup | null;
94
154
  meshBindGroup: BindGroup | null;
@@ -102,12 +162,31 @@ export function buildPerFrameBindGroups(
102
162
  // Mesh (#2) chain = inner b0 buffer (meshStorageBuffer.buffer); variant 'mesh'.
103
163
  let viewBindGroup: BindGroup | null = null;
104
164
  let meshBindGroup: BindGroup | null = null;
105
- // feat-20260609-hdrp-cluster-fragment-ggx M4 / w19: HDRP unified group(2)
106
- // BindGroup. Non-null when `frameState.isHdrpActive` AND HDRP buffer
107
- // allocation succeeded; consumed by recordMainPass at `setBindGroup(2, ...)`.
165
+ // Standard clustered group(2) bind group. Non-null when the prepared
166
+ // topology selects the cluster transport and its persistent allocation
167
+ // succeeds; consumed by recordMainPass at `setBindGroup(2, ...)`.
108
168
  let hdrpClusterBindGroup: BindGroup | null = null;
109
- const hdrpClusterMembershipBindGroup: BindGroup | null = frameState.isHdrpActive
110
- ? frameState.hdrpClusterMembershipBindGroup
169
+ // The prepared Standard topology is the per-frame authority for whether
170
+ // group(2) carries Cluster storage. A missing declaration is the prewarm /
171
+ // non-Standard path and must not allocate or bind the Cluster bundle based
172
+ // on the install-time pipeline identity alone.
173
+ const clusteredLighting = standardLighting?.kind === 'clustered';
174
+ const membershipLayout = pipelineState.hdrpClusterMembershipBindGroupLayout;
175
+ const membership = frameState.hdrpClusterMembership;
176
+ const expectedHdrpBuffers =
177
+ hasValidated && clusteredLighting
178
+ ? getOrCreateHdrpBuffers(internals, frameState.installedPipelineConfig?.clusterGrid)
179
+ : null;
180
+ const currentMembership =
181
+ expectedHdrpBuffers !== null &&
182
+ membership?.clusterGridBuffer === expectedHdrpBuffers.clusterGridBuffer
183
+ ? projectDeviceOwnedBindGroup(membership, internals.device, membershipLayout)
184
+ : null;
185
+ if (membership !== null && currentMembership === null) {
186
+ frameState.hdrpClusterMembership = null;
187
+ }
188
+ const hdrpClusterMembershipBindGroup: BindGroup | null = clusteredLighting
189
+ ? currentMembership
111
190
  : null;
112
191
  if (hasValidated) {
113
192
  // Shadow atlas view comes from the typed graph target.
@@ -150,6 +229,9 @@ export function buildPerFrameBindGroups(
150
229
  graphSpotShadowView !== undefined
151
230
  ? graphSpotShadowView
152
231
  : pipelineState.shadowFallbackTextureView;
232
+ const projectorAvailable = pipelineState.projectorAvailable !== false;
233
+ const b11View = graphTargets?.projector ?? pipelineState.defaultWhiteTextureView;
234
+ const b12Sampler = graphTargets?.projectorSampler ?? pipelineState.defaultSampler;
153
235
  viewBindGroup = getOrCreateFromChain(
154
236
  frameState.viewBindGroupCache,
155
237
  [
@@ -161,110 +243,118 @@ export function buildPerFrameBindGroups(
161
243
  b5View,
162
244
  pipelineState.shadowParamsBuffer,
163
245
  b8View,
246
+ ...(projectorAvailable ? [b11View, b12Sampler] : []),
164
247
  pipelineState.pointsLinesViewBuffer ?? pipelineState.viewUniformBuffer,
165
248
  ],
166
249
  'view-main',
167
250
  () => {
168
- const viewBindGroupResult = internals.device.createBindGroup({
169
- label: 'pbr-view-bg',
170
- layout: pipelineState.viewBindGroupLayout,
171
- entries: [
172
- {
173
- binding: 0,
174
- resource: {
175
- kind: 'buffer',
176
- value: { buffer: pipelineState.viewUniformBuffer, size: VIEW_UNIFORM_BYTES },
177
- },
251
+ const entries: BindGroupEntry[] = [
252
+ {
253
+ binding: 0,
254
+ resource: {
255
+ kind: 'buffer',
256
+ value: { buffer: pipelineState.viewUniformBuffer, size: VIEW_UNIFORM_BYTES },
178
257
  },
179
- {
180
- binding: 1,
181
- resource: {
182
- kind: 'buffer',
183
- value: { buffer: pipelineState.pointLightsBuffer },
184
- },
258
+ },
259
+ {
260
+ binding: 1,
261
+ resource: {
262
+ kind: 'buffer',
263
+ value: { buffer: pipelineState.pointLightsBuffer },
185
264
  },
186
- {
187
- binding: 2,
188
- resource: {
189
- kind: 'buffer',
190
- value: { buffer: pipelineState.spotLightsBuffer },
191
- },
265
+ },
266
+ {
267
+ binding: 2,
268
+ resource: {
269
+ kind: 'buffer',
270
+ value: { buffer: pipelineState.spotLightsBuffer },
192
271
  },
193
- {
194
- binding: 3,
195
- resource: {
196
- kind: 'textureView',
197
- value: b3View,
198
- },
272
+ },
273
+ {
274
+ binding: 3,
275
+ resource: {
276
+ kind: 'textureView',
277
+ value: b3View,
199
278
  },
200
- {
201
- binding: 4,
202
- resource: {
203
- kind: 'sampler',
204
- value: shadowSampler,
205
- },
279
+ },
280
+ {
281
+ binding: 4,
282
+ resource: {
283
+ kind: 'sampler',
284
+ value: shadowSampler,
206
285
  },
207
- // feat-20260612-point-light-shadows-urp-hdrp Round-2 F-1:
208
- // cube_array shadow atlas view (real ShadowAtlas when point
209
- // shadows are active; else 1x1x6 fallback).
210
- {
211
- binding: 5,
212
- resource: {
213
- kind: 'textureView',
214
- value: b5View,
215
- },
286
+ },
287
+ // feat-20260612-point-light-shadows-urp-hdrp Round-2 F-1:
288
+ // cube_array shadow atlas view (real ShadowAtlas when point
289
+ // shadows are active; else 1x1x6 fallback).
290
+ {
291
+ binding: 5,
292
+ resource: {
293
+ kind: 'textureView',
294
+ value: b5View,
216
295
  },
217
- // feat-20260612-point-light-shadows-urp-hdrp Round-2 F-1:
218
- // shadowParams UBO (`array<vec4<f32>, 4>` = 64 B). Lane N
219
- // stores `(near, far, 1/(far-near), 0)` for the point light
220
- // with shadowAtlasLayer === N. Updated per frame from
221
- // `pointShadowSnapshots` below.
222
- {
223
- binding: 6,
224
- resource: {
225
- kind: 'buffer',
226
- value: { buffer: pipelineState.shadowParamsBuffer },
227
- },
296
+ },
297
+ // feat-20260612-point-light-shadows-urp-hdrp Round-2 F-1:
298
+ // shadowParams UBO (`array<vec4<f32>, 4>` = 64 B). Lane N
299
+ // stores `(near, far, 1/(far-near), 0)` for the point light
300
+ // with shadowAtlasLayer === N. Updated per frame from
301
+ // `pointShadowSnapshots` below.
302
+ {
303
+ binding: 6,
304
+ resource: {
305
+ kind: 'buffer',
306
+ value: { buffer: pipelineState.shadowParamsBuffer },
228
307
  },
229
- // feat-20260613-csm-cascaded-shadow-maps M5 / w28 (rebased to
230
- // binding 7 on 2026-06-13 to make room for point-shadow 5/6):
231
- // forward shaders declare binding 7 in common.wgsl (shared
232
- // view BGL) but never reference it; only shadow_caster.wgsl
233
- // reads it. Host writes a stable singleton buffer so every
234
- // forward bind group entry stays populated.
235
- {
236
- binding: 7,
237
- resource: {
238
- kind: 'buffer',
239
- value: { buffer: pipelineState.shadowCasterCascadeBuffer },
240
- },
308
+ },
309
+ // feat-20260613-csm-cascaded-shadow-maps M5 / w28 (rebased to
310
+ // binding 7 on 2026-06-13 to make room for point-shadow 5/6):
311
+ // forward shaders declare binding 7 in common.wgsl (shared
312
+ // view BGL) but never reference it; only shadow_caster.wgsl
313
+ // reads it. Host writes a stable singleton buffer so every
314
+ // forward bind group entry stays populated.
315
+ {
316
+ binding: 7,
317
+ resource: {
318
+ kind: 'buffer',
319
+ value: { buffer: pipelineState.shadowCasterCascadeBuffer },
241
320
  },
242
- // feat-20260625-spot-light-shadow-mapping M3 / w21 (D-5):
243
- // spot shadow 2D atlas (real spotShadowDepth view when spot
244
- // shadows run this frame, else the 1x1 depth fallback). Always-on.
245
- {
246
- binding: 8,
247
- resource: {
248
- kind: 'textureView',
249
- value: b8View,
250
- },
321
+ },
322
+ // feat-20260625-spot-light-shadow-mapping M3 / w21 (D-5):
323
+ // spot shadow 2D atlas (real spotShadowDepth view when spot
324
+ // shadows run this frame, else the 1x1 depth fallback). Always-on.
325
+ {
326
+ binding: 8,
327
+ resource: {
328
+ kind: 'textureView',
329
+ value: b8View,
251
330
  },
252
- {
253
- binding: 10,
254
- resource: {
255
- kind: 'buffer',
256
- value: {
257
- buffer: pipelineState.pointsLinesViewBuffer ?? pipelineState.viewUniformBuffer,
258
- size: POINTS_LINES_VIEW_BYTES,
259
- },
331
+ },
332
+ {
333
+ binding: 10,
334
+ resource: {
335
+ kind: 'buffer',
336
+ value: {
337
+ buffer: pipelineState.pointsLinesViewBuffer ?? pipelineState.viewUniformBuffer,
338
+ size: POINTS_LINES_VIEW_BYTES,
260
339
  },
261
340
  },
262
- // feat-20260625-spot-light-shadow-mapping w25: the per-spot
263
- // fragment-read lightViewProj matrices fold into the View UBO
264
- // (binding 0, `view.spotLightViewProj`) — no standalone binding 9
265
- // (WebGL2 fragment uniform-buffer budget). binding 10 is the
266
- // dedicated vertex-only Points/Lines viewport UBO.
267
- ],
341
+ },
342
+ ...(projectorAvailable
343
+ ? [
344
+ { binding: 11, resource: { kind: 'textureView' as const, value: b11View } },
345
+ { binding: 12, resource: { kind: 'sampler' as const, value: b12Sampler } },
346
+ ]
347
+ : []),
348
+ // feat-20260625-spot-light-shadow-mapping w25: the per-spot
349
+ // fragment-read lightViewProj matrices fold into the View UBO
350
+ // (binding 0, `view.spotLightViewProj`) — no standalone binding 9
351
+ // (WebGL2 fragment uniform-buffer budget). binding 10 is the
352
+ // dedicated vertex-only Points/Lines viewport UBO.
353
+ ];
354
+ const viewBindGroupResult = internals.device.createBindGroup({
355
+ label: 'pbr-view-bg',
356
+ layout: pipelineState.viewBindGroupLayout,
357
+ entries,
268
358
  });
269
359
  if (!viewBindGroupResult.ok) throw viewBindGroupResult.error;
270
360
  return viewBindGroupResult.value;
@@ -321,11 +411,8 @@ export function buildPerFrameBindGroups(
321
411
  // alongside `meshBindGroupCache` keyed on the mesh SSBO + cluster
322
412
  // buffer identities; cache invalidates on a buffer-grow event the same
323
413
  // way the mesh path does (handle id rotation).
324
- if (frameState.isHdrpActive) {
325
- const hdrpBuffers = getOrCreateHdrpBuffers(
326
- internals,
327
- frameState.installedPipelineConfig?.clusterGrid,
328
- );
414
+ if (clusteredLighting) {
415
+ const hdrpBuffers = expectedHdrpBuffers;
329
416
  if (hdrpBuffers !== null) {
330
417
  // D-3: buffer dimension uses the inner `.buffer` (same constraint
331
418
  // as the mesh path) so a grow event rotates the chain key.
@@ -364,8 +451,7 @@ export function buildPerFrameBindGroups(
364
451
 
365
452
  /**
366
453
  * feat-20260704 M3/w18: per-frame lighting preparation, extracted verbatim from
367
- * `recordFrame`. (1) URP-only multi-light warn-once (directional / point / spot
368
- * over `LIGHT_ARRAY_MAX_SLOTS`; HDRP gated off — 256 SSBO lights). (2) pin the
454
+ * `recordFrame`. (1) pin the
369
455
  * point + spot shadow snapshot lists onto frameState for the URP shadow caster
370
456
  * pass closures + lazy-allocate the point-shadow cube_array atlas on first
371
457
  * non-empty frame (AC-09 zero-shadow zero-alloc). (3) destructure ExtractedLights
@@ -378,12 +464,21 @@ export function prepareFrameLighting(
378
464
  internals: RenderSystemInternals,
379
465
  frameState: RenderFrameState,
380
466
  lights: ExtractedLights,
381
- ): {
382
- light: DirectionalLightSnapshot;
383
- pointLights: ExtractedLights['point'];
384
- spotLights: ExtractedLights['spot'];
385
- totalLightCount: number;
386
- } {
467
+ camera: CameraSnapshot,
468
+ pipelineState?: Pick<
469
+ PipelineState,
470
+ 'hdrpClusterMembershipPipeline' | 'hdrpClusterMembershipBindGroupLayout'
471
+ >,
472
+ ): Result<
473
+ {
474
+ light: DirectionalLightSnapshot;
475
+ pointLights: ExtractedLights['point'];
476
+ spotLights: ExtractedLights['spot'];
477
+ totalLightCount: number;
478
+ standard: StandardTopologyInputValue;
479
+ },
480
+ RenderError
481
+ > {
387
482
  if (lights.directionalCount > 1) {
388
483
  warnMultiLightDirectional(frameState, lights.directionalCount);
389
484
  }
@@ -394,21 +489,6 @@ export function prepareFrameLighting(
394
489
  // defaults true on the single component, so the warn condition can never
395
490
  // arise. The error class + RuntimeErrorCode member have been removed in M4 (m4-t2).
396
491
 
397
- // feat-20260608-cluster-lighting M6 / w23 (F-4 fix): URP-only multi-light
398
- // warn. HDRP supports 256 punctual lights via SSBO; the 4-slot first-slice
399
- // cap is irrelevant under HDRP, so gate on `!isHdrpActive` to silence noise.
400
- // Sibling tweak-20260608-rhi-hdr-renderable-caps-and-warn-once (#320)
401
- // extracted the warn into warnMultiLight{Point,Spot} helpers (warn-once
402
- // dedup); we keep the helpers and add the HDRP gate on top.
403
- if (!frameState.isHdrpActive) {
404
- if (lights.point.length > LIGHT_ARRAY_MAX_SLOTS) {
405
- warnMultiLightPoint(frameState, lights.point.length);
406
- }
407
- if (lights.spot.length > LIGHT_ARRAY_MAX_SLOTS) {
408
- warnMultiLightSpot(frameState, lights.spot.length);
409
- }
410
- }
411
-
412
492
  // feat-20260612-point-light-shadows-urp-hdrp M3 / T-M3-2 (plan-strategy §D-1):
413
493
  // project lights.pointShadow onto frameState so the URP point shadow caster
414
494
  // pass can read the snapshot list during graph execute. Lazy-allocate the
@@ -424,10 +504,10 @@ export function prepareFrameLighting(
424
504
  if (lights.pointShadow.length > 0) {
425
505
  if (frameState.pointShadowAtlas === null) {
426
506
  const firstSnap = lights.pointShadow[0];
427
- const faceSize = firstSnap?.mapSize ?? 512;
507
+ const faceSize = firstSnap?.mapSize ?? SHADOW_ATLAS_DEFAULT_FACE_SIZE;
428
508
  frameState.pointShadowAtlas = new ShadowAtlas(internals.device, {
429
509
  faceSize,
430
- layers: 4,
510
+ layers: SHADOW_ATLAS_DEFAULT_LAYERS,
431
511
  });
432
512
  }
433
513
  try {
@@ -471,73 +551,77 @@ export function prepareFrameLighting(
471
551
  const totalLightCount =
472
552
  (directionalLight !== undefined ? 1 : 0) + pointLights.length + spotLights.length;
473
553
 
474
- return { light, pointLights, spotLights, totalLightCount };
475
- }
476
-
477
- /**
478
- * feat-20260704 M3/w18: per-frame full rewrite of the PointLight + SpotLight
479
- * std430 storage buffers, extracted verbatim from `recordFrame`.
480
- *
481
- * feat-20260519-light-casters-point-spot-pbr M3 / w20 (D-S1 + D-S2 + D-S6):
482
- * header (16 B count u32 + 12 B pad) at offset 0 + first-slice cap N=4 slots
483
- * packed sequentially. The N>4 fail-fast upstream ensures the counts are <= 4
484
- * when the listener registry consumed the structured error; the slice keeps
485
- * the buffer write bounded even if downstream listeners ignore the RhiError
486
- * (charter P9 graceful degradation: surplus entities dropped, frame records).
487
- *
488
- * @internal
489
- */
490
- export function writePointSpotLightBuffers(
491
- internals: RenderSystemInternals,
492
- pipelineState: PipelineState,
493
- lights: ExtractedLights,
494
- ): void {
495
- const pointSlots =
496
- lights.point.length <= LIGHT_ARRAY_MAX_SLOTS
497
- ? lights.point
498
- : lights.point.slice(0, LIGHT_ARRAY_MAX_SLOTS);
499
- const pointHeader = packLightArrayHeader(pointSlots.length);
500
- const pointHeaderUpload = internals.device.queue.writeBuffer(
501
- pipelineState.pointLightsBuffer,
502
- 0,
503
- new Uint8Array(pointHeader),
504
- );
505
- if (!pointHeaderUpload.ok) throw pointHeaderUpload.error;
506
- for (let i = 0; i < pointSlots.length; i++) {
507
- const slot = pointSlots[i];
508
- if (slot === undefined) continue;
509
- const packed = packPointLight(slot);
510
- const offset = LIGHT_ARRAY_HEADER_BYTES + i * POINT_LIGHT_STD430_BYTES;
511
- const writeRes = internals.device.queue.writeBuffer(
512
- pipelineState.pointLightsBuffer,
513
- offset,
514
- packed,
515
- );
516
- if (!writeRes.ok) throw writeRes.error;
554
+ const clusterGrid = frameState.installedPipelineConfig?.clusterGrid ?? DEFAULT_CLUSTER_GRID;
555
+ const local = [
556
+ ...pointLights.map((source) => ({
557
+ kind: 'point' as const,
558
+ shadowed: source.shadowAtlasLayer !== undefined && source.shadowAtlasLayer >= 0,
559
+ position: source.position,
560
+ range:
561
+ Number.isFinite(source.invRangeSquared) && source.invRangeSquared > 0
562
+ ? Math.sqrt(1 / source.invRangeSquared)
563
+ : 1000,
564
+ source,
565
+ })),
566
+ ...spotLights.map((source) => ({
567
+ kind: 'spot' as const,
568
+ shadowed:
569
+ source.castShadow && source.shadowAtlasTile >= 0 && source.lightViewProj !== undefined,
570
+ position: source.position,
571
+ range:
572
+ Number.isFinite(source.invRangeSquared) && source.invRangeSquared > 0
573
+ ? Math.sqrt(1 / source.invRangeSquared)
574
+ : 1000,
575
+ source,
576
+ })),
577
+ ];
578
+ const prepared = prepareStandardLighting({
579
+ directional: directionalLight,
580
+ local,
581
+ view: computeViewMatrix(camera),
582
+ projection: computeProjectionMatrix(camera),
583
+ near: camera.near,
584
+ far: camera.far,
585
+ grid: clusterGrid,
586
+ lightCount: internals.standardProfile?.lightCount ?? DEFAULT_STANDARD_PROFILE.lightCount,
587
+ renderPath: internals.standardProfile?.renderPath ?? DEFAULT_STANDARD_PROFILE.renderPath,
588
+ });
589
+ if (!prepared.ok) return prepared;
590
+ // Storage-capable Standard frames keep the same Cluster ABI even when the
591
+ // local-light corpus is empty. Directional-only scenes still need the
592
+ // unified mesh group(2) contract so their PBR draws do not select a second
593
+ // direct layout; the existing transport emits a zero-light payload and the
594
+ // graph remains structurally identical. The non-storage backend is the
595
+ // sole direct exception because no proven Cluster representation exists.
596
+ if (prepared.value.local.length === 0 && !internals.device.caps.storageBuffer) {
597
+ return ok({
598
+ light,
599
+ pointLights,
600
+ spotLights,
601
+ totalLightCount,
602
+ standard: { kind: 'no-local-lights', prepared: prepared.value },
603
+ });
517
604
  }
518
- const spotSlots =
519
- lights.spot.length <= LIGHT_ARRAY_MAX_SLOTS
520
- ? lights.spot
521
- : lights.spot.slice(0, LIGHT_ARRAY_MAX_SLOTS);
522
- const spotHeader = packLightArrayHeader(spotSlots.length);
523
- const spotHeaderUpload = internals.device.queue.writeBuffer(
524
- pipelineState.spotLightsBuffer,
525
- 0,
526
- new Uint8Array(spotHeader),
605
+ const transport = selectStandardClusterTransport(
606
+ {
607
+ compute: internals.device.caps.compute,
608
+ storageBuffer: internals.device.caps.storageBuffer,
609
+ membershipPipelineReady:
610
+ pipelineState !== undefined &&
611
+ pipelineState.hdrpClusterMembershipPipeline !== null &&
612
+ pipelineState.hdrpClusterMembershipBindGroupLayout !== null,
613
+ },
614
+ prepared.value,
527
615
  );
528
- if (!spotHeaderUpload.ok) throw spotHeaderUpload.error;
529
- for (let i = 0; i < spotSlots.length; i++) {
530
- const slot = spotSlots[i];
531
- if (slot === undefined) continue;
532
- const packed = packSpotLight(slot);
533
- const offset = LIGHT_ARRAY_HEADER_BYTES + i * SPOT_LIGHT_STD430_BYTES;
534
- const writeRes = internals.device.queue.writeBuffer(
535
- pipelineState.spotLightsBuffer,
536
- offset,
537
- packed,
538
- );
539
- if (!writeRes.ok) throw writeRes.error;
540
- }
616
+ if (!transport.ok) return transport;
617
+
618
+ return ok({
619
+ light,
620
+ pointLights,
621
+ spotLights,
622
+ totalLightCount,
623
+ standard: { kind: 'clustered', prepared: prepared.value, transport: transport.value },
624
+ });
541
625
  }
542
626
 
543
627
  /**
@@ -623,17 +707,9 @@ export function warnZeroLightStandard(
623
707
  }
624
708
 
625
709
  /**
626
- * feat-20260704 M3/w18: HDRP per-frame CPU cluster binning + light-data /
627
- * cluster-grid / light-index-list / cluster-uniform / SSAO uniform buffer
628
- * uploads. Extracted verbatim from `recordFrame`. Runs only when the HDRP
629
- * pipeline is active. A zero-punctual frame explicitly clears the persistent
630
- * cluster buffers so stale membership cannot leak across frames; direct and
631
- * non-clustered paths remain untouched.
632
- *
633
- * feat-20260608-cluster-lighting M5 / w21 + M6 / w23 + r2 fix-up: fail-soft
634
- * semantics preserved verbatim — index-list-overflow and light-budget-exceeded
635
- * fire once per frame (via `frameState.hdrpOncePerFrameFired`) and continue
636
- * rendering.
710
+ * Write the one prepared Standard Cluster payload into the persistent buffers.
711
+ * Membership has already been derived by `prepareFrameLighting`; this function
712
+ * only packs/uploads that immutable result and never re-runs the binner.
637
713
  *
638
714
  * @internal
639
715
  */
@@ -641,253 +717,107 @@ export function writeHdrpClusterAndSsaoBuffers(
641
717
  internals: RenderSystemInternals,
642
718
  frameState: RenderFrameState,
643
719
  camera: CameraSnapshot,
644
- pointLights: ExtractedLights['point'],
645
- spotLights: ExtractedLights['spot'],
720
+ prepared: PreparedStandardLighting,
721
+ transport: StandardClusterTransport,
646
722
  profilePhase?: RecordProfileRunner,
647
- useGpuMembership = false,
723
+ gpuMembershipPipelineReady = false,
648
724
  membershipBindGroupLayout: BindGroupLayout | null = null,
649
- ): void {
650
- const hdrpLightCount = pointLights.length + spotLights.length;
651
- if (!frameState.isHdrpActive) return;
652
- if (hdrpLightCount === 0) {
653
- const grid = frameState.installedPipelineConfig?.clusterGrid ?? DEFAULT_CLUSTER_GRID;
654
- const hdrpBuffers = getOrCreateHdrpBuffers(internals, grid);
655
- if (hdrpBuffers === null) return;
656
- const lightCapacity = hdrpBuffers.storageBuffer ? 256 : HDRP_UNIFORM_LIGHT_CAPACITY;
657
- const clusterSsaoConfig = frameState.installedPipelineConfig?.ssao;
658
- const clusterSsaoIntensity =
659
- clusterSsaoConfig !== undefined && clusterSsaoConfig.enabled === true
660
- ? (clusterSsaoConfig.intensity ?? 1.0)
661
- : 0;
662
- const clusterUniformPayload = packClusterUniform(
663
- grid,
664
- camera.near,
665
- camera.far,
666
- clusterSsaoIntensity,
667
- 0,
668
- lightCapacity,
725
+ projectorLightSlotIndex?: number,
726
+ ): Result<void, RenderError> {
727
+ const configuredClusterGrid = prepared.layout.grid;
728
+ const gpuMembership = transport.producer === 'gpu';
729
+ if (gpuMembership && (!gpuMembershipPipelineReady || membershipBindGroupLayout === null)) {
730
+ return err(
731
+ new StandardClusterTransportUnavailableError(
732
+ prepared.local.length,
733
+ 'the selected GPU Cluster producer requires a ready membership pipeline and bind-group layout',
734
+ ),
669
735
  );
670
- const clusterUniformUpload = internals.device.queue.writeBuffer(
671
- hdrpBuffers.clusterUniformBuffer,
672
- 0,
673
- new Uint8Array(clusterUniformPayload),
674
- );
675
- if (!clusterUniformUpload.ok) internals.errorRegistry.fire(clusterUniformUpload.error);
676
- const clusterGridClear = internals.device.queue.writeBuffer(
677
- hdrpBuffers.clusterGridBuffer,
678
- 0,
679
- new Uint8Array(hdrpBuffers.clusterGridBytes),
680
- );
681
- if (!clusterGridClear.ok) internals.errorRegistry.fire(clusterGridClear.error);
682
- const lightIndexListClear = internals.device.queue.writeBuffer(
683
- hdrpBuffers.lightIndexListBuffer,
684
- 0,
685
- new Uint8Array(hdrpBuffers.lightIndexListBytes),
686
- );
687
- if (!lightIndexListClear.ok) internals.errorRegistry.fire(lightIndexListClear.error);
688
- return;
689
736
  }
690
- const HDRP_LIGHT_BUDGET = 256;
691
- let effectivePointLights = pointLights;
692
- let effectiveSpotLights = spotLights;
693
- if (hdrpLightCount > HDRP_LIGHT_BUDGET) {
694
- if (!frameState.hdrpOncePerFrameFired.has('hdrp-light-budget-exceeded')) {
695
- frameState.hdrpOncePerFrameFired.add('hdrp-light-budget-exceeded');
696
- internals.errorRegistry.fire(
697
- new HdrpLightBudgetExceededError(hdrpLightCount, HDRP_LIGHT_BUDGET),
698
- );
699
- }
700
- if (pointLights.length >= HDRP_LIGHT_BUDGET) {
701
- effectivePointLights = pointLights.slice(0, HDRP_LIGHT_BUDGET);
702
- effectiveSpotLights = [];
703
- } else {
704
- effectivePointLights = pointLights;
705
- effectiveSpotLights = spotLights.slice(0, HDRP_LIGHT_BUDGET - pointLights.length);
706
- }
737
+ const hdrpBuffers = getOrCreateHdrpBuffers(internals, configuredClusterGrid);
738
+ if (hdrpBuffers === null) {
739
+ return err(
740
+ new StandardClusterTransportUnavailableError(
741
+ prepared.local.length,
742
+ 'the selected Standard Cluster transport could not allocate its persistent buffers',
743
+ ),
744
+ );
707
745
  }
708
- const effectiveLightCount = effectivePointLights.length + effectiveSpotLights.length;
709
- const configuredClusterGrid =
710
- frameState.installedPipelineConfig?.clusterGrid ?? DEFAULT_CLUSTER_GRID;
711
- let gpuMembership = useGpuMembership && membershipBindGroupLayout !== null;
746
+ const existingMembership = projectDeviceOwnedBindGroup(
747
+ frameState.hdrpClusterMembership,
748
+ internals.device,
749
+ membershipBindGroupLayout,
750
+ );
751
+ const membershipMatchesBuffers =
752
+ existingMembership !== null &&
753
+ frameState.hdrpClusterMembership?.clusterGridBuffer === hdrpBuffers.clusterGridBuffer;
754
+ if (!membershipMatchesBuffers) frameState.hdrpClusterMembership = null;
712
755
  if (
713
756
  gpuMembership &&
714
757
  membershipBindGroupLayout !== null &&
715
- frameState.hdrpClusterMembershipBindGroup === null
758
+ frameState.hdrpClusterMembership === null
716
759
  ) {
717
- const hdrpBuffers = getOrCreateHdrpBuffers(internals, configuredClusterGrid);
718
- if (hdrpBuffers !== null) {
719
- frameState.hdrpClusterMembershipBindGroup = createHdrpClusterMembershipBindGroup(
720
- internals,
721
- hdrpBuffers,
722
- membershipBindGroupLayout,
760
+ const bindGroup = createHdrpClusterMembershipBindGroup(
761
+ internals,
762
+ hdrpBuffers,
763
+ membershipBindGroupLayout,
764
+ );
765
+ if (bindGroup === null) {
766
+ return err(
767
+ new StandardClusterTransportUnavailableError(
768
+ prepared.local.length,
769
+ 'the selected GPU Cluster producer could not create its membership bind group',
770
+ ),
723
771
  );
724
772
  }
725
- gpuMembership = frameState.hdrpClusterMembershipBindGroup !== null;
773
+ frameState.hdrpClusterMembership = {
774
+ device: internals.device,
775
+ layout: membershipBindGroupLayout,
776
+ bindGroup,
777
+ clusterGridBuffer: hdrpBuffers.clusterGridBuffer,
778
+ };
726
779
  }
727
780
 
728
- const { clusterGrid, gridX, gridY, gridZ, clusterGridBuf, lightIndexListBuf, lightIndexCount } =
729
- runHdrpClusterProfilePhase(profilePhase, 'record/scene-state/hdrp-cluster/binner', () => {
730
- const binnerProfile: ClusterBinProfileRunner | undefined =
731
- profilePhase === undefined
732
- ? undefined
733
- : <T>(phase: ClusterBinProfilePhase, action: () => T): T =>
734
- runHdrpClusterProfilePhase(
735
- profilePhase,
736
- `record/scene-state/hdrp-cluster/binner/${phase}` as RenderHdrpClusterPhase,
737
- action,
738
- );
739
- const {
740
- hdrpLights,
741
- clusterGrid,
742
- gridX,
743
- gridY,
744
- gridZ,
745
- clusterGridBuf,
746
- lightIndexListBuf,
747
- projMatrix,
748
- viewMatrix,
749
- } = runHdrpClusterProfilePhase(
750
- profilePhase,
751
- 'record/scene-state/hdrp-cluster/binner/input-preparation',
752
- () => {
753
- const hdrpLights: Array<{ position: Vec3; range: number }> = [];
754
- for (const pl of effectivePointLights) {
755
- const range =
756
- Number.isFinite(pl.invRangeSquared) && pl.invRangeSquared > 0
757
- ? Math.sqrt(1 / pl.invRangeSquared)
758
- : 1000;
759
- hdrpLights.push({ position: pl.position, range });
760
- }
761
- for (const sl of effectiveSpotLights) {
762
- const range =
763
- Number.isFinite(sl.invRangeSquared) && sl.invRangeSquared > 0
764
- ? Math.sqrt(1 / sl.invRangeSquared)
765
- : 1000;
766
- hdrpLights.push({ position: sl.position, range });
767
- }
768
-
769
- const clusterGrid =
770
- frameState.installedPipelineConfig?.clusterGrid ?? DEFAULT_CLUSTER_GRID;
771
- const gridX = clusterGrid.x;
772
- const gridY = clusterGrid.y;
773
- const gridZ = clusterGrid.z;
774
- const clusterCount = gridX * gridY * gridZ;
775
-
776
- const projMatrix = computeProjectionMatrix(camera);
777
- const viewMatrix = computeViewMatrix(camera);
778
-
779
- const clusterGridBuf =
780
- frameState.hdrpClusterGridScratch === null ||
781
- frameState.hdrpClusterGridScratch.length < clusterCount * 2
782
- ? new Uint32Array(clusterCount * 2)
783
- : frameState.hdrpClusterGridScratch;
784
- frameState.hdrpClusterGridScratch = clusterGridBuf;
785
- const lightIndexListBuf =
786
- frameState.hdrpLightIndexListScratch === null ||
787
- frameState.hdrpLightIndexListScratch.length < LIGHT_INDEX_LIST_CAPACITY
788
- ? new Uint32Array(LIGHT_INDEX_LIST_CAPACITY)
789
- : frameState.hdrpLightIndexListScratch;
790
- frameState.hdrpLightIndexListScratch = lightIndexListBuf;
791
-
792
- return {
793
- hdrpLights,
794
- clusterGrid,
795
- gridX,
796
- gridY,
797
- gridZ,
798
- clusterGridBuf,
799
- lightIndexListBuf,
800
- projMatrix,
801
- viewMatrix,
802
- };
803
- },
804
- );
805
-
806
- const binResult = runHdrpClusterProfilePhase(
807
- profilePhase,
808
- 'record/scene-state/hdrp-cluster/binner/bin-core',
809
- () =>
810
- bin(
811
- hdrpLights,
812
- viewMatrix,
813
- projMatrix,
814
- { x: gridX, y: gridY, z: gridZ },
815
- camera.near,
816
- camera.far,
817
- clusterGridBuf,
818
- lightIndexListBuf,
819
- LIGHT_INDEX_LIST_CAPACITY,
820
- frameState.hdrpClusterBinScratch,
821
- binnerProfile,
822
- { writeMembership: !gpuMembership },
823
- ),
824
- );
825
-
826
- if (!binResult.ok && !frameState.hdrpOncePerFrameFired.has('hdrp-index-list-overflow')) {
827
- frameState.hdrpOncePerFrameFired.add('hdrp-index-list-overflow');
828
- const detail = binResult.error.detail;
829
- internals.errorRegistry.fire(
830
- new HdrpIndexListOverflowError(detail.actual, detail.capacity),
831
- );
832
- }
781
+ const gridX = prepared.layout.grid.x;
782
+ const gridY = prepared.layout.grid.y;
783
+ const gridZ = prepared.layout.grid.z;
784
+ const clusterGridBuf = prepared.clusterGrid;
785
+ const lightIndexListBuf = prepared.lightIndexList;
786
+ const lightIndexCount = prepared.membershipEntryCount;
833
787
 
834
- // Falsify injection point: FORGEAX_HDRP_FALSIFY_CLUSTER_GRID_ZERO
835
- // zeroes the cluster_grid buffer so every fragment culls every light.
836
- // Used by hello-hdrp-lighting smoke FALSIFY=cluster-grid-zero to
837
- // prove the smoke has discriminability (must FAIL vs baseline).
838
- // Read process via globalThis to keep this file @types/node-free.
839
- const envFalsify = (globalThis as { process?: { env?: Record<string, string | undefined> } })
840
- .process?.env;
841
- if (envFalsify?.FORGEAX_HDRP_FALSIFY_CLUSTER_GRID_ZERO) {
842
- clusterGridBuf.fill(0);
843
- }
844
-
845
- return {
846
- clusterGrid,
847
- gridX,
848
- gridY,
849
- gridZ,
850
- clusterGridBuf,
851
- lightIndexListBuf,
852
- lightIndexCount: binResult.ok ? binResult.value : 0,
853
- };
854
- });
788
+ // Falsify injection point retained for the real prepared payload. It edits
789
+ // only this frame's projection and never creates a second membership owner.
790
+ const envFalsify = (globalThis as { process?: { env?: Record<string, string | undefined> } })
791
+ .process?.env;
792
+ if (envFalsify?.FORGEAX_HDRP_FALSIFY_CLUSTER_GRID_ZERO) clusterGridBuf.fill(0);
855
793
 
856
794
  const hdrpPayload = runHdrpClusterProfilePhase(
857
795
  profilePhase,
858
796
  'record/scene-state/hdrp-cluster/payload-packing',
859
797
  () => {
860
- const hdrpBuffers = getOrCreateHdrpBuffers(internals, clusterGrid);
861
- if (hdrpBuffers === null) return null;
862
-
863
- const lightCapacity = hdrpBuffers.storageBuffer ? 256 : HDRP_UNIFORM_LIGHT_CAPACITY;
864
- const lightDataPayload = new Float32Array(lightCapacity * 16);
798
+ const lightDataPayload = new Float32Array(prepared.layout.lightDataSlotCount * 16);
865
799
  let slotIdx = 0;
866
- for (const pl of effectivePointLights) {
867
- if (slotIdx >= lightCapacity) break;
868
- // feat-20260612-point-light-shadows-urp-hdrp M4 / T-M4-4
869
- // (plan-strategy §D-8): thread the optional shadow info through
870
- // packLightSlot. PointLightSnapshot.shadowAtlasLayer is set by
871
- // extract's join pass when the entity carries PointLightShadow;
872
- // sentinel -1 means no shadow (unshadowed evalPoint path).
873
- const layer = pl.shadowAtlasLayer;
800
+ for (const local of prepared.local) {
801
+ const source = local.source;
802
+ if (source === undefined) return null;
803
+ const projectorSelected =
804
+ projectorLightSlotIndex !== undefined && projectorLightSlotIndex === slotIdx;
874
805
  const packed =
875
- layer !== undefined &&
876
- layer >= 0 &&
877
- pl.shadowNear !== undefined &&
878
- pl.shadowFar !== undefined
879
- ? packLightSlot(pl, {
880
- shadowAtlasLayer: layer,
881
- near: pl.shadowNear,
882
- far: pl.shadowFar,
883
- })
884
- : packLightSlot(pl);
885
- lightDataPayload.set(packed, slotIdx * 16);
886
- slotIdx += 1;
887
- }
888
- for (const sl of effectiveSpotLights) {
889
- if (slotIdx >= lightCapacity) break;
890
- const packed = packLightSlot(sl);
806
+ source.kind === 'point' &&
807
+ source.shadowAtlasLayer !== undefined &&
808
+ source.shadowAtlasLayer >= 0 &&
809
+ source.shadowNear !== undefined &&
810
+ source.shadowFar !== undefined
811
+ ? packLightSlot(
812
+ source,
813
+ {
814
+ shadowAtlasLayer: source.shadowAtlasLayer,
815
+ near: source.shadowNear,
816
+ far: source.shadowFar,
817
+ },
818
+ projectorSelected,
819
+ )
820
+ : packLightSlot(source, undefined, projectorSelected);
891
821
  lightDataPayload.set(packed, slotIdx * 16);
892
822
  slotIdx += 1;
893
823
  }
@@ -908,8 +838,7 @@ export function writeHdrpClusterAndSsaoBuffers(
908
838
  camera.near,
909
839
  camera.far,
910
840
  clusterSsaoIntensity,
911
- Math.min(effectivePointLights.length + effectiveSpotLights.length, lightCapacity),
912
- lightCapacity,
841
+ transport.admittedLightCount,
913
842
  );
914
843
 
915
844
  return {
@@ -920,39 +849,53 @@ export function writeHdrpClusterAndSsaoBuffers(
920
849
  },
921
850
  );
922
851
 
923
- runHdrpClusterProfilePhase(profilePhase, 'record/scene-state/hdrp-cluster/buffer-upload', () => {
924
- if (hdrpPayload !== null) {
852
+ if (hdrpPayload === null) {
853
+ return err(
854
+ new StandardClusterTransportUnavailableError(
855
+ prepared.local.length,
856
+ 'the prepared Standard Cluster payload could not be materialized',
857
+ ),
858
+ );
859
+ }
860
+
861
+ const bufferUpload = runHdrpClusterProfilePhase(
862
+ profilePhase,
863
+ 'record/scene-state/hdrp-cluster/buffer-upload',
864
+ (): Result<void, RenderError> => {
925
865
  const { hdrpBuffers, lightDataUploadPayload, clusterUniformPayload } = hdrpPayload;
926
866
  const lightDataUpload = internals.device.queue.writeBuffer(
927
867
  hdrpBuffers.lightDataBuffer,
928
868
  0,
929
869
  lightDataUploadPayload,
930
870
  );
931
- if (!lightDataUpload.ok) internals.errorRegistry.fire(lightDataUpload.error);
871
+ if (!lightDataUpload.ok)
872
+ return err(mapClusterUploadFailure('light-data', lightDataUpload.error));
932
873
 
933
- if (hdrpBuffers.storageBuffer) {
934
- if (gpuMembership) {
935
- const lightBoundsUpload = internals.device.queue.writeBuffer(
936
- hdrpBuffers.lightBoundsBuffer,
937
- 0,
938
- frameState.hdrpClusterBinScratch.lightBounds.subarray(0, effectiveLightCount * 6),
939
- );
940
- if (!lightBoundsUpload.ok) internals.errorRegistry.fire(lightBoundsUpload.error);
941
- }
942
- const clusterGridUpload = internals.device.queue.writeBuffer(
943
- hdrpBuffers.clusterGridBuffer,
874
+ if (gpuMembership) {
875
+ const lightBoundsUpload = internals.device.queue.writeBuffer(
876
+ hdrpBuffers.lightBoundsBuffer,
944
877
  0,
945
- clusterGridBuf.subarray(0, gridX * gridY * gridZ * 2),
878
+ prepared.lightBounds,
946
879
  );
947
- if (!clusterGridUpload.ok) internals.errorRegistry.fire(clusterGridUpload.error);
880
+ if (!lightBoundsUpload.ok)
881
+ return err(mapClusterUploadFailure('light-bounds', lightBoundsUpload.error));
882
+ }
883
+ const clusterGridUpload = internals.device.queue.writeBuffer(
884
+ hdrpBuffers.clusterGridBuffer,
885
+ 0,
886
+ clusterGridBuf.subarray(0, gridX * gridY * gridZ * 2),
887
+ );
888
+ if (!clusterGridUpload.ok)
889
+ return err(mapClusterUploadFailure('cluster-grid', clusterGridUpload.error));
948
890
 
949
- if (!gpuMembership && lightIndexCount > 0) {
950
- const lightIndexListUpload = internals.device.queue.writeBuffer(
951
- hdrpBuffers.lightIndexListBuffer,
952
- 0,
953
- lightIndexListBuf.subarray(0, lightIndexCount),
954
- );
955
- if (!lightIndexListUpload.ok) internals.errorRegistry.fire(lightIndexListUpload.error);
891
+ if (!gpuMembership && lightIndexCount > 0) {
892
+ const lightIndexListUpload = internals.device.queue.writeBuffer(
893
+ hdrpBuffers.lightIndexListBuffer,
894
+ 0,
895
+ lightIndexListBuf.subarray(0, lightIndexCount),
896
+ );
897
+ if (!lightIndexListUpload.ok) {
898
+ return err(mapClusterUploadFailure('light-index-list', lightIndexListUpload.error));
956
899
  }
957
900
  }
958
901
 
@@ -961,56 +904,67 @@ export function writeHdrpClusterAndSsaoBuffers(
961
904
  0,
962
905
  clusterUniformPayload,
963
906
  );
964
- if (!clusterUniformUpload.ok) internals.errorRegistry.fire(clusterUniformUpload.error);
965
- }
907
+ if (!clusterUniformUpload.ok) {
908
+ return err(mapClusterUploadFailure('cluster-uniform', clusterUniformUpload.error));
909
+ }
910
+ return ok(undefined);
911
+ },
912
+ );
913
+ if (!bufferUpload.ok) return bufferUpload;
966
914
 
967
- // ── feat-20260612-hdrp-ssao M1 / w6 + M7 / w33 ───────────────
968
- // Per-frame SSAO uniform write (plan-strategy D-1 + D-C):
969
- // view + projection + inverseProjection at offsets 0/64/128 +
970
- // intensityPad (vec4 — x=intensity, y=radius, z=bias) at offset 192;
971
- // total 256 B (matches host SSAO_UNIFORM_BYTES + WGSL struct).
972
- // Single writeBuffer covers all four fields so one queue entry
973
- // updates the entire UBO.
974
- // Separate from View UBO (592 B invariant); does not affect
975
- // material PSO bytecode.
976
- //
977
- // Writes when HDRP is active; config.ssao?.enabled guard comes in
978
- // M4 / w19 after the config.ssao type narrowing is added.
979
- {
915
+ // ── feat-20260612-hdrp-ssao M1 / w6 + M7 / w33 ───────────────
916
+ // Per-frame SSAO uniform write (plan-strategy D-1 + D-C):
917
+ // view + projection + inverseProjection at offsets 0/64/128 +
918
+ // intensityPad (vec4 — x=intensity, y=radius, z=bias) at offset 192;
919
+ // total 256 B (matches host SSAO_UNIFORM_BYTES + WGSL struct).
920
+ // Single writeBuffer covers all four fields so one queue entry
921
+ // updates the entire UBO.
922
+ // Separate from View UBO (592 B invariant); does not affect
923
+ // material PSO bytecode.
924
+ //
925
+ // Writes when HDRP is active; config.ssao?.enabled guard comes in
926
+ // M4 / w19 after the config.ssao type narrowing is added.
927
+ const ssaoUpload = runHdrpClusterProfilePhase(
928
+ profilePhase,
929
+ 'record/scene-state/hdrp-cluster/buffer-upload',
930
+ (): Result<void, RenderError> => {
980
931
  const ssaoBufs = getOrCreateSsaoBuffers(internals);
981
- if (ssaoBufs !== null) {
982
- const sProj = computeProjectionMatrix(camera);
983
- const sView = computeViewMatrix(camera);
984
- // inverseProjection = inverse(projection): NDC -> view-space.
985
- const invProjOnly = mat4.create();
986
- mat4.invert(invProjOnly, sProj);
932
+ if (ssaoBufs === null) return ok(undefined);
933
+ const sProj = computeProjectionMatrix(camera);
934
+ const sView = computeViewMatrix(camera);
935
+ // inverseProjection = inverse(projection): NDC -> view-space.
936
+ const invProjOnly = mat4.create();
937
+ mat4.invert(invProjOnly, sProj);
987
938
 
988
- // Float32Array of 64 (256 B): 3 mat4 (48) + intensityPad vec4 (4)
989
- // + 12 trailing padding floats. We only fill the declared region.
990
- const ssaoUniformPayload = new Float32Array(64);
991
- ssaoUniformPayload.set(sView, 0);
992
- ssaoUniformPayload.set(sProj, 16);
993
- ssaoUniformPayload.set(invProjOnly, 32);
994
- // intensityPad carries the same resolved values as the graph SSAO
995
- // calc pass; this early write keeps the shared UBO valid before the
996
- // typed fullscreen pass executes.
997
- const ssaoConfig = frameState.installedPipelineConfig?.ssao;
998
- const parameters = getSsaoParameters(
999
- ssaoConfig !== undefined && ssaoConfig.enabled === true ? ssaoConfig : undefined,
1000
- );
1001
- ssaoUniformPayload[48] = parameters.intensity;
1002
- ssaoUniformPayload[49] = parameters.radius;
1003
- ssaoUniformPayload[50] = parameters.bias;
939
+ // Float32Array of 64 (256 B): 3 mat4 (48) + intensityPad vec4 (4)
940
+ // + 12 trailing padding floats. We only fill the declared region.
941
+ const ssaoUniformPayload = new Float32Array(64);
942
+ ssaoUniformPayload.set(sView, 0);
943
+ ssaoUniformPayload.set(sProj, 16);
944
+ ssaoUniformPayload.set(invProjOnly, 32);
945
+ // intensityPad carries the same resolved values as the graph SSAO
946
+ // calc pass; this early write keeps the shared UBO valid before the
947
+ // typed fullscreen pass executes.
948
+ const ssaoConfig = frameState.installedPipelineConfig?.ssao;
949
+ const parameters = getSsaoParameters(
950
+ ssaoConfig !== undefined && ssaoConfig.enabled === true ? ssaoConfig : undefined,
951
+ );
952
+ ssaoUniformPayload[48] = parameters.intensity;
953
+ ssaoUniformPayload[49] = parameters.radius;
954
+ ssaoUniformPayload[50] = parameters.bias;
1004
955
 
1005
- const ssaoUniformRes = internals.device.queue.writeBuffer(
1006
- ssaoBufs.uniformBuffer,
1007
- 0,
1008
- ssaoUniformPayload,
1009
- );
1010
- if (!ssaoUniformRes.ok) internals.errorRegistry.fire(ssaoUniformRes.error);
1011
- }
1012
- }
1013
- });
956
+ const ssaoUniformRes = internals.device.queue.writeBuffer(
957
+ ssaoBufs.uniformBuffer,
958
+ 0,
959
+ ssaoUniformPayload,
960
+ );
961
+ if (!ssaoUniformRes.ok)
962
+ return err(mapClusterUploadFailure('ssao-uniform', ssaoUniformRes.error));
963
+ return ok(undefined);
964
+ },
965
+ );
966
+ if (!ssaoUpload.ok) return ssaoUpload;
967
+ return ok(undefined);
1014
968
  }
1015
969
 
1016
970
  /**