@forgeax/engine-render 0.1.20 → 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 (372) 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 +21 -18
  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-4XINDLAY.mjs → chunk-5YADUCA7.mjs} +1552 -1361
  21. package/dist/chunk-5YADUCA7.mjs.map +1 -0
  22. package/dist/{chunk-TBQJOK2L.mjs → chunk-6OPXDQ2E.mjs} +364 -51
  23. package/dist/chunk-6OPXDQ2E.mjs.map +1 -0
  24. package/dist/{chunk-M2MP4ZOU.mjs → chunk-BXBQQKFQ.mjs} +3 -9
  25. package/dist/chunk-BXBQQKFQ.mjs.map +1 -0
  26. package/dist/{chunk-CDTWTHZR.mjs → chunk-FS2KNUQH.mjs} +548 -6
  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/construct-renderer.mjs +5826 -1003
  42. package/dist/construct-renderer.mjs.map +1 -1
  43. package/dist/environment/frame.d.ts.map +1 -1
  44. package/dist/environment/lifecycle.d.ts +5 -0
  45. package/dist/environment/lifecycle.d.ts.map +1 -1
  46. package/dist/errors/render.d.ts +141 -93
  47. package/dist/errors/render.d.ts.map +1 -1
  48. package/dist/extract/directional-shadow-projection.d.ts +23 -0
  49. package/dist/extract/directional-shadow-projection.d.ts.map +1 -0
  50. package/dist/extract/environment.d.ts +14 -0
  51. package/dist/extract/environment.d.ts.map +1 -1
  52. package/dist/extract/visibility.d.ts.map +1 -1
  53. package/dist/features/fullscreen.d.ts.map +1 -1
  54. package/dist/features/targets.d.ts.map +1 -1
  55. package/dist/features/types.d.ts +10 -0
  56. package/dist/features/types.d.ts.map +1 -1
  57. package/dist/fullscreen-post-process-pass.d.ts +13 -2
  58. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  59. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  60. package/dist/gpu-driven/production-raster.d.ts +0 -1
  61. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  62. package/dist/gpu-scene.d.ts.map +1 -1
  63. package/dist/hdrp-buffers.d.ts +19 -17
  64. package/dist/hdrp-buffers.d.ts.map +1 -1
  65. package/dist/ibl/IblPipelineCache.d.ts +4 -0
  66. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  67. package/dist/ibl/kernel-cache.d.ts +36 -0
  68. package/dist/ibl/kernel-cache.d.ts.map +1 -0
  69. package/dist/index.d.ts +28 -3
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.mjs +4 -4
  72. package/dist/index.mjs.map +1 -1
  73. package/dist/inspection-types.d.ts +51 -0
  74. package/dist/inspection-types.d.ts.map +1 -1
  75. package/dist/instances-derived-bounds.d.ts +50 -0
  76. package/dist/instances-derived-bounds.d.ts.map +1 -0
  77. package/dist/internal.mjs +1 -1
  78. package/dist/light-buffer-layout.d.ts +10 -1
  79. package/dist/light-buffer-layout.d.ts.map +1 -1
  80. package/dist/mesh-material-bindings.d.ts +54 -0
  81. package/dist/mesh-material-bindings.d.ts.map +1 -1
  82. package/dist/pbr-pipeline.d.ts +14 -4
  83. package/dist/pbr-pipeline.d.ts.map +1 -1
  84. package/dist/pipeline/standard-forward-lane.d.ts +2 -1
  85. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  86. package/dist/pipeline/standard-lighting/errors.d.ts +4 -0
  87. package/dist/pipeline/standard-lighting/errors.d.ts.map +1 -0
  88. package/dist/pipeline/standard-lighting/graph.d.ts +24 -0
  89. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -0
  90. package/dist/pipeline/standard-lighting/inspection.d.ts +38 -0
  91. package/dist/pipeline/standard-lighting/inspection.d.ts.map +1 -0
  92. package/dist/pipeline/standard-lighting/layout.d.ts +28 -0
  93. package/dist/pipeline/standard-lighting/layout.d.ts.map +1 -0
  94. package/dist/pipeline/standard-lighting/light-frame.d.ts +33 -0
  95. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -0
  96. package/dist/pipeline/standard-lighting/membership.d.ts +14 -0
  97. package/dist/pipeline/standard-lighting/membership.d.ts.map +1 -0
  98. package/dist/pipeline/standard-lighting/prepare.d.ts +16 -0
  99. package/dist/pipeline/standard-lighting/prepare.d.ts.map +1 -0
  100. package/dist/pipeline/standard-lighting/topology.d.ts +45 -0
  101. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -0
  102. package/dist/pipeline/standard-lighting/transport.d.ts +36 -0
  103. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -0
  104. package/dist/pipeline/standard-pipeline.d.ts +4 -13
  105. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  106. package/dist/pipeline/standard-post.d.ts.map +1 -1
  107. package/dist/pipeline/standard-profile.d.ts +5 -26
  108. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  109. package/dist/pipeline-spec.d.ts +40 -7
  110. package/dist/pipeline-spec.d.ts.map +1 -1
  111. package/dist/point-shadow-inspection.d.ts +21 -0
  112. package/dist/point-shadow-inspection.d.ts.map +1 -0
  113. package/dist/record/frame-lighting.d.ts +27 -32
  114. package/dist/record/frame-lighting.d.ts.map +1 -1
  115. package/dist/record/frame-snapshot.d.ts +28 -14
  116. package/dist/record/frame-snapshot.d.ts.map +1 -1
  117. package/dist/record/frame-targets.d.ts +5 -0
  118. package/dist/record/frame-targets.d.ts.map +1 -1
  119. package/dist/record/frame.d.ts +10 -4
  120. package/dist/record/frame.d.ts.map +1 -1
  121. package/dist/record/gpu-pass-timing/contract.d.ts +96 -0
  122. package/dist/record/gpu-pass-timing/contract.d.ts.map +1 -0
  123. package/dist/record/gpu-pass-timing/errors.d.ts +18 -0
  124. package/dist/record/gpu-pass-timing/errors.d.ts.map +1 -0
  125. package/dist/record/gpu-pass-timing/index.d.ts +8 -0
  126. package/dist/record/gpu-pass-timing/index.d.ts.map +1 -0
  127. package/dist/record/gpu-pass-timing/parser.d.ts +11 -0
  128. package/dist/record/gpu-pass-timing/parser.d.ts.map +1 -0
  129. package/dist/record/gpu-pass-timing/session.d.ts +43 -0
  130. package/dist/record/gpu-pass-timing/session.d.ts.map +1 -0
  131. package/dist/record/gpu-timing.d.ts +8 -3
  132. package/dist/record/gpu-timing.d.ts.map +1 -1
  133. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  134. package/dist/record/main-pass-material.d.ts +22 -1
  135. package/dist/record/main-pass-material.d.ts.map +1 -1
  136. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  137. package/dist/record/main-pass.d.ts +2 -0
  138. package/dist/record/main-pass.d.ts.map +1 -1
  139. package/dist/record/render-context.d.ts +43 -2
  140. package/dist/record/render-context.d.ts.map +1 -1
  141. package/dist/record/shadow-pass.d.ts.map +1 -1
  142. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  143. package/dist/record/typed-frame-graph.d.ts +72 -4
  144. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  145. package/dist/record/view-ubo.d.ts +7 -4
  146. package/dist/record/view-ubo.d.ts.map +1 -1
  147. package/dist/reflection/filter.d.ts +22 -0
  148. package/dist/reflection/filter.d.ts.map +1 -0
  149. package/dist/reflection/gpu-table.d.ts +25 -0
  150. package/dist/reflection/gpu-table.d.ts.map +1 -0
  151. package/dist/reflection/projection.d.ts +91 -0
  152. package/dist/reflection/projection.d.ts.map +1 -0
  153. package/dist/render-contract.d.ts +49 -7
  154. package/dist/render-contract.d.ts.map +1 -1
  155. package/dist/render-graph-primitives.d.ts +1 -0
  156. package/dist/render-graph-primitives.d.ts.map +1 -1
  157. package/dist/render-pipeline.d.ts +42 -2
  158. package/dist/render-pipeline.d.ts.map +1 -1
  159. package/dist/render-system-extract.d.ts +58 -17
  160. package/dist/render-system-extract.d.ts.map +1 -1
  161. package/dist/render-system.d.ts +17 -2
  162. package/dist/render-system.d.ts.map +1 -1
  163. package/dist/renderer.d.ts +43 -0
  164. package/dist/renderer.d.ts.map +1 -0
  165. package/dist/scene/render-scene.d.ts +14 -0
  166. package/dist/scene/render-scene.d.ts.map +1 -1
  167. package/dist/shadow-atlas.d.ts +3 -3
  168. package/dist/shadow-atlas.d.ts.map +1 -1
  169. package/dist/ssao-buffers.d.ts +3 -0
  170. package/dist/ssao-buffers.d.ts.map +1 -1
  171. package/dist/targets/contracts.d.ts +66 -0
  172. package/dist/targets/contracts.d.ts.map +1 -0
  173. package/dist/targets/graph-projection.d.ts +29 -0
  174. package/dist/targets/graph-projection.d.ts.map +1 -0
  175. package/dist/targets/material-source.d.ts +34 -0
  176. package/dist/targets/material-source.d.ts.map +1 -0
  177. package/dist/targets/owner.d.ts +32 -0
  178. package/dist/targets/owner.d.ts.map +1 -0
  179. package/dist/targets/physical.d.ts +18 -0
  180. package/dist/targets/physical.d.ts.map +1 -0
  181. package/dist/targets/readback.d.ts +39 -0
  182. package/dist/targets/readback.d.ts.map +1 -0
  183. package/dist/temporal/index.mjs +3 -3
  184. package/dist/typed-render-graph-primitives.d.ts +1 -1
  185. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  186. package/dist/typed-shadow-passes.d.ts +1 -1
  187. package/dist/typed-shadow-passes.d.ts.map +1 -1
  188. package/dist/volume/passes.d.ts +2 -2
  189. package/dist/volume/passes.d.ts.map +1 -1
  190. package/package.json +20 -20
  191. package/src/__tests__/bloom-exact-zero.unit.test.ts +48 -19
  192. package/src/__tests__/bloom-lifecycle.integration.test.ts +41 -13
  193. package/src/__tests__/camera-target-consumption.unit.test.ts +53 -0
  194. package/src/__tests__/cluster-binner.unit.test.ts +35 -0
  195. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +7 -2
  196. package/src/__tests__/cube-camera-scheduler.unit.test.ts +70 -0
  197. package/src/__tests__/cube-camera-single-submit.perf.test.ts +75 -0
  198. package/src/__tests__/cube-camera-view-matrices.unit.test.ts +41 -0
  199. package/src/__tests__/cube-camera-vocabulary.unit.test.ts +47 -0
  200. package/src/__tests__/cube-camera.integration.test.ts +42 -0
  201. package/src/__tests__/device-feature-admission.unit.test.ts +73 -0
  202. package/src/__tests__/direct-light-snapshot.unit.test.ts +3 -3
  203. package/src/__tests__/docs-gate-green.unit.test.ts +38 -0
  204. package/src/__tests__/factory-contract.integration.test.ts +255 -29
  205. package/src/__tests__/fog-component.unit.test.ts +67 -0
  206. package/src/__tests__/fog.integration.test.ts +86 -0
  207. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +37 -0
  208. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +10 -9
  209. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +17 -1
  210. package/src/__tests__/gpu-driven-production.integration.test.ts +16 -17
  211. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +12 -4
  212. package/src/__tests__/gpu-pass-timing-browser-runner.ts +281 -0
  213. package/src/__tests__/gpu-pass-timing-coexistence.integration.test.ts +80 -0
  214. package/src/__tests__/gpu-pass-timing-contract.unit.test.ts +243 -0
  215. package/src/__tests__/gpu-pass-timing-exact-zero.unit.test.ts +131 -0
  216. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +230 -0
  217. package/src/__tests__/gpu-pass-timing-observation.unit.test.ts +233 -0
  218. package/src/__tests__/gpu-pass-timing-session.unit.test.ts +438 -0
  219. package/src/__tests__/gpu-pass-timing.browser.test.ts +26 -0
  220. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +125 -0
  221. package/src/__tests__/hdrp-group2-visibility.unit.test.ts +24 -0
  222. package/src/__tests__/instances-culling.integration.test.ts +94 -0
  223. package/src/__tests__/instances-derived-bounds.unit.test.ts +118 -0
  224. package/src/__tests__/material-observation.integration.test.ts +101 -0
  225. package/src/__tests__/pbr-view-layout.unit.test.ts +16 -0
  226. package/src/__tests__/point-shadow-inspection.unit.test.ts +60 -0
  227. package/src/__tests__/public-surface.test-d.ts +10 -0
  228. package/src/__tests__/reflection-probe-authoring.unit.test.ts +71 -0
  229. package/src/__tests__/reflection-probe-filter.unit.test.ts +54 -0
  230. package/src/__tests__/reflection-probe-ibl-topology.unit.test.ts +45 -0
  231. package/src/__tests__/reflection-probe-pass-encode.integration.test.ts +101 -0
  232. package/src/__tests__/reflection-probe-performance.perf.test.ts +60 -0
  233. package/src/__tests__/reflection-probe-recovery.integration.test.ts +55 -0
  234. package/src/__tests__/reflection-probe-selection.unit.test.ts +46 -0
  235. package/src/__tests__/render-error-code-owner.test-d.ts +18 -3
  236. package/src/__tests__/render-feature-docs.unit.test.ts +1 -1
  237. package/src/__tests__/render-target-admission.unit.test.ts +80 -0
  238. package/src/__tests__/render-target-graph-projection.unit.test.ts +102 -0
  239. package/src/__tests__/render-target-inspection.unit.test.ts +52 -0
  240. package/src/__tests__/render-target-material-source.test-d.ts +30 -0
  241. package/src/__tests__/render-target-material-source.unit.test.ts +97 -0
  242. package/src/__tests__/render-target-owner-placement.unit.test.ts +203 -0
  243. package/src/__tests__/render-target-owner.unit.test.ts +122 -0
  244. package/src/__tests__/render-target-probe-soak.perf.test.ts +94 -0
  245. package/src/__tests__/render-target-public-schema.json +107 -0
  246. package/src/__tests__/render-target-public-schema.unit.test.ts +91 -0
  247. package/src/__tests__/render-target-public-surface.test-d.ts +83 -0
  248. package/src/__tests__/render-target-readback.integration.test.ts +90 -0
  249. package/src/__tests__/render-target-recovery.integration.test.ts +85 -0
  250. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +48 -0
  251. package/src/__tests__/renderer-lifecycle.integration.test.ts +14 -0
  252. package/src/__tests__/shader-stage-surface.unit.test.ts +19 -4
  253. package/src/__tests__/skin-motion-regression.unit.test.ts +132 -0
  254. package/src/__tests__/skinned-shadow-caster.test.ts +11 -2
  255. package/src/__tests__/standard-cluster-capability.integration.test.ts +63 -0
  256. package/src/__tests__/standard-cluster-transport.unit.test.ts +121 -0
  257. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +232 -0
  258. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +8 -5
  259. package/src/__tests__/standard-lighting-inspection.integration.test.ts +138 -0
  260. package/src/__tests__/standard-lighting-ssot.unit.test.ts +18 -0
  261. package/src/__tests__/standard-lighting-write.unit.test.ts +377 -0
  262. package/src/__tests__/standard-lighting.integration.test.ts +67 -0
  263. package/src/__tests__/standard-pipeline.dawn.test.ts +206 -5
  264. package/src/__tests__/standard-pipeline.integration.test.ts +181 -150
  265. package/src/__tests__/standard-probe-binding.unit.test.ts +25 -0
  266. package/src/__tests__/standard-profile-oracle.unit.test.ts +23 -38
  267. package/src/__tests__/typed-pipeline-topology.unit.test.ts +395 -14
  268. package/src/__tests__/urp-pipeline-targets.unit.test.ts +2 -2
  269. package/src/__tests__/visibility-resolver.unit.test.ts +15 -0
  270. package/src/assembly/device-feature-admission.ts +73 -0
  271. package/src/assembly/directional-shadow-inspection.ts +122 -0
  272. package/src/assembly/factory.ts +877 -286
  273. package/src/assembly/host-contract.ts +25 -0
  274. package/src/assembly/render-target-host.ts +847 -0
  275. package/src/assets/asset-decoders.ts +1 -1
  276. package/src/assets/render-asset-port.ts +7 -1
  277. package/src/capture/cube-views.ts +69 -0
  278. package/src/capture/scheduler.ts +265 -0
  279. package/src/components/__tests__/schema-contract.unit.test.ts +12 -0
  280. package/src/components/__tests__/schema-parity.unit.test.ts +9 -0
  281. package/src/components/camera.ts +4 -0
  282. package/src/components/cube-camera.ts +52 -0
  283. package/src/components/directional-light.ts +22 -2
  284. package/src/components/directional-shadow-filter.ts +57 -0
  285. package/src/components/index.ts +2 -0
  286. package/src/components/light-helpers.ts +81 -7
  287. package/src/components/reflection-probe.ts +49 -0
  288. package/src/environment/frame.ts +6 -24
  289. package/src/environment/lifecycle.ts +12 -0
  290. package/src/errors/render.ts +286 -142
  291. package/src/extract/directional-shadow-projection.ts +61 -0
  292. package/src/extract/environment.ts +57 -0
  293. package/src/extract/visibility.ts +25 -1
  294. package/src/features/fullscreen.ts +3 -0
  295. package/src/features/targets.ts +4 -1
  296. package/src/features/types.ts +10 -0
  297. package/src/fullscreen-post-process-pass.ts +43 -2
  298. package/src/gpu-driven/batch-topology.ts +1 -0
  299. package/src/gpu-driven/production-raster.ts +1 -3
  300. package/src/gpu-scene.ts +8 -1
  301. package/src/hdrp-buffers.ts +165 -59
  302. package/src/ibl/IblPipelineCache.ts +11 -0
  303. package/src/ibl/kernel-cache.ts +88 -0
  304. package/src/index.ts +148 -0
  305. package/src/inspection-types.ts +56 -0
  306. package/src/instances-derived-bounds.ts +171 -0
  307. package/src/light-buffer-layout.ts +20 -5
  308. package/src/mesh-material-bindings.ts +132 -0
  309. package/src/pbr-pipeline.ts +106 -17
  310. package/src/pipeline/standard-forward-lane.ts +46 -16
  311. package/src/pipeline/standard-lighting/errors.ts +6 -0
  312. package/src/pipeline/standard-lighting/graph.ts +157 -0
  313. package/src/pipeline/standard-lighting/inspection.ts +88 -0
  314. package/src/pipeline/standard-lighting/layout.ts +31 -0
  315. package/src/pipeline/standard-lighting/light-frame.ts +34 -0
  316. package/src/pipeline/standard-lighting/membership.ts +43 -0
  317. package/src/pipeline/standard-lighting/prepare.ts +37 -0
  318. package/src/pipeline/standard-lighting/topology.ts +120 -0
  319. package/src/pipeline/standard-lighting/transport.ts +77 -0
  320. package/src/pipeline/standard-pipeline.ts +122 -193
  321. package/src/pipeline/standard-post.ts +26 -7
  322. package/src/pipeline/standard-profile.ts +17 -47
  323. package/src/pipeline-spec.ts +84 -21
  324. package/src/point-shadow-inspection.ts +55 -0
  325. package/src/record/__tests__/temporal-payload.unit.test.ts +42 -1
  326. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +73 -0
  327. package/src/record/__tests__/view-ubo-layout.unit.test.ts +40 -0
  328. package/src/record/frame-lighting.ts +367 -427
  329. package/src/record/frame-snapshot.ts +35 -27
  330. package/src/record/frame-targets.ts +17 -0
  331. package/src/record/frame.ts +261 -74
  332. package/src/record/gpu-pass-timing/contract.ts +180 -0
  333. package/src/record/gpu-pass-timing/errors.ts +34 -0
  334. package/src/record/gpu-pass-timing/index.ts +37 -0
  335. package/src/record/gpu-pass-timing/parser.ts +97 -0
  336. package/src/record/gpu-pass-timing/session.ts +938 -0
  337. package/src/record/gpu-timing.ts +21 -12
  338. package/src/record/main-pass-geometry.ts +120 -80
  339. package/src/record/main-pass-material.ts +95 -9
  340. package/src/record/main-pass-sprite-draws.ts +54 -25
  341. package/src/record/main-pass.ts +71 -15
  342. package/src/record/render-context.ts +72 -0
  343. package/src/record/shadow-pass.ts +17 -28
  344. package/src/record/skybox-post-pass.ts +20 -4
  345. package/src/record/typed-frame-graph.ts +893 -44
  346. package/src/record/view-ubo.ts +63 -28
  347. package/src/reflection/filter.ts +81 -0
  348. package/src/reflection/gpu-table.ts +44 -0
  349. package/src/reflection/projection.ts +281 -0
  350. package/src/render-contract.ts +76 -5
  351. package/src/render-graph-primitives.ts +9 -4
  352. package/src/render-pipeline.ts +117 -2
  353. package/src/render-system-extract.ts +557 -100
  354. package/src/render-system.ts +1095 -66
  355. package/src/renderer.ts +222 -0
  356. package/src/scene/render-scene.ts +115 -9
  357. package/src/shadow-atlas.ts +5 -5
  358. package/src/ssao-buffers.ts +9 -2
  359. package/src/targets/contracts.ts +182 -0
  360. package/src/targets/graph-projection.ts +169 -0
  361. package/src/targets/material-source.ts +143 -0
  362. package/src/targets/owner.ts +214 -0
  363. package/src/targets/physical.ts +18 -0
  364. package/src/targets/readback.ts +153 -0
  365. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +59 -20
  366. package/src/typed-render-graph-primitives.ts +5 -3
  367. package/src/typed-shadow-passes.ts +46 -40
  368. package/src/volume/passes.ts +15 -9
  369. package/dist/chunk-4XINDLAY.mjs.map +0 -1
  370. package/dist/chunk-CDTWTHZR.mjs.map +0 -1
  371. package/dist/chunk-M2MP4ZOU.mjs.map +0 -1
  372. package/dist/chunk-TBQJOK2L.mjs.map +0 -1
@@ -2,40 +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
8
  type BindGroupEntry,
9
9
  type BindGroupLayout,
10
+ type RhiDevice,
10
11
  RhiError,
11
12
  type TextureView,
12
13
  } from '@forgeax/engine-rhi';
13
- import { toShared } from '@forgeax/engine-types';
14
- import { bin, type ClusterBinProfilePhase, type ClusterBinProfileRunner } from '../cluster-binner';
14
+ import { err, ok, type Result, toShared } from '@forgeax/engine-types';
15
15
  import {
16
- HdrpIndexListOverflowError,
17
- HdrpLightBudgetExceededError,
18
16
  PointShadowAtlasBoundsViolationError,
19
17
  PointShadowAtlasUninitializedError,
18
+ type RenderError,
19
+ RendererOperationError,
20
+ StandardClusterTransportUnavailableError,
20
21
  } from '../errors/render';
21
22
  import {
22
23
  createHdrpClusterMembershipBindGroup,
23
24
  createHdrpUnifiedBindGroup,
24
25
  getOrCreateHdrpBuffers,
25
- HDRP_UNIFORM_LIGHT_CAPACITY,
26
26
  packClusterUniform,
27
27
  } from '../hdrp-buffers';
28
+ import { packLightSlot } from '../light-buffer-layout';
28
29
  import {
29
- LIGHT_ARRAY_HEADER_BYTES,
30
- LIGHT_ARRAY_MAX_SLOTS,
31
- POINT_LIGHT_STD430_BYTES,
32
- packLightArrayHeader,
33
- packLightSlot,
34
- packPointLight,
35
- packSpotLight,
36
- SPOT_LIGHT_STD430_BYTES,
37
- } from '../light-buffer-layout';
38
- 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';
39
41
  import type { CameraSnapshot, RenderHdrpClusterPhase } from '../render-contract';
40
42
  import type {
41
43
  DirectionalLightSnapshot,
@@ -44,7 +46,11 @@ import type {
44
46
  SkyboxSnapshot,
45
47
  SkylightSnapshot,
46
48
  } from '../render-system-extract';
47
- import { ShadowAtlas } from '../shadow-atlas';
49
+ import {
50
+ SHADOW_ATLAS_DEFAULT_FACE_SIZE,
51
+ SHADOW_ATLAS_DEFAULT_LAYERS,
52
+ ShadowAtlas,
53
+ } from '../shadow-atlas';
48
54
  import { getOrCreateSsaoBuffers } from '../ssao-buffers';
49
55
  import { getSsaoParameters } from '../ssao-config';
50
56
  import type { BindGroupCounts, RenderFrameState } from './frame-snapshot';
@@ -53,13 +59,43 @@ import {
53
59
  computeViewMatrix,
54
60
  isLitMaterialSnapshot,
55
61
  warnMultiLightDirectional,
56
- warnMultiLightPoint,
57
- warnMultiLightSpot,
58
62
  } from './helpers';
59
63
  import { getOrCreateFromChain, MESH_SSBO_BYTES, MESH_UBO_FULL_ARRAY_BYTES } from './mesh-ssbo';
60
64
  import type { PipelineState, RecordProfileRunner, RenderSystemInternals } from './render-context';
61
65
  import { POINTS_LINES_VIEW_BYTES, VIEW_UNIFORM_BYTES } from './view-ubo';
62
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
+
63
99
  function runHdrpClusterProfilePhase<T>(
64
100
  runner: RecordProfileRunner | undefined,
65
101
  phase: RenderHdrpClusterPhase,
@@ -68,6 +104,26 @@ function runHdrpClusterProfilePhase<T>(
68
104
  return runner === undefined ? action() : runner(phase, action);
69
105
  }
70
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
+
71
127
  /**
72
128
  * feat-20260704 M3/w18: per-frame bind-group cache resolution, extracted
73
129
  * verbatim from `recordFrame`. Walks the handle-identity WeakMap caches
@@ -92,6 +148,7 @@ export function buildPerFrameBindGroups(
92
148
  readonly projectorSampler?: import('@forgeax/engine-rhi').Sampler | undefined;
93
149
  },
94
150
  includeView = true,
151
+ standardLighting?: StandardTopologyInputValue,
95
152
  ): {
96
153
  viewBindGroup: BindGroup | null;
97
154
  meshBindGroup: BindGroup | null;
@@ -105,12 +162,31 @@ export function buildPerFrameBindGroups(
105
162
  // Mesh (#2) chain = inner b0 buffer (meshStorageBuffer.buffer); variant 'mesh'.
106
163
  let viewBindGroup: BindGroup | null = null;
107
164
  let meshBindGroup: BindGroup | null = null;
108
- // feat-20260609-hdrp-cluster-fragment-ggx M4 / w19: HDRP unified group(2)
109
- // BindGroup. Non-null when `frameState.isHdrpActive` AND HDRP buffer
110
- // 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, ...)`.
111
168
  let hdrpClusterBindGroup: BindGroup | null = null;
112
- const hdrpClusterMembershipBindGroup: BindGroup | null = frameState.isHdrpActive
113
- ? 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
114
190
  : null;
115
191
  if (hasValidated) {
116
192
  // Shadow atlas view comes from the typed graph target.
@@ -335,11 +411,8 @@ export function buildPerFrameBindGroups(
335
411
  // alongside `meshBindGroupCache` keyed on the mesh SSBO + cluster
336
412
  // buffer identities; cache invalidates on a buffer-grow event the same
337
413
  // way the mesh path does (handle id rotation).
338
- if (frameState.isHdrpActive) {
339
- const hdrpBuffers = getOrCreateHdrpBuffers(
340
- internals,
341
- frameState.installedPipelineConfig?.clusterGrid,
342
- );
414
+ if (clusteredLighting) {
415
+ const hdrpBuffers = expectedHdrpBuffers;
343
416
  if (hdrpBuffers !== null) {
344
417
  // D-3: buffer dimension uses the inner `.buffer` (same constraint
345
418
  // as the mesh path) so a grow event rotates the chain key.
@@ -378,8 +451,7 @@ export function buildPerFrameBindGroups(
378
451
 
379
452
  /**
380
453
  * feat-20260704 M3/w18: per-frame lighting preparation, extracted verbatim from
381
- * `recordFrame`. (1) URP-only multi-light warn-once (directional / point / spot
382
- * over `LIGHT_ARRAY_MAX_SLOTS`; HDRP gated off — 256 SSBO lights). (2) pin the
454
+ * `recordFrame`. (1) pin the
383
455
  * point + spot shadow snapshot lists onto frameState for the URP shadow caster
384
456
  * pass closures + lazy-allocate the point-shadow cube_array atlas on first
385
457
  * non-empty frame (AC-09 zero-shadow zero-alloc). (3) destructure ExtractedLights
@@ -392,12 +464,21 @@ export function prepareFrameLighting(
392
464
  internals: RenderSystemInternals,
393
465
  frameState: RenderFrameState,
394
466
  lights: ExtractedLights,
395
- ): {
396
- light: DirectionalLightSnapshot;
397
- pointLights: ExtractedLights['point'];
398
- spotLights: ExtractedLights['spot'];
399
- totalLightCount: number;
400
- } {
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
+ > {
401
482
  if (lights.directionalCount > 1) {
402
483
  warnMultiLightDirectional(frameState, lights.directionalCount);
403
484
  }
@@ -408,21 +489,6 @@ export function prepareFrameLighting(
408
489
  // defaults true on the single component, so the warn condition can never
409
490
  // arise. The error class + RuntimeErrorCode member have been removed in M4 (m4-t2).
410
491
 
411
- // feat-20260608-cluster-lighting M6 / w23 (F-4 fix): URP-only multi-light
412
- // warn. HDRP supports 256 punctual lights via SSBO; the 4-slot first-slice
413
- // cap is irrelevant under HDRP, so gate on `!isHdrpActive` to silence noise.
414
- // Sibling tweak-20260608-rhi-hdr-renderable-caps-and-warn-once (#320)
415
- // extracted the warn into warnMultiLight{Point,Spot} helpers (warn-once
416
- // dedup); we keep the helpers and add the HDRP gate on top.
417
- if (!frameState.isHdrpActive) {
418
- if (lights.point.length > LIGHT_ARRAY_MAX_SLOTS) {
419
- warnMultiLightPoint(frameState, lights.point.length);
420
- }
421
- if (lights.spot.length > LIGHT_ARRAY_MAX_SLOTS) {
422
- warnMultiLightSpot(frameState, lights.spot.length);
423
- }
424
- }
425
-
426
492
  // feat-20260612-point-light-shadows-urp-hdrp M3 / T-M3-2 (plan-strategy §D-1):
427
493
  // project lights.pointShadow onto frameState so the URP point shadow caster
428
494
  // pass can read the snapshot list during graph execute. Lazy-allocate the
@@ -438,10 +504,10 @@ export function prepareFrameLighting(
438
504
  if (lights.pointShadow.length > 0) {
439
505
  if (frameState.pointShadowAtlas === null) {
440
506
  const firstSnap = lights.pointShadow[0];
441
- const faceSize = firstSnap?.mapSize ?? 512;
507
+ const faceSize = firstSnap?.mapSize ?? SHADOW_ATLAS_DEFAULT_FACE_SIZE;
442
508
  frameState.pointShadowAtlas = new ShadowAtlas(internals.device, {
443
509
  faceSize,
444
- layers: 4,
510
+ layers: SHADOW_ATLAS_DEFAULT_LAYERS,
445
511
  });
446
512
  }
447
513
  try {
@@ -485,73 +551,77 @@ export function prepareFrameLighting(
485
551
  const totalLightCount =
486
552
  (directionalLight !== undefined ? 1 : 0) + pointLights.length + spotLights.length;
487
553
 
488
- return { light, pointLights, spotLights, totalLightCount };
489
- }
490
-
491
- /**
492
- * feat-20260704 M3/w18: per-frame full rewrite of the PointLight + SpotLight
493
- * std430 storage buffers, extracted verbatim from `recordFrame`.
494
- *
495
- * feat-20260519-light-casters-point-spot-pbr M3 / w20 (D-S1 + D-S2 + D-S6):
496
- * header (16 B count u32 + 12 B pad) at offset 0 + first-slice cap N=4 slots
497
- * packed sequentially. The N>4 fail-fast upstream ensures the counts are <= 4
498
- * when the listener registry consumed the structured error; the slice keeps
499
- * the buffer write bounded even if downstream listeners ignore the RhiError
500
- * (charter P9 graceful degradation: surplus entities dropped, frame records).
501
- *
502
- * @internal
503
- */
504
- export function writePointSpotLightBuffers(
505
- internals: RenderSystemInternals,
506
- pipelineState: PipelineState,
507
- lights: ExtractedLights,
508
- ): void {
509
- const pointSlots =
510
- lights.point.length <= LIGHT_ARRAY_MAX_SLOTS
511
- ? lights.point
512
- : lights.point.slice(0, LIGHT_ARRAY_MAX_SLOTS);
513
- const pointHeader = packLightArrayHeader(pointSlots.length);
514
- const pointHeaderUpload = internals.device.queue.writeBuffer(
515
- pipelineState.pointLightsBuffer,
516
- 0,
517
- new Uint8Array(pointHeader),
518
- );
519
- if (!pointHeaderUpload.ok) throw pointHeaderUpload.error;
520
- for (let i = 0; i < pointSlots.length; i++) {
521
- const slot = pointSlots[i];
522
- if (slot === undefined) continue;
523
- const packed = packPointLight(slot);
524
- const offset = LIGHT_ARRAY_HEADER_BYTES + i * POINT_LIGHT_STD430_BYTES;
525
- const writeRes = internals.device.queue.writeBuffer(
526
- pipelineState.pointLightsBuffer,
527
- offset,
528
- packed,
529
- );
530
- 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
+ });
531
604
  }
532
- const spotSlots =
533
- lights.spot.length <= LIGHT_ARRAY_MAX_SLOTS
534
- ? lights.spot
535
- : lights.spot.slice(0, LIGHT_ARRAY_MAX_SLOTS);
536
- const spotHeader = packLightArrayHeader(spotSlots.length);
537
- const spotHeaderUpload = internals.device.queue.writeBuffer(
538
- pipelineState.spotLightsBuffer,
539
- 0,
540
- 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,
541
615
  );
542
- if (!spotHeaderUpload.ok) throw spotHeaderUpload.error;
543
- for (let i = 0; i < spotSlots.length; i++) {
544
- const slot = spotSlots[i];
545
- if (slot === undefined) continue;
546
- const packed = packSpotLight(slot);
547
- const offset = LIGHT_ARRAY_HEADER_BYTES + i * SPOT_LIGHT_STD430_BYTES;
548
- const writeRes = internals.device.queue.writeBuffer(
549
- pipelineState.spotLightsBuffer,
550
- offset,
551
- packed,
552
- );
553
- if (!writeRes.ok) throw writeRes.error;
554
- }
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
+ });
555
625
  }
556
626
 
557
627
  /**
@@ -637,17 +707,9 @@ export function warnZeroLightStandard(
637
707
  }
638
708
 
639
709
  /**
640
- * feat-20260704 M3/w18: HDRP per-frame CPU cluster binning + light-data /
641
- * cluster-grid / light-index-list / cluster-uniform / SSAO uniform buffer
642
- * uploads. Extracted verbatim from `recordFrame`. Runs only when the HDRP
643
- * pipeline is active. A zero-punctual frame explicitly clears the persistent
644
- * cluster buffers so stale membership cannot leak across frames; direct and
645
- * non-clustered paths remain untouched.
646
- *
647
- * feat-20260608-cluster-lighting M5 / w21 + M6 / w23 + r2 fix-up: fail-soft
648
- * semantics preserved verbatim — index-list-overflow and light-budget-exceeded
649
- * fire once per frame (via `frameState.hdrpOncePerFrameFired`) and continue
650
- * 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.
651
713
  *
652
714
  * @internal
653
715
  */
@@ -655,253 +717,107 @@ export function writeHdrpClusterAndSsaoBuffers(
655
717
  internals: RenderSystemInternals,
656
718
  frameState: RenderFrameState,
657
719
  camera: CameraSnapshot,
658
- pointLights: ExtractedLights['point'],
659
- spotLights: ExtractedLights['spot'],
720
+ prepared: PreparedStandardLighting,
721
+ transport: StandardClusterTransport,
660
722
  profilePhase?: RecordProfileRunner,
661
- useGpuMembership = false,
723
+ gpuMembershipPipelineReady = false,
662
724
  membershipBindGroupLayout: BindGroupLayout | null = null,
663
- ): void {
664
- const hdrpLightCount = pointLights.length + spotLights.length;
665
- if (!frameState.isHdrpActive) return;
666
- if (hdrpLightCount === 0) {
667
- const grid = frameState.installedPipelineConfig?.clusterGrid ?? DEFAULT_CLUSTER_GRID;
668
- const hdrpBuffers = getOrCreateHdrpBuffers(internals, grid);
669
- if (hdrpBuffers === null) return;
670
- const lightCapacity = hdrpBuffers.storageBuffer ? 256 : HDRP_UNIFORM_LIGHT_CAPACITY;
671
- const clusterSsaoConfig = frameState.installedPipelineConfig?.ssao;
672
- const clusterSsaoIntensity =
673
- clusterSsaoConfig !== undefined && clusterSsaoConfig.enabled === true
674
- ? (clusterSsaoConfig.intensity ?? 1.0)
675
- : 0;
676
- const clusterUniformPayload = packClusterUniform(
677
- grid,
678
- camera.near,
679
- camera.far,
680
- clusterSsaoIntensity,
681
- 0,
682
- lightCapacity,
683
- );
684
- const clusterUniformUpload = internals.device.queue.writeBuffer(
685
- hdrpBuffers.clusterUniformBuffer,
686
- 0,
687
- new Uint8Array(clusterUniformPayload),
688
- );
689
- if (!clusterUniformUpload.ok) internals.errorRegistry.fire(clusterUniformUpload.error);
690
- const clusterGridClear = internals.device.queue.writeBuffer(
691
- hdrpBuffers.clusterGridBuffer,
692
- 0,
693
- new Uint8Array(hdrpBuffers.clusterGridBytes),
694
- );
695
- if (!clusterGridClear.ok) internals.errorRegistry.fire(clusterGridClear.error);
696
- const lightIndexListClear = internals.device.queue.writeBuffer(
697
- hdrpBuffers.lightIndexListBuffer,
698
- 0,
699
- new Uint8Array(hdrpBuffers.lightIndexListBytes),
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
+ ),
700
735
  );
701
- if (!lightIndexListClear.ok) internals.errorRegistry.fire(lightIndexListClear.error);
702
- return;
703
736
  }
704
- const HDRP_LIGHT_BUDGET = 256;
705
- let effectivePointLights = pointLights;
706
- let effectiveSpotLights = spotLights;
707
- if (hdrpLightCount > HDRP_LIGHT_BUDGET) {
708
- if (!frameState.hdrpOncePerFrameFired.has('hdrp-light-budget-exceeded')) {
709
- frameState.hdrpOncePerFrameFired.add('hdrp-light-budget-exceeded');
710
- internals.errorRegistry.fire(
711
- new HdrpLightBudgetExceededError(hdrpLightCount, HDRP_LIGHT_BUDGET),
712
- );
713
- }
714
- if (pointLights.length >= HDRP_LIGHT_BUDGET) {
715
- effectivePointLights = pointLights.slice(0, HDRP_LIGHT_BUDGET);
716
- effectiveSpotLights = [];
717
- } else {
718
- effectivePointLights = pointLights;
719
- effectiveSpotLights = spotLights.slice(0, HDRP_LIGHT_BUDGET - pointLights.length);
720
- }
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
+ );
721
745
  }
722
- const effectiveLightCount = effectivePointLights.length + effectiveSpotLights.length;
723
- const configuredClusterGrid =
724
- frameState.installedPipelineConfig?.clusterGrid ?? DEFAULT_CLUSTER_GRID;
725
- 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;
726
755
  if (
727
756
  gpuMembership &&
728
757
  membershipBindGroupLayout !== null &&
729
- frameState.hdrpClusterMembershipBindGroup === null
758
+ frameState.hdrpClusterMembership === null
730
759
  ) {
731
- const hdrpBuffers = getOrCreateHdrpBuffers(internals, configuredClusterGrid);
732
- if (hdrpBuffers !== null) {
733
- frameState.hdrpClusterMembershipBindGroup = createHdrpClusterMembershipBindGroup(
734
- internals,
735
- hdrpBuffers,
736
- 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
+ ),
737
771
  );
738
772
  }
739
- gpuMembership = frameState.hdrpClusterMembershipBindGroup !== null;
773
+ frameState.hdrpClusterMembership = {
774
+ device: internals.device,
775
+ layout: membershipBindGroupLayout,
776
+ bindGroup,
777
+ clusterGridBuffer: hdrpBuffers.clusterGridBuffer,
778
+ };
740
779
  }
741
780
 
742
- const { clusterGrid, gridX, gridY, gridZ, clusterGridBuf, lightIndexListBuf, lightIndexCount } =
743
- runHdrpClusterProfilePhase(profilePhase, 'record/scene-state/hdrp-cluster/binner', () => {
744
- const binnerProfile: ClusterBinProfileRunner | undefined =
745
- profilePhase === undefined
746
- ? undefined
747
- : <T>(phase: ClusterBinProfilePhase, action: () => T): T =>
748
- runHdrpClusterProfilePhase(
749
- profilePhase,
750
- `record/scene-state/hdrp-cluster/binner/${phase}` as RenderHdrpClusterPhase,
751
- action,
752
- );
753
- const {
754
- hdrpLights,
755
- clusterGrid,
756
- gridX,
757
- gridY,
758
- gridZ,
759
- clusterGridBuf,
760
- lightIndexListBuf,
761
- projMatrix,
762
- viewMatrix,
763
- } = runHdrpClusterProfilePhase(
764
- profilePhase,
765
- 'record/scene-state/hdrp-cluster/binner/input-preparation',
766
- () => {
767
- const hdrpLights: Array<{ position: Vec3; range: number }> = [];
768
- for (const pl of effectivePointLights) {
769
- const range =
770
- Number.isFinite(pl.invRangeSquared) && pl.invRangeSquared > 0
771
- ? Math.sqrt(1 / pl.invRangeSquared)
772
- : 1000;
773
- hdrpLights.push({ position: pl.position, range });
774
- }
775
- for (const sl of effectiveSpotLights) {
776
- const range =
777
- Number.isFinite(sl.invRangeSquared) && sl.invRangeSquared > 0
778
- ? Math.sqrt(1 / sl.invRangeSquared)
779
- : 1000;
780
- hdrpLights.push({ position: sl.position, range });
781
- }
782
-
783
- const clusterGrid =
784
- frameState.installedPipelineConfig?.clusterGrid ?? DEFAULT_CLUSTER_GRID;
785
- const gridX = clusterGrid.x;
786
- const gridY = clusterGrid.y;
787
- const gridZ = clusterGrid.z;
788
- const clusterCount = gridX * gridY * gridZ;
789
-
790
- const projMatrix = computeProjectionMatrix(camera);
791
- const viewMatrix = computeViewMatrix(camera);
792
-
793
- const clusterGridBuf =
794
- frameState.hdrpClusterGridScratch === null ||
795
- frameState.hdrpClusterGridScratch.length < clusterCount * 2
796
- ? new Uint32Array(clusterCount * 2)
797
- : frameState.hdrpClusterGridScratch;
798
- frameState.hdrpClusterGridScratch = clusterGridBuf;
799
- const lightIndexListBuf =
800
- frameState.hdrpLightIndexListScratch === null ||
801
- frameState.hdrpLightIndexListScratch.length < LIGHT_INDEX_LIST_CAPACITY
802
- ? new Uint32Array(LIGHT_INDEX_LIST_CAPACITY)
803
- : frameState.hdrpLightIndexListScratch;
804
- frameState.hdrpLightIndexListScratch = lightIndexListBuf;
805
-
806
- return {
807
- hdrpLights,
808
- clusterGrid,
809
- gridX,
810
- gridY,
811
- gridZ,
812
- clusterGridBuf,
813
- lightIndexListBuf,
814
- projMatrix,
815
- viewMatrix,
816
- };
817
- },
818
- );
819
-
820
- const binResult = runHdrpClusterProfilePhase(
821
- profilePhase,
822
- 'record/scene-state/hdrp-cluster/binner/bin-core',
823
- () =>
824
- bin(
825
- hdrpLights,
826
- viewMatrix,
827
- projMatrix,
828
- { x: gridX, y: gridY, z: gridZ },
829
- camera.near,
830
- camera.far,
831
- clusterGridBuf,
832
- lightIndexListBuf,
833
- LIGHT_INDEX_LIST_CAPACITY,
834
- frameState.hdrpClusterBinScratch,
835
- binnerProfile,
836
- { writeMembership: !gpuMembership },
837
- ),
838
- );
839
-
840
- if (!binResult.ok && !frameState.hdrpOncePerFrameFired.has('hdrp-index-list-overflow')) {
841
- frameState.hdrpOncePerFrameFired.add('hdrp-index-list-overflow');
842
- const detail = binResult.error.detail;
843
- internals.errorRegistry.fire(
844
- new HdrpIndexListOverflowError(detail.actual, detail.capacity),
845
- );
846
- }
847
-
848
- // Falsify injection point: FORGEAX_HDRP_FALSIFY_CLUSTER_GRID_ZERO
849
- // zeroes the cluster_grid buffer so every fragment culls every light.
850
- // Used by hello-hdrp-lighting smoke FALSIFY=cluster-grid-zero to
851
- // prove the smoke has discriminability (must FAIL vs baseline).
852
- // Read process via globalThis to keep this file @types/node-free.
853
- const envFalsify = (globalThis as { process?: { env?: Record<string, string | undefined> } })
854
- .process?.env;
855
- if (envFalsify?.FORGEAX_HDRP_FALSIFY_CLUSTER_GRID_ZERO) {
856
- clusterGridBuf.fill(0);
857
- }
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;
858
787
 
859
- return {
860
- clusterGrid,
861
- gridX,
862
- gridY,
863
- gridZ,
864
- clusterGridBuf,
865
- lightIndexListBuf,
866
- lightIndexCount: binResult.ok ? binResult.value : 0,
867
- };
868
- });
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);
869
793
 
870
794
  const hdrpPayload = runHdrpClusterProfilePhase(
871
795
  profilePhase,
872
796
  'record/scene-state/hdrp-cluster/payload-packing',
873
797
  () => {
874
- const hdrpBuffers = getOrCreateHdrpBuffers(internals, clusterGrid);
875
- if (hdrpBuffers === null) return null;
876
-
877
- const lightCapacity = hdrpBuffers.storageBuffer ? 256 : HDRP_UNIFORM_LIGHT_CAPACITY;
878
- const lightDataPayload = new Float32Array(lightCapacity * 16);
798
+ const lightDataPayload = new Float32Array(prepared.layout.lightDataSlotCount * 16);
879
799
  let slotIdx = 0;
880
- for (const pl of effectivePointLights) {
881
- if (slotIdx >= lightCapacity) break;
882
- // feat-20260612-point-light-shadows-urp-hdrp M4 / T-M4-4
883
- // (plan-strategy §D-8): thread the optional shadow info through
884
- // packLightSlot. PointLightSnapshot.shadowAtlasLayer is set by
885
- // extract's join pass when the entity carries PointLightShadow;
886
- // sentinel -1 means no shadow (unshadowed evalPoint path).
887
- 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;
888
805
  const packed =
889
- layer !== undefined &&
890
- layer >= 0 &&
891
- pl.shadowNear !== undefined &&
892
- pl.shadowFar !== undefined
893
- ? packLightSlot(pl, {
894
- shadowAtlasLayer: layer,
895
- near: pl.shadowNear,
896
- far: pl.shadowFar,
897
- })
898
- : packLightSlot(pl);
899
- lightDataPayload.set(packed, slotIdx * 16);
900
- slotIdx += 1;
901
- }
902
- for (const sl of effectiveSpotLights) {
903
- if (slotIdx >= lightCapacity) break;
904
- 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);
905
821
  lightDataPayload.set(packed, slotIdx * 16);
906
822
  slotIdx += 1;
907
823
  }
@@ -922,8 +838,7 @@ export function writeHdrpClusterAndSsaoBuffers(
922
838
  camera.near,
923
839
  camera.far,
924
840
  clusterSsaoIntensity,
925
- Math.min(effectivePointLights.length + effectiveSpotLights.length, lightCapacity),
926
- lightCapacity,
841
+ transport.admittedLightCount,
927
842
  );
928
843
 
929
844
  return {
@@ -934,39 +849,53 @@ export function writeHdrpClusterAndSsaoBuffers(
934
849
  },
935
850
  );
936
851
 
937
- runHdrpClusterProfilePhase(profilePhase, 'record/scene-state/hdrp-cluster/buffer-upload', () => {
938
- 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> => {
939
865
  const { hdrpBuffers, lightDataUploadPayload, clusterUniformPayload } = hdrpPayload;
940
866
  const lightDataUpload = internals.device.queue.writeBuffer(
941
867
  hdrpBuffers.lightDataBuffer,
942
868
  0,
943
869
  lightDataUploadPayload,
944
870
  );
945
- if (!lightDataUpload.ok) internals.errorRegistry.fire(lightDataUpload.error);
871
+ if (!lightDataUpload.ok)
872
+ return err(mapClusterUploadFailure('light-data', lightDataUpload.error));
946
873
 
947
- if (hdrpBuffers.storageBuffer) {
948
- if (gpuMembership) {
949
- const lightBoundsUpload = internals.device.queue.writeBuffer(
950
- hdrpBuffers.lightBoundsBuffer,
951
- 0,
952
- frameState.hdrpClusterBinScratch.lightBounds.subarray(0, effectiveLightCount * 6),
953
- );
954
- if (!lightBoundsUpload.ok) internals.errorRegistry.fire(lightBoundsUpload.error);
955
- }
956
- const clusterGridUpload = internals.device.queue.writeBuffer(
957
- hdrpBuffers.clusterGridBuffer,
874
+ if (gpuMembership) {
875
+ const lightBoundsUpload = internals.device.queue.writeBuffer(
876
+ hdrpBuffers.lightBoundsBuffer,
958
877
  0,
959
- clusterGridBuf.subarray(0, gridX * gridY * gridZ * 2),
878
+ prepared.lightBounds,
960
879
  );
961
- 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));
962
890
 
963
- if (!gpuMembership && lightIndexCount > 0) {
964
- const lightIndexListUpload = internals.device.queue.writeBuffer(
965
- hdrpBuffers.lightIndexListBuffer,
966
- 0,
967
- lightIndexListBuf.subarray(0, lightIndexCount),
968
- );
969
- 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));
970
899
  }
971
900
  }
972
901
 
@@ -975,56 +904,67 @@ export function writeHdrpClusterAndSsaoBuffers(
975
904
  0,
976
905
  clusterUniformPayload,
977
906
  );
978
- if (!clusterUniformUpload.ok) internals.errorRegistry.fire(clusterUniformUpload.error);
979
- }
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;
980
914
 
981
- // ── feat-20260612-hdrp-ssao M1 / w6 + M7 / w33 ───────────────
982
- // Per-frame SSAO uniform write (plan-strategy D-1 + D-C):
983
- // view + projection + inverseProjection at offsets 0/64/128 +
984
- // intensityPad (vec4 — x=intensity, y=radius, z=bias) at offset 192;
985
- // total 256 B (matches host SSAO_UNIFORM_BYTES + WGSL struct).
986
- // Single writeBuffer covers all four fields so one queue entry
987
- // updates the entire UBO.
988
- // Separate from View UBO (592 B invariant); does not affect
989
- // material PSO bytecode.
990
- //
991
- // Writes when HDRP is active; config.ssao?.enabled guard comes in
992
- // M4 / w19 after the config.ssao type narrowing is added.
993
- {
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> => {
994
931
  const ssaoBufs = getOrCreateSsaoBuffers(internals);
995
- if (ssaoBufs !== null) {
996
- const sProj = computeProjectionMatrix(camera);
997
- const sView = computeViewMatrix(camera);
998
- // inverseProjection = inverse(projection): NDC -> view-space.
999
- const invProjOnly = mat4.create();
1000
- 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);
1001
938
 
1002
- // Float32Array of 64 (256 B): 3 mat4 (48) + intensityPad vec4 (4)
1003
- // + 12 trailing padding floats. We only fill the declared region.
1004
- const ssaoUniformPayload = new Float32Array(64);
1005
- ssaoUniformPayload.set(sView, 0);
1006
- ssaoUniformPayload.set(sProj, 16);
1007
- ssaoUniformPayload.set(invProjOnly, 32);
1008
- // intensityPad carries the same resolved values as the graph SSAO
1009
- // calc pass; this early write keeps the shared UBO valid before the
1010
- // typed fullscreen pass executes.
1011
- const ssaoConfig = frameState.installedPipelineConfig?.ssao;
1012
- const parameters = getSsaoParameters(
1013
- ssaoConfig !== undefined && ssaoConfig.enabled === true ? ssaoConfig : undefined,
1014
- );
1015
- ssaoUniformPayload[48] = parameters.intensity;
1016
- ssaoUniformPayload[49] = parameters.radius;
1017
- 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;
1018
955
 
1019
- const ssaoUniformRes = internals.device.queue.writeBuffer(
1020
- ssaoBufs.uniformBuffer,
1021
- 0,
1022
- ssaoUniformPayload,
1023
- );
1024
- if (!ssaoUniformRes.ok) internals.errorRegistry.fire(ssaoUniformRes.error);
1025
- }
1026
- }
1027
- });
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);
1028
968
  }
1029
969
 
1030
970
  /**