@forgeax/engine-render 0.1.20 → 0.1.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (645) hide show
  1. package/README.md +365 -29
  2. package/dist/assembly/backend-contract.d.ts +13 -0
  3. package/dist/assembly/backend-contract.d.ts.map +1 -1
  4. package/dist/assembly/device-feature-admission.d.ts +18 -0
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -0
  6. package/dist/assembly/directional-shadow-inspection.d.ts +38 -0
  7. package/dist/assembly/directional-shadow-inspection.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +30 -19
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/host-contract.d.ts +19 -0
  11. package/dist/assembly/host-contract.d.ts.map +1 -1
  12. package/dist/assembly/render-target-host.d.ts +46 -0
  13. package/dist/assembly/render-target-host.d.ts.map +1 -0
  14. package/dist/assets/render-asset-port.d.ts.map +1 -1
  15. package/dist/authoring.mjs +4 -1
  16. package/dist/authoring.mjs.map +1 -1
  17. package/dist/capture/cube-views.d.ts +21 -0
  18. package/dist/capture/cube-views.d.ts.map +1 -0
  19. package/dist/capture/scheduler.d.ts +54 -0
  20. package/dist/capture/scheduler.d.ts.map +1 -0
  21. package/dist/{chunk-PCHBPYML.mjs → chunk-4L3P6DT5.mjs} +2 -2
  22. package/dist/chunk-4L3P6DT5.mjs.map +1 -0
  23. package/dist/{chunk-VFPLTKCU.mjs → chunk-4NPUASZR.mjs} +15 -4
  24. package/dist/chunk-4NPUASZR.mjs.map +1 -0
  25. package/dist/chunk-DIYW3M5D.mjs +5085 -0
  26. package/dist/chunk-DIYW3M5D.mjs.map +1 -0
  27. package/dist/chunk-FBC4R44J.mjs +1003 -0
  28. package/dist/chunk-FBC4R44J.mjs.map +1 -0
  29. package/dist/{chunk-CDTWTHZR.mjs → chunk-FF7K3FSO.mjs} +474 -168
  30. package/dist/chunk-FF7K3FSO.mjs.map +1 -0
  31. package/dist/chunk-HZUPSYW7.mjs +1104 -0
  32. package/dist/chunk-HZUPSYW7.mjs.map +1 -0
  33. package/dist/{chunk-TBQJOK2L.mjs → chunk-QODA5UKU.mjs} +321 -269
  34. package/dist/chunk-QODA5UKU.mjs.map +1 -0
  35. package/dist/chunk-YALPS44A.mjs +296 -0
  36. package/dist/chunk-YALPS44A.mjs.map +1 -0
  37. package/dist/{chunk-4XINDLAY.mjs → chunk-ZUWKAZ2I.mjs} +1963 -2178
  38. package/dist/chunk-ZUWKAZ2I.mjs.map +1 -0
  39. package/dist/cluster-binner.d.ts +5 -0
  40. package/dist/cluster-binner.d.ts.map +1 -1
  41. package/dist/components/camera.d.ts +1 -0
  42. package/dist/components/camera.d.ts.map +1 -1
  43. package/dist/components/cube-camera.d.ts +20 -0
  44. package/dist/components/cube-camera.d.ts.map +1 -0
  45. package/dist/components/directional-light.d.ts +11 -2
  46. package/dist/components/directional-light.d.ts.map +1 -1
  47. package/dist/components/directional-shadow-filter.d.ts +25 -0
  48. package/dist/components/directional-shadow-filter.d.ts.map +1 -0
  49. package/dist/components/index.d.ts +5 -0
  50. package/dist/components/index.d.ts.map +1 -1
  51. package/dist/components/light-helpers.d.ts +4 -0
  52. package/dist/components/light-helpers.d.ts.map +1 -1
  53. package/dist/components/light-probe.d.ts +9 -0
  54. package/dist/components/light-probe.d.ts.map +1 -0
  55. package/dist/components/rect-area-light.d.ts +12 -0
  56. package/dist/components/rect-area-light.d.ts.map +1 -0
  57. package/dist/components/reflection-probe.d.ts +17 -0
  58. package/dist/components/reflection-probe.d.ts.map +1 -0
  59. package/dist/components/scene-instance.d.ts +4 -0
  60. package/dist/components/scene-instance.d.ts.map +1 -1
  61. package/dist/components/spot-light.d.ts +3 -0
  62. package/dist/components/spot-light.d.ts.map +1 -1
  63. package/dist/construct-renderer.mjs +14836 -9620
  64. package/dist/construct-renderer.mjs.map +1 -1
  65. package/dist/device/gpu-residency.d.ts +8 -1
  66. package/dist/device/gpu-residency.d.ts.map +1 -1
  67. package/dist/device/lifecycle-transaction.d.ts +2 -0
  68. package/dist/device/lifecycle-transaction.d.ts.map +1 -0
  69. package/dist/environment/frame.d.ts.map +1 -1
  70. package/dist/environment/lifecycle.d.ts +5 -0
  71. package/dist/environment/lifecycle.d.ts.map +1 -1
  72. package/dist/errors/gpu-driven.d.ts +26 -0
  73. package/dist/errors/gpu-driven.d.ts.map +1 -0
  74. package/dist/errors/index.d.ts +1 -0
  75. package/dist/errors/index.d.ts.map +1 -1
  76. package/dist/errors/recover.d.ts +9 -1
  77. package/dist/errors/recover.d.ts.map +1 -1
  78. package/dist/errors/render.d.ts +166 -93
  79. package/dist/errors/render.d.ts.map +1 -1
  80. package/dist/extract/directional-shadow-projection.d.ts +23 -0
  81. package/dist/extract/directional-shadow-projection.d.ts.map +1 -0
  82. package/dist/extract/environment.d.ts +14 -0
  83. package/dist/extract/environment.d.ts.map +1 -1
  84. package/dist/extract/gpu-driven.d.ts +50 -0
  85. package/dist/extract/gpu-driven.d.ts.map +1 -0
  86. package/dist/extract/visibility.d.ts.map +1 -1
  87. package/dist/features/fullscreen.d.ts.map +1 -1
  88. package/dist/features/targets.d.ts.map +1 -1
  89. package/dist/features/types.d.ts +10 -0
  90. package/dist/features/types.d.ts.map +1 -1
  91. package/dist/fullscreen-post-process-pass.d.ts +13 -2
  92. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  93. package/dist/gpu-driven/batch-topology.d.ts +34 -0
  94. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  95. package/dist/gpu-driven/prepared-draw.d.ts +63 -0
  96. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -0
  97. package/dist/gpu-driven/production-raster.d.ts +97 -2
  98. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  99. package/dist/gpu-driven/view-gpu.d.ts +64 -1
  100. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  101. package/dist/gpu-scene-schema.d.ts +14 -0
  102. package/dist/gpu-scene-schema.d.ts.map +1 -1
  103. package/dist/gpu-scene.d.ts +8 -1
  104. package/dist/gpu-scene.d.ts.map +1 -1
  105. package/dist/gpu-usage.d.ts +0 -1
  106. package/dist/gpu-usage.d.ts.map +1 -1
  107. package/dist/hdrp-buffers.d.ts +21 -19
  108. package/dist/hdrp-buffers.d.ts.map +1 -1
  109. package/dist/ibl/IblPipelineCache.d.ts +4 -0
  110. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  111. package/dist/ibl/kernel-cache.d.ts +36 -0
  112. package/dist/ibl/kernel-cache.d.ts.map +1 -0
  113. package/dist/index.d.ts +39 -3
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.mjs +91 -6
  116. package/dist/index.mjs.map +1 -1
  117. package/dist/inspection-types.d.ts +214 -1
  118. package/dist/inspection-types.d.ts.map +1 -1
  119. package/dist/instances-derived-bounds.d.ts +50 -0
  120. package/dist/instances-derived-bounds.d.ts.map +1 -0
  121. package/dist/internal.d.ts +9 -0
  122. package/dist/internal.d.ts.map +1 -1
  123. package/dist/internal.mjs +23 -2
  124. package/dist/internal.mjs.map +1 -1
  125. package/dist/light-buffer-layout.d.ts +58 -140
  126. package/dist/light-buffer-layout.d.ts.map +1 -1
  127. package/dist/materials.d.ts.map +1 -1
  128. package/dist/mesh-material-bindings.d.ts +54 -0
  129. package/dist/mesh-material-bindings.d.ts.map +1 -1
  130. package/dist/pbr-pipeline.d.ts +38 -14
  131. package/dist/pbr-pipeline.d.ts.map +1 -1
  132. package/dist/pipeline/standard-forward-lane.d.ts +2 -1
  133. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  134. package/dist/pipeline/standard-lighting/errors.d.ts +4 -0
  135. package/dist/pipeline/standard-lighting/errors.d.ts.map +1 -0
  136. package/dist/pipeline/standard-lighting/graph.d.ts +24 -0
  137. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -0
  138. package/dist/pipeline/standard-lighting/inspection.d.ts +38 -0
  139. package/dist/pipeline/standard-lighting/inspection.d.ts.map +1 -0
  140. package/dist/pipeline/standard-lighting/layout.d.ts +28 -0
  141. package/dist/pipeline/standard-lighting/layout.d.ts.map +1 -0
  142. package/dist/pipeline/standard-lighting/light-frame.d.ts +33 -0
  143. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -0
  144. package/dist/pipeline/standard-lighting/membership.d.ts +14 -0
  145. package/dist/pipeline/standard-lighting/membership.d.ts.map +1 -0
  146. package/dist/pipeline/standard-lighting/prepare.d.ts +16 -0
  147. package/dist/pipeline/standard-lighting/prepare.d.ts.map +1 -0
  148. package/dist/pipeline/standard-lighting/topology.d.ts +45 -0
  149. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -0
  150. package/dist/pipeline/standard-lighting/transport.d.ts +36 -0
  151. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -0
  152. package/dist/pipeline/standard-pipeline.d.ts +4 -13
  153. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  154. package/dist/pipeline/standard-post.d.ts.map +1 -1
  155. package/dist/pipeline/standard-profile.d.ts +5 -26
  156. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  157. package/dist/pipeline-builder.d.ts +2 -0
  158. package/dist/pipeline-builder.d.ts.map +1 -1
  159. package/dist/pipeline-spec.d.ts +40 -7
  160. package/dist/pipeline-spec.d.ts.map +1 -1
  161. package/dist/plugin.d.ts.map +1 -1
  162. package/dist/point-shadow-inspection.d.ts +21 -0
  163. package/dist/point-shadow-inspection.d.ts.map +1 -0
  164. package/dist/prepare/extended-lighting/ltc-resources.d.ts +31 -0
  165. package/dist/prepare/extended-lighting/ltc-resources.d.ts.map +1 -0
  166. package/dist/prepare/extended-lighting/resources.d.ts +72 -0
  167. package/dist/prepare/extended-lighting/resources.d.ts.map +1 -0
  168. package/dist/prepare/extended-lighting/spot-modifiers.d.ts +42 -0
  169. package/dist/prepare/extended-lighting/spot-modifiers.d.ts.map +1 -0
  170. package/dist/prepare/extended-lighting/state.d.ts +24 -0
  171. package/dist/prepare/extended-lighting/state.d.ts.map +1 -0
  172. package/dist/record/frame-lighting.d.ts +33 -36
  173. package/dist/record/frame-lighting.d.ts.map +1 -1
  174. package/dist/record/frame-snapshot.d.ts +78 -24
  175. package/dist/record/frame-snapshot.d.ts.map +1 -1
  176. package/dist/record/frame-targets.d.ts +5 -0
  177. package/dist/record/frame-targets.d.ts.map +1 -1
  178. package/dist/record/frame.d.ts +16 -6
  179. package/dist/record/frame.d.ts.map +1 -1
  180. package/dist/record/gpu-pass-timing/contract.d.ts +96 -0
  181. package/dist/record/gpu-pass-timing/contract.d.ts.map +1 -0
  182. package/dist/record/gpu-pass-timing/errors.d.ts +18 -0
  183. package/dist/record/gpu-pass-timing/errors.d.ts.map +1 -0
  184. package/dist/record/gpu-pass-timing/index.d.ts +8 -0
  185. package/dist/record/gpu-pass-timing/index.d.ts.map +1 -0
  186. package/dist/record/gpu-pass-timing/parser.d.ts +11 -0
  187. package/dist/record/gpu-pass-timing/parser.d.ts.map +1 -0
  188. package/dist/record/gpu-pass-timing/session.d.ts +43 -0
  189. package/dist/record/gpu-pass-timing/session.d.ts.map +1 -0
  190. package/dist/record/gpu-timing.d.ts +20 -3
  191. package/dist/record/gpu-timing.d.ts.map +1 -1
  192. package/dist/record/helpers.d.ts +0 -20
  193. package/dist/record/helpers.d.ts.map +1 -1
  194. package/dist/record/main-pass-geometry.d.ts +6 -1
  195. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  196. package/dist/record/main-pass-material.d.ts +25 -2
  197. package/dist/record/main-pass-material.d.ts.map +1 -1
  198. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  199. package/dist/record/main-pass.d.ts +2 -0
  200. package/dist/record/main-pass.d.ts.map +1 -1
  201. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  202. package/dist/record/render-context.d.ts +98 -6
  203. package/dist/record/render-context.d.ts.map +1 -1
  204. package/dist/record/shadow-pass.d.ts.map +1 -1
  205. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  206. package/dist/record/typed-frame-graph.d.ts +119 -4
  207. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  208. package/dist/record/view-ubo.d.ts +7 -5
  209. package/dist/record/view-ubo.d.ts.map +1 -1
  210. package/dist/reflection/filter.d.ts +22 -0
  211. package/dist/reflection/filter.d.ts.map +1 -0
  212. package/dist/reflection/gpu-table.d.ts +25 -0
  213. package/dist/reflection/gpu-table.d.ts.map +1 -0
  214. package/dist/reflection/inspection.d.ts +10 -0
  215. package/dist/reflection/inspection.d.ts.map +1 -0
  216. package/dist/reflection/projection.d.ts +140 -0
  217. package/dist/reflection/projection.d.ts.map +1 -0
  218. package/dist/reflection/record-owner.d.ts +60 -0
  219. package/dist/reflection/record-owner.d.ts.map +1 -0
  220. package/dist/render-contract.d.ts +62 -14
  221. package/dist/render-contract.d.ts.map +1 -1
  222. package/dist/render-graph-primitives.d.ts +1 -0
  223. package/dist/render-graph-primitives.d.ts.map +1 -1
  224. package/dist/render-pipeline.d.ts +49 -2
  225. package/dist/render-pipeline.d.ts.map +1 -1
  226. package/dist/render-system-extract.d.ts +160 -31
  227. package/dist/render-system-extract.d.ts.map +1 -1
  228. package/dist/render-system.d.ts +28 -2
  229. package/dist/render-system.d.ts.map +1 -1
  230. package/dist/renderer.d.ts +43 -0
  231. package/dist/renderer.d.ts.map +1 -0
  232. package/dist/scene/probe-blend-record.d.ts +22 -0
  233. package/dist/scene/probe-blend-record.d.ts.map +1 -0
  234. package/dist/scene/probe-blend.d.ts +142 -0
  235. package/dist/scene/probe-blend.d.ts.map +1 -0
  236. package/dist/scene/render-scene-types.d.ts +0 -6
  237. package/dist/scene/render-scene-types.d.ts.map +1 -1
  238. package/dist/scene/render-scene.d.ts +117 -9
  239. package/dist/scene/render-scene.d.ts.map +1 -1
  240. package/dist/scene/visibility/bounds.d.ts +20 -0
  241. package/dist/scene/visibility/bounds.d.ts.map +1 -0
  242. package/dist/scene/visibility/budget.d.ts +16 -0
  243. package/dist/scene/visibility/budget.d.ts.map +1 -0
  244. package/dist/scene/visibility/facet.d.ts +73 -0
  245. package/dist/scene/visibility/facet.d.ts.map +1 -0
  246. package/dist/scene/visibility/gpu-frame-timing.d.ts +41 -0
  247. package/dist/scene/visibility/gpu-frame-timing.d.ts.map +1 -0
  248. package/dist/scene/visibility/gpu-lod.d.ts +46 -0
  249. package/dist/scene/visibility/gpu-lod.d.ts.map +1 -0
  250. package/dist/scene/visibility/inspection.d.ts +132 -0
  251. package/dist/scene/visibility/inspection.d.ts.map +1 -0
  252. package/dist/scene/visibility/lod-selector.d.ts +25 -0
  253. package/dist/scene/visibility/lod-selector.d.ts.map +1 -0
  254. package/dist/scene/visibility/occlusion-confidence.d.ts +81 -0
  255. package/dist/scene/visibility/occlusion-confidence.d.ts.map +1 -0
  256. package/dist/scene/visibility/occlusion-pass.d.ts +57 -0
  257. package/dist/scene/visibility/occlusion-pass.d.ts.map +1 -0
  258. package/dist/scene/visibility/occlusion-query-pool.d.ts +57 -0
  259. package/dist/scene/visibility/occlusion-query-pool.d.ts.map +1 -0
  260. package/dist/scene/visibility/occlusion-runtime.d.ts +96 -0
  261. package/dist/scene/visibility/occlusion-runtime.d.ts.map +1 -0
  262. package/dist/scene/visibility/types.d.ts +34 -0
  263. package/dist/scene/visibility/types.d.ts.map +1 -0
  264. package/dist/shadow-atlas.d.ts +3 -3
  265. package/dist/shadow-atlas.d.ts.map +1 -1
  266. package/dist/ssao-buffers.d.ts +3 -0
  267. package/dist/ssao-buffers.d.ts.map +1 -1
  268. package/dist/ssr/admission.d.ts +122 -0
  269. package/dist/ssr/admission.d.ts.map +1 -0
  270. package/dist/ssr/composition.d.ts +32 -0
  271. package/dist/ssr/composition.d.ts.map +1 -0
  272. package/dist/ssr/errors.d.ts +39 -0
  273. package/dist/ssr/errors.d.ts.map +1 -0
  274. package/dist/ssr/identity.d.ts +13 -0
  275. package/dist/ssr/identity.d.ts.map +1 -0
  276. package/dist/systems/skin-palette-allocator.d.ts +1 -1
  277. package/dist/targets/contracts.d.ts +66 -0
  278. package/dist/targets/contracts.d.ts.map +1 -0
  279. package/dist/targets/graph-projection.d.ts +29 -0
  280. package/dist/targets/graph-projection.d.ts.map +1 -0
  281. package/dist/targets/material-source.d.ts +34 -0
  282. package/dist/targets/material-source.d.ts.map +1 -0
  283. package/dist/targets/owner.d.ts +32 -0
  284. package/dist/targets/owner.d.ts.map +1 -0
  285. package/dist/targets/physical.d.ts +18 -0
  286. package/dist/targets/physical.d.ts.map +1 -0
  287. package/dist/targets/readback.d.ts +39 -0
  288. package/dist/targets/readback.d.ts.map +1 -0
  289. package/dist/temporal/frame-coordinator.d.ts +13 -0
  290. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  291. package/dist/temporal/frame.d.ts +2 -0
  292. package/dist/temporal/frame.d.ts.map +1 -1
  293. package/dist/temporal/index.mjs +6 -5
  294. package/dist/temporal/view.d.ts +7 -0
  295. package/dist/temporal/view.d.ts.map +1 -1
  296. package/dist/tilemap-chunk-extract-system.d.ts +1 -1
  297. package/dist/typed-render-graph-primitives.d.ts +5 -1
  298. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  299. package/dist/typed-shadow-passes.d.ts +1 -1
  300. package/dist/typed-shadow-passes.d.ts.map +1 -1
  301. package/dist/volume/passes.d.ts +9 -5
  302. package/dist/volume/passes.d.ts.map +1 -1
  303. package/package.json +20 -20
  304. package/src/__tests__/bloom-exact-zero.unit.test.ts +48 -19
  305. package/src/__tests__/bloom-lifecycle.integration.test.ts +41 -13
  306. package/src/__tests__/camera-target-consumption.unit.test.ts +53 -0
  307. package/src/__tests__/cluster-binner.unit.test.ts +35 -0
  308. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +12 -2
  309. package/src/__tests__/cube-camera-scheduler.unit.test.ts +70 -0
  310. package/src/__tests__/cube-camera-single-submit.perf.test.ts +75 -0
  311. package/src/__tests__/cube-camera-view-matrices.unit.test.ts +41 -0
  312. package/src/__tests__/cube-camera-vocabulary.unit.test.ts +47 -0
  313. package/src/__tests__/cube-camera.integration.test.ts +42 -0
  314. package/src/__tests__/detached-inspection.integration.test.ts +6 -0
  315. package/src/__tests__/device-feature-admission.unit.test.ts +85 -0
  316. package/src/__tests__/direct-light-snapshot.unit.test.ts +62 -6
  317. package/src/__tests__/docs-gate-green.unit.test.ts +38 -0
  318. package/src/__tests__/environment-generation.integration.test.ts +16 -0
  319. package/src/__tests__/evidence-identity.ts +13 -0
  320. package/src/__tests__/evidence-identity.unit.test.ts +24 -0
  321. package/src/__tests__/extended-lighting-recovery.browser.test.ts +14 -0
  322. package/src/__tests__/extended-lighting-recovery.dawn.test.ts +14 -0
  323. package/src/__tests__/extended-lighting-recovery.integration.test.ts +81 -0
  324. package/src/__tests__/extended-lighting-resources.unit.test.ts +51 -0
  325. package/src/__tests__/extended-lighting-zero-feature.integration.test.ts +52 -0
  326. package/src/__tests__/factory-contract.integration.test.ts +288 -29
  327. package/src/__tests__/fog-component.unit.test.ts +67 -0
  328. package/src/__tests__/fog.integration.test.ts +86 -0
  329. package/src/__tests__/frame-observation.contract.test.ts +15 -0
  330. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +37 -0
  331. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +146 -0
  332. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +10 -8
  333. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +18 -1
  334. package/src/__tests__/gpu-driven-production.integration.test.ts +579 -17
  335. package/src/__tests__/gpu-driven-scaling.unit.test.ts +66 -2
  336. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +26 -1
  337. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +101 -6
  338. package/src/__tests__/gpu-driven-view.browser.test.ts +3 -3
  339. package/src/__tests__/gpu-driven-view.dawn.test.ts +3 -3
  340. package/src/__tests__/gpu-driven-view.unit.test.ts +50 -0
  341. package/src/__tests__/gpu-lod-evidence.ts +188 -0
  342. package/src/__tests__/gpu-lod-fallback.integration.test.ts +10 -0
  343. package/src/__tests__/gpu-lod-indirect.integration.test.ts +40 -0
  344. package/src/__tests__/gpu-lod-parity.dawn.test.ts +29 -0
  345. package/src/__tests__/gpu-lod-readiness.integration.test.ts +52 -0
  346. package/src/__tests__/gpu-lod-schema.unit.test.ts +66 -0
  347. package/src/__tests__/gpu-lod-selector.unit.test.ts +28 -0
  348. package/src/__tests__/gpu-lod-silhouette.browser.test.ts +13 -0
  349. package/src/__tests__/gpu-pass-timing-browser-runner.ts +281 -0
  350. package/src/__tests__/gpu-pass-timing-coexistence.integration.test.ts +80 -0
  351. package/src/__tests__/gpu-pass-timing-contract.unit.test.ts +243 -0
  352. package/src/__tests__/gpu-pass-timing-exact-zero.unit.test.ts +131 -0
  353. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +230 -0
  354. package/src/__tests__/gpu-pass-timing-observation.unit.test.ts +233 -0
  355. package/src/__tests__/gpu-pass-timing-session.unit.test.ts +438 -0
  356. package/src/__tests__/gpu-pass-timing.browser.test.ts +26 -0
  357. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +125 -0
  358. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +66 -1
  359. package/src/__tests__/gpu-scene.dawn.test.ts +8 -1
  360. package/src/__tests__/gpu-scene.unit.test.ts +49 -3
  361. package/src/__tests__/hdrp-group2-visibility.unit.test.ts +24 -0
  362. package/src/__tests__/instances-culling.integration.test.ts +94 -0
  363. package/src/__tests__/instances-derived-bounds.unit.test.ts +118 -0
  364. package/src/__tests__/light-buffer-layout.unit.test.ts +113 -3
  365. package/src/__tests__/light-inspection.unit.test.ts +55 -0
  366. package/src/__tests__/light-resource-error.unit.test.ts +43 -0
  367. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +109 -0
  368. package/src/__tests__/lod-bounds.unit.test.ts +42 -0
  369. package/src/__tests__/lod-cpu-lane.integration.test.ts +55 -0
  370. package/src/__tests__/lod-cpu-no-change.perf.test.ts +19 -0
  371. package/src/__tests__/lod-gpu-frame-timing.unit.test.ts +49 -0
  372. package/src/__tests__/lod-multi-view.integration.test.ts +63 -0
  373. package/src/__tests__/lod-occlusion-device-loss.integration.test.ts +27 -0
  374. package/src/__tests__/lod-occlusion-final-decision.integration.test.ts +47 -0
  375. package/src/__tests__/lod-occlusion-inspection.integration.test.ts +217 -0
  376. package/src/__tests__/lod-occlusion-inspection.schema.test-d.ts +14 -0
  377. package/src/__tests__/lod-occlusion-inspection.unit.test.ts +72 -0
  378. package/src/__tests__/lod-occlusion-visual-evidence.ts +91 -0
  379. package/src/__tests__/lod-occlusion.browser.test.ts +15 -0
  380. package/src/__tests__/lod-occlusion.dawn.test.ts +20 -0
  381. package/src/__tests__/lod-selector.profile.test.ts +35 -0
  382. package/src/__tests__/lod-selector.unit.test.ts +68 -0
  383. package/src/__tests__/lod-visibility-identity.unit.test.ts +198 -0
  384. package/src/__tests__/lod-visibility-lifecycle.integration.test.ts +132 -0
  385. package/src/__tests__/ltc-resources.unit.test.ts +32 -0
  386. package/src/__tests__/material-contract-inventory.unit.test.ts +17 -0
  387. package/src/__tests__/material-observation.integration.test.ts +101 -0
  388. package/src/__tests__/occlusion-confidence.unit.test.ts +168 -0
  389. package/src/__tests__/occlusion-invalidation.integration.test.ts +37 -0
  390. package/src/__tests__/occlusion-pass-routing.unit.test.ts +16 -0
  391. package/src/__tests__/occlusion-pass.dawn.test.ts +20 -0
  392. package/src/__tests__/occlusion-pass.integration.test.ts +43 -0
  393. package/src/__tests__/occlusion-pass.rhi-null.test.ts +22 -0
  394. package/src/__tests__/occlusion-query-pool.rhi-null.test.ts +21 -0
  395. package/src/__tests__/occlusion-query-pool.unit.test.ts +95 -0
  396. package/src/__tests__/occlusion-query-ticket.integration.test.ts +47 -0
  397. package/src/__tests__/occlusion-query.dawn.test.ts +16 -0
  398. package/src/__tests__/occlusion-query.evidence.ts +178 -0
  399. package/src/__tests__/occlusion-query.rhi-null.test.ts +31 -0
  400. package/src/__tests__/occlusion-runtime.unit.test.ts +120 -0
  401. package/src/__tests__/pbr-view-layout.unit.test.ts +16 -0
  402. package/src/__tests__/point-shadow-inspection.unit.test.ts +60 -0
  403. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +196 -0
  404. package/src/__tests__/probe-bindings.unit.test.ts +200 -0
  405. package/src/__tests__/probe-blend-numeric.unit.test.ts +67 -0
  406. package/src/__tests__/probe-blend-oracle.unit.test.ts +136 -0
  407. package/src/__tests__/probe-capacity.integration.test.ts +97 -0
  408. package/src/__tests__/probe-recovery.integration.test.ts +104 -0
  409. package/src/__tests__/probe-scene-dirty.integration.test.ts +211 -0
  410. package/src/__tests__/profiler-phase-catalog.test.ts +4 -1
  411. package/src/__tests__/profiler-phase-token.test.ts +1 -0
  412. package/src/__tests__/public-surface.test-d.ts +10 -0
  413. package/src/__tests__/rect-area-boundary.unit.test.ts +24 -0
  414. package/src/__tests__/reflection-fallback-graph.unit.test.ts +67 -0
  415. package/src/__tests__/reflection-fallback-inspection.unit.test.ts +77 -0
  416. package/src/__tests__/reflection-fallback-lifecycle.integration.test.ts +21 -0
  417. package/src/__tests__/reflection-fallback-projection.unit.test.ts +90 -0
  418. package/src/__tests__/reflection-fallback-source.unit.test.ts +32 -0
  419. package/src/__tests__/reflection-probe-authoring.unit.test.ts +71 -0
  420. package/src/__tests__/reflection-probe-filter.unit.test.ts +54 -0
  421. package/src/__tests__/reflection-probe-ibl-topology.unit.test.ts +45 -0
  422. package/src/__tests__/reflection-probe-owner-placement.unit.test.ts +39 -0
  423. package/src/__tests__/reflection-probe-pass-encode.integration.test.ts +101 -0
  424. package/src/__tests__/reflection-probe-performance.perf.test.ts +60 -0
  425. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +120 -0
  426. package/src/__tests__/reflection-probe-recovery.integration.test.ts +55 -0
  427. package/src/__tests__/reflection-probe-selection.unit.test.ts +46 -0
  428. package/src/__tests__/render-error-code-owner.test-d.ts +19 -3
  429. package/src/__tests__/render-feature-docs.unit.test.ts +12 -7
  430. package/src/__tests__/render-graph-fallback.integration.test.ts +24 -0
  431. package/src/__tests__/render-perf/extended-lighting.bench.test.ts +66 -0
  432. package/src/__tests__/render-scene-projection.unit.test.ts +447 -18
  433. package/src/__tests__/render-target-admission.unit.test.ts +80 -0
  434. package/src/__tests__/render-target-graph-projection.unit.test.ts +102 -0
  435. package/src/__tests__/render-target-inspection.unit.test.ts +52 -0
  436. package/src/__tests__/render-target-material-source.test-d.ts +30 -0
  437. package/src/__tests__/render-target-material-source.unit.test.ts +97 -0
  438. package/src/__tests__/render-target-owner-placement.unit.test.ts +203 -0
  439. package/src/__tests__/render-target-owner.unit.test.ts +122 -0
  440. package/src/__tests__/render-target-probe-soak.perf.test.ts +94 -0
  441. package/src/__tests__/render-target-public-schema.json +107 -0
  442. package/src/__tests__/render-target-public-schema.unit.test.ts +91 -0
  443. package/src/__tests__/render-target-public-surface.test-d.ts +83 -0
  444. package/src/__tests__/render-target-readback.integration.test.ts +90 -0
  445. package/src/__tests__/render-target-recovery.integration.test.ts +85 -0
  446. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +124 -0
  447. package/src/__tests__/renderer-lifecycle.integration.test.ts +14 -0
  448. package/src/__tests__/shader-stage-surface.unit.test.ts +19 -4
  449. package/src/__tests__/skin-motion-regression.unit.test.ts +154 -0
  450. package/src/__tests__/skinned-shadow-caster.test.ts +13 -4
  451. package/src/__tests__/spot-modifier-projection.unit.test.ts +70 -0
  452. package/src/__tests__/ssr-admission-matrix.integration.test.ts +206 -0
  453. package/src/__tests__/ssr-composition.unit.test.ts +46 -0
  454. package/src/__tests__/ssr-fallback-zero-work.bench.test.ts +94 -0
  455. package/src/__tests__/ssr-recovery-errors.unit.test.ts +58 -0
  456. package/src/__tests__/standard-cluster-capability.integration.test.ts +63 -0
  457. package/src/__tests__/standard-cluster-transport.unit.test.ts +121 -0
  458. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +299 -0
  459. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +15 -5
  460. package/src/__tests__/standard-lighting-inspection.integration.test.ts +138 -0
  461. package/src/__tests__/standard-lighting-ssot.unit.test.ts +18 -0
  462. package/src/__tests__/standard-lighting-write.unit.test.ts +441 -0
  463. package/src/__tests__/standard-lighting.integration.test.ts +67 -0
  464. package/src/__tests__/standard-pipeline.dawn.test.ts +206 -5
  465. package/src/__tests__/standard-pipeline.integration.test.ts +181 -150
  466. package/src/__tests__/standard-probe-binding.unit.test.ts +25 -0
  467. package/src/__tests__/standard-profile-oracle.unit.test.ts +23 -38
  468. package/src/__tests__/temporal-fallback-generation.integration.test.ts +16 -0
  469. package/src/__tests__/temporal-view.unit.test.ts +3 -3
  470. package/src/__tests__/transmission-prewarm.unit.test.ts +69 -0
  471. package/src/__tests__/typed-pipeline-topology.unit.test.ts +395 -14
  472. package/src/__tests__/urp-pipeline-targets.unit.test.ts +2 -2
  473. package/src/__tests__/visibility-budget.integration.test.ts +140 -0
  474. package/src/__tests__/visibility-budget.unit.test.ts +70 -0
  475. package/src/__tests__/visibility-extract.integration.test.ts +11 -6
  476. package/src/__tests__/visibility-resolver.unit.test.ts +15 -0
  477. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +6 -3
  478. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +31 -28
  479. package/src/assembly/backend-contract.ts +13 -0
  480. package/src/assembly/device-feature-admission.ts +80 -0
  481. package/src/assembly/directional-shadow-inspection.ts +122 -0
  482. package/src/assembly/factory.ts +1411 -366
  483. package/src/assembly/host-contract.ts +35 -0
  484. package/src/assembly/render-target-host.ts +847 -0
  485. package/src/assets/asset-decoders.ts +1 -1
  486. package/src/assets/render-asset-port.ts +7 -1
  487. package/src/capture/cube-views.ts +69 -0
  488. package/src/capture/scheduler.ts +265 -0
  489. package/src/cluster-binner.ts +31 -0
  490. package/src/components/__tests__/lighting-authoring.unit.test.ts +39 -0
  491. package/src/components/__tests__/rect-area-light.unit.test.ts +33 -0
  492. package/src/components/__tests__/schema-contract.unit.test.ts +12 -0
  493. package/src/components/__tests__/schema-parity.unit.test.ts +9 -0
  494. package/src/components/camera.ts +4 -0
  495. package/src/components/cube-camera.ts +52 -0
  496. package/src/components/directional-light.ts +22 -2
  497. package/src/components/directional-shadow-filter.ts +57 -0
  498. package/src/components/index.ts +5 -0
  499. package/src/components/light-helpers.ts +133 -8
  500. package/src/components/light-probe.ts +13 -0
  501. package/src/components/rect-area-light.ts +15 -0
  502. package/src/components/reflection-probe.ts +49 -0
  503. package/src/components/scene-instance.ts +6 -0
  504. package/src/components/spot-light.ts +3 -0
  505. package/src/device/__tests__/extended-lighting-recovery.unit.test.ts +24 -0
  506. package/src/device/gpu-residency.ts +140 -9
  507. package/src/device/lifecycle-transaction.ts +12 -0
  508. package/src/environment/frame.ts +6 -24
  509. package/src/environment/lifecycle.ts +20 -1
  510. package/src/errors/gpu-driven.ts +92 -0
  511. package/src/errors/index.ts +1 -0
  512. package/src/errors/recover.ts +32 -0
  513. package/src/errors/render.ts +341 -143
  514. package/src/extract/camera.ts +3 -3
  515. package/src/extract/directional-shadow-projection.ts +61 -0
  516. package/src/extract/environment.ts +57 -0
  517. package/src/extract/gpu-driven.ts +108 -0
  518. package/src/extract/visibility.ts +2 -1
  519. package/src/features/fullscreen.ts +3 -0
  520. package/src/features/targets.ts +4 -1
  521. package/src/features/types.ts +10 -0
  522. package/src/fullscreen-post-process-pass.ts +43 -2
  523. package/src/gpu-driven/batch-topology.ts +207 -28
  524. package/src/gpu-driven/prepared-draw.ts +208 -0
  525. package/src/gpu-driven/production-raster.ts +953 -43
  526. package/src/gpu-driven/view-gpu.ts +411 -29
  527. package/src/gpu-scene-schema.ts +13 -0
  528. package/src/gpu-scene.ts +140 -64
  529. package/src/gpu-usage.ts +0 -1
  530. package/src/hdrp-buffers.ts +169 -63
  531. package/src/ibl/IblPipelineCache.ts +11 -0
  532. package/src/ibl/kernel-cache.ts +88 -0
  533. package/src/index.ts +222 -0
  534. package/src/inspection-types.ts +276 -1
  535. package/src/instances-derived-bounds.ts +171 -0
  536. package/src/internal.ts +38 -0
  537. package/src/light-buffer-layout.ts +155 -310
  538. package/src/materials.ts +16 -2
  539. package/src/mesh-material-bindings.ts +132 -0
  540. package/src/pbr-pipeline.ts +255 -60
  541. package/src/pipeline/standard-forward-lane.ts +93 -16
  542. package/src/pipeline/standard-lighting/errors.ts +6 -0
  543. package/src/pipeline/standard-lighting/graph.ts +158 -0
  544. package/src/pipeline/standard-lighting/inspection.ts +88 -0
  545. package/src/pipeline/standard-lighting/layout.ts +31 -0
  546. package/src/pipeline/standard-lighting/light-frame.ts +35 -0
  547. package/src/pipeline/standard-lighting/membership.ts +43 -0
  548. package/src/pipeline/standard-lighting/prepare.ts +37 -0
  549. package/src/pipeline/standard-lighting/topology.ts +120 -0
  550. package/src/pipeline/standard-lighting/transport.ts +78 -0
  551. package/src/pipeline/standard-pipeline.ts +124 -193
  552. package/src/pipeline/standard-post.ts +26 -7
  553. package/src/pipeline/standard-profile.ts +17 -47
  554. package/src/pipeline-builder.ts +5 -2
  555. package/src/pipeline-spec.ts +95 -21
  556. package/src/plugin.ts +4 -0
  557. package/src/point-shadow-inspection.ts +55 -0
  558. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +56 -0
  559. package/src/prepare/extended-lighting/ltc-resources.ts +136 -0
  560. package/src/prepare/extended-lighting/resources.ts +389 -0
  561. package/src/prepare/extended-lighting/spot-modifiers.ts +194 -0
  562. package/src/prepare/extended-lighting/state.ts +92 -0
  563. package/src/record/__tests__/gpu-timing.unit.test.ts +177 -0
  564. package/src/record/__tests__/temporal-payload.unit.test.ts +64 -1
  565. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +73 -0
  566. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +20 -1
  567. package/src/record/__tests__/view-ubo-layout.unit.test.ts +40 -0
  568. package/src/record/frame-lighting.ts +591 -452
  569. package/src/record/frame-snapshot.ts +92 -39
  570. package/src/record/frame-targets.ts +17 -0
  571. package/src/record/frame.ts +662 -158
  572. package/src/record/gpu-pass-timing/contract.ts +180 -0
  573. package/src/record/gpu-pass-timing/errors.ts +34 -0
  574. package/src/record/gpu-pass-timing/index.ts +37 -0
  575. package/src/record/gpu-pass-timing/parser.ts +97 -0
  576. package/src/record/gpu-pass-timing/session.ts +938 -0
  577. package/src/record/gpu-timing.ts +112 -39
  578. package/src/record/helpers.ts +0 -58
  579. package/src/record/main-pass-geometry.ts +324 -119
  580. package/src/record/main-pass-material.ts +103 -10
  581. package/src/record/main-pass-sprite-draws.ts +99 -103
  582. package/src/record/main-pass.ts +71 -15
  583. package/src/record/mesh-ssbo.ts +47 -36
  584. package/src/record/render-context.ts +136 -3
  585. package/src/record/shadow-pass.ts +83 -68
  586. package/src/record/skybox-post-pass.ts +20 -4
  587. package/src/record/typed-frame-graph.ts +1034 -43
  588. package/src/record/view-ubo.ts +72 -47
  589. package/src/reflection/filter.ts +81 -0
  590. package/src/reflection/gpu-table.ts +44 -0
  591. package/src/reflection/inspection.ts +67 -0
  592. package/src/reflection/projection.ts +476 -0
  593. package/src/reflection/record-owner.ts +1283 -0
  594. package/src/render-contract.ts +93 -10
  595. package/src/render-graph-primitives.ts +9 -4
  596. package/src/render-pipeline.ts +122 -2
  597. package/src/render-system-extract.ts +1041 -225
  598. package/src/render-system.ts +1310 -85
  599. package/src/renderer.ts +222 -0
  600. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +36 -0
  601. package/src/scene/probe-blend-record.ts +54 -0
  602. package/src/scene/probe-blend.ts +813 -0
  603. package/src/scene/render-scene-types.ts +0 -7
  604. package/src/scene/render-scene.ts +942 -171
  605. package/src/scene/visibility/bounds.ts +57 -0
  606. package/src/scene/visibility/budget.ts +40 -0
  607. package/src/scene/visibility/facet.ts +400 -0
  608. package/src/scene/visibility/gpu-frame-timing.ts +101 -0
  609. package/src/scene/visibility/gpu-lod.ts +119 -0
  610. package/src/scene/visibility/inspection.ts +313 -0
  611. package/src/scene/visibility/lod-selector.ts +84 -0
  612. package/src/scene/visibility/occlusion-confidence.ts +478 -0
  613. package/src/scene/visibility/occlusion-pass.ts +165 -0
  614. package/src/scene/visibility/occlusion-query-pool.ts +178 -0
  615. package/src/scene/visibility/occlusion-runtime.ts +672 -0
  616. package/src/scene/visibility/types.ts +50 -0
  617. package/src/shadow-atlas.ts +5 -5
  618. package/src/ssao-buffers.ts +9 -2
  619. package/src/ssr/admission.ts +451 -0
  620. package/src/ssr/composition.ts +73 -0
  621. package/src/ssr/errors.ts +76 -0
  622. package/src/ssr/identity.ts +12 -0
  623. package/src/systems/skin-palette-allocator.ts +1 -1
  624. package/src/targets/contracts.ts +182 -0
  625. package/src/targets/graph-projection.ts +169 -0
  626. package/src/targets/material-source.ts +143 -0
  627. package/src/targets/owner.ts +214 -0
  628. package/src/targets/physical.ts +18 -0
  629. package/src/targets/readback.ts +153 -0
  630. package/src/temporal/frame-coordinator.ts +26 -0
  631. package/src/temporal/frame.ts +14 -1
  632. package/src/temporal/view.ts +11 -0
  633. package/src/tilemap-chunk-extract-system.ts +4 -4
  634. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +59 -20
  635. package/src/typed-render-graph-primitives.ts +53 -3
  636. package/src/typed-shadow-passes.ts +46 -40
  637. package/src/volume/passes.ts +61 -58
  638. package/dist/.tsbuildinfo +0 -1
  639. package/dist/chunk-4XINDLAY.mjs.map +0 -1
  640. package/dist/chunk-CDTWTHZR.mjs.map +0 -1
  641. package/dist/chunk-M2MP4ZOU.mjs +0 -73
  642. package/dist/chunk-M2MP4ZOU.mjs.map +0 -1
  643. package/dist/chunk-PCHBPYML.mjs.map +0 -1
  644. package/dist/chunk-TBQJOK2L.mjs.map +0 -1
  645. package/dist/chunk-VFPLTKCU.mjs.map +0 -1
@@ -2,40 +2,48 @@
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 { DIRECT_LIGHT_SLOT_FLOAT_COUNT, packDirectLightSlot } 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 {
40
+ COOKIE_MATRIX_BYTES,
41
+ COOKIE_SLICE_SIZE,
42
+ IES_SLICE_HEIGHT,
43
+ IES_SLICE_WIDTH,
44
+ } from '../prepare/extended-lighting/resources';
45
+ import type { ReflectionProbeTable } from '../reflection/gpu-table';
46
+ import type { ReflectionProbeSelectionResult } from '../reflection/projection';
39
47
  import type { CameraSnapshot, RenderHdrpClusterPhase } from '../render-contract';
40
48
  import type {
41
49
  DirectionalLightSnapshot,
@@ -44,22 +52,58 @@ import type {
44
52
  SkyboxSnapshot,
45
53
  SkylightSnapshot,
46
54
  } from '../render-system-extract';
47
- import { ShadowAtlas } from '../shadow-atlas';
55
+ import {
56
+ SHADOW_ATLAS_DEFAULT_FACE_SIZE,
57
+ SHADOW_ATLAS_DEFAULT_LAYERS,
58
+ ShadowAtlas,
59
+ } from '../shadow-atlas';
48
60
  import { getOrCreateSsaoBuffers } from '../ssao-buffers';
49
61
  import { getSsaoParameters } from '../ssao-config';
62
+ // extendedLighting keeps Spot modifier factors on the existing light record
63
+ // path; no additional render pass or submit owner is introduced.
50
64
  import type { BindGroupCounts, RenderFrameState } from './frame-snapshot';
51
65
  import {
52
66
  computeProjectionMatrix,
53
67
  computeViewMatrix,
54
68
  isLitMaterialSnapshot,
55
69
  warnMultiLightDirectional,
56
- warnMultiLightPoint,
57
- warnMultiLightSpot,
58
70
  } from './helpers';
59
71
  import { getOrCreateFromChain, MESH_SSBO_BYTES, MESH_UBO_FULL_ARRAY_BYTES } from './mesh-ssbo';
60
72
  import type { PipelineState, RecordProfileRunner, RenderSystemInternals } from './render-context';
61
73
  import { POINTS_LINES_VIEW_BYTES, VIEW_UNIFORM_BYTES } from './view-ubo';
62
74
 
75
+ export interface StandardReflectionProbeBinding {
76
+ readonly probeIndex: number | undefined;
77
+ readonly useSkylight: boolean;
78
+ }
79
+
80
+ export function resolveReflectionProbeBinding(
81
+ selection: ReflectionProbeSelectionResult,
82
+ table?: ReflectionProbeTable,
83
+ ): StandardReflectionProbeBinding {
84
+ if (selection.kind !== 'probe') return { probeIndex: undefined, useSkylight: true };
85
+ const primitiveKey = `${selection.worldId}:${selection.entityKey}`;
86
+ const tableIndex = table?.cpuIndexByPrimitive(primitiveKey);
87
+ return { probeIndex: tableIndex ?? selection.entityKey, useSkylight: false };
88
+ }
89
+
90
+ export function projectDeviceOwnedBindGroup<T>(
91
+ state:
92
+ | {
93
+ readonly device: RhiDevice;
94
+ readonly layout: BindGroupLayout;
95
+ readonly bindGroup: T;
96
+ }
97
+ | null
98
+ | undefined,
99
+ device: RhiDevice,
100
+ layout: BindGroupLayout | null,
101
+ ): T | null {
102
+ return state !== null && state !== undefined && state.device === device && state.layout === layout
103
+ ? state.bindGroup
104
+ : null;
105
+ }
106
+
63
107
  function runHdrpClusterProfilePhase<T>(
64
108
  runner: RecordProfileRunner | undefined,
65
109
  phase: RenderHdrpClusterPhase,
@@ -68,6 +112,26 @@ function runHdrpClusterProfilePhase<T>(
68
112
  return runner === undefined ? action() : runner(phase, action);
69
113
  }
70
114
 
115
+ function mapClusterUploadFailure(
116
+ resource: string,
117
+ error: {
118
+ readonly code: string;
119
+ readonly expected: string;
120
+ readonly hint: string;
121
+ readonly detail?: unknown;
122
+ },
123
+ ): RendererOperationError<'device-operation-failed'> {
124
+ return new RendererOperationError('device-operation-failed', {
125
+ operation: 'draw',
126
+ cause: {
127
+ code: error.code,
128
+ expected: `queue.writeBuffer succeeds for Standard Cluster ${resource}`,
129
+ hint: `repair the device write for Standard Cluster ${resource} and retry the frame`,
130
+ detail: error,
131
+ },
132
+ });
133
+ }
134
+
71
135
  /**
72
136
  * feat-20260704 M3/w18: per-frame bind-group cache resolution, extracted
73
137
  * verbatim from `recordFrame`. Walks the handle-identity WeakMap caches
@@ -92,25 +156,45 @@ export function buildPerFrameBindGroups(
92
156
  readonly projectorSampler?: import('@forgeax/engine-rhi').Sampler | undefined;
93
157
  },
94
158
  includeView = true,
159
+ standardLighting?: StandardTopologyInputValue,
95
160
  ): {
96
161
  viewBindGroup: BindGroup | null;
97
162
  meshBindGroup: BindGroup | null;
98
163
  hdrpClusterBindGroup: BindGroup | null;
99
164
  hdrpClusterMembershipBindGroup: BindGroup | null;
100
165
  } {
101
- // View main (#1) chain = b0(viewUniformBuffer), b1(pointLightsBuffer),
102
- // b2(spotLightsBuffer), b3(graph shadowDepth view or
166
+ // View main (#1) chain = b0(viewUniformBuffer), b3(graph shadowDepth view or
103
167
  // shadowFallbackTextureView), b4(shadowSampler), b5(atlas view), b6
104
- // (shadowParams); variant 'view-main'.
168
+ // (shadowParams); variant 'view-main'. Local lights are never mirrored in
169
+ // the view group: Standard consumes the one Cluster group(2) payload.
105
170
  // Mesh (#2) chain = inner b0 buffer (meshStorageBuffer.buffer); variant 'mesh'.
106
171
  let viewBindGroup: BindGroup | null = null;
107
172
  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, ...)`.
173
+ // Standard clustered group(2) bind group. Non-null when the prepared
174
+ // topology selects the cluster transport and its persistent allocation
175
+ // succeeds; consumed by recordMainPass at `setBindGroup(2, ...)`.
111
176
  let hdrpClusterBindGroup: BindGroup | null = null;
112
- const hdrpClusterMembershipBindGroup: BindGroup | null = frameState.isHdrpActive
113
- ? frameState.hdrpClusterMembershipBindGroup
177
+ // The prepared Standard topology is the per-frame authority for whether
178
+ // group(2) carries Cluster storage. A missing declaration is the prewarm /
179
+ // non-Standard path and must not allocate or bind the Cluster bundle based
180
+ // on the install-time pipeline identity alone.
181
+ const clusteredLighting = standardLighting?.kind === 'clustered';
182
+ const membershipLayout = pipelineState.hdrpClusterMembershipBindGroupLayout;
183
+ const membership = frameState.hdrpClusterMembership;
184
+ const expectedHdrpBuffers =
185
+ hasValidated && clusteredLighting
186
+ ? getOrCreateHdrpBuffers(internals, frameState.installedPipelineConfig?.clusterGrid)
187
+ : null;
188
+ const currentMembership =
189
+ expectedHdrpBuffers !== null &&
190
+ membership?.clusterGridBuffer === expectedHdrpBuffers.clusterGridBuffer
191
+ ? projectDeviceOwnedBindGroup(membership, internals.device, membershipLayout)
192
+ : null;
193
+ if (membership !== null && currentMembership === null) {
194
+ frameState.hdrpClusterMembership = null;
195
+ }
196
+ const hdrpClusterMembershipBindGroup: BindGroup | null = clusteredLighting
197
+ ? currentMembership
114
198
  : null;
115
199
  if (hasValidated) {
116
200
  // Shadow atlas view comes from the typed graph target.
@@ -153,6 +237,47 @@ export function buildPerFrameBindGroups(
153
237
  graphSpotShadowView !== undefined
154
238
  ? graphSpotShadowView
155
239
  : pipelineState.shadowFallbackTextureView;
240
+ const extendedLighting = pipelineState.extendedLightingAvailable ?? true;
241
+ const iesProfileTextureView = pipelineState.iesProfileTextureView;
242
+ const cookieTextureView = pipelineState.cookieTextureView;
243
+ const cookieMatrixBuffer = pipelineState.cookieMatrixBuffer;
244
+ const ltcLambertTextureView = pipelineState.ltcLambertTextureView;
245
+ const ltcGgxTextureView = pipelineState.ltcGgxTextureView;
246
+ const extendedLightingResources =
247
+ extendedLighting &&
248
+ iesProfileTextureView !== undefined &&
249
+ cookieTextureView !== undefined &&
250
+ cookieMatrixBuffer !== undefined &&
251
+ ltcLambertTextureView !== undefined &&
252
+ ltcGgxTextureView !== undefined
253
+ ? {
254
+ sampler: pipelineState.defaultSampler,
255
+ iesProfileTextureView,
256
+ cookieTextureView,
257
+ ltcLambertTextureView,
258
+ ltcGgxTextureView,
259
+ cookieMatrixBuffer,
260
+ }
261
+ : undefined;
262
+ if (extendedLighting && extendedLightingResources === undefined) {
263
+ return {
264
+ viewBindGroup: null,
265
+ meshBindGroup: null,
266
+ hdrpClusterBindGroup: null,
267
+ hdrpClusterMembershipBindGroup,
268
+ };
269
+ }
270
+ const extendedLightingViews =
271
+ extendedLightingResources !== undefined
272
+ ? [
273
+ extendedLightingResources.sampler,
274
+ extendedLightingResources.iesProfileTextureView,
275
+ extendedLightingResources.cookieTextureView,
276
+ extendedLightingResources.ltcLambertTextureView,
277
+ extendedLightingResources.ltcGgxTextureView,
278
+ extendedLightingResources.cookieMatrixBuffer,
279
+ ]
280
+ : [];
156
281
  const projectorAvailable = pipelineState.projectorAvailable !== false;
157
282
  const b11View = graphTargets?.projector ?? pipelineState.defaultWhiteTextureView;
158
283
  const b12Sampler = graphTargets?.projectorSampler ?? pipelineState.defaultSampler;
@@ -160,14 +285,15 @@ export function buildPerFrameBindGroups(
160
285
  frameState.viewBindGroupCache,
161
286
  [
162
287
  pipelineState.viewUniformBuffer,
163
- pipelineState.pointLightsBuffer,
164
- pipelineState.spotLightsBuffer,
165
288
  b3View,
166
289
  shadowSampler,
167
290
  b5View,
168
291
  pipelineState.shadowParamsBuffer,
169
292
  b8View,
170
- ...(projectorAvailable ? [b11View, b12Sampler] : []),
293
+ ...extendedLightingViews,
294
+ ...(extendedLightingResources === undefined && projectorAvailable
295
+ ? [b11View, b12Sampler]
296
+ : []),
171
297
  pipelineState.pointsLinesViewBuffer ?? pipelineState.viewUniformBuffer,
172
298
  ],
173
299
  'view-main',
@@ -180,20 +306,6 @@ export function buildPerFrameBindGroups(
180
306
  value: { buffer: pipelineState.viewUniformBuffer, size: VIEW_UNIFORM_BYTES },
181
307
  },
182
308
  },
183
- {
184
- binding: 1,
185
- resource: {
186
- kind: 'buffer',
187
- value: { buffer: pipelineState.pointLightsBuffer },
188
- },
189
- },
190
- {
191
- binding: 2,
192
- resource: {
193
- kind: 'buffer',
194
- value: { buffer: pipelineState.spotLightsBuffer },
195
- },
196
- },
197
309
  {
198
310
  binding: 3,
199
311
  resource: {
@@ -263,12 +375,61 @@ export function buildPerFrameBindGroups(
263
375
  },
264
376
  },
265
377
  },
266
- ...(projectorAvailable
378
+ ...(extendedLightingResources === undefined && projectorAvailable
267
379
  ? [
268
380
  { binding: 11, resource: { kind: 'textureView' as const, value: b11View } },
269
381
  { binding: 12, resource: { kind: 'sampler' as const, value: b12Sampler } },
270
382
  ]
271
383
  : []),
384
+ ...(extendedLightingResources !== undefined
385
+ ? [
386
+ {
387
+ binding: 9,
388
+ resource: {
389
+ kind: 'sampler' as const,
390
+ value: extendedLightingResources.sampler,
391
+ },
392
+ },
393
+ {
394
+ binding: 11,
395
+ resource: {
396
+ kind: 'textureView' as const,
397
+ value: extendedLightingResources.iesProfileTextureView,
398
+ },
399
+ },
400
+ {
401
+ binding: 12,
402
+ resource: {
403
+ kind: 'textureView' as const,
404
+ value: extendedLightingResources.cookieTextureView,
405
+ },
406
+ },
407
+ {
408
+ binding: 13,
409
+ resource: {
410
+ kind: 'textureView' as const,
411
+ value: extendedLightingResources.ltcLambertTextureView,
412
+ },
413
+ },
414
+ {
415
+ binding: 14,
416
+ resource: {
417
+ kind: 'textureView' as const,
418
+ value: extendedLightingResources.ltcGgxTextureView,
419
+ },
420
+ },
421
+ {
422
+ binding: 15,
423
+ resource: {
424
+ kind: 'buffer' as const,
425
+ value: {
426
+ buffer: extendedLightingResources.cookieMatrixBuffer,
427
+ size: COOKIE_MATRIX_BYTES,
428
+ },
429
+ },
430
+ },
431
+ ]
432
+ : []),
272
433
  // feat-20260625-spot-light-shadow-mapping w25: the per-spot
273
434
  // fragment-read lightViewProj matrices fold into the View UBO
274
435
  // (binding 0, `view.spotLightViewProj`) — no standalone binding 9
@@ -335,11 +496,8 @@ export function buildPerFrameBindGroups(
335
496
  // alongside `meshBindGroupCache` keyed on the mesh SSBO + cluster
336
497
  // buffer identities; cache invalidates on a buffer-grow event the same
337
498
  // way the mesh path does (handle id rotation).
338
- if (frameState.isHdrpActive) {
339
- const hdrpBuffers = getOrCreateHdrpBuffers(
340
- internals,
341
- frameState.installedPipelineConfig?.clusterGrid,
342
- );
499
+ if (clusteredLighting) {
500
+ const hdrpBuffers = expectedHdrpBuffers;
343
501
  if (hdrpBuffers !== null) {
344
502
  // D-3: buffer dimension uses the inner `.buffer` (same constraint
345
503
  // as the mesh path) so a grow event rotates the chain key.
@@ -378,13 +536,11 @@ export function buildPerFrameBindGroups(
378
536
 
379
537
  /**
380
538
  * 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
383
- * point + spot shadow snapshot lists onto frameState for the URP shadow caster
539
+ * `recordFrame`. (1) pin the point + spot shadow snapshot lists onto frameState
540
+ * for the shadow caster
384
541
  * pass closures + lazy-allocate the point-shadow cube_array atlas on first
385
- * non-empty frame (AC-09 zero-shadow zero-alloc). (3) destructure ExtractedLights
386
- * into the directional-fallback `light` (zero-intensity Case C default), point /
387
- * spot arrays, and totalLightCount.
542
+ * non-empty frame (AC-09 zero-shadow zero-alloc). (3) derive the directional
543
+ * fallback and the unified local-light corpus used by Standard Cluster.
388
544
  *
389
545
  * @internal
390
546
  */
@@ -392,12 +548,22 @@ export function prepareFrameLighting(
392
548
  internals: RenderSystemInternals,
393
549
  frameState: RenderFrameState,
394
550
  lights: ExtractedLights,
395
- ): {
396
- light: DirectionalLightSnapshot;
397
- pointLights: ExtractedLights['point'];
398
- spotLights: ExtractedLights['spot'];
399
- totalLightCount: number;
400
- } {
551
+ camera: CameraSnapshot,
552
+ pipelineState?: Pick<
553
+ PipelineState,
554
+ 'hdrpClusterMembershipPipeline' | 'hdrpClusterMembershipBindGroupLayout'
555
+ >,
556
+ ): Result<
557
+ {
558
+ light: DirectionalLightSnapshot;
559
+ pointLights: ExtractedLights['point'];
560
+ spotLights: ExtractedLights['spot'];
561
+ rectLights: ExtractedLights['rect'];
562
+ totalLightCount: number;
563
+ standard: StandardTopologyInputValue;
564
+ },
565
+ RenderError
566
+ > {
401
567
  if (lights.directionalCount > 1) {
402
568
  warnMultiLightDirectional(frameState, lights.directionalCount);
403
569
  }
@@ -408,21 +574,6 @@ export function prepareFrameLighting(
408
574
  // defaults true on the single component, so the warn condition can never
409
575
  // arise. The error class + RuntimeErrorCode member have been removed in M4 (m4-t2).
410
576
 
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
577
  // feat-20260612-point-light-shadows-urp-hdrp M3 / T-M3-2 (plan-strategy §D-1):
427
578
  // project lights.pointShadow onto frameState so the URP point shadow caster
428
579
  // pass can read the snapshot list during graph execute. Lazy-allocate the
@@ -438,10 +589,10 @@ export function prepareFrameLighting(
438
589
  if (lights.pointShadow.length > 0) {
439
590
  if (frameState.pointShadowAtlas === null) {
440
591
  const firstSnap = lights.pointShadow[0];
441
- const faceSize = firstSnap?.mapSize ?? 512;
592
+ const faceSize = firstSnap?.mapSize ?? SHADOW_ATLAS_DEFAULT_FACE_SIZE;
442
593
  frameState.pointShadowAtlas = new ShadowAtlas(internals.device, {
443
594
  faceSize,
444
- layers: 4,
595
+ layers: SHADOW_ATLAS_DEFAULT_LAYERS,
445
596
  });
446
597
  }
447
598
  try {
@@ -462,14 +613,16 @@ export function prepareFrameLighting(
462
613
  //
463
614
  // - lights.directional : feeds the View UBO at slot [16..23]
464
615
  // (lightDir + lightColor; existing path).
465
- // - lights.point[] : packed into pointLightsBuffer (std430).
466
- // - lights.spot[] : packed into spotLightsBuffer (std430).
616
+ // - lights.point/spot/rect[] : packed into the one unified Standard
617
+ // Cluster payload below. No per-kind light
618
+ // transport exists at the renderer boundary.
467
619
  //
468
620
  // Each variant carries the discriminant `kind` so the record-time packing
469
621
  // call sites can run exhaustive switch (charter P2 + AC-03).
470
622
  const directionalLight: DirectionalLightSnapshot | undefined = lights.directional;
471
623
  const pointLights = lights.point;
472
624
  const spotLights = lights.spot;
625
+ const rectLights = lights.rect;
473
626
 
474
627
  // Case C: 0 DirectionalLight = legitimate scene; the View UBO falls
475
628
  // back to a zero-intensity directional payload so the shader's
@@ -483,74 +636,191 @@ export function prepareFrameLighting(
483
636
  };
484
637
 
485
638
  const totalLightCount =
486
- (directionalLight !== undefined ? 1 : 0) + pointLights.length + spotLights.length;
639
+ (directionalLight !== undefined ? 1 : 0) +
640
+ pointLights.length +
641
+ spotLights.length +
642
+ rectLights.length;
643
+
644
+ const clusterGrid = frameState.installedPipelineConfig?.clusterGrid ?? DEFAULT_CLUSTER_GRID;
645
+ const local = [
646
+ ...pointLights.map((source) => ({
647
+ kind: 'point' as const,
648
+ shadowed: source.shadowAtlasLayer !== undefined && source.shadowAtlasLayer >= 0,
649
+ position: source.position,
650
+ range:
651
+ Number.isFinite(source.invRangeSquared) && source.invRangeSquared > 0
652
+ ? Math.sqrt(1 / source.invRangeSquared)
653
+ : 1000,
654
+ source,
655
+ })),
656
+ ...spotLights.map((source) => ({
657
+ kind: 'spot' as const,
658
+ shadowed:
659
+ source.castShadow && source.shadowAtlasTile >= 0 && source.lightViewProj !== undefined,
660
+ position: source.position,
661
+ range:
662
+ Number.isFinite(source.invRangeSquared) && source.invRangeSquared > 0
663
+ ? Math.sqrt(1 / source.invRangeSquared)
664
+ : 1000,
665
+ source,
666
+ })),
667
+ ...rectLights.map((source) => ({
668
+ kind: 'rect-area' as const,
669
+ shadowed: false,
670
+ position: source.position,
671
+ range:
672
+ (Number.isFinite(source.invRangeSquared) && source.invRangeSquared > 0
673
+ ? Math.sqrt(1 / source.invRangeSquared)
674
+ : 1000) + Math.hypot(source.halfWidth, source.halfHeight),
675
+ source,
676
+ })),
677
+ ];
678
+ const prepared = prepareStandardLighting({
679
+ directional: directionalLight,
680
+ local,
681
+ view: computeViewMatrix(camera),
682
+ projection: computeProjectionMatrix(camera),
683
+ near: camera.near,
684
+ far: camera.far,
685
+ grid: clusterGrid,
686
+ lightCount: internals.standardProfile?.lightCount ?? DEFAULT_STANDARD_PROFILE.lightCount,
687
+ renderPath: internals.standardProfile?.renderPath ?? DEFAULT_STANDARD_PROFILE.renderPath,
688
+ });
689
+ if (!prepared.ok) return prepared;
690
+ // Storage-capable Standard frames keep the same Cluster ABI even when the
691
+ // local-light corpus is empty. Directional-only scenes still need the
692
+ // unified mesh group(2) contract so their PBR draws do not select a second
693
+ // local-light layout; the existing transport emits a zero-light payload and
694
+ // the graph remains structurally identical. The no-storage backend has no
695
+ // storage-backed local-light representation, so a directional-only frame
696
+ // remains the sole no-local transport case.
697
+ if (prepared.value.local.length === 0 && !internals.device.caps.storageBuffer) {
698
+ return ok({
699
+ light,
700
+ pointLights,
701
+ spotLights,
702
+ rectLights,
703
+ totalLightCount,
704
+ standard: { kind: 'no-local-lights', prepared: prepared.value },
705
+ });
706
+ }
707
+ const transport = selectStandardClusterTransport(
708
+ {
709
+ compute: internals.device.caps.compute,
710
+ storageBuffer: internals.device.caps.storageBuffer,
711
+ membershipPipelineReady:
712
+ pipelineState !== undefined &&
713
+ pipelineState.hdrpClusterMembershipPipeline !== null &&
714
+ pipelineState.hdrpClusterMembershipBindGroupLayout !== null,
715
+ },
716
+ prepared.value,
717
+ );
718
+ if (!transport.ok) return transport;
487
719
 
488
- return { light, pointLights, spotLights, totalLightCount };
720
+ return ok({
721
+ light,
722
+ pointLights,
723
+ spotLights,
724
+ rectLights,
725
+ totalLightCount,
726
+ standard: { kind: 'clustered', prepared: prepared.value, transport: transport.value },
727
+ });
489
728
  }
490
729
 
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(
730
+ function modifierDigest(data: Uint8Array): number {
731
+ let hash = 2166136261;
732
+ for (let index = 0; index < data.length; index += 1) {
733
+ hash ^= data[index] ?? 0;
734
+ hash = Math.imul(hash, 16777619);
735
+ }
736
+ return hash >>> 0;
737
+ }
738
+
739
+ /** Upload authored Spot modifier payloads into the renderer-owned array lane. */
740
+ export function writeSpotModifierTextures(
505
741
  internals: RenderSystemInternals,
506
742
  pipelineState: PipelineState,
507
- lights: ExtractedLights,
743
+ spots: readonly ExtractedLights['spot'][number][],
508
744
  ): 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;
745
+ const iesTexture = pipelineState.iesProfileTexture;
746
+ const cookieTexture = pipelineState.cookieTexture;
747
+ const cookieMatrixBuffer = pipelineState.cookieMatrixBuffer;
748
+ if (iesTexture === undefined && cookieTexture === undefined && cookieMatrixBuffer === undefined)
749
+ return;
750
+ let state = pipelineState.spotModifierUploadState;
751
+ if (state === undefined) {
752
+ state = { ies: new Map(), cookie: new Map(), cookieMatrix: new Map() };
753
+ pipelineState.spotModifierUploadState = state;
754
+ } else if (state.cookieMatrix === undefined) {
755
+ state = { ...state, cookieMatrix: new Map() };
756
+ pipelineState.spotModifierUploadState = state;
531
757
  }
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),
541
- );
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;
758
+ for (const spot of spots) {
759
+ if (spot.iesProfileSlice !== undefined && spot.iesProfileData !== undefined && iesTexture) {
760
+ if (spot.iesProfileData.byteLength === IES_SLICE_WIDTH * IES_SLICE_HEIGHT * 2) {
761
+ const digest = modifierDigest(spot.iesProfileData);
762
+ if (state.ies.get(spot.iesProfileSlice) !== digest) {
763
+ const result = internals.device.queue.writeTexture(
764
+ { texture: iesTexture, mipLevel: 0, origin: { x: 0, y: 0, z: spot.iesProfileSlice } },
765
+ spot.iesProfileData,
766
+ { offset: 0, bytesPerRow: IES_SLICE_WIDTH * 2, rowsPerImage: IES_SLICE_HEIGHT },
767
+ { width: IES_SLICE_WIDTH, height: IES_SLICE_HEIGHT, depthOrArrayLayers: 1 },
768
+ );
769
+ if (!result.ok) throw result.error;
770
+ state.ies.set(spot.iesProfileSlice, digest);
771
+ }
772
+ }
773
+ }
774
+ // Authored Cookie and the SpotLight projector share the same
775
+ // fixed 256x256 array transport on the extended topology. Their shader
776
+ // projection remains distinct: Cookie uses cookieMatrices, projector
777
+ // uses View.spotLightViewProj. Upload both identities when a frame carries
778
+ // both fields, while the direct-light metadata selects the one in use.
779
+ for (const modifier of [
780
+ {
781
+ slice: spot.cookieSlice,
782
+ data: spot.cookieData,
783
+ matrix: spot.cookieMatrix,
784
+ },
785
+ {
786
+ slice: spot.projectorSlice,
787
+ data: spot.projectorData,
788
+ matrix: spot.projectorMatrix,
789
+ },
790
+ ]) {
791
+ if (modifier.slice !== undefined && modifier.data !== undefined && cookieTexture) {
792
+ if (modifier.data.byteLength === COOKIE_SLICE_SIZE * COOKIE_SLICE_SIZE * 4) {
793
+ const digest = modifierDigest(modifier.data);
794
+ if (state.cookie.get(modifier.slice) !== digest) {
795
+ const result = internals.device.queue.writeTexture(
796
+ { texture: cookieTexture, mipLevel: 0, origin: { x: 0, y: 0, z: modifier.slice } },
797
+ modifier.data,
798
+ { offset: 0, bytesPerRow: COOKIE_SLICE_SIZE * 4, rowsPerImage: COOKIE_SLICE_SIZE },
799
+ { width: COOKIE_SLICE_SIZE, height: COOKIE_SLICE_SIZE, depthOrArrayLayers: 1 },
800
+ );
801
+ if (!result.ok) throw result.error;
802
+ state.cookie.set(modifier.slice, digest);
803
+ }
804
+ }
805
+ }
806
+ if (modifier.slice !== undefined && modifier.matrix !== undefined && cookieMatrixBuffer) {
807
+ const matrixBytes = new Uint8Array(
808
+ modifier.matrix.buffer,
809
+ modifier.matrix.byteOffset,
810
+ modifier.matrix.byteLength,
811
+ );
812
+ const digest = modifierDigest(matrixBytes);
813
+ if (state.cookieMatrix.get(modifier.slice) !== digest) {
814
+ const result = internals.device.queue.writeBuffer(
815
+ cookieMatrixBuffer,
816
+ modifier.slice * 16 * Float32Array.BYTES_PER_ELEMENT,
817
+ modifier.matrix,
818
+ );
819
+ if (!result.ok) throw result.error;
820
+ state.cookieMatrix.set(modifier.slice, digest);
821
+ }
822
+ }
823
+ }
554
824
  }
555
825
  }
556
826
 
@@ -637,17 +907,9 @@ export function warnZeroLightStandard(
637
907
  }
638
908
 
639
909
  /**
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.
910
+ * Write the one prepared Standard Cluster payload into the persistent buffers.
911
+ * Membership has already been derived by `prepareFrameLighting`; this function
912
+ * only packs/uploads that immutable result and never re-runs the binner.
651
913
  *
652
914
  * @internal
653
915
  */
@@ -655,254 +917,104 @@ export function writeHdrpClusterAndSsaoBuffers(
655
917
  internals: RenderSystemInternals,
656
918
  frameState: RenderFrameState,
657
919
  camera: CameraSnapshot,
658
- pointLights: ExtractedLights['point'],
659
- spotLights: ExtractedLights['spot'],
920
+ prepared: PreparedStandardLighting,
921
+ transport: StandardClusterTransport,
660
922
  profilePhase?: RecordProfileRunner,
661
- useGpuMembership = false,
923
+ gpuMembershipPipelineReady = false,
662
924
  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,
925
+ projectorLightSlotIndex?: number,
926
+ pipelineState?: PipelineState,
927
+ ): Result<void, RenderError> {
928
+ const configuredClusterGrid = prepared.layout.grid;
929
+ const gpuMembership = transport.producer === 'gpu';
930
+ if (gpuMembership && (!gpuMembershipPipelineReady || membershipBindGroupLayout === null)) {
931
+ return err(
932
+ new StandardClusterTransportUnavailableError(
933
+ prepared.local.length,
934
+ 'the selected GPU Cluster producer requires a ready membership pipeline and bind-group layout',
935
+ ),
683
936
  );
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),
700
- );
701
- if (!lightIndexListClear.ok) internals.errorRegistry.fire(lightIndexListClear.error);
702
- return;
703
937
  }
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
- }
938
+ const hdrpBuffers = getOrCreateHdrpBuffers(internals, configuredClusterGrid);
939
+ if (hdrpBuffers === null) {
940
+ return err(
941
+ new StandardClusterTransportUnavailableError(
942
+ prepared.local.length,
943
+ 'the selected Standard Cluster transport could not allocate its persistent buffers',
944
+ ),
945
+ );
721
946
  }
722
- const effectiveLightCount = effectivePointLights.length + effectiveSpotLights.length;
723
- const configuredClusterGrid =
724
- frameState.installedPipelineConfig?.clusterGrid ?? DEFAULT_CLUSTER_GRID;
725
- let gpuMembership = useGpuMembership && membershipBindGroupLayout !== null;
947
+ const existingMembership = projectDeviceOwnedBindGroup(
948
+ frameState.hdrpClusterMembership,
949
+ internals.device,
950
+ membershipBindGroupLayout,
951
+ );
952
+ const membershipMatchesBuffers =
953
+ existingMembership !== null &&
954
+ frameState.hdrpClusterMembership?.clusterGridBuffer === hdrpBuffers.clusterGridBuffer;
955
+ if (!membershipMatchesBuffers) frameState.hdrpClusterMembership = null;
726
956
  if (
727
957
  gpuMembership &&
728
958
  membershipBindGroupLayout !== null &&
729
- frameState.hdrpClusterMembershipBindGroup === null
959
+ frameState.hdrpClusterMembership === null
730
960
  ) {
731
- const hdrpBuffers = getOrCreateHdrpBuffers(internals, configuredClusterGrid);
732
- if (hdrpBuffers !== null) {
733
- frameState.hdrpClusterMembershipBindGroup = createHdrpClusterMembershipBindGroup(
734
- internals,
735
- hdrpBuffers,
736
- membershipBindGroupLayout,
961
+ const bindGroup = createHdrpClusterMembershipBindGroup(
962
+ internals,
963
+ hdrpBuffers,
964
+ membershipBindGroupLayout,
965
+ );
966
+ if (bindGroup === null) {
967
+ return err(
968
+ new StandardClusterTransportUnavailableError(
969
+ prepared.local.length,
970
+ 'the selected GPU Cluster producer could not create its membership bind group',
971
+ ),
737
972
  );
738
973
  }
739
- gpuMembership = frameState.hdrpClusterMembershipBindGroup !== null;
974
+ frameState.hdrpClusterMembership = {
975
+ device: internals.device,
976
+ layout: membershipBindGroupLayout,
977
+ bindGroup,
978
+ clusterGridBuffer: hdrpBuffers.clusterGridBuffer,
979
+ };
740
980
  }
741
981
 
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
- );
982
+ if (pipelineState !== undefined) {
983
+ const preparedSpots: ExtractedLights['spot'][number][] = [];
984
+ for (const local of prepared.local) {
985
+ if (local.source?.kind === 'spot') preparedSpots.push(local.source);
986
+ }
987
+ writeSpotModifierTextures(internals, pipelineState, preparedSpots);
988
+ }
819
989
 
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
- );
990
+ const gridX = prepared.layout.grid.x;
991
+ const gridY = prepared.layout.grid.y;
992
+ const gridZ = prepared.layout.grid.z;
993
+ const clusterGridBuf = prepared.clusterGrid;
994
+ const lightIndexListBuf = prepared.lightIndexList;
995
+ const lightIndexCount = prepared.membershipEntryCount;
839
996
 
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
- }
858
-
859
- return {
860
- clusterGrid,
861
- gridX,
862
- gridY,
863
- gridZ,
864
- clusterGridBuf,
865
- lightIndexListBuf,
866
- lightIndexCount: binResult.ok ? binResult.value : 0,
867
- };
868
- });
997
+ // Falsify injection point retained for the real prepared payload. It edits
998
+ // only this frame's projection and never creates a second membership owner.
999
+ const envFalsify = (globalThis as { process?: { env?: Record<string, string | undefined> } })
1000
+ .process?.env;
1001
+ if (envFalsify?.FORGEAX_HDRP_FALSIFY_CLUSTER_GRID_ZERO) clusterGridBuf.fill(0);
869
1002
 
870
1003
  const hdrpPayload = runHdrpClusterProfilePhase(
871
1004
  profilePhase,
872
1005
  'record/scene-state/hdrp-cluster/payload-packing',
873
1006
  () => {
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);
1007
+ const lightDataPayload = new Float32Array(
1008
+ prepared.layout.lightDataSlotCount * DIRECT_LIGHT_SLOT_FLOAT_COUNT,
1009
+ );
879
1010
  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;
888
- 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);
905
- lightDataPayload.set(packed, slotIdx * 16);
1011
+ for (const local of prepared.local) {
1012
+ const source = local.source;
1013
+ if (source === undefined) return null;
1014
+ const projectorSelected =
1015
+ projectorLightSlotIndex === undefined ? undefined : projectorLightSlotIndex === slotIdx;
1016
+ const packed = packDirectLightSlot(source, projectorSelected);
1017
+ lightDataPayload.set(packed, slotIdx * DIRECT_LIGHT_SLOT_FLOAT_COUNT);
906
1018
  slotIdx += 1;
907
1019
  }
908
1020
 
@@ -922,51 +1034,67 @@ export function writeHdrpClusterAndSsaoBuffers(
922
1034
  camera.near,
923
1035
  camera.far,
924
1036
  clusterSsaoIntensity,
925
- Math.min(effectivePointLights.length + effectiveSpotLights.length, lightCapacity),
926
- lightCapacity,
1037
+ transport.admittedLightCount,
927
1038
  );
928
1039
 
929
1040
  return {
930
1041
  hdrpBuffers,
931
- lightDataUploadPayload: lightDataPayload.subarray(0, slotIdx * 16),
1042
+ lightDataUploadPayload: lightDataPayload.subarray(
1043
+ 0,
1044
+ slotIdx * DIRECT_LIGHT_SLOT_FLOAT_COUNT,
1045
+ ),
932
1046
  clusterUniformPayload: new Uint8Array(clusterUniformPayload),
933
1047
  };
934
1048
  },
935
1049
  );
936
1050
 
937
- runHdrpClusterProfilePhase(profilePhase, 'record/scene-state/hdrp-cluster/buffer-upload', () => {
938
- if (hdrpPayload !== null) {
1051
+ if (hdrpPayload === null) {
1052
+ return err(
1053
+ new StandardClusterTransportUnavailableError(
1054
+ prepared.local.length,
1055
+ 'the prepared Standard Cluster payload could not be materialized',
1056
+ ),
1057
+ );
1058
+ }
1059
+
1060
+ const bufferUpload = runHdrpClusterProfilePhase(
1061
+ profilePhase,
1062
+ 'record/scene-state/hdrp-cluster/buffer-upload',
1063
+ (): Result<void, RenderError> => {
939
1064
  const { hdrpBuffers, lightDataUploadPayload, clusterUniformPayload } = hdrpPayload;
940
1065
  const lightDataUpload = internals.device.queue.writeBuffer(
941
1066
  hdrpBuffers.lightDataBuffer,
942
1067
  0,
943
1068
  lightDataUploadPayload,
944
1069
  );
945
- if (!lightDataUpload.ok) internals.errorRegistry.fire(lightDataUpload.error);
1070
+ if (!lightDataUpload.ok)
1071
+ return err(mapClusterUploadFailure('light-data', lightDataUpload.error));
946
1072
 
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,
1073
+ if (gpuMembership) {
1074
+ const lightBoundsUpload = internals.device.queue.writeBuffer(
1075
+ hdrpBuffers.lightBoundsBuffer,
958
1076
  0,
959
- clusterGridBuf.subarray(0, gridX * gridY * gridZ * 2),
1077
+ prepared.lightBounds,
960
1078
  );
961
- if (!clusterGridUpload.ok) internals.errorRegistry.fire(clusterGridUpload.error);
1079
+ if (!lightBoundsUpload.ok)
1080
+ return err(mapClusterUploadFailure('light-bounds', lightBoundsUpload.error));
1081
+ }
1082
+ const clusterGridUpload = internals.device.queue.writeBuffer(
1083
+ hdrpBuffers.clusterGridBuffer,
1084
+ 0,
1085
+ clusterGridBuf.subarray(0, gridX * gridY * gridZ * 2),
1086
+ );
1087
+ if (!clusterGridUpload.ok)
1088
+ return err(mapClusterUploadFailure('cluster-grid', clusterGridUpload.error));
962
1089
 
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);
1090
+ if (!gpuMembership && lightIndexCount > 0) {
1091
+ const lightIndexListUpload = internals.device.queue.writeBuffer(
1092
+ hdrpBuffers.lightIndexListBuffer,
1093
+ 0,
1094
+ lightIndexListBuf.subarray(0, lightIndexCount),
1095
+ );
1096
+ if (!lightIndexListUpload.ok) {
1097
+ return err(mapClusterUploadFailure('light-index-list', lightIndexListUpload.error));
970
1098
  }
971
1099
  }
972
1100
 
@@ -975,56 +1103,67 @@ export function writeHdrpClusterAndSsaoBuffers(
975
1103
  0,
976
1104
  clusterUniformPayload,
977
1105
  );
978
- if (!clusterUniformUpload.ok) internals.errorRegistry.fire(clusterUniformUpload.error);
979
- }
1106
+ if (!clusterUniformUpload.ok) {
1107
+ return err(mapClusterUploadFailure('cluster-uniform', clusterUniformUpload.error));
1108
+ }
1109
+ return ok(undefined);
1110
+ },
1111
+ );
1112
+ if (!bufferUpload.ok) return bufferUpload;
980
1113
 
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
- {
1114
+ // ── feat-20260612-hdrp-ssao M1 / w6 + M7 / w33 ───────────────
1115
+ // Per-frame SSAO uniform write (plan-strategy D-1 + D-C):
1116
+ // view + projection + inverseProjection at offsets 0/64/128 +
1117
+ // intensityPad (vec4 — x=intensity, y=radius, z=bias) at offset 192;
1118
+ // total 256 B (matches host SSAO_UNIFORM_BYTES + WGSL struct).
1119
+ // Single writeBuffer covers all four fields so one queue entry
1120
+ // updates the entire UBO.
1121
+ // Separate from View UBO (592 B invariant); does not affect
1122
+ // material PSO bytecode.
1123
+ //
1124
+ // Writes when HDRP is active; config.ssao?.enabled guard comes in
1125
+ // M4 / w19 after the config.ssao type narrowing is added.
1126
+ const ssaoUpload = runHdrpClusterProfilePhase(
1127
+ profilePhase,
1128
+ 'record/scene-state/hdrp-cluster/buffer-upload',
1129
+ (): Result<void, RenderError> => {
994
1130
  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);
1131
+ if (ssaoBufs === null) return ok(undefined);
1132
+ const sProj = computeProjectionMatrix(camera);
1133
+ const sView = computeViewMatrix(camera);
1134
+ // inverseProjection = inverse(projection): NDC -> view-space.
1135
+ const invProjOnly = mat4.create();
1136
+ mat4.invert(invProjOnly, sProj);
1001
1137
 
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;
1138
+ // Float32Array of 64 (256 B): 3 mat4 (48) + intensityPad vec4 (4)
1139
+ // + 12 trailing padding floats. We only fill the declared region.
1140
+ const ssaoUniformPayload = new Float32Array(64);
1141
+ ssaoUniformPayload.set(sView, 0);
1142
+ ssaoUniformPayload.set(sProj, 16);
1143
+ ssaoUniformPayload.set(invProjOnly, 32);
1144
+ // intensityPad carries the same resolved values as the graph SSAO
1145
+ // calc pass; this early write keeps the shared UBO valid before the
1146
+ // typed fullscreen pass executes.
1147
+ const ssaoConfig = frameState.installedPipelineConfig?.ssao;
1148
+ const parameters = getSsaoParameters(
1149
+ ssaoConfig !== undefined && ssaoConfig.enabled === true ? ssaoConfig : undefined,
1150
+ );
1151
+ ssaoUniformPayload[48] = parameters.intensity;
1152
+ ssaoUniformPayload[49] = parameters.radius;
1153
+ ssaoUniformPayload[50] = parameters.bias;
1018
1154
 
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
- });
1155
+ const ssaoUniformRes = internals.device.queue.writeBuffer(
1156
+ ssaoBufs.uniformBuffer,
1157
+ 0,
1158
+ ssaoUniformPayload,
1159
+ );
1160
+ if (!ssaoUniformRes.ok)
1161
+ return err(mapClusterUploadFailure('ssao-uniform', ssaoUniformRes.error));
1162
+ return ok(undefined);
1163
+ },
1164
+ );
1165
+ if (!ssaoUpload.ok) return ssaoUpload;
1166
+ return ok(undefined);
1028
1167
  }
1029
1168
 
1030
1169
  /**