@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
@@ -0,0 +1,222 @@
1
+ import { err, ok } from '@forgeax/engine-types';
2
+ import type { RenderError } from './errors/render.js';
3
+ import { FrameReceiptStaleError } from './errors/render.js';
4
+ import type {
5
+ GpuPassTimingObservation,
6
+ GpuPassTimingReason,
7
+ GpuPassTimingRef,
8
+ } from './record/gpu-pass-timing/index.js';
9
+ import type {
10
+ FrameObservationRequest,
11
+ FrameReceipt,
12
+ FrameReceiptObservation,
13
+ RenderResult,
14
+ } from './render-contract.js';
15
+
16
+ /**
17
+ * Internal Render producer callback; public callers receive only frozen facts.
18
+ * The callback is receipt-bound and never exposes RHI handles or query state.
19
+ */
20
+ export type GpuPassTimingObservationSource = () => Promise<GpuPassTimingObservation>;
21
+
22
+ /**
23
+ * Receipt observation seam for the public `draw()` then `observe()` route.
24
+ * `status`, `completeness`, and `latestKnownGood` are separate projections;
25
+ * pass duration is not frame latency. Timing errors retain the closed code,
26
+ * expected, hint, and detail fields so a caller can follow producer recovery.
27
+ * See the bounded contract and fail-closed validator in the GPU pass timing
28
+ * record and benchmark modules.
29
+ */
30
+ export interface GpuPassTimingObservationStore {
31
+ register(receipt: FrameReceipt, source?: GpuPassTimingObservationSource): void;
32
+ observe(
33
+ receipt: FrameReceipt,
34
+ request: FrameObservationRequest,
35
+ ): Promise<RenderResult<FrameReceiptObservation, RenderError>>;
36
+ inspect(): {
37
+ readonly status: GpuPassTimingObservation['status'];
38
+ readonly completeness: 'complete' | 'partial' | 'unavailable' | 'failed';
39
+ readonly latestKnownGood?: GpuPassTimingRef | undefined;
40
+ };
41
+ }
42
+
43
+ /**
44
+ * Preserve the receipt observation contract without constructing the timing
45
+ * retention store when GPU timing was not opted in.
46
+ */
47
+ export async function observeGpuPassTimingDisabled(
48
+ receipt: FrameReceipt,
49
+ request: FrameObservationRequest,
50
+ currentDeviceGeneration: () => number,
51
+ ): Promise<RenderResult<FrameReceiptObservation, RenderError>> {
52
+ const completed = await receipt.completed;
53
+ if (!completed.ok) return completed;
54
+ if (receipt.deviceGeneration !== currentDeviceGeneration()) {
55
+ return err(
56
+ new FrameReceiptStaleError({
57
+ frameId: receipt.frameId,
58
+ receiptGeneration: receipt.deviceGeneration,
59
+ currentGeneration: currentDeviceGeneration(),
60
+ }),
61
+ );
62
+ }
63
+ const base = {
64
+ frameId: receipt.frameId,
65
+ deviceGeneration: receipt.deviceGeneration,
66
+ include: freeze([...request.include]),
67
+ } as const;
68
+ if (!request.include.includes('timings')) return ok(freeze(base));
69
+ return ok(freeze({ ...base, timings: notEnabled }));
70
+ }
71
+
72
+ interface ReceiptRecord {
73
+ readonly source: GpuPassTimingObservationSource;
74
+ }
75
+
76
+ interface ObservationStoreOptions {
77
+ readonly retentionFrames: number;
78
+ readonly currentDeviceGeneration: () => number;
79
+ }
80
+
81
+ const notEnabled: GpuPassTimingObservation = {
82
+ status: 'unavailable',
83
+ reason: {
84
+ code: 'gpu-timing-not-enabled',
85
+ expected: 'gpuPassTiming is enabled in RendererOptions',
86
+ hint: 'set RendererOptions.gpuPassTiming to opt into bounded timing facts',
87
+ detail: {},
88
+ },
89
+ capability: { timestampQuery: false, timestampPeriodNanoseconds: null },
90
+ };
91
+
92
+ function failedObservation(
93
+ error: GpuPassTimingReason,
94
+ latestKnownGood?: GpuPassTimingRef,
95
+ ): GpuPassTimingObservation {
96
+ return {
97
+ status: 'failed',
98
+ error,
99
+ ...(latestKnownGood === undefined ? {} : { latestKnownGood }),
100
+ };
101
+ }
102
+
103
+ function retentionExpired(): GpuPassTimingObservation {
104
+ return failedObservation({
105
+ code: 'timing-retention-expired',
106
+ expected: 'the requested receipt remains inside the bounded timing retention window',
107
+ hint: 'observe a newer receipt before the retention window expires',
108
+ detail: {},
109
+ });
110
+ }
111
+
112
+ function freeze<T>(value: T): T {
113
+ if (value === null || typeof value !== 'object' || Object.isFrozen(value)) return value;
114
+ Object.freeze(value);
115
+ for (const child of Object.values(value as Record<string, unknown>)) freeze(child);
116
+ return value;
117
+ }
118
+
119
+ /**
120
+ * Keep receipt-bound observations in a bounded Render-owned retention ring.
121
+ * This helper does not expose RHI handles, query objects, or a timing
122
+ * controller; the public lifecycle remains `draw` then `observe`.
123
+ * Omitting `timings` from the observation request does not materialize facts.
124
+ */
125
+ export function createGpuPassTimingObservationStore(
126
+ options: ObservationStoreOptions,
127
+ ): GpuPassTimingObservationStore {
128
+ const records = new Map<FrameReceipt, ReceiptRecord>();
129
+ const order: FrameReceipt[] = [];
130
+ const expired = new WeakSet<FrameReceipt>();
131
+ let latestKnownGood: GpuPassTimingRef | undefined;
132
+ let currentStatus: GpuPassTimingObservation['status'] = 'unavailable';
133
+ let currentCompleteness: 'complete' | 'partial' | 'unavailable' | 'failed' = 'unavailable';
134
+
135
+ const evict = (): void => {
136
+ while (order.length > options.retentionFrames) {
137
+ const old = order.shift();
138
+ if (old === undefined) break;
139
+ records.delete(old);
140
+ expired.add(old);
141
+ }
142
+ };
143
+
144
+ return {
145
+ register(receipt, source): void {
146
+ records.set(receipt, { source: source ?? (async () => notEnabled) });
147
+ order.push(receipt);
148
+ evict();
149
+ },
150
+ async observe(receipt, request): Promise<RenderResult<FrameReceiptObservation, RenderError>> {
151
+ const record = records.get(receipt);
152
+ if (record === undefined) {
153
+ if (expired.has(receipt) && request.include.includes('timings')) {
154
+ return ok(
155
+ freeze({
156
+ frameId: receipt.frameId,
157
+ deviceGeneration: receipt.deviceGeneration,
158
+ include: freeze([...request.include]),
159
+ timings: retentionExpired(),
160
+ }),
161
+ );
162
+ }
163
+ return err(
164
+ new FrameReceiptStaleError({
165
+ frameId: receipt.frameId,
166
+ receiptGeneration: receipt.deviceGeneration,
167
+ currentGeneration: options.currentDeviceGeneration(),
168
+ }),
169
+ );
170
+ }
171
+ const completed = await receipt.completed;
172
+ if (!completed.ok) return completed;
173
+ if (receipt.deviceGeneration !== options.currentDeviceGeneration()) {
174
+ return err(
175
+ new FrameReceiptStaleError({
176
+ frameId: receipt.frameId,
177
+ receiptGeneration: receipt.deviceGeneration,
178
+ currentGeneration: options.currentDeviceGeneration(),
179
+ }),
180
+ );
181
+ }
182
+ const base = {
183
+ frameId: receipt.frameId,
184
+ deviceGeneration: receipt.deviceGeneration,
185
+ include: freeze([...request.include]),
186
+ } as const;
187
+ if (!request.include.includes('timings')) return ok(freeze(base));
188
+ let timings: GpuPassTimingObservation;
189
+ try {
190
+ timings = await record.source();
191
+ } catch (cause) {
192
+ timings = failedObservation(
193
+ {
194
+ code: 'timestamp-readback-failed',
195
+ expected: 'the Render-owned timing observation completes without throwing',
196
+ hint: 'observe a later receipt and inspect latestKnownGood',
197
+ detail: { frameId: receipt.frameId },
198
+ cause: { message: cause instanceof Error ? cause.message : String(cause) },
199
+ },
200
+ latestKnownGood,
201
+ );
202
+ }
203
+ currentStatus = timings.status;
204
+ currentCompleteness = timings.status;
205
+ if (timings.status === 'complete') {
206
+ latestKnownGood = {
207
+ frameId: timings.frame.frameId,
208
+ deviceGeneration: timings.frame.deviceGeneration,
209
+ graphGeneration: timings.frame.graphGeneration,
210
+ };
211
+ }
212
+ return ok(freeze({ ...base, timings }));
213
+ },
214
+ inspect() {
215
+ return freeze({
216
+ status: currentStatus,
217
+ completeness: currentCompleteness,
218
+ latestKnownGood,
219
+ });
220
+ },
221
+ };
222
+ }
@@ -16,6 +16,7 @@ import {
16
16
  PointLight,
17
17
  PointLightShadow,
18
18
  PostProcessParams,
19
+ ReflectionProbe,
19
20
  SkyboxBackground,
20
21
  Skylight,
21
22
  SpotLight,
@@ -30,8 +31,14 @@ import {
30
31
  } from '../gpu-driven/batch-topology';
31
32
  import { GpuScene } from '../gpu-scene';
32
33
  import type { PersistentRenderSceneInspection } from '../inspection-types';
34
+ import { fingerprintNumericArray, InstanceBoundsCache } from '../instances-derived-bounds';
33
35
  import type { PointsLinesInspection } from '../points-lines/inspection';
34
36
  import type { PointsLinesRetainedSnapshot } from '../points-lines/snapshot';
37
+ import {
38
+ type ReflectionProbeFact,
39
+ ReflectionProbeProjection,
40
+ type ReflectionProbeSelectionResult,
41
+ } from '../reflection/projection';
35
42
  import type { CameraSnapshot } from '../render-contract';
36
43
  import type {
37
44
  DispatchEntry,
@@ -220,6 +227,22 @@ function worldBounds(snapshot: RenderableSnapshot): RenderSceneBounds | undefine
220
227
  return { min, max };
221
228
  }
222
229
 
230
+ function boundsFromArray(bounds: ArrayLike<number>): RenderSceneBounds | undefined {
231
+ if (bounds.length < 6) return undefined;
232
+ const min: [number, number, number] = [Number(bounds[0]), Number(bounds[1]), Number(bounds[2])];
233
+ const max: [number, number, number] = [Number(bounds[3]), Number(bounds[4]), Number(bounds[5])];
234
+ if (
235
+ !min.every(Number.isFinite) ||
236
+ !max.every(Number.isFinite) ||
237
+ min[0] > max[0] ||
238
+ min[1] > max[1] ||
239
+ min[2] > max[2]
240
+ ) {
241
+ return undefined;
242
+ }
243
+ return { min, max };
244
+ }
245
+
223
246
  /**
224
247
  * The renderer's single rebuildable CPU scene authority.
225
248
  *
@@ -241,6 +264,7 @@ export class RenderScene {
241
264
  // derived value by snapshot identity so repeated view queries do not redo
242
265
  // the eight-corner transform for every view in the same frame.
243
266
  private readonly worldBoundsCache = new WeakMap<RenderableSnapshot, RenderSceneBounds | null>();
267
+ private readonly instanceBoundsCache = new InstanceBoundsCache();
244
268
  private orderedSlots: number[] = [];
245
269
  private materialized: readonly RenderableSnapshot[] | undefined;
246
270
  private slotSnapshot: readonly RenderSceneSlot[] | undefined;
@@ -552,11 +576,7 @@ export class RenderScene {
552
576
  const record = this.slots[slot];
553
577
  if (record === undefined) continue;
554
578
  const snapshot = record.snapshot;
555
- let candidate = this.worldBoundsCache.get(snapshot);
556
- if (candidate === undefined && !this.worldBoundsCache.has(snapshot)) {
557
- candidate = worldBounds(snapshot) ?? null;
558
- this.worldBoundsCache.set(snapshot, candidate);
559
- }
579
+ const candidate = this.cullingWorldBounds(snapshot);
560
580
  if (candidate === null || candidate === undefined) continue;
561
581
  if (intersects(candidate, bounds)) records.push(record);
562
582
  }
@@ -629,6 +649,7 @@ export class RenderScene {
629
649
 
630
650
  private release(record: RenderSceneSlot): void {
631
651
  this.slots[record.slot] = undefined;
652
+ this.instanceBoundsCache.invalidate(record.entityKey, record.worldId);
632
653
  this.unindexMaterials(record);
633
654
  const entities = this.slotsByWorld.get(record.worldId);
634
655
  entities?.delete(record.entityKey);
@@ -706,6 +727,41 @@ export class RenderScene {
706
727
  this.materialized = undefined;
707
728
  this.slotSnapshot = undefined;
708
729
  }
730
+
731
+ /**
732
+ * Return the CPU culling projection for one retained snapshot. Instances
733
+ * use a renderer-derived union, while the GPU scene keeps the mesh-local
734
+ * bounds for its independent per-instance visibility pass. Unknown or
735
+ * empty instance facts deliberately return undefined (conservative no-cull).
736
+ */
737
+ cullingWorldBounds(snapshot: RenderableSnapshot): RenderSceneBounds | undefined {
738
+ let candidate = this.worldBoundsCache.get(snapshot);
739
+ if (candidate !== undefined || this.worldBoundsCache.has(snapshot)) {
740
+ return candidate ?? undefined;
741
+ }
742
+ if (snapshot.instances !== undefined) {
743
+ const instanceCount = snapshot.instances.instanceCount;
744
+ if (instanceCount === 0 || snapshot.localAabb === undefined) {
745
+ this.worldBoundsCache.set(snapshot, null);
746
+ return undefined;
747
+ }
748
+ const derived = this.instanceBoundsCache.get({
749
+ worldId: snapshot.worldId,
750
+ entityKey: snapshot.entityKey,
751
+ meshGeneration: fingerprintNumericArray(snapshot.localAabb) ^ (snapshot.assetHandle >>> 0),
752
+ transformGeneration: fingerprintNumericArray(snapshot.transform.world),
753
+ matrixGeneration: snapshot.instances.archVersion,
754
+ meshAabb: snapshot.localAabb,
755
+ entityWorld: snapshot.transform.world,
756
+ transforms: snapshot.instances.transforms,
757
+ });
758
+ candidate = boundsFromArray(derived ?? []);
759
+ } else {
760
+ candidate = worldBounds(snapshot);
761
+ }
762
+ this.worldBoundsCache.set(snapshot, candidate ?? null);
763
+ return candidate;
764
+ }
709
765
  }
710
766
 
711
767
  function readTransformWorld(world: World, entity: EntityHandle): Float32Array | undefined {
@@ -762,6 +818,7 @@ const RENDER_RELEVANT_COMPONENT_IDS = new Set([
762
818
  componentId(Skylight),
763
819
  componentId(SkyboxBackground),
764
820
  componentId(PostProcessParams),
821
+ componentId(ReflectionProbe),
765
822
  componentId(VolumetricFog),
766
823
  ]);
767
824
 
@@ -810,6 +867,7 @@ function cameraFrusta(cameras: readonly CameraSnapshot[]): readonly Float32Array
810
867
  function cullPersistentFrame(
811
868
  frame: ExtractedFrame,
812
869
  candidates: readonly RenderableSnapshot[],
870
+ boundsOf: (snapshot: RenderableSnapshot) => RenderSceneBounds | undefined = worldBounds,
813
871
  ): ExtractedFrame {
814
872
  const planes = cameraFrusta(frame.cameras);
815
873
  const visible: RenderableSnapshot[] = [];
@@ -822,10 +880,16 @@ function cullPersistentFrame(
822
880
  const candidate = candidates[index];
823
881
  if (candidate === undefined) continue;
824
882
  let isVisible = true;
825
- if (candidate.localAabb !== undefined) {
883
+ const candidateBounds = boundsOf(candidate);
884
+ if (candidateBounds !== undefined) {
826
885
  total += 1;
827
886
  const worldAabb = box3.create();
828
- box3.transformBox3(worldAabb, candidate.localAabb, candidate.transform.world);
887
+ worldAabb[0] = candidateBounds.min[0] ?? 0;
888
+ worldAabb[1] = candidateBounds.min[1] ?? 0;
889
+ worldAabb[2] = candidateBounds.min[2] ?? 0;
890
+ worldAabb[3] = candidateBounds.max[0] ?? 0;
891
+ worldAabb[4] = candidateBounds.max[1] ?? 0;
892
+ worldAabb[5] = candidateBounds.max[2] ?? 0;
829
893
  isVisible = planes.length === 0;
830
894
  for (const cameraPlanes of planes) {
831
895
  if (
@@ -879,6 +943,7 @@ export class PersistentRenderScene {
879
943
  | { readonly projection: RenderScene; readonly capture: RenderSceneSubmissionCapture }
880
944
  | undefined;
881
945
  private pointsLinesInspections: readonly PointsLinesInspection[] = [];
946
+ private readonly reflectionProbes = new ReflectionProbeProjection();
882
947
 
883
948
  constructor(private readonly options: PersistentRenderSceneOptions = {}) {}
884
949
 
@@ -887,6 +952,37 @@ export class PersistentRenderScene {
887
952
  return this.materialSnapshotCaches;
888
953
  }
889
954
 
955
+ /** Project probe facts and per-primitive selection into the persistent scene owner. */
956
+ projectReflectionProbes(probes: readonly ReflectionProbeFact[]): void {
957
+ const projection = this.composition?.projection;
958
+ if (projection === undefined) return;
959
+ const primitives = projection
960
+ .slotsSnapshot()
961
+ .map((slot) => {
962
+ const resolved = worldBounds(slot.snapshot);
963
+ if (resolved === undefined || resolved === null) return undefined;
964
+ return {
965
+ worldId: slot.worldId,
966
+ entityKey: slot.entityKey,
967
+ center: [
968
+ (resolved.min[0] + resolved.max[0]) / 2,
969
+ (resolved.min[1] + resolved.max[1]) / 2,
970
+ (resolved.min[2] + resolved.max[2]) / 2,
971
+ ] as [number, number, number],
972
+ };
973
+ })
974
+ .filter((primitive): primitive is NonNullable<typeof primitive> => primitive !== undefined);
975
+ this.reflectionProbes.update(probes, primitives);
976
+ }
977
+
978
+ reflectionProbeSelection(worldId: number, entityKey: number): ReflectionProbeSelectionResult {
979
+ return this.reflectionProbes.selection(worldId, entityKey);
980
+ }
981
+
982
+ reflectionProbeProjection(): ReturnType<ReflectionProbeProjection['snapshot']> {
983
+ return this.reflectionProbes.snapshot();
984
+ }
985
+
890
986
  extractComposition(
891
987
  worlds: readonly World[],
892
988
  owner: { readonly cameraOwner: number; readonly resourceOwner: number },
@@ -1195,6 +1291,7 @@ export class PersistentRenderScene {
1195
1291
  transmissionDemand.apply(transmissionDemandOperations(candidateFrame.renderables));
1196
1292
  const topology = new BatchTopology();
1197
1293
  topology.rebuild(projection.slotsSnapshot());
1294
+ this.projectReflectionProbes(candidateFrame.reflectionProbes ?? []);
1198
1295
  const token = {};
1199
1296
  const entry: PersistentCompositionEntry = {
1200
1297
  token,
@@ -1224,7 +1321,11 @@ export class PersistentRenderScene {
1224
1321
  const candidates = projection
1225
1322
  .materialize()
1226
1323
  .map((snapshot) => projection.withTemporal(snapshot));
1227
- return cullPersistentFrame({ ...candidateFrame, renderables: [...candidates] }, candidates);
1324
+ return cullPersistentFrame(
1325
+ { ...candidateFrame, renderables: [...candidates] },
1326
+ candidates,
1327
+ (snapshot) => projection.cullingWorldBounds(snapshot),
1328
+ );
1228
1329
  }
1229
1330
 
1230
1331
  /** Internal topology fact; the renderer does not expose demand as public API. */
@@ -1246,10 +1347,15 @@ export class PersistentRenderScene {
1246
1347
  buildCandidateFrame: () => ExtractedFrame,
1247
1348
  ): ExtractedFrame {
1248
1349
  const candidateFrame = buildCandidateFrame();
1350
+ this.projectReflectionProbes(candidateFrame.reflectionProbes ?? []);
1249
1351
  const candidates = entry.projection
1250
1352
  .materialize()
1251
1353
  .map((snapshot) => entry.projection.withTemporal(snapshot));
1252
- return cullPersistentFrame({ ...candidateFrame, renderables: [...candidates] }, candidates);
1354
+ return cullPersistentFrame(
1355
+ { ...candidateFrame, renderables: [...candidates] },
1356
+ candidates,
1357
+ (snapshot) => entry.projection.cullingWorldBounds(snapshot),
1358
+ );
1253
1359
  }
1254
1360
 
1255
1361
  private syncGpuOwner(
@@ -3,8 +3,8 @@
3
3
  // feat-20260612-point-light-shadows-urp-hdrp M3 / T-M3-1 (plan-strategy §D-1).
4
4
  //
5
5
  // Lazy-allocated `texture_depth_cube_array` (depth32float) + per-face 2D
6
- // TextureView cache. One layer per shadow-casting PointLight (cap = 4 enforced
7
- // by the PointLightShadow ECS component cardinality). Total texel footprint at
6
+ // TextureView cache. One layer per admitted shadow-casting PointLight; the
7
+ // renderer owns the bounded atlas admission policy. Total texel footprint at
8
8
  // the 512x512 default = 4 layers x 6 faces x 512 x 512 x 4 B = 24 MiB.
9
9
  //
10
10
  // Why a separate module (vs inlining into createRenderer / pipelineState):
@@ -53,19 +53,19 @@ import { GPU_TEXTURE_USAGE_RENDER_ATTACHMENT_AND_TEXTURE_BINDING } from './gpu-t
53
53
  export interface ShadowAtlasOptions {
54
54
  /** Per-face square size in pixels. Default 512 (PointLightShadow.mapSize default). */
55
55
  readonly faceSize?: number;
56
- /** Number of cube layers. Default 4 (= PointLightShadow cardinality cap). */
56
+ /** Number of cube layers. Default is the renderer atlas admission capacity. */
57
57
  readonly layers?: number;
58
58
  }
59
59
 
60
60
  /** Default per-face square size in pixels. Mirrors PointLightShadow.mapSize default. */
61
61
  export const SHADOW_ATLAS_DEFAULT_FACE_SIZE = 512;
62
- /** Default cube layer count. Mirrors PointLightShadow cardinality cap. */
62
+ /** Default cube layer count. Renderer-owned point-shadow admission capacity. */
63
63
  export const SHADOW_ATLAS_DEFAULT_LAYERS = 4;
64
64
 
65
65
  /**
66
66
  * Cube-array depth atlas for point-light shadows.
67
67
  *
68
- * Shape: `texture_depth_cube_array` (depth32float) with `layers = 4` and per-
68
+ * Shape: `texture_depth_cube_array` (depth32float) with `layers` and per-
69
69
  * face size `faceSize x faceSize`. The cube_array sampling view feeds
70
70
  * `@group(0) @binding(5)` in URP shaders; the per-face 2D view feeds the
71
71
  * shadow-caster render pass attachment for one (layer, face) at a time
@@ -106,6 +106,7 @@ export const SSAO_UNIFORM_BYTES = UNIFORM_BYTES;
106
106
  // recompile across enable/disable.
107
107
 
108
108
  interface SsaoFallbackResources {
109
+ readonly device: RenderSystemRuntime['device'];
109
110
  readonly texture: Texture;
110
111
  readonly view: TextureView;
111
112
  readonly sampler: Sampler;
@@ -126,7 +127,8 @@ export function getOrCreateSsaoFallbackTexture(
126
127
  runtime: RenderSystemRuntime,
127
128
  ): SsaoFallbackResources | null {
128
129
  const cached = fallbackCache.get(runtime);
129
- if (cached !== undefined) return cached;
130
+ if (cached !== undefined && cached.device === runtime.device) return cached;
131
+ if (cached !== undefined) fallbackCache.delete(runtime);
130
132
 
131
133
  const device = runtime.device;
132
134
  const texRes = device.createTexture({
@@ -191,6 +193,7 @@ export function getOrCreateSsaoFallbackTexture(
191
193
  }
192
194
 
193
195
  const resources: SsaoFallbackResources = {
196
+ device,
194
197
  texture: texRes.value,
195
198
  view: viewRes.value,
196
199
  sampler: samplerRes.value,
@@ -205,6 +208,8 @@ export function getOrCreateSsaoFallbackTexture(
205
208
  * Fields are readonly per charter P5: one resource, one owner.
206
209
  */
207
210
  export interface SsaoBuffers {
211
+ /** Device identity that owns every handle in this bundle. */
212
+ readonly device: RenderSystemRuntime['device'];
208
213
  /** 64 padded vec4 uniform buffer for hemisphere samples. */
209
214
  readonly kernelBuffer: Buffer;
210
215
  readonly kernelBytes: number;
@@ -229,7 +234,8 @@ const cache = new WeakMap<RenderSystemRuntime, SsaoBuffers>();
229
234
  */
230
235
  export function getOrCreateSsaoBuffers(runtime: RenderSystemRuntime): SsaoBuffers | null {
231
236
  const cached = cache.get(runtime);
232
- if (cached !== undefined) return cached;
237
+ if (cached !== undefined && cached.device === runtime.device) return cached;
238
+ if (cached !== undefined) cache.delete(runtime);
233
239
 
234
240
  const device = runtime.device;
235
241
 
@@ -322,6 +328,7 @@ export function getOrCreateSsaoBuffers(runtime: RenderSystemRuntime): SsaoBuffer
322
328
  }
323
329
 
324
330
  const buffers: SsaoBuffers = {
331
+ device,
325
332
  kernelBuffer: kernelBufferRes.value,
326
333
  kernelBytes: SSAO_KERNEL_SAMPLE_COUNT * BYTES_PER_KERNEL_ELEMENT,
327
334
  noiseTexture: noiseTexRes.value,