@forgeax/engine-render 0.1.20 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (372) hide show
  1. package/README.md +215 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/assembly/device-feature-admission.d.ts +18 -0
  4. package/dist/assembly/device-feature-admission.d.ts.map +1 -0
  5. package/dist/assembly/directional-shadow-inspection.d.ts +38 -0
  6. package/dist/assembly/directional-shadow-inspection.d.ts.map +1 -0
  7. package/dist/assembly/factory.d.ts +21 -18
  8. package/dist/assembly/factory.d.ts.map +1 -1
  9. package/dist/assembly/host-contract.d.ts +9 -0
  10. package/dist/assembly/host-contract.d.ts.map +1 -1
  11. package/dist/assembly/render-target-host.d.ts +46 -0
  12. package/dist/assembly/render-target-host.d.ts.map +1 -0
  13. package/dist/assets/render-asset-port.d.ts.map +1 -1
  14. package/dist/authoring.mjs +3 -0
  15. package/dist/authoring.mjs.map +1 -1
  16. package/dist/capture/cube-views.d.ts +21 -0
  17. package/dist/capture/cube-views.d.ts.map +1 -0
  18. package/dist/capture/scheduler.d.ts +54 -0
  19. package/dist/capture/scheduler.d.ts.map +1 -0
  20. package/dist/{chunk-4XINDLAY.mjs → chunk-5YADUCA7.mjs} +1552 -1361
  21. package/dist/chunk-5YADUCA7.mjs.map +1 -0
  22. package/dist/{chunk-TBQJOK2L.mjs → chunk-6OPXDQ2E.mjs} +364 -51
  23. package/dist/chunk-6OPXDQ2E.mjs.map +1 -0
  24. package/dist/{chunk-M2MP4ZOU.mjs → chunk-BXBQQKFQ.mjs} +3 -9
  25. package/dist/chunk-BXBQQKFQ.mjs.map +1 -0
  26. package/dist/{chunk-CDTWTHZR.mjs → chunk-FS2KNUQH.mjs} +548 -6
  27. package/dist/chunk-FS2KNUQH.mjs.map +1 -0
  28. package/dist/components/camera.d.ts +1 -0
  29. package/dist/components/camera.d.ts.map +1 -1
  30. package/dist/components/cube-camera.d.ts +20 -0
  31. package/dist/components/cube-camera.d.ts.map +1 -0
  32. package/dist/components/directional-light.d.ts +11 -2
  33. package/dist/components/directional-light.d.ts.map +1 -1
  34. package/dist/components/directional-shadow-filter.d.ts +25 -0
  35. package/dist/components/directional-shadow-filter.d.ts.map +1 -0
  36. package/dist/components/index.d.ts +2 -0
  37. package/dist/components/index.d.ts.map +1 -1
  38. package/dist/components/light-helpers.d.ts.map +1 -1
  39. package/dist/components/reflection-probe.d.ts +17 -0
  40. package/dist/components/reflection-probe.d.ts.map +1 -0
  41. package/dist/construct-renderer.mjs +5826 -1003
  42. package/dist/construct-renderer.mjs.map +1 -1
  43. package/dist/environment/frame.d.ts.map +1 -1
  44. package/dist/environment/lifecycle.d.ts +5 -0
  45. package/dist/environment/lifecycle.d.ts.map +1 -1
  46. package/dist/errors/render.d.ts +141 -93
  47. package/dist/errors/render.d.ts.map +1 -1
  48. package/dist/extract/directional-shadow-projection.d.ts +23 -0
  49. package/dist/extract/directional-shadow-projection.d.ts.map +1 -0
  50. package/dist/extract/environment.d.ts +14 -0
  51. package/dist/extract/environment.d.ts.map +1 -1
  52. package/dist/extract/visibility.d.ts.map +1 -1
  53. package/dist/features/fullscreen.d.ts.map +1 -1
  54. package/dist/features/targets.d.ts.map +1 -1
  55. package/dist/features/types.d.ts +10 -0
  56. package/dist/features/types.d.ts.map +1 -1
  57. package/dist/fullscreen-post-process-pass.d.ts +13 -2
  58. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  59. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  60. package/dist/gpu-driven/production-raster.d.ts +0 -1
  61. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  62. package/dist/gpu-scene.d.ts.map +1 -1
  63. package/dist/hdrp-buffers.d.ts +19 -17
  64. package/dist/hdrp-buffers.d.ts.map +1 -1
  65. package/dist/ibl/IblPipelineCache.d.ts +4 -0
  66. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  67. package/dist/ibl/kernel-cache.d.ts +36 -0
  68. package/dist/ibl/kernel-cache.d.ts.map +1 -0
  69. package/dist/index.d.ts +28 -3
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.mjs +4 -4
  72. package/dist/index.mjs.map +1 -1
  73. package/dist/inspection-types.d.ts +51 -0
  74. package/dist/inspection-types.d.ts.map +1 -1
  75. package/dist/instances-derived-bounds.d.ts +50 -0
  76. package/dist/instances-derived-bounds.d.ts.map +1 -0
  77. package/dist/internal.mjs +1 -1
  78. package/dist/light-buffer-layout.d.ts +10 -1
  79. package/dist/light-buffer-layout.d.ts.map +1 -1
  80. package/dist/mesh-material-bindings.d.ts +54 -0
  81. package/dist/mesh-material-bindings.d.ts.map +1 -1
  82. package/dist/pbr-pipeline.d.ts +14 -4
  83. package/dist/pbr-pipeline.d.ts.map +1 -1
  84. package/dist/pipeline/standard-forward-lane.d.ts +2 -1
  85. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  86. package/dist/pipeline/standard-lighting/errors.d.ts +4 -0
  87. package/dist/pipeline/standard-lighting/errors.d.ts.map +1 -0
  88. package/dist/pipeline/standard-lighting/graph.d.ts +24 -0
  89. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -0
  90. package/dist/pipeline/standard-lighting/inspection.d.ts +38 -0
  91. package/dist/pipeline/standard-lighting/inspection.d.ts.map +1 -0
  92. package/dist/pipeline/standard-lighting/layout.d.ts +28 -0
  93. package/dist/pipeline/standard-lighting/layout.d.ts.map +1 -0
  94. package/dist/pipeline/standard-lighting/light-frame.d.ts +33 -0
  95. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -0
  96. package/dist/pipeline/standard-lighting/membership.d.ts +14 -0
  97. package/dist/pipeline/standard-lighting/membership.d.ts.map +1 -0
  98. package/dist/pipeline/standard-lighting/prepare.d.ts +16 -0
  99. package/dist/pipeline/standard-lighting/prepare.d.ts.map +1 -0
  100. package/dist/pipeline/standard-lighting/topology.d.ts +45 -0
  101. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -0
  102. package/dist/pipeline/standard-lighting/transport.d.ts +36 -0
  103. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -0
  104. package/dist/pipeline/standard-pipeline.d.ts +4 -13
  105. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  106. package/dist/pipeline/standard-post.d.ts.map +1 -1
  107. package/dist/pipeline/standard-profile.d.ts +5 -26
  108. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  109. package/dist/pipeline-spec.d.ts +40 -7
  110. package/dist/pipeline-spec.d.ts.map +1 -1
  111. package/dist/point-shadow-inspection.d.ts +21 -0
  112. package/dist/point-shadow-inspection.d.ts.map +1 -0
  113. package/dist/record/frame-lighting.d.ts +27 -32
  114. package/dist/record/frame-lighting.d.ts.map +1 -1
  115. package/dist/record/frame-snapshot.d.ts +28 -14
  116. package/dist/record/frame-snapshot.d.ts.map +1 -1
  117. package/dist/record/frame-targets.d.ts +5 -0
  118. package/dist/record/frame-targets.d.ts.map +1 -1
  119. package/dist/record/frame.d.ts +10 -4
  120. package/dist/record/frame.d.ts.map +1 -1
  121. package/dist/record/gpu-pass-timing/contract.d.ts +96 -0
  122. package/dist/record/gpu-pass-timing/contract.d.ts.map +1 -0
  123. package/dist/record/gpu-pass-timing/errors.d.ts +18 -0
  124. package/dist/record/gpu-pass-timing/errors.d.ts.map +1 -0
  125. package/dist/record/gpu-pass-timing/index.d.ts +8 -0
  126. package/dist/record/gpu-pass-timing/index.d.ts.map +1 -0
  127. package/dist/record/gpu-pass-timing/parser.d.ts +11 -0
  128. package/dist/record/gpu-pass-timing/parser.d.ts.map +1 -0
  129. package/dist/record/gpu-pass-timing/session.d.ts +43 -0
  130. package/dist/record/gpu-pass-timing/session.d.ts.map +1 -0
  131. package/dist/record/gpu-timing.d.ts +8 -3
  132. package/dist/record/gpu-timing.d.ts.map +1 -1
  133. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  134. package/dist/record/main-pass-material.d.ts +22 -1
  135. package/dist/record/main-pass-material.d.ts.map +1 -1
  136. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  137. package/dist/record/main-pass.d.ts +2 -0
  138. package/dist/record/main-pass.d.ts.map +1 -1
  139. package/dist/record/render-context.d.ts +43 -2
  140. package/dist/record/render-context.d.ts.map +1 -1
  141. package/dist/record/shadow-pass.d.ts.map +1 -1
  142. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  143. package/dist/record/typed-frame-graph.d.ts +72 -4
  144. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  145. package/dist/record/view-ubo.d.ts +7 -4
  146. package/dist/record/view-ubo.d.ts.map +1 -1
  147. package/dist/reflection/filter.d.ts +22 -0
  148. package/dist/reflection/filter.d.ts.map +1 -0
  149. package/dist/reflection/gpu-table.d.ts +25 -0
  150. package/dist/reflection/gpu-table.d.ts.map +1 -0
  151. package/dist/reflection/projection.d.ts +91 -0
  152. package/dist/reflection/projection.d.ts.map +1 -0
  153. package/dist/render-contract.d.ts +49 -7
  154. package/dist/render-contract.d.ts.map +1 -1
  155. package/dist/render-graph-primitives.d.ts +1 -0
  156. package/dist/render-graph-primitives.d.ts.map +1 -1
  157. package/dist/render-pipeline.d.ts +42 -2
  158. package/dist/render-pipeline.d.ts.map +1 -1
  159. package/dist/render-system-extract.d.ts +58 -17
  160. package/dist/render-system-extract.d.ts.map +1 -1
  161. package/dist/render-system.d.ts +17 -2
  162. package/dist/render-system.d.ts.map +1 -1
  163. package/dist/renderer.d.ts +43 -0
  164. package/dist/renderer.d.ts.map +1 -0
  165. package/dist/scene/render-scene.d.ts +14 -0
  166. package/dist/scene/render-scene.d.ts.map +1 -1
  167. package/dist/shadow-atlas.d.ts +3 -3
  168. package/dist/shadow-atlas.d.ts.map +1 -1
  169. package/dist/ssao-buffers.d.ts +3 -0
  170. package/dist/ssao-buffers.d.ts.map +1 -1
  171. package/dist/targets/contracts.d.ts +66 -0
  172. package/dist/targets/contracts.d.ts.map +1 -0
  173. package/dist/targets/graph-projection.d.ts +29 -0
  174. package/dist/targets/graph-projection.d.ts.map +1 -0
  175. package/dist/targets/material-source.d.ts +34 -0
  176. package/dist/targets/material-source.d.ts.map +1 -0
  177. package/dist/targets/owner.d.ts +32 -0
  178. package/dist/targets/owner.d.ts.map +1 -0
  179. package/dist/targets/physical.d.ts +18 -0
  180. package/dist/targets/physical.d.ts.map +1 -0
  181. package/dist/targets/readback.d.ts +39 -0
  182. package/dist/targets/readback.d.ts.map +1 -0
  183. package/dist/temporal/index.mjs +3 -3
  184. package/dist/typed-render-graph-primitives.d.ts +1 -1
  185. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  186. package/dist/typed-shadow-passes.d.ts +1 -1
  187. package/dist/typed-shadow-passes.d.ts.map +1 -1
  188. package/dist/volume/passes.d.ts +2 -2
  189. package/dist/volume/passes.d.ts.map +1 -1
  190. package/package.json +20 -20
  191. package/src/__tests__/bloom-exact-zero.unit.test.ts +48 -19
  192. package/src/__tests__/bloom-lifecycle.integration.test.ts +41 -13
  193. package/src/__tests__/camera-target-consumption.unit.test.ts +53 -0
  194. package/src/__tests__/cluster-binner.unit.test.ts +35 -0
  195. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +7 -2
  196. package/src/__tests__/cube-camera-scheduler.unit.test.ts +70 -0
  197. package/src/__tests__/cube-camera-single-submit.perf.test.ts +75 -0
  198. package/src/__tests__/cube-camera-view-matrices.unit.test.ts +41 -0
  199. package/src/__tests__/cube-camera-vocabulary.unit.test.ts +47 -0
  200. package/src/__tests__/cube-camera.integration.test.ts +42 -0
  201. package/src/__tests__/device-feature-admission.unit.test.ts +73 -0
  202. package/src/__tests__/direct-light-snapshot.unit.test.ts +3 -3
  203. package/src/__tests__/docs-gate-green.unit.test.ts +38 -0
  204. package/src/__tests__/factory-contract.integration.test.ts +255 -29
  205. package/src/__tests__/fog-component.unit.test.ts +67 -0
  206. package/src/__tests__/fog.integration.test.ts +86 -0
  207. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +37 -0
  208. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +10 -9
  209. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +17 -1
  210. package/src/__tests__/gpu-driven-production.integration.test.ts +16 -17
  211. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +12 -4
  212. package/src/__tests__/gpu-pass-timing-browser-runner.ts +281 -0
  213. package/src/__tests__/gpu-pass-timing-coexistence.integration.test.ts +80 -0
  214. package/src/__tests__/gpu-pass-timing-contract.unit.test.ts +243 -0
  215. package/src/__tests__/gpu-pass-timing-exact-zero.unit.test.ts +131 -0
  216. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +230 -0
  217. package/src/__tests__/gpu-pass-timing-observation.unit.test.ts +233 -0
  218. package/src/__tests__/gpu-pass-timing-session.unit.test.ts +438 -0
  219. package/src/__tests__/gpu-pass-timing.browser.test.ts +26 -0
  220. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +125 -0
  221. package/src/__tests__/hdrp-group2-visibility.unit.test.ts +24 -0
  222. package/src/__tests__/instances-culling.integration.test.ts +94 -0
  223. package/src/__tests__/instances-derived-bounds.unit.test.ts +118 -0
  224. package/src/__tests__/material-observation.integration.test.ts +101 -0
  225. package/src/__tests__/pbr-view-layout.unit.test.ts +16 -0
  226. package/src/__tests__/point-shadow-inspection.unit.test.ts +60 -0
  227. package/src/__tests__/public-surface.test-d.ts +10 -0
  228. package/src/__tests__/reflection-probe-authoring.unit.test.ts +71 -0
  229. package/src/__tests__/reflection-probe-filter.unit.test.ts +54 -0
  230. package/src/__tests__/reflection-probe-ibl-topology.unit.test.ts +45 -0
  231. package/src/__tests__/reflection-probe-pass-encode.integration.test.ts +101 -0
  232. package/src/__tests__/reflection-probe-performance.perf.test.ts +60 -0
  233. package/src/__tests__/reflection-probe-recovery.integration.test.ts +55 -0
  234. package/src/__tests__/reflection-probe-selection.unit.test.ts +46 -0
  235. package/src/__tests__/render-error-code-owner.test-d.ts +18 -3
  236. package/src/__tests__/render-feature-docs.unit.test.ts +1 -1
  237. package/src/__tests__/render-target-admission.unit.test.ts +80 -0
  238. package/src/__tests__/render-target-graph-projection.unit.test.ts +102 -0
  239. package/src/__tests__/render-target-inspection.unit.test.ts +52 -0
  240. package/src/__tests__/render-target-material-source.test-d.ts +30 -0
  241. package/src/__tests__/render-target-material-source.unit.test.ts +97 -0
  242. package/src/__tests__/render-target-owner-placement.unit.test.ts +203 -0
  243. package/src/__tests__/render-target-owner.unit.test.ts +122 -0
  244. package/src/__tests__/render-target-probe-soak.perf.test.ts +94 -0
  245. package/src/__tests__/render-target-public-schema.json +107 -0
  246. package/src/__tests__/render-target-public-schema.unit.test.ts +91 -0
  247. package/src/__tests__/render-target-public-surface.test-d.ts +83 -0
  248. package/src/__tests__/render-target-readback.integration.test.ts +90 -0
  249. package/src/__tests__/render-target-recovery.integration.test.ts +85 -0
  250. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +48 -0
  251. package/src/__tests__/renderer-lifecycle.integration.test.ts +14 -0
  252. package/src/__tests__/shader-stage-surface.unit.test.ts +19 -4
  253. package/src/__tests__/skin-motion-regression.unit.test.ts +132 -0
  254. package/src/__tests__/skinned-shadow-caster.test.ts +11 -2
  255. package/src/__tests__/standard-cluster-capability.integration.test.ts +63 -0
  256. package/src/__tests__/standard-cluster-transport.unit.test.ts +121 -0
  257. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +232 -0
  258. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +8 -5
  259. package/src/__tests__/standard-lighting-inspection.integration.test.ts +138 -0
  260. package/src/__tests__/standard-lighting-ssot.unit.test.ts +18 -0
  261. package/src/__tests__/standard-lighting-write.unit.test.ts +377 -0
  262. package/src/__tests__/standard-lighting.integration.test.ts +67 -0
  263. package/src/__tests__/standard-pipeline.dawn.test.ts +206 -5
  264. package/src/__tests__/standard-pipeline.integration.test.ts +181 -150
  265. package/src/__tests__/standard-probe-binding.unit.test.ts +25 -0
  266. package/src/__tests__/standard-profile-oracle.unit.test.ts +23 -38
  267. package/src/__tests__/typed-pipeline-topology.unit.test.ts +395 -14
  268. package/src/__tests__/urp-pipeline-targets.unit.test.ts +2 -2
  269. package/src/__tests__/visibility-resolver.unit.test.ts +15 -0
  270. package/src/assembly/device-feature-admission.ts +73 -0
  271. package/src/assembly/directional-shadow-inspection.ts +122 -0
  272. package/src/assembly/factory.ts +877 -286
  273. package/src/assembly/host-contract.ts +25 -0
  274. package/src/assembly/render-target-host.ts +847 -0
  275. package/src/assets/asset-decoders.ts +1 -1
  276. package/src/assets/render-asset-port.ts +7 -1
  277. package/src/capture/cube-views.ts +69 -0
  278. package/src/capture/scheduler.ts +265 -0
  279. package/src/components/__tests__/schema-contract.unit.test.ts +12 -0
  280. package/src/components/__tests__/schema-parity.unit.test.ts +9 -0
  281. package/src/components/camera.ts +4 -0
  282. package/src/components/cube-camera.ts +52 -0
  283. package/src/components/directional-light.ts +22 -2
  284. package/src/components/directional-shadow-filter.ts +57 -0
  285. package/src/components/index.ts +2 -0
  286. package/src/components/light-helpers.ts +81 -7
  287. package/src/components/reflection-probe.ts +49 -0
  288. package/src/environment/frame.ts +6 -24
  289. package/src/environment/lifecycle.ts +12 -0
  290. package/src/errors/render.ts +286 -142
  291. package/src/extract/directional-shadow-projection.ts +61 -0
  292. package/src/extract/environment.ts +57 -0
  293. package/src/extract/visibility.ts +25 -1
  294. package/src/features/fullscreen.ts +3 -0
  295. package/src/features/targets.ts +4 -1
  296. package/src/features/types.ts +10 -0
  297. package/src/fullscreen-post-process-pass.ts +43 -2
  298. package/src/gpu-driven/batch-topology.ts +1 -0
  299. package/src/gpu-driven/production-raster.ts +1 -3
  300. package/src/gpu-scene.ts +8 -1
  301. package/src/hdrp-buffers.ts +165 -59
  302. package/src/ibl/IblPipelineCache.ts +11 -0
  303. package/src/ibl/kernel-cache.ts +88 -0
  304. package/src/index.ts +148 -0
  305. package/src/inspection-types.ts +56 -0
  306. package/src/instances-derived-bounds.ts +171 -0
  307. package/src/light-buffer-layout.ts +20 -5
  308. package/src/mesh-material-bindings.ts +132 -0
  309. package/src/pbr-pipeline.ts +106 -17
  310. package/src/pipeline/standard-forward-lane.ts +46 -16
  311. package/src/pipeline/standard-lighting/errors.ts +6 -0
  312. package/src/pipeline/standard-lighting/graph.ts +157 -0
  313. package/src/pipeline/standard-lighting/inspection.ts +88 -0
  314. package/src/pipeline/standard-lighting/layout.ts +31 -0
  315. package/src/pipeline/standard-lighting/light-frame.ts +34 -0
  316. package/src/pipeline/standard-lighting/membership.ts +43 -0
  317. package/src/pipeline/standard-lighting/prepare.ts +37 -0
  318. package/src/pipeline/standard-lighting/topology.ts +120 -0
  319. package/src/pipeline/standard-lighting/transport.ts +77 -0
  320. package/src/pipeline/standard-pipeline.ts +122 -193
  321. package/src/pipeline/standard-post.ts +26 -7
  322. package/src/pipeline/standard-profile.ts +17 -47
  323. package/src/pipeline-spec.ts +84 -21
  324. package/src/point-shadow-inspection.ts +55 -0
  325. package/src/record/__tests__/temporal-payload.unit.test.ts +42 -1
  326. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +73 -0
  327. package/src/record/__tests__/view-ubo-layout.unit.test.ts +40 -0
  328. package/src/record/frame-lighting.ts +367 -427
  329. package/src/record/frame-snapshot.ts +35 -27
  330. package/src/record/frame-targets.ts +17 -0
  331. package/src/record/frame.ts +261 -74
  332. package/src/record/gpu-pass-timing/contract.ts +180 -0
  333. package/src/record/gpu-pass-timing/errors.ts +34 -0
  334. package/src/record/gpu-pass-timing/index.ts +37 -0
  335. package/src/record/gpu-pass-timing/parser.ts +97 -0
  336. package/src/record/gpu-pass-timing/session.ts +938 -0
  337. package/src/record/gpu-timing.ts +21 -12
  338. package/src/record/main-pass-geometry.ts +120 -80
  339. package/src/record/main-pass-material.ts +95 -9
  340. package/src/record/main-pass-sprite-draws.ts +54 -25
  341. package/src/record/main-pass.ts +71 -15
  342. package/src/record/render-context.ts +72 -0
  343. package/src/record/shadow-pass.ts +17 -28
  344. package/src/record/skybox-post-pass.ts +20 -4
  345. package/src/record/typed-frame-graph.ts +893 -44
  346. package/src/record/view-ubo.ts +63 -28
  347. package/src/reflection/filter.ts +81 -0
  348. package/src/reflection/gpu-table.ts +44 -0
  349. package/src/reflection/projection.ts +281 -0
  350. package/src/render-contract.ts +76 -5
  351. package/src/render-graph-primitives.ts +9 -4
  352. package/src/render-pipeline.ts +117 -2
  353. package/src/render-system-extract.ts +557 -100
  354. package/src/render-system.ts +1095 -66
  355. package/src/renderer.ts +222 -0
  356. package/src/scene/render-scene.ts +115 -9
  357. package/src/shadow-atlas.ts +5 -5
  358. package/src/ssao-buffers.ts +9 -2
  359. package/src/targets/contracts.ts +182 -0
  360. package/src/targets/graph-projection.ts +169 -0
  361. package/src/targets/material-source.ts +143 -0
  362. package/src/targets/owner.ts +214 -0
  363. package/src/targets/physical.ts +18 -0
  364. package/src/targets/readback.ts +153 -0
  365. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +59 -20
  366. package/src/typed-render-graph-primitives.ts +5 -3
  367. package/src/typed-shadow-passes.ts +46 -40
  368. package/src/volume/passes.ts +15 -9
  369. package/dist/chunk-4XINDLAY.mjs.map +0 -1
  370. package/dist/chunk-CDTWTHZR.mjs.map +0 -1
  371. package/dist/chunk-M2MP4ZOU.mjs.map +0 -1
  372. package/dist/chunk-TBQJOK2L.mjs.map +0 -1
@@ -1,4 +1,5 @@
1
1
  import type { EntityHandle, World } from '@forgeax/engine-ecs';
2
+ import { createWorldProjection } from '@forgeax/engine-ecs/projection';
2
3
  import {
3
4
  projectHierarchy,
4
5
  type SceneHierarchyDiagnostic,
@@ -37,6 +38,14 @@ interface ResolutionState {
37
38
  readonly source: VisibilitySource;
38
39
  }
39
40
 
41
+ interface VisibilityCacheEntry {
42
+ readonly changes: ReturnType<typeof createWorldProjection>;
43
+ readonly hierarchy: SceneHierarchySnapshot;
44
+ readonly snapshot: VisibilitySnapshot;
45
+ }
46
+
47
+ const VISIBILITY_CACHE = new WeakMap<World, VisibilityCacheEntry>();
48
+
40
49
  /**
41
50
  * Resolve author intent against the scene-owned valid parent projection.
42
51
  * Diagnostics are preserved for callers to repair hierarchy input before retry.
@@ -45,6 +54,19 @@ export function resolveVisibility(
45
54
  world: World,
46
55
  hierarchy: SceneHierarchySnapshot = projectHierarchy(world),
47
56
  ): VisibilitySnapshot {
57
+ const cached = VISIBILITY_CACHE.get(world);
58
+ const changes = cached?.changes ?? createWorldProjection(world, { components: [Visibility] });
59
+ if (cached !== undefined) {
60
+ const evidence = changes.poll();
61
+ if (
62
+ evidence.status === 'delta' &&
63
+ evidence.changes.length === 0 &&
64
+ cached.hierarchy === hierarchy
65
+ ) {
66
+ return cached.snapshot;
67
+ }
68
+ }
69
+
48
70
  // Visibility is an opt-in component. Query only the owning archetypes
49
71
  // instead of walking every Entity row with an optional column; the latter
50
72
  // made a World with no visibility intents pay a full-scene scan on each
@@ -123,7 +145,7 @@ export function resolveVisibility(
123
145
  };
124
146
  };
125
147
 
126
- return {
148
+ const snapshot: VisibilitySnapshot = {
127
149
  diagnostics: hierarchy.diagnostics,
128
150
  hasAnyIntent: intentCount > 0,
129
151
  hasAnyHiddenIntent,
@@ -136,4 +158,6 @@ export function resolveVisibility(
136
158
  return resolveEntity?.(entity)?.effective ?? 'visible';
137
159
  },
138
160
  };
161
+ VISIBILITY_CACHE.set(world, { changes, hierarchy, snapshot });
162
+ return snapshot;
139
163
  }
@@ -28,6 +28,9 @@ export function createFullscreenRenderFeature(
28
28
  const resourceName = `fullscreen.${options.identity.replace(/[^a-z0-9.-]/gi, '-').toLowerCase()}`;
29
29
  return Object.freeze({
30
30
  identity: options.identity,
31
+ requiredFullscreenPostProcesses: Object.freeze([
32
+ Object.freeze({ identity: options.identity, source: options.source }),
33
+ ]),
31
34
  extract: (_context: RenderFeatureExtractContext) => ok(undefined),
32
35
  plan: (_data: undefined, _context: RenderFeaturePlanContext) =>
33
36
  ok({
@@ -57,8 +57,11 @@ export function resolveStandardRenderFeatureTargets(input: {
57
57
  readonly colorAttachmentFormat: TextureFormat;
58
58
  }): readonly RenderFeatureTargetHandle[] {
59
59
  const sampleCount: 1 | 4 = input.antialias === 'msaa' && input.multisample ? 4 : 1;
60
+ // Standard always exposes the scene to features as a linear float target;
61
+ // the shared Output Transform owns the eventual RGBA8 write for every
62
+ // antialias mode, including no-AA/WebGL2.
60
63
  const linearLdr =
61
- (input.tonemap === 'none' && input.storageBuffer) ||
64
+ input.tonemap === 'none' ||
62
65
  input.antialias === 'fxaa' ||
63
66
  (input.antialias === 'msaa' && input.multisample);
64
67
  const colorFormat =
@@ -98,6 +98,16 @@ export interface RenderFeature<FrameData> {
98
98
  * the same lazy module cache used by prepared graphics.
99
99
  */
100
100
  readonly requiredMaterialShaders?: readonly string[];
101
+ /**
102
+ * Fullscreen program sources that must be module-ready before the first
103
+ * frame. The renderer seeds the same lazy post-process module cache used by
104
+ * the typed graph, so a synchronous frame driver cannot observe a cleared
105
+ * target while a boot-installed effect is compiling.
106
+ */
107
+ readonly requiredFullscreenPostProcesses?: readonly {
108
+ readonly identity: string;
109
+ readonly source: string;
110
+ }[];
101
111
  extract(context: RenderFeatureExtractContext): Result<FrameData, RenderError>;
102
112
  plan(data: FrameData, context: RenderFeaturePlanContext): Result<RenderFeaturePlan, RenderError>;
103
113
  }
@@ -67,7 +67,8 @@ export type PostProcessReadSampleType = 'depth';
67
67
  * no paramSchema (params are passed inline via a {type, value} struct — plan-strategy D-4).
68
68
  *
69
69
  * `params` carries the WGSL struct definition + default value. When `params` is
70
- * `undefined`, the post-process shader has no UBO (e.g. FXAA — Finding M2-1).
70
+ * `undefined`, the post-process shader has no params UBO. Special graph-owned
71
+ * passes such as FXAA may still register an internal policy UBO separately.
71
72
  *
72
73
  * `reads` declares graph resource keys this pass samples as input texture(s).
73
74
  * If omitted, the pass samples the swap-chain color attachment (default path).
@@ -81,7 +82,7 @@ export interface PostProcessShaderEntry {
81
82
  /** Composed WGSL source for the fragment stage (post-naga_oil). */
82
83
  readonly source: string;
83
84
  /**
84
- * Params UBO schema. When undefined, the shader has no uniform buffer (e.g. FXAA)
85
+ * Params UBO schema. When undefined, the generic shader has no uniform buffer
85
86
  * and the BGL degrades to 2 entries (texture@0 + sampler@1). When present, the
86
87
  * primitive eager-creates a params UBO (at register, sized `byteSize`) and binds
87
88
  * it at bindgroup(1) binding(2) as part of a 3-entry BGL
@@ -104,6 +105,46 @@ export interface PostProcessShaderEntry {
104
105
  readonly reads?: readonly (string | PostProcessReadEntry)[] | undefined;
105
106
  }
106
107
 
108
+ function postProcessSourceDigest(source: string): string {
109
+ let first = 0x811c9dc5;
110
+ let second = 0x9e3779b9;
111
+ for (let index = 0; index < source.length; index += 1) {
112
+ const code = source.charCodeAt(index);
113
+ first = Math.imul(first ^ code, 0x01000193);
114
+ second = Math.imul(second ^ (code + index), 0x01000193);
115
+ }
116
+ return `${source.length.toString(16)}-${(first >>> 0).toString(16).padStart(8, '0')}-${(second >>> 0).toString(16).padStart(8, '0')}`;
117
+ }
118
+
119
+ /** Content-addressed PSO label shared by the post-process pipeline factory. */
120
+ export function postProcessShaderPipelineLabel(identity: string, source: string): string {
121
+ return `post-process-${identity}-pso-${postProcessSourceDigest(source)}`;
122
+ }
123
+
124
+ /** Content-addressed shader-module label shared by prewarm and lazy paths. */
125
+ export function postProcessShaderModuleLabel(identity: string, source: string): string {
126
+ return `${postProcessShaderPipelineLabel(identity, source)}-module`;
127
+ }
128
+
129
+ /**
130
+ * Stable declaration identity used by graph admission and the renderer's
131
+ * device-bound pipeline cache. Shader source, bind-group-affecting params,
132
+ * and graph reads must move together when a feature reuses its identity.
133
+ */
134
+ export function postProcessShaderEntrySignature(entry: PostProcessShaderEntry): string {
135
+ return JSON.stringify({
136
+ source: entry.source,
137
+ params:
138
+ entry.params === undefined
139
+ ? undefined
140
+ : {
141
+ byteSize: entry.params.byteSize,
142
+ defaultValue: Array.from(entry.params.defaultValue),
143
+ },
144
+ reads: entry.reads,
145
+ });
146
+ }
147
+
107
148
  /** Whether a feature binding opts into the temporal multi-read contract. */
108
149
  export function isTemporalFullscreenBinding(values: Readonly<Record<string, unknown>>): boolean {
109
150
  return (
@@ -53,6 +53,7 @@ function eligibleKeys(slot: RenderSceneSlot): readonly GpuDrivenBatchKey[] {
53
53
  if (
54
54
  draws === undefined ||
55
55
  draws.length === 0 ||
56
+ snapshot.instances?.instanceCount === 0 ||
56
57
  snapshot.localAabb === undefined ||
57
58
  snapshot.skin !== undefined ||
58
59
  snapshot.spriteInstances !== undefined
@@ -427,7 +427,6 @@ export class GpuDrivenProduction {
427
427
  readonly meshes: ReadonlyMap<number, MeshGpuHandles>;
428
428
  readonly viewBindGroupLayout: BindGroupLayout;
429
429
  readonly meshResidencyEpoch: number;
430
- readonly hdrp: boolean;
431
430
  }): Result<PreparedGpuDrivenFrame | undefined, RhiError> {
432
431
  this.gpuOwnedSnapshotsMaterialized = 0;
433
432
  this.filteredPlanBuilds = 0;
@@ -435,7 +434,6 @@ export class GpuDrivenProduction {
435
434
  this.validatedGpuOwnedRows = 0;
436
435
  this.cpuFallbackDrawItems = 0;
437
436
  if (
438
- input.hdrp ||
439
437
  input.scene === undefined ||
440
438
  !this.device.caps.compute ||
441
439
  !this.device.caps.storageBuffer ||
@@ -611,7 +609,7 @@ export class GpuDrivenProduction {
611
609
  // shared by the GPU-driven raster pipeline, so even rigid draws
612
610
  // must provide the binding-10 offset (zero selects the frame
613
611
  // default slot).
614
- pass.setBindGroup(0, viewBindGroup, [0]);
612
+ pass.setBindGroup(0, viewBindGroup, [0, 0]);
615
613
  let currentPipeline: RenderPipeline | undefined;
616
614
  let currentVertex: GraphBuffer | undefined;
617
615
  let currentIndex: GraphBuffer | undefined;
package/src/gpu-scene.ts CHANGED
@@ -633,7 +633,14 @@ export class GpuScene {
633
633
  record: RenderSceneSlot,
634
634
  writes: Record<GpuSceneTableName, number[]>,
635
635
  ): void {
636
- const instanceCount = Math.max(1, record.snapshot.instances?.instanceCount ?? 1);
636
+ // A present Instances component is authoritative even when its array is
637
+ // empty: preserve zero so neither the GPU Scene nor the indirect topology
638
+ // turns it into a synthetic identity instance. Entities without the
639
+ // component retain the ordinary one-instance identity path.
640
+ const instanceCount =
641
+ record.snapshot.instances === undefined
642
+ ? 1
643
+ : Math.max(0, Math.floor(record.snapshot.instances.instanceCount));
637
644
  const drawCount = Math.max(1, record.snapshot.gpuDrivenDraws?.length ?? 0);
638
645
  const materialCount = Math.max(1, record.snapshot.materials.length);
639
646
  const existing = this.allocations.get(record.slot);
@@ -48,6 +48,7 @@ import {
48
48
  GPU_BUFFER_USAGE_UNIFORM,
49
49
  } from './gpu-usage';
50
50
  import { BYTES_PER_LIGHT_SLOT } from './light-buffer-layout';
51
+ import { createPbrSkinMeshBindGroupEntries } from './pbr-pipeline';
51
52
  import {
52
53
  CLUSTER_GRID_STRIDE_U32,
53
54
  DEFAULT_CLUSTER_GRID,
@@ -55,20 +56,12 @@ import {
55
56
  MAX_LIGHTS,
56
57
  } from './pipeline/standard-profile';
57
58
  import { createHdrpBindGroupLayoutDescriptor } from './pipeline-spec';
58
- import { MESH_PER_ENTITY_STRIDE, MESH_UBO_FULL_ARRAY_BYTES } from './record/mesh-ssbo';
59
+ import { MESH_PER_ENTITY_STRIDE } from './record/mesh-ssbo';
59
60
  import type { RenderSystemRuntime } from './record/render-context';
60
61
  import { getOrCreateSsaoFallbackTexture } from './ssao-buffers';
61
62
 
62
63
  export { getOrCreateSsaoFallbackTexture } from './ssao-buffers';
63
64
 
64
- /**
65
- * WebGL2 HDRP downlevel light-list capacity. 128 x 64 B = 8 KiB, below the
66
- * WebGL2 minimum uniform-buffer binding size of 16 KiB. The storage path
67
- * keeps the full MAX_LIGHTS budget; the uniform path uses the same LightSlot
68
- * POD and only changes the transport.
69
- */
70
- export const HDRP_UNIFORM_LIGHT_CAPACITY = 128;
71
-
72
65
  /** Bind-group layout used only by the WebGPU cluster membership producer. */
73
66
  export function createHdrpClusterMembershipBindGroupLayoutDescriptor(): BindGroupLayoutDescriptor {
74
67
  return {
@@ -111,9 +104,9 @@ export function createHdrpClusterMembershipBindGroupLayoutDescriptor(): BindGrou
111
104
  * cluster 4 buffer).
112
105
  */
113
106
  export interface HdrpBuffers {
114
- /** Whether bindings 3..5 carry storage buffers or uniform-buffer fallback data. */
115
- readonly storageBuffer: boolean;
116
- /** light_data -- 256 x 64 B on storage, 128 x 64 B on uniform fallback. */
107
+ /** Device identity that owns every handle in this bundle. */
108
+ readonly device: RenderSystemRuntime['device'];
109
+ /** light_data -- the full 256 x 64 B Standard light budget. */
117
110
  readonly lightDataBuffer: Buffer;
118
111
  readonly lightDataBytes: number;
119
112
  /** cluster_grid SSBO -- gridX*gridY*gridZ * 2 u32, BGL slot 4. */
@@ -145,66 +138,92 @@ const cache = new WeakMap<RenderSystemRuntime, HdrpBuffers>();
145
138
  * pipeline disable (charter P3 explicit failure).
146
139
  *
147
140
  * `grid` defaults to `DEFAULT_CLUSTER_GRID` when undefined (matches the M5
148
- * record-stage default). The buffers are sized at the *first* call's grid; a
149
- * later install with a different grid keeps the existing allocation as long as
150
- * it has enough capacity (gridX*gridY*gridZ <= cached.grid product). Different
151
- * grids that exceed capacity are not currently re-allocated -- M-future.
141
+ * record-stage default). A smaller grid reuses the existing capacity. When a
142
+ * later install requests a larger grid, this owner allocates a complete new
143
+ * bundle, publishes it atomically, and retires the previous bundle after the
144
+ * device queue fence so graph and bind-group caches never observe a partially
145
+ * replaced resource set.
152
146
  */
153
147
  export function getOrCreateHdrpBuffers(
154
148
  runtime: RenderSystemRuntime,
155
149
  grid: { x: number; y: number; z: number } = DEFAULT_CLUSTER_GRID,
156
150
  ): HdrpBuffers | null {
151
+ // Cluster has one code-level transport. Capability failure is handled by
152
+ // Standard transport admission; this owner never creates a second UBO ABI.
153
+ if (runtime.device.caps?.storageBuffer !== true) return null;
154
+ // Contract tests and reduced host adapters may advertise storage buffers
155
+ // without implementing the resource-creation surface. Treat that as a
156
+ // capability miss rather than dereferencing an absent method; callers
157
+ // already route `null` through the structured cluster-transport fallback.
158
+ const deviceSurface = runtime.device as unknown as {
159
+ readonly createBuffer?: unknown;
160
+ readonly createBindGroupLayout?: unknown;
161
+ readonly destroyBuffer?: unknown;
162
+ };
163
+ if (
164
+ typeof deviceSurface.createBuffer !== 'function' ||
165
+ typeof deviceSurface.createBindGroupLayout !== 'function' ||
166
+ typeof deviceSurface.destroyBuffer !== 'function'
167
+ ) {
168
+ return null;
169
+ }
157
170
  const cached = cache.get(runtime);
158
- if (cached !== undefined) return cached;
159
-
171
+ if (cached !== undefined && cached.device === runtime.device) {
172
+ const requestedCells = grid.x * grid.y * grid.z;
173
+ const cachedCells = cached.grid.x * cached.grid.y * cached.grid.z;
174
+ if (requestedCells <= cachedCells) return cached;
175
+ }
160
176
  const device = runtime.device;
161
- const storageBuffer = runtime.device.caps?.storageBuffer === true;
162
177
  const clusterUniformBytes = 32;
163
- const lightDataBytes =
164
- (storageBuffer ? MAX_LIGHTS : HDRP_UNIFORM_LIGHT_CAPACITY) * BYTES_PER_LIGHT_SLOT;
178
+ const lightDataBytes = MAX_LIGHTS * BYTES_PER_LIGHT_SLOT;
165
179
  const clusterCells = grid.x * grid.y * grid.z;
166
- const clusterGridBytes = storageBuffer
167
- ? clusterCells * CLUSTER_GRID_STRIDE_U32 * 4
168
- : clusterUniformBytes;
169
- const lightIndexListBytes = storageBuffer ? LIGHT_INDEX_LIST_CAPACITY * 4 : clusterUniformBytes;
180
+ const clusterGridBytes = clusterCells * CLUSTER_GRID_STRIDE_U32 * 4;
181
+ const lightIndexListBytes = LIGHT_INDEX_LIST_CAPACITY * 4;
170
182
  const lightBoundsBytes = MAX_LIGHTS * 6 * 4;
183
+ const created: Buffer[] = [];
184
+ const releaseCreated = (): void => {
185
+ for (const buffer of created) {
186
+ const result = device.destroyBuffer(buffer);
187
+ if (!result.ok) runtime.errorRegistry.fire(result.error);
188
+ }
189
+ created.length = 0;
190
+ };
191
+ const failAllocation = (error: RhiError): null => {
192
+ runtime.errorRegistry.fire(error);
193
+ releaseCreated();
194
+ return null;
195
+ };
171
196
 
172
197
  const lightData = device.createBuffer({
173
198
  label: 'hdrp-light-data',
174
199
  size: lightDataBytes,
175
- usage:
176
- (storageBuffer ? GPU_BUFFER_USAGE_STORAGE : GPU_BUFFER_USAGE_UNIFORM) |
177
- GPU_BUFFER_USAGE_COPY_DST,
200
+ usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
178
201
  mappedAtCreation: false,
179
202
  });
180
203
  if (!lightData.ok) {
181
- runtime.errorRegistry.fire(lightData.error);
182
- return null;
204
+ return failAllocation(lightData.error);
183
205
  }
206
+ created.push(lightData.value);
184
207
  const clusterGrid = device.createBuffer({
185
208
  label: 'hdrp-cluster-grid',
186
209
  size: clusterGridBytes,
187
- usage:
188
- (storageBuffer ? GPU_BUFFER_USAGE_STORAGE : GPU_BUFFER_USAGE_UNIFORM) |
189
- GPU_BUFFER_USAGE_COPY_DST,
210
+ usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
190
211
  mappedAtCreation: false,
191
212
  });
192
213
  if (!clusterGrid.ok) {
193
- runtime.errorRegistry.fire(clusterGrid.error);
194
- return null;
214
+ return failAllocation(clusterGrid.error);
195
215
  }
216
+ created.push(clusterGrid.value);
196
217
  const lightIndexList = device.createBuffer({
197
218
  label: 'hdrp-light-index-list',
198
219
  size: lightIndexListBytes,
199
- usage:
200
- (storageBuffer ? GPU_BUFFER_USAGE_STORAGE : GPU_BUFFER_USAGE_UNIFORM) |
201
- GPU_BUFFER_USAGE_COPY_DST,
220
+ usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
202
221
  mappedAtCreation: false,
203
222
  });
204
223
  if (!lightIndexList.ok) {
205
- runtime.errorRegistry.fire(lightIndexList.error);
206
- return null;
224
+ return failAllocation(lightIndexList.error);
207
225
  }
226
+ created.push(lightIndexList.value);
208
227
  const clusterUniform = device.createBuffer({
209
228
  label: 'hdrp-cluster-uniform',
210
229
  size: clusterUniformBytes,
@@ -212,36 +231,30 @@ export function getOrCreateHdrpBuffers(
212
231
  mappedAtCreation: false,
213
232
  });
214
233
  if (!clusterUniform.ok) {
215
- runtime.errorRegistry.fire(clusterUniform.error);
216
- return null;
234
+ return failAllocation(clusterUniform.error);
217
235
  }
236
+ created.push(clusterUniform.value);
218
237
  const lightBounds = device.createBuffer({
219
238
  label: 'hdrp-light-bounds',
220
239
  size: lightBoundsBytes,
221
- usage:
222
- (storageBuffer ? GPU_BUFFER_USAGE_STORAGE : GPU_BUFFER_USAGE_UNIFORM) |
223
- GPU_BUFFER_USAGE_COPY_DST,
240
+ usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_DST,
224
241
  mappedAtCreation: false,
225
242
  });
226
243
  if (!lightBounds.ok) {
227
- runtime.errorRegistry.fire(lightBounds.error);
228
- return null;
244
+ return failAllocation(lightBounds.error);
229
245
  }
246
+ created.push(lightBounds.value);
230
247
 
231
248
  // Create the unified 7-entry BGL layout for group(2)
232
249
  // (plan D-6, feat-20260609-hdrp-cluster-fragment-ggx M3).
233
250
  // The HDRP BGL descriptor is owned by pipeline-spec and shared with the
234
251
  // pipeline dispatcher and its byte-identical tests.
235
- const storageBufferCap = runtime.device.caps?.storageBuffer === true;
236
- const unifiedBglRes = device.createBindGroupLayout(
237
- createHdrpBindGroupLayoutDescriptor(storageBufferCap),
238
- );
252
+ const unifiedBglRes = device.createBindGroupLayout(createHdrpBindGroupLayoutDescriptor());
239
253
  if (!unifiedBglRes.ok) {
240
- runtime.errorRegistry.fire(unifiedBglRes.error);
241
- return null;
254
+ return failAllocation(unifiedBglRes.error);
242
255
  }
243
256
  const buffers: HdrpBuffers = {
244
- storageBuffer,
257
+ device,
245
258
  lightDataBuffer: lightData.value,
246
259
  lightDataBytes,
247
260
  clusterGridBuffer: clusterGrid.value,
@@ -256,16 +269,57 @@ export function getOrCreateHdrpBuffers(
256
269
  unifiedBindGroupLayout: unifiedBglRes.value,
257
270
  };
258
271
  cache.set(runtime, buffers);
272
+ if (cached !== undefined) {
273
+ retireHdrpBuffers(runtime, cached);
274
+ }
259
275
  return buffers;
260
276
  }
261
277
 
278
+ /**
279
+ * Drop the cached bundle before a device recovery or renderer disposal. The
280
+ * bundle carries the old device identity, so retirement fences that device's
281
+ * queue instead of consulting the replacement runtime device.
282
+ */
283
+ export function resetHdrpBuffers(runtime: RenderSystemRuntime): void {
284
+ const cached = cache.get(runtime);
285
+ if (cached === undefined) return;
286
+ cache.delete(runtime);
287
+ retireHdrpBuffers(runtime, cached);
288
+ }
289
+
290
+ function retireHdrpBuffers(runtime: RenderSystemRuntime, buffers: HdrpBuffers): void {
291
+ const device = buffers.device;
292
+ const destroy = (): void => {
293
+ for (const buffer of [
294
+ buffers.lightDataBuffer,
295
+ buffers.clusterGridBuffer,
296
+ buffers.lightIndexListBuffer,
297
+ buffers.clusterUniformBuffer,
298
+ buffers.lightBoundsBuffer,
299
+ ]) {
300
+ const result = device.destroyBuffer(buffer);
301
+ if (!result.ok) runtime.errorRegistry.fire(result.error);
302
+ }
303
+ };
304
+ const queue = device.queue as {
305
+ readonly onSubmittedWorkDone?: () => Promise<undefined>;
306
+ };
307
+ if (typeof queue.onSubmittedWorkDone === 'function') {
308
+ queue.onSubmittedWorkDone().then(destroy, destroy);
309
+ } else {
310
+ // Minimal null/test devices may not expose queue fences; they also have no
311
+ // submitted work that can retain the old bundle, so release synchronously.
312
+ destroy();
313
+ }
314
+ }
315
+
262
316
  /**
263
317
  * Build the 8-float cluster_uniform std140 payload.
264
318
  *
265
319
  * [0] gridX u32 (cast in shader)
266
320
  * [1] gridY u32
267
321
  * [2] gridZ u32
268
- * [3] lightCount u32 (vec4 alignment; capped by the active transport)
322
+ * [3] lightCount u32 (vec4 alignment; capped by MAX_LIGHTS)
269
323
  * [4] near f32
270
324
  * [5] far f32
271
325
  * [6] logFarOverNear f32
@@ -286,7 +340,6 @@ export function packClusterUniform(
286
340
  far: number,
287
341
  ssaoIntensity: number = 0,
288
342
  lightCount: number = 0,
289
- lightCapacity: number = HDRP_UNIFORM_LIGHT_CAPACITY,
290
343
  ): ArrayBuffer {
291
344
  const buf = new ArrayBuffer(32);
292
345
  const u32 = new Uint32Array(buf);
@@ -294,7 +347,7 @@ export function packClusterUniform(
294
347
  u32[0] = grid.x >>> 0;
295
348
  u32[1] = grid.y >>> 0;
296
349
  u32[2] = grid.z >>> 0;
297
- u32[3] = Math.min(Math.max(lightCount, 0), lightCapacity) >>> 0;
350
+ u32[3] = Math.min(Math.max(lightCount, 0), MAX_LIGHTS) >>> 0;
298
351
  f32[4] = near;
299
352
  f32[5] = far;
300
353
  // logFarOverNear: log(far/near) used by the shader for log-z slice mapping.
@@ -318,7 +371,7 @@ export function packClusterUniform(
318
371
  * feat-20260609-hdrp-cluster-fragment-ggx M4 / w19. Called per-frame from the
319
372
  * recordFrame HDRP block after binner+writeBuffer; the resulting BindGroup
320
373
  * lands on `passCtx.hdrpClusterBindGroup` and recordMainPass binds it at
321
- * group(2) when `frameState.isHdrpActive`.
374
+ * group(2) when the prepared Standard topology selects clustered lighting.
322
375
  *
323
376
  * Returns `null` on `device.createBindGroup` failure (a structured RhiError
324
377
  * is fired on `runtime.errorRegistry`); recordMainPass gracefully falls back
@@ -386,7 +439,7 @@ export function createHdrpUnifiedBindGroup(
386
439
  value: {
387
440
  buffer: meshStorageBuffer,
388
441
  offset: 0,
389
- size: hdrpBuffers.storageBuffer ? MESH_PER_ENTITY_STRIDE : MESH_UBO_FULL_ARRAY_BYTES,
442
+ size: MESH_PER_ENTITY_STRIDE,
390
443
  },
391
444
  },
392
445
  },
@@ -435,6 +488,59 @@ export function createHdrpUnifiedBindGroup(
435
488
  return result.value;
436
489
  }
437
490
 
491
+ /** Build the clustered skin group(2) bind group with one shared palette window. */
492
+ export function createHdrpSkinUnifiedBindGroup(
493
+ runtime: RenderSystemRuntime,
494
+ hdrpBuffers: HdrpBuffers,
495
+ layout: BindGroupLayout,
496
+ meshStorageBuffer: Buffer,
497
+ paletteBuffer: Buffer,
498
+ paletteBindingWindowBytes: number,
499
+ ssaoOptions: SsaoBindOptions = { enabled: false },
500
+ ): BindGroup | null {
501
+ const fallback = getOrCreateSsaoFallbackTexture(runtime);
502
+ if (fallback === null) return null;
503
+ const meshSize = MESH_PER_ENTITY_STRIDE;
504
+ const result = runtime.device.createBindGroup({
505
+ label: 'hdrp-skin-unified-bg-group2',
506
+ layout,
507
+ entries: [
508
+ ...createPbrSkinMeshBindGroupEntries(
509
+ meshStorageBuffer,
510
+ meshSize,
511
+ paletteBuffer,
512
+ paletteBindingWindowBytes,
513
+ ),
514
+ { binding: 3, resource: { kind: 'buffer', value: { buffer: hdrpBuffers.lightDataBuffer } } },
515
+ {
516
+ binding: 4,
517
+ resource: { kind: 'buffer', value: { buffer: hdrpBuffers.clusterGridBuffer } },
518
+ },
519
+ {
520
+ binding: 5,
521
+ resource: { kind: 'buffer', value: { buffer: hdrpBuffers.lightIndexListBuffer } },
522
+ },
523
+ {
524
+ binding: 6,
525
+ resource: { kind: 'buffer', value: { buffer: hdrpBuffers.clusterUniformBuffer } },
526
+ },
527
+ {
528
+ binding: 7,
529
+ resource: {
530
+ kind: 'textureView',
531
+ value: ssaoOptions.enabled ? ssaoOptions.ssaoBlurredView : fallback.view,
532
+ },
533
+ },
534
+ { binding: 8, resource: { kind: 'sampler', value: fallback.sampler } },
535
+ ],
536
+ });
537
+ if (!result.ok) {
538
+ runtime.errorRegistry.fire(result.error);
539
+ return null;
540
+ }
541
+ return result.value;
542
+ }
543
+
438
544
  /**
439
545
  * Build the compute-only bind group for ordered cluster membership output.
440
546
  * Its group is intentionally separate from the fragment group: the same
@@ -43,6 +43,14 @@ import {
43
43
  type TextureView,
44
44
  } from '@forgeax/engine-rhi';
45
45
  import type { DeviceScope } from '../device/device-scope';
46
+ import { createIblKernelCache, type IblKernelCache } from './kernel-cache';
47
+
48
+ export {
49
+ createIblKernelCache,
50
+ type IblKernelCache,
51
+ resetIblKernelCaches,
52
+ } from './kernel-cache';
53
+
46
54
  import { GPU_SHADER_STAGE_FRAGMENT, GPU_SHADER_STAGE_VERTEX } from '../gpu-stage';
47
55
  import {
48
56
  GPU_TEXTURE_USAGE_COPY_DST,
@@ -90,6 +98,8 @@ export interface IblPipelineError {
90
98
  * (idempotent).
91
99
  */
92
100
  export interface IblPipelineCache {
101
+ /** Reusable generation-scoped kernel state; per-probe outputs stay elsewhere. */
102
+ readonly probeKernel: IblKernelCache;
93
103
  /** Color format shared by all precompute outputs for this device. */
94
104
  outputFormat?: TextureFormat;
95
105
  /** Equirectangular-to-cubemap pipeline. */
@@ -151,6 +161,7 @@ export function getOrCreateIblCache(scope: DeviceScope): IblPipelineCache {
151
161
  if (existing !== undefined) return existing;
152
162
 
153
163
  const cache: IblPipelineCache = {
164
+ probeKernel: createIblKernelCache(scope.generation),
154
165
  irradianceBakeCount: 0,
155
166
  prefilterBakeCount: 0,
156
167
  brdfLutBakeCount: 0,