@forgeax/engine-render 0.1.33 → 0.1.34

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 (518) hide show
  1. package/README.md +1010 -124
  2. package/dist/assembly/backend-contract.d.ts +2 -0
  3. package/dist/assembly/backend-contract.d.ts.map +1 -1
  4. package/dist/assembly/barrel-distortion-inspection.d.ts +3 -0
  5. package/dist/assembly/barrel-distortion-inspection.d.ts.map +1 -0
  6. package/dist/assembly/canvas-draw-guard.d.ts +3 -0
  7. package/dist/assembly/canvas-draw-guard.d.ts.map +1 -0
  8. package/dist/assembly/cloud-shadow-entry.d.ts +4 -0
  9. package/dist/assembly/cloud-shadow-entry.d.ts.map +1 -0
  10. package/dist/assembly/dynamic-geometry-host.d.ts +4 -1
  11. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  12. package/dist/assembly/dynamic-geometry-runtime.d.ts +1 -1
  13. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -1
  14. package/dist/assembly/factory.d.ts +1 -1
  15. package/dist/assembly/factory.d.ts.map +1 -1
  16. package/dist/assembly/host-contract.d.ts +3 -3
  17. package/dist/assembly/host-contract.d.ts.map +1 -1
  18. package/dist/assembly/material/assembly.d.ts.map +1 -1
  19. package/dist/assembly/material/pipeline-helpers.d.ts +5 -0
  20. package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -1
  21. package/dist/assembly/material-shader-policy.d.ts +16 -2
  22. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  23. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -1
  24. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
  25. package/dist/assembly/recovery/renderer-recover.d.ts +2 -0
  26. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  27. package/dist/assembly/render-feature-post-processes.d.ts +6 -0
  28. package/dist/assembly/render-feature-post-processes.d.ts.map +1 -0
  29. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  30. package/dist/assembly/renderer-feature-inspection.d.ts +1 -1
  31. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -1
  32. package/dist/assembly/renderer-frame-transaction.d.ts +12 -0
  33. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  34. package/dist/assembly/renderer-inspection.d.ts +11 -0
  35. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  36. package/dist/assembly/webgpu-pbr-ready.d.ts +1 -0
  37. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -1
  38. package/dist/assembly/webgpu-ready-per-pass.d.ts +39 -0
  39. package/dist/assembly/webgpu-ready-per-pass.d.ts.map +1 -0
  40. package/dist/assembly/webgpu-ready.d.ts +1 -1
  41. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  42. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -1
  43. package/dist/assembly/webgpu-renderer-observation-owner.d.ts +35 -0
  44. package/dist/assembly/webgpu-renderer-observation-owner.d.ts.map +1 -0
  45. package/dist/assembly/webgpu-renderer.d.ts +1 -1
  46. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  47. package/dist/authoring.mjs +1 -1
  48. package/dist/barrel-distortion.d.ts +47 -0
  49. package/dist/barrel-distortion.d.ts.map +1 -0
  50. package/dist/{chunk-PA5DRJNI.mjs → chunk-ELJFJ2BU.mjs} +2 -2
  51. package/dist/chunk-ELJFJ2BU.mjs.map +1 -0
  52. package/dist/{chunk-ZDQWGQW7.mjs → chunk-HEB564WF.mjs} +4 -4
  53. package/dist/{chunk-ZDQWGQW7.mjs.map → chunk-HEB564WF.mjs.map} +1 -1
  54. package/dist/{chunk-VOZWDM3L.mjs → chunk-MKDIT2EM.mjs} +1307 -188
  55. package/dist/chunk-MKDIT2EM.mjs.map +1 -0
  56. package/dist/{chunk-LEAXOMYJ.mjs → chunk-OCDLEI6I.mjs} +3 -3
  57. package/dist/{chunk-LEAXOMYJ.mjs.map → chunk-OCDLEI6I.mjs.map} +1 -1
  58. package/dist/chunk-ODZWR6JQ.mjs +4927 -0
  59. package/dist/chunk-ODZWR6JQ.mjs.map +1 -0
  60. package/dist/{chunk-3RTCJTH4.mjs → chunk-SHFUFJ5N.mjs} +184 -13
  61. package/dist/chunk-SHFUFJ5N.mjs.map +1 -0
  62. package/dist/{chunk-OO3DRYK7.mjs → chunk-UI5SFODX.mjs} +176 -96
  63. package/dist/chunk-UI5SFODX.mjs.map +1 -0
  64. package/dist/{chunk-PLAXOOX5.mjs → chunk-UINHLCZA.mjs} +561 -79
  65. package/dist/chunk-UINHLCZA.mjs.map +1 -0
  66. package/dist/{chunk-NB2S7ZRX.mjs → chunk-WEX45HKH.mjs} +127 -386
  67. package/dist/chunk-WEX45HKH.mjs.map +1 -0
  68. package/dist/cloud/density.d.ts +55 -0
  69. package/dist/cloud/density.d.ts.map +1 -0
  70. package/dist/cloud/extract.d.ts +25 -0
  71. package/dist/cloud/extract.d.ts.map +1 -0
  72. package/dist/cloud/feature.d.ts +57 -0
  73. package/dist/cloud/feature.d.ts.map +1 -0
  74. package/dist/cloud/frame-context.d.ts +8 -0
  75. package/dist/cloud/frame-context.d.ts.map +1 -0
  76. package/dist/cloud/inspection.d.ts +59 -0
  77. package/dist/cloud/inspection.d.ts.map +1 -0
  78. package/dist/cloud/optics.d.ts +56 -0
  79. package/dist/cloud/optics.d.ts.map +1 -0
  80. package/dist/cloud/parameters.d.ts +73 -0
  81. package/dist/cloud/parameters.d.ts.map +1 -0
  82. package/dist/cloud/resources.d.ts +60 -0
  83. package/dist/cloud/resources.d.ts.map +1 -0
  84. package/dist/cloud/shadow.d.ts +42 -0
  85. package/dist/cloud/shadow.d.ts.map +1 -0
  86. package/dist/cloud/temporal.d.ts +74 -0
  87. package/dist/cloud/temporal.d.ts.map +1 -0
  88. package/dist/components/barrel-distortion.d.ts +13 -0
  89. package/dist/components/barrel-distortion.d.ts.map +1 -0
  90. package/dist/components/cloud-layer.d.ts +29 -0
  91. package/dist/components/cloud-layer.d.ts.map +1 -0
  92. package/dist/components/index.d.ts +2 -0
  93. package/dist/components/index.d.ts.map +1 -1
  94. package/dist/components/instances.d.ts +20 -6
  95. package/dist/components/instances.d.ts.map +1 -1
  96. package/dist/construct-renderer.d.ts +1 -1
  97. package/dist/construct-renderer.d.ts.map +1 -1
  98. package/dist/construct-renderer.mjs +6759 -2875
  99. package/dist/construct-renderer.mjs.map +1 -1
  100. package/dist/dynamic-geometry.d.ts +5 -1
  101. package/dist/dynamic-geometry.d.ts.map +1 -1
  102. package/dist/errors/cloud.d.ts +59 -0
  103. package/dist/errors/cloud.d.ts.map +1 -0
  104. package/dist/errors/render.d.ts +18 -4
  105. package/dist/errors/render.d.ts.map +1 -1
  106. package/dist/examples/single-layer-medium-surface.d.ts +47 -0
  107. package/dist/examples/single-layer-medium-surface.d.ts.map +1 -0
  108. package/dist/extract/camera.d.ts.map +1 -1
  109. package/dist/extract/contracts.d.ts +5 -1
  110. package/dist/extract/contracts.d.ts.map +1 -1
  111. package/dist/features/barrel-distortion.d.ts +29 -0
  112. package/dist/features/barrel-distortion.d.ts.map +1 -0
  113. package/dist/features/error-reporting.d.ts +6 -2
  114. package/dist/features/error-reporting.d.ts.map +1 -1
  115. package/dist/features/host.d.ts +10 -0
  116. package/dist/features/host.d.ts.map +1 -1
  117. package/dist/features/plan.d.ts +8 -0
  118. package/dist/features/plan.d.ts.map +1 -1
  119. package/dist/features/render-graph-contribution.d.ts +5 -0
  120. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  121. package/dist/features/render-graph-raster.d.ts +1 -0
  122. package/dist/features/render-graph-raster.d.ts.map +1 -1
  123. package/dist/features/targets.d.ts +2 -0
  124. package/dist/features/targets.d.ts.map +1 -1
  125. package/dist/features/types.d.ts +44 -0
  126. package/dist/features/types.d.ts.map +1 -1
  127. package/dist/fullscreen-post-process-pass.d.ts +15 -7
  128. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  129. package/dist/gpu-dirty-ranges.d.ts +11 -0
  130. package/dist/gpu-dirty-ranges.d.ts.map +1 -0
  131. package/dist/gpu-driven/batch-topology.d.ts +17 -0
  132. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  133. package/dist/gpu-driven/material-artifacts.d.ts +6 -0
  134. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
  135. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  136. package/dist/gpu-driven/production-raster-material.d.ts +178 -0
  137. package/dist/gpu-driven/production-raster-material.d.ts.map +1 -0
  138. package/dist/gpu-driven/production-raster.d.ts +47 -3
  139. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  140. package/dist/gpu-driven/resource-allocation.d.ts +31 -0
  141. package/dist/gpu-driven/resource-allocation.d.ts.map +1 -0
  142. package/dist/gpu-driven/shadow-ownership.d.ts +14 -0
  143. package/dist/gpu-driven/shadow-ownership.d.ts.map +1 -0
  144. package/dist/gpu-driven/view-gpu.d.ts +26 -2
  145. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  146. package/dist/gpu-scene.d.ts +5 -0
  147. package/dist/gpu-scene.d.ts.map +1 -1
  148. package/dist/hdrp-buffers.d.ts +2 -2
  149. package/dist/hdrp-buffers.d.ts.map +1 -1
  150. package/dist/ibl/skylight-bind-group.d.ts +10 -1
  151. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  152. package/dist/index.d.ts +27 -7
  153. package/dist/index.d.ts.map +1 -1
  154. package/dist/index.mjs +258 -8
  155. package/dist/index.mjs.map +1 -1
  156. package/dist/inspection-types.d.ts +135 -1
  157. package/dist/inspection-types.d.ts.map +1 -1
  158. package/dist/instance-buffer-cache.d.ts +4 -4
  159. package/dist/instance-buffer-cache.d.ts.map +1 -1
  160. package/dist/instances.d.ts +48 -100
  161. package/dist/instances.d.ts.map +1 -1
  162. package/dist/internal.mjs +7 -7
  163. package/dist/pbr-pipeline.d.ts +19 -1
  164. package/dist/pbr-pipeline.d.ts.map +1 -1
  165. package/dist/pipeline/single-layer-medium-passes.d.ts +73 -0
  166. package/dist/pipeline/single-layer-medium-passes.d.ts.map +1 -0
  167. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  168. package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
  169. package/dist/pipeline/standard-output/types.d.ts +4 -2
  170. package/dist/pipeline/standard-output/types.d.ts.map +1 -1
  171. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  172. package/dist/pipeline/standard-post.d.ts.map +1 -1
  173. package/dist/pipeline-builder.d.ts +1 -1
  174. package/dist/pipeline-builder.d.ts.map +1 -1
  175. package/dist/plugin.d.ts.map +1 -1
  176. package/dist/prepare/prepared-graphics-resolver.d.ts +2 -0
  177. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  178. package/dist/record/barrel-distortion-frame.d.ts +15 -0
  179. package/dist/record/barrel-distortion-frame.d.ts.map +1 -0
  180. package/dist/record/dynamic-geometry-consumption.d.ts +10 -2
  181. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -1
  182. package/dist/record/fold-buckets.d.ts +95 -0
  183. package/dist/record/fold-buckets.d.ts.map +1 -0
  184. package/dist/record/fold-instance-buffer.d.ts +6 -0
  185. package/dist/record/fold-instance-buffer.d.ts.map +1 -0
  186. package/dist/record/frame-lighting.d.ts +1 -0
  187. package/dist/record/frame-lighting.d.ts.map +1 -1
  188. package/dist/record/frame-snapshot.d.ts +37 -3
  189. package/dist/record/frame-snapshot.d.ts.map +1 -1
  190. package/dist/record/frame.d.ts +10 -61
  191. package/dist/record/frame.d.ts.map +1 -1
  192. package/dist/record/gpu-draw-receipts.d.ts +11 -0
  193. package/dist/record/gpu-draw-receipts.d.ts.map +1 -0
  194. package/dist/record/gpu-timing.d.ts +8 -0
  195. package/dist/record/gpu-timing.d.ts.map +1 -1
  196. package/dist/record/main-pass-geometry.d.ts +21 -5
  197. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  198. package/dist/record/main-pass-material.d.ts +7 -1
  199. package/dist/record/main-pass-material.d.ts.map +1 -1
  200. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  201. package/dist/record/main-pass.d.ts +13 -2
  202. package/dist/record/main-pass.d.ts.map +1 -1
  203. package/dist/record/motion-blur-receipt.d.ts +11 -0
  204. package/dist/record/motion-blur-receipt.d.ts.map +1 -0
  205. package/dist/record/probe-blend-buffer.d.ts +16 -0
  206. package/dist/record/probe-blend-buffer.d.ts.map +1 -0
  207. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  208. package/dist/record/render-context.d.ts +22 -3
  209. package/dist/record/render-context.d.ts.map +1 -1
  210. package/dist/record/shadow-pass.d.ts +2 -0
  211. package/dist/record/shadow-pass.d.ts.map +1 -1
  212. package/dist/record/skybox-post-pass.d.ts +1 -1
  213. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  214. package/dist/record/typed-frame-graph.d.ts +22 -2
  215. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  216. package/dist/record/view-ubo.d.ts +11 -3
  217. package/dist/record/view-ubo.d.ts.map +1 -1
  218. package/dist/record/volume-params.d.ts +9 -0
  219. package/dist/record/volume-params.d.ts.map +1 -0
  220. package/dist/recovery/render-system-candidate.d.ts +10 -0
  221. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  222. package/dist/reflection/record-owner.d.ts.map +1 -1
  223. package/dist/render-contract.d.ts +42 -4
  224. package/dist/render-contract.d.ts.map +1 -1
  225. package/dist/render-graph-primitives.d.ts.map +1 -1
  226. package/dist/render-pipeline.d.ts +103 -2
  227. package/dist/render-pipeline.d.ts.map +1 -1
  228. package/dist/render-system-extract-tail.d.ts +3 -2
  229. package/dist/render-system-extract-tail.d.ts.map +1 -1
  230. package/dist/render-system-extract.d.ts +16 -9
  231. package/dist/render-system-extract.d.ts.map +1 -1
  232. package/dist/render-system-presentation.d.ts +23 -0
  233. package/dist/render-system-presentation.d.ts.map +1 -0
  234. package/dist/render-system-producer-roots.d.ts +10 -0
  235. package/dist/render-system-producer-roots.d.ts.map +1 -0
  236. package/dist/render-system-residency.d.ts +12 -0
  237. package/dist/render-system-residency.d.ts.map +1 -0
  238. package/dist/render-system.d.ts +19 -14
  239. package/dist/render-system.d.ts.map +1 -1
  240. package/dist/scene/probe-blend.d.ts +27 -0
  241. package/dist/scene/probe-blend.d.ts.map +1 -1
  242. package/dist/scene/render-scene.d.ts +12 -5
  243. package/dist/scene/render-scene.d.ts.map +1 -1
  244. package/dist/ssao-buffers.d.ts +9 -0
  245. package/dist/ssao-buffers.d.ts.map +1 -1
  246. package/dist/surface/dynamic-input.d.ts +133 -0
  247. package/dist/surface/dynamic-input.d.ts.map +1 -0
  248. package/dist/surface/gpu-driven.d.ts +41 -0
  249. package/dist/surface/gpu-driven.d.ts.map +1 -0
  250. package/dist/surface/submission-observation.d.ts +136 -0
  251. package/dist/surface/submission-observation.d.ts.map +1 -0
  252. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  253. package/dist/temporal/gpu.d.ts +13 -1
  254. package/dist/temporal/gpu.d.ts.map +1 -1
  255. package/dist/temporal/index.mjs +5 -5
  256. package/dist/temporal/inspection.d.ts.map +1 -1
  257. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  258. package/dist/temporal/target-coverage-attachment.d.ts.map +1 -1
  259. package/dist/transmission/single-layer-medium.d.ts +89 -0
  260. package/dist/transmission/single-layer-medium.d.ts.map +1 -0
  261. package/dist/typed-render-graph-primitives.d.ts +14 -2
  262. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  263. package/dist/volume/component.d.ts +9 -4
  264. package/dist/volume/component.d.ts.map +1 -1
  265. package/dist/volume/extract.d.ts +1 -1
  266. package/dist/volume/extract.d.ts.map +1 -1
  267. package/dist/volume/inspection.d.ts +2 -0
  268. package/dist/volume/inspection.d.ts.map +1 -1
  269. package/dist/volume/passes.d.ts +4 -1
  270. package/dist/volume/passes.d.ts.map +1 -1
  271. package/dist/volume/resources.d.ts.map +1 -1
  272. package/package.json +22 -21
  273. package/src/__tests__/atmosphere-daylight.dawn.test.ts +72 -0
  274. package/src/__tests__/barrel-distortion-lkg.integration.test.ts +353 -0
  275. package/src/__tests__/barrel-distortion.dawn.test.ts +41 -0
  276. package/src/__tests__/barrel-distortion.unit.test.ts +364 -0
  277. package/src/__tests__/construct-renderer-identity.unit.test.ts +49 -0
  278. package/src/__tests__/detached-recovery-graph.unit.test.ts +277 -0
  279. package/src/__tests__/device-loss-fanout.unit.test.ts +36 -1
  280. package/src/__tests__/docs-gate-green.unit.test.ts +112 -1
  281. package/src/__tests__/factory-contract.integration.test.ts +295 -45
  282. package/src/__tests__/feature-depth-input.dawn.test.ts +129 -2
  283. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +14 -3
  284. package/src/__tests__/gpu-dirty-ranges.unit.test.ts +22 -0
  285. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +16 -3
  286. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +12 -0
  287. package/src/__tests__/gpu-driven-material-artifacts.unit.test.ts +143 -0
  288. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +2 -2
  289. package/src/__tests__/gpu-driven-production.integration.test.ts +482 -3
  290. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +6 -5
  291. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +94 -6
  292. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +18 -2
  293. package/src/__tests__/gpu-driven-view.browser.test.ts +5 -8
  294. package/src/__tests__/gpu-driven-view.dawn.test.ts +6 -2
  295. package/src/__tests__/gpu-driven-view.unit.test.ts +1 -1
  296. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +1 -0
  297. package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -4
  298. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +1 -0
  299. package/src/__tests__/gpu-scene.unit.test.ts +119 -9
  300. package/src/__tests__/instance-allocation-failure.unit.test.ts +57 -0
  301. package/src/__tests__/instances-culling.integration.test.ts +10 -36
  302. package/src/__tests__/instances-store.unit.test.ts +97 -305
  303. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  304. package/src/__tests__/main-pass-material-medium-selection.unit.test.ts +146 -0
  305. package/src/__tests__/material-shader-prewarm.unit.test.ts +59 -0
  306. package/src/__tests__/material-snapshot-mutation.integration.test.ts +39 -1
  307. package/src/__tests__/morph-culling-reentry.dawn.test.ts +5 -5
  308. package/src/__tests__/morph-visual-evidence.integration.test.ts +8 -7
  309. package/src/__tests__/motion-blur-compute.dawn.test.ts +13 -7
  310. package/src/__tests__/motion-blur.pixel.integration.test.ts +2 -2
  311. package/src/__tests__/nineslice-handle-surface.unit.test.ts +4 -4
  312. package/src/__tests__/pbr-view-layout.unit.test.ts +9 -0
  313. package/src/__tests__/physical-clearcoat.integration.test.ts +58 -0
  314. package/src/__tests__/prepared-graphics-capability.unit.test.ts +66 -2
  315. package/src/__tests__/probe-bindings.unit.test.ts +582 -1
  316. package/src/__tests__/probe-scene-dirty.integration.test.ts +28 -1
  317. package/src/__tests__/receipt-observation-retirement.unit.test.ts +65 -0
  318. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +69 -0
  319. package/src/__tests__/render-error-code-owner.test-d.ts +7 -0
  320. package/src/__tests__/render-feature-diagnostics.unit.test.ts +15 -2
  321. package/src/__tests__/render-feature-error-reporting.unit.test.ts +21 -1
  322. package/src/__tests__/render-feature-prepared-diagnostics.unit.test.ts +10 -1
  323. package/src/__tests__/render-feature-retry.unit.test.ts +12 -1
  324. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +145 -19
  325. package/src/__tests__/render-scene-projection.unit.test.ts +36 -11
  326. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +20 -20
  327. package/src/__tests__/render-target-probe-soak.perf.test.ts +2 -2
  328. package/src/__tests__/render-temporal-environment-consumer.browser.test.ts +3 -3
  329. package/src/__tests__/render-temporal-environment-consumer.dawn.test.ts +5 -2
  330. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +41 -7
  331. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +16 -0
  332. package/src/__tests__/renderer-frame-transaction.integration.test.ts +26 -10
  333. package/src/__tests__/resource-allocation.unit.test.ts +123 -0
  334. package/src/__tests__/single-layer-medium-passes.unit.test.ts +275 -0
  335. package/src/__tests__/skin-motion-regression.unit.test.ts +70 -1
  336. package/src/__tests__/skin-palette-recycling.dawn.test.ts +97 -0
  337. package/src/__tests__/skinned-shadow-caster.test.ts +15 -0
  338. package/src/__tests__/ssr-carrier-public-contract.unit.test.ts +21 -0
  339. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +4 -4
  340. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +2 -2
  341. package/src/__tests__/ssr-gpu-dispatch.ts +1 -1
  342. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +6 -2
  343. package/src/__tests__/standard-output-chain-contract.unit.test.ts +28 -0
  344. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +46 -1
  345. package/src/__tests__/standard-pipeline.integration.test.ts +182 -1
  346. package/src/__tests__/surface-dynamic-buffer-lifecycle.unit.test.ts +506 -0
  347. package/src/__tests__/surface-submission-observation.unit.test.ts +812 -0
  348. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +32 -0
  349. package/src/__tests__/temporal-performance.integration.test.ts +3 -3
  350. package/src/__tests__/visibility-instances.integration.test.ts +7 -19
  351. package/src/__tests__/visibility-producer-matrix.integration.test.ts +9 -20
  352. package/src/__tests__/volume-optics-oracle.unit.test.ts +4 -2
  353. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +30 -20
  354. package/src/__tests__/volumetric-fog-errors.unit.test.ts +4 -3
  355. package/src/__tests__/volumetric-fog-multiple.unit.test.ts +55 -0
  356. package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +1 -1
  357. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +113 -9
  358. package/src/__tests__/volumetric-fog-topology.unit.test.ts +5 -3
  359. package/src/assembly/backend-contract.ts +30 -0
  360. package/src/assembly/barrel-distortion-inspection.ts +22 -0
  361. package/src/assembly/canvas-draw-guard.ts +11 -0
  362. package/src/assembly/cloud-shadow-entry.ts +22 -0
  363. package/src/assembly/dynamic-geometry-host.ts +65 -37
  364. package/src/assembly/dynamic-geometry-runtime.ts +7 -1
  365. package/src/assembly/factory.ts +2 -30
  366. package/src/assembly/host-contract.ts +3 -3
  367. package/src/assembly/material/assembly.ts +25 -13
  368. package/src/assembly/material/pipeline-helpers.ts +53 -12
  369. package/src/assembly/material-shader-policy.ts +55 -0
  370. package/src/assembly/material-shader-prewarm.ts +21 -10
  371. package/src/assembly/recovery/device-loss-fanout.ts +13 -0
  372. package/src/assembly/recovery/renderer-recover.ts +41 -18
  373. package/src/assembly/render-feature-post-processes.ts +54 -0
  374. package/src/assembly/renderer-facade.ts +4 -1
  375. package/src/assembly/renderer-feature-inspection.ts +8 -2
  376. package/src/assembly/renderer-frame-transaction.ts +23 -0
  377. package/src/assembly/renderer-inspection.ts +28 -0
  378. package/src/assembly/skin-palette-owner.ts +1 -1
  379. package/src/assembly/temporal-post-process-prewarm.ts +3 -3
  380. package/src/assembly/webgpu-pbr-ready.ts +9 -1
  381. package/src/assembly/webgpu-ready-per-pass.ts +104 -0
  382. package/src/assembly/webgpu-ready.ts +115 -61
  383. package/src/assembly/webgpu-renderer-bootstrap.ts +0 -12
  384. package/src/assembly/webgpu-renderer-observation-owner.ts +222 -0
  385. package/src/assembly/webgpu-renderer.ts +214 -252
  386. package/src/assembly/webgpu-vertex-layouts.ts +1 -1
  387. package/src/barrel-distortion.ts +247 -0
  388. package/src/cloud/__tests__/cloud-density-parity.dawn.test.ts +173 -0
  389. package/src/cloud/__tests__/cloud-layer-framebuffer-y.dawn.test.ts +265 -0
  390. package/src/cloud/__tests__/cloud-layer.browser.test.ts +432 -0
  391. package/src/cloud/__tests__/cloud-layer.unit.test.ts +634 -0
  392. package/src/cloud/density.ts +600 -0
  393. package/src/cloud/extract.ts +110 -0
  394. package/src/cloud/feature.ts +1810 -0
  395. package/src/cloud/frame-context.ts +42 -0
  396. package/src/cloud/inspection.ts +130 -0
  397. package/src/cloud/optics.ts +285 -0
  398. package/src/cloud/parameters.ts +255 -0
  399. package/src/cloud/resources.ts +122 -0
  400. package/src/cloud/shadow.ts +167 -0
  401. package/src/cloud/temporal.ts +226 -0
  402. package/src/components/barrel-distortion.ts +46 -0
  403. package/src/components/cloud-layer.ts +47 -0
  404. package/src/components/index.ts +2 -0
  405. package/src/components/instances.ts +18 -45
  406. package/src/components/sprite-instances.ts +1 -1
  407. package/src/construct-renderer.ts +1 -1
  408. package/src/dynamic-geometry.ts +12 -1
  409. package/src/errors/cloud.ts +103 -0
  410. package/src/errors/render.ts +44 -7
  411. package/src/examples/single-layer-medium-surface.ts +59 -0
  412. package/src/extract/camera.ts +15 -1
  413. package/src/extract/contracts.ts +5 -0
  414. package/src/features/barrel-distortion.ts +172 -0
  415. package/src/features/error-reporting.ts +26 -6
  416. package/src/features/host.ts +115 -14
  417. package/src/features/plan.ts +8 -0
  418. package/src/features/render-graph-contribution.ts +9 -0
  419. package/src/features/render-graph-raster.ts +45 -3
  420. package/src/features/targets.ts +66 -0
  421. package/src/features/types.ts +47 -0
  422. package/src/fullscreen-post-process-pass.ts +41 -9
  423. package/src/gpu-dirty-ranges.ts +48 -0
  424. package/src/gpu-driven/batch-topology.ts +53 -0
  425. package/src/gpu-driven/material-artifacts.ts +33 -25
  426. package/src/gpu-driven/prepared-draw.ts +12 -4
  427. package/src/gpu-driven/production-raster-material.ts +1894 -0
  428. package/src/gpu-driven/production-raster.ts +825 -784
  429. package/src/gpu-driven/resource-allocation.ts +142 -0
  430. package/src/gpu-driven/shadow-ownership.ts +71 -0
  431. package/src/gpu-driven/view-gpu.ts +164 -10
  432. package/src/gpu-scene.ts +75 -84
  433. package/src/hdrp-buffers.ts +4 -3
  434. package/src/ibl/skylight-bind-group.ts +49 -7
  435. package/src/index.ts +196 -9
  436. package/src/inspection-types.ts +162 -0
  437. package/src/instance-buffer-cache.ts +4 -4
  438. package/src/instances.ts +149 -569
  439. package/src/pbr-pipeline.ts +219 -13
  440. package/src/pipeline/single-layer-medium-passes.ts +267 -0
  441. package/src/pipeline/standard-forward-lane.ts +313 -57
  442. package/src/pipeline/standard-output/graph.ts +8 -2
  443. package/src/pipeline/standard-output/types.ts +12 -1
  444. package/src/pipeline/standard-pipeline.ts +291 -18
  445. package/src/pipeline/standard-post.ts +90 -24
  446. package/src/pipeline-builder.ts +1 -1
  447. package/src/plugin.ts +4 -0
  448. package/src/points-lines/__tests__/points-lines.bench.ts +17 -8
  449. package/src/prepare/prepared-graphics-resolver.ts +2 -0
  450. package/src/record/__tests__/main-pass-geometry.unit.test.ts +30 -0
  451. package/src/record/__tests__/sprite-material-abi.unit.test.ts +1 -1
  452. package/src/record/__tests__/temporal-payload.unit.test.ts +1 -1
  453. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +11 -0
  454. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -0
  455. package/src/record/__tests__/view-ubo-layout.unit.test.ts +4 -3
  456. package/src/record/barrel-distortion-frame.ts +110 -0
  457. package/src/record/dynamic-geometry-consumption.ts +39 -9
  458. package/src/record/fold-buckets.ts +852 -0
  459. package/src/record/fold-instance-buffer.ts +51 -0
  460. package/src/record/frame-lighting.ts +36 -2
  461. package/src/record/frame-snapshot.ts +39 -3
  462. package/src/record/frame.ts +217 -1084
  463. package/src/record/gpu-draw-receipts.ts +38 -0
  464. package/src/record/gpu-timing.ts +48 -5
  465. package/src/record/main-pass-geometry.ts +251 -82
  466. package/src/record/main-pass-material.ts +66 -4
  467. package/src/record/main-pass-sprite-draws.ts +7 -65
  468. package/src/record/main-pass.ts +156 -20
  469. package/src/record/motion-blur-receipt.ts +91 -0
  470. package/src/record/probe-blend-buffer.ts +180 -0
  471. package/src/record/recovery-pipeline.ts +35 -3
  472. package/src/record/render-context.ts +39 -3
  473. package/src/record/shadow-pass.ts +30 -1
  474. package/src/record/skybox-post-pass.ts +28 -4
  475. package/src/record/typed-frame-graph.ts +770 -134
  476. package/src/record/view-ubo.ts +26 -3
  477. package/src/record/volume-params.ts +225 -0
  478. package/src/recovery/render-system-candidate.ts +235 -42
  479. package/src/reflection/record-owner.ts +7 -3
  480. package/src/render-contract.ts +48 -4
  481. package/src/render-graph-primitives.ts +7 -1
  482. package/src/render-pipeline.ts +130 -0
  483. package/src/render-system-extract-tail.ts +57 -61
  484. package/src/render-system-extract.ts +74 -76
  485. package/src/render-system-presentation.ts +56 -0
  486. package/src/render-system-producer-roots.ts +96 -0
  487. package/src/render-system-residency.ts +159 -0
  488. package/src/render-system.ts +347 -363
  489. package/src/scene/probe-blend.ts +96 -4
  490. package/src/scene/render-scene.ts +349 -280
  491. package/src/ssao-buffers.ts +95 -2
  492. package/src/surface/__tests__/dynamic-input.unit.test.ts +374 -0
  493. package/src/surface/__tests__/gpu-driven.unit.test.ts +87 -0
  494. package/src/surface/dynamic-input.ts +658 -0
  495. package/src/surface/gpu-driven.ts +147 -0
  496. package/src/surface/submission-observation.ts +517 -0
  497. package/src/systems/skin-palette-allocator.ts +4 -1
  498. package/src/temporal/gpu.ts +101 -17
  499. package/src/temporal/inspection.ts +7 -1
  500. package/src/temporal/standard-scene-data.ts +5 -0
  501. package/src/temporal/target-coverage-attachment.ts +4 -0
  502. package/src/transmission/__tests__/single-layer-medium.unit.test.ts +149 -0
  503. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +12 -11
  504. package/src/transmission/single-layer-medium.ts +322 -0
  505. package/src/typed-render-graph-primitives.ts +70 -2
  506. package/src/volume/component.ts +10 -9
  507. package/src/volume/extract.ts +11 -5
  508. package/src/volume/inspection.ts +3 -0
  509. package/src/volume/passes.ts +87 -4
  510. package/src/volume/resources.ts +2 -1
  511. package/dist/chunk-3RTCJTH4.mjs.map +0 -1
  512. package/dist/chunk-NB2S7ZRX.mjs.map +0 -1
  513. package/dist/chunk-NNQL7CB3.mjs +0 -1713
  514. package/dist/chunk-NNQL7CB3.mjs.map +0 -1
  515. package/dist/chunk-OO3DRYK7.mjs.map +0 -1
  516. package/dist/chunk-PA5DRJNI.mjs.map +0 -1
  517. package/dist/chunk-PLAXOOX5.mjs.map +0 -1
  518. package/dist/chunk-VOZWDM3L.mjs.map +0 -1
@@ -0,0 +1,4927 @@
1
+ import { CLOUD_EXTINCTION_COEFFICIENT, CLOUD_QUALITY_PROFILES, cloudLayerFormationKey, CloudLayerCacheInvalidError, cloudShadowResolutionForQuality, PointShapeValue, pointShapeFromU32, cloudViewDistanceForQuality } from './chunk-UINHLCZA.mjs';
2
+ import { PointsLinesBudgetExceededError, createVisibilityBudget, PointsLinesStyleUnsupportedError, PointsLinesMaterialUnsupportedError, PointsLinesInvalidStyleError, PointsLinesTopologyMismatchError } from './chunk-WEX45HKH.mjs';
3
+ import { ok, err, deriveMaterialDynamicInputLayout } from '@forgeax/engine-types';
4
+ import { validateR32FloatReceipt } from '@forgeax/engine-rhi';
5
+
6
+ var BARREL_DISTORTION_POST_PROCESS_ID = "fullscreen.forgeax.barrel-distortion";
7
+ var BARREL_DISTORTION_FEATURE_IDENTITY = BARREL_DISTORTION_POST_PROCESS_ID;
8
+ var BARREL_DISTORTION_WGSL_COORDINATE = (
9
+ /* wgsl */
10
+ `
11
+ struct BarrelDistortionParams {
12
+ strength : f32,
13
+ centerX : f32,
14
+ centerY : f32,
15
+ radiusSquared : f32,
16
+ };
17
+
18
+ fn barrel_distortion_display_to_scene_uv(
19
+ inputUv : vec2<f32>,
20
+ params : BarrelDistortionParams,
21
+ dimensions : vec2<f32>,
22
+ ) -> vec2<f32> {
23
+ let aspect = dimensions.x / max(dimensions.y, 1.0);
24
+ let p = vec2<f32>(
25
+ 2.0 * aspect * (inputUv.x - params.centerX),
26
+ 2.0 * (inputUv.y - params.centerY),
27
+ );
28
+ let t = dot(p, p) / max(params.radiusSquared, 1e-6);
29
+ let scale = (1.0 - params.strength) / (1.0 - params.strength * t);
30
+ return clamp(
31
+ vec2<f32>(
32
+ params.centerX + (inputUv.x - params.centerX) * scale,
33
+ params.centerY + (inputUv.y - params.centerY) * scale,
34
+ ),
35
+ vec2<f32>(0.0),
36
+ vec2<f32>(1.0),
37
+ );
38
+ }
39
+ `
40
+ );
41
+ var BARREL_DISTORTION_WGSL = (
42
+ /* wgsl */
43
+ `${BARREL_DISTORTION_WGSL_COORDINATE}
44
+ struct FullscreenOutput {
45
+ @builtin(position) position : vec4<f32>,
46
+ @location(0) uv : vec2<f32>,
47
+ };
48
+
49
+ fn fullscreen_triangle(vertex_index : u32) -> FullscreenOutput {
50
+ var x : f32 = -1.0;
51
+ var y : f32 = -1.0;
52
+ if (vertex_index == 1u) { x = 3.0; }
53
+ if (vertex_index == 2u) { y = 3.0; }
54
+ let u : f32 = (x + 1.0) * 0.5;
55
+ let v : f32 = 1.0 - (y + 1.0) * 0.5;
56
+ var out : FullscreenOutput;
57
+ out.position = vec4<f32>(x, y, 0.0, 1.0);
58
+ out.uv = vec2<f32>(u, v);
59
+ return out;
60
+ }
61
+
62
+ @group(1) @binding(0) var sceneColor : texture_2d<f32>;
63
+ @group(1) @binding(1) var sceneSampler : sampler;
64
+
65
+ @group(1) @binding(2) var<uniform> params : BarrelDistortionParams;
66
+
67
+ @vertex
68
+ fn vs_main(@builtin(vertex_index) vertex_index : u32) -> FullscreenOutput {
69
+ return fullscreen_triangle(vertex_index);
70
+ }
71
+
72
+ @fragment
73
+ fn fs_main(input : FullscreenOutput) -> @location(0) vec4<f32> {
74
+ let dimensions = vec2<f32>(textureDimensions(sceneColor));
75
+ let sampleUv = barrel_distortion_display_to_scene_uv(input.uv, params, dimensions);
76
+ return textureSampleLevel(sceneColor, sceneSampler, sampleUv, 0.0);
77
+ }
78
+ `
79
+ );
80
+ function createBarrelDistortionRenderFeature() {
81
+ return Object.freeze({
82
+ identity: BARREL_DISTORTION_FEATURE_IDENTITY,
83
+ requiredCapabilities: ["rgba16floatRenderable"],
84
+ requiredFullscreenPostProcesses: [
85
+ { identity: BARREL_DISTORTION_POST_PROCESS_ID, source: BARREL_DISTORTION_WGSL }
86
+ ],
87
+ extract: (context) => ok({ active: (context.selectedCamera?.barrelDistortion?.strength ?? 0) > 0 }),
88
+ plan: (data, _context) => ok({
89
+ resources: data.active ? [
90
+ {
91
+ kind: "fullscreen-program",
92
+ name: BARREL_DISTORTION_POST_PROCESS_ID,
93
+ source: BARREL_DISTORTION_WGSL,
94
+ reads: ["ldrColor"],
95
+ params: { byteSize: 16, defaultValue: new Uint8Array(16) }
96
+ },
97
+ {
98
+ kind: "graphics-program",
99
+ name: "barrel-distortion-pipeline",
100
+ program: {
101
+ shader: BARREL_DISTORTION_POST_PROCESS_ID,
102
+ vertexLayout: "none",
103
+ colorFormats: ["rgba16float"],
104
+ sampleCount: 1
105
+ }
106
+ },
107
+ {
108
+ kind: "graphics-bindings",
109
+ name: "barrel-distortion-bindings",
110
+ program: "barrel-distortion-pipeline",
111
+ values: {
112
+ group: 1,
113
+ fullscreen: true,
114
+ shader: BARREL_DISTORTION_POST_PROCESS_ID,
115
+ input: "barrel-input"
116
+ },
117
+ logicalTargets: { input: "barrel-input" }
118
+ }
119
+ ] : [],
120
+ passes: data.active ? [
121
+ {
122
+ kind: "raster",
123
+ name: "barrel-distortion",
124
+ colorAttachments: [
125
+ { target: "barrel-output", loadOp: "clear", storeOp: "store" }
126
+ ],
127
+ sampledTargets: ["barrel-input"],
128
+ draws: [
129
+ {
130
+ program: "barrel-distortion-pipeline",
131
+ bindings: ["barrel-distortion-bindings"],
132
+ vertexData: [],
133
+ vertexLayout: "none",
134
+ draw: { kind: "draw", vertexCount: 3, instanceCount: 1 }
135
+ }
136
+ ]
137
+ }
138
+ ] : []
139
+ })
140
+ });
141
+ }
142
+ var CLOUD_SHAPE_CELLS = 4;
143
+ var CLOUD_WEATHER_CELLS = 2;
144
+ var CLOUD_DETAIL_CELLS = CLOUD_SHAPE_CELLS * 3;
145
+ var CLOUD_VERTICAL_CELLS = 1;
146
+ var CLOUD_VERTICAL_NOISE_CELLS = 2;
147
+ var CLOUD_DETAIL_VERTICAL_CELLS = 3;
148
+ function saturate(value) {
149
+ return Math.min(1, Math.max(0, value));
150
+ }
151
+ function smoothstep(edge0, edge1, value) {
152
+ const t = saturate((value - edge0) / Math.max(1e-6, edge1 - edge0));
153
+ return t * t * (3 - 2 * t);
154
+ }
155
+ function fract(value) {
156
+ return value - Math.floor(value);
157
+ }
158
+ function wrapInteger(value, period) {
159
+ const wrapped2 = value % period;
160
+ return wrapped2 < 0 ? wrapped2 + period : wrapped2;
161
+ }
162
+ function hash3(x, y, z, seed) {
163
+ let value = Math.imul(x | 0, 73244475);
164
+ value = Math.imul(value ^ Math.imul(y | 0, 295559667), 73244475);
165
+ value = Math.imul(value ^ Math.imul(z | 0, 214175), 73244475);
166
+ value ^= seed | 0;
167
+ value = Math.imul(value ^ value >>> 16, 73244475);
168
+ value = Math.imul(value ^ value >>> 13, 668265261);
169
+ return ((value ^ value >>> 16) >>> 0) / 4294967296;
170
+ }
171
+ function valueNoise(x, y, z, seed, periodX = 0, periodY = 0, periodZ = 0) {
172
+ const ix = Math.floor(x);
173
+ const iy = Math.floor(y);
174
+ const iz = Math.floor(z);
175
+ const fx = x - ix;
176
+ const fy = y - iy;
177
+ const fz = z - iz;
178
+ const ux = fx * fx * (3 - 2 * fx);
179
+ const uy = fy * fy * (3 - 2 * fy);
180
+ const uz = fz * fz * (3 - 2 * fz);
181
+ const lattice = (cellX, cellY, cellZ) => hash3(
182
+ periodX > 0 ? wrapInteger(cellX, periodX) : cellX,
183
+ periodY > 0 ? wrapInteger(cellY, periodY) : cellY,
184
+ periodZ > 0 ? wrapInteger(cellZ, periodZ) : cellZ,
185
+ seed
186
+ );
187
+ const c000 = lattice(ix, iy, iz);
188
+ const c100 = lattice(ix + 1, iy, iz);
189
+ const c010 = lattice(ix, iy + 1, iz);
190
+ const c110 = lattice(ix + 1, iy + 1, iz);
191
+ const c001 = lattice(ix, iy, iz + 1);
192
+ const c101 = lattice(ix + 1, iy, iz + 1);
193
+ const c011 = lattice(ix, iy + 1, iz + 1);
194
+ const c111 = lattice(ix + 1, iy + 1, iz + 1);
195
+ const x00 = c000 + (c100 - c000) * ux;
196
+ const x10 = c010 + (c110 - c010) * ux;
197
+ const x01 = c001 + (c101 - c001) * ux;
198
+ const x11 = c011 + (c111 - c011) * ux;
199
+ return x00 + (x10 - x00) * uy + (x01 + (x11 - x01) * uy - (x00 + (x10 - x00) * uy)) * uz;
200
+ }
201
+ function cellularNoise(x, y, z, seed, periodXZ = CLOUD_DETAIL_CELLS, periodY = 0) {
202
+ const ix = Math.floor(x);
203
+ const iy = Math.floor(y);
204
+ const iz = Math.floor(z);
205
+ const fx = x - ix;
206
+ const fy = y - iy;
207
+ const fz = z - iz;
208
+ let nearest = Number.POSITIVE_INFINITY;
209
+ for (let dz = -1; dz <= 1; dz += 1) {
210
+ for (let dy = -1; dy <= 1; dy += 1) {
211
+ for (let dx = -1; dx <= 1; dx += 1) {
212
+ const random = hash3(
213
+ wrapInteger(ix + dx, periodXZ),
214
+ periodY > 0 ? wrapInteger(iy + dy, periodY) : iy + dy,
215
+ wrapInteger(iz + dz, periodXZ),
216
+ seed ^ 2654435769
217
+ );
218
+ const px = dx + fract(random * 17) - fx;
219
+ const py = dy + fract(random * 31) - fy;
220
+ const pz = dz + fract(random * 47) - fz;
221
+ nearest = Math.min(nearest, px * px + py * py + pz * pz);
222
+ }
223
+ }
224
+ }
225
+ return 1 - saturate(Math.sqrt(nearest) * 1.25);
226
+ }
227
+ function heightEnvelope(height, body, weather) {
228
+ const lowerEdge = 0.05 + (1 - body) * 0.1;
229
+ const crownEdge = Math.min(0.96, 0.6 + body * 0.3 + weather * 0.08);
230
+ return smoothstep(lowerEdge, Math.min(1, lowerEdge + 0.12), height) * (1 - smoothstep(crownEdge, Math.min(1, crownEdge + 0.14), height));
231
+ }
232
+ function composeCloudDensity(height, formation, coverage) {
233
+ const threshold = 1 - coverage * (0.45 + formation.weather * 0.55);
234
+ const covered = smoothstep(0, 0.4, (formation.body - threshold) / Math.max(1e-3, 1 - threshold));
235
+ const shaped = covered * heightEnvelope(height, formation.body, formation.weather);
236
+ const erosion = (1 - formation.erosion) * 0.18;
237
+ return saturate((shaped - erosion) / (1 - erosion));
238
+ }
239
+ function formationField(x, y, z, seed, detailRepeat = 1) {
240
+ const macroWarpX = valueNoise(
241
+ x * CLOUD_WEATHER_CELLS,
242
+ y * CLOUD_VERTICAL_NOISE_CELLS + 3,
243
+ z * CLOUD_WEATHER_CELLS,
244
+ seed + 41023,
245
+ CLOUD_WEATHER_CELLS,
246
+ 2,
247
+ CLOUD_WEATHER_CELLS
248
+ ) - 0.5;
249
+ const macroWarpZ = valueNoise(
250
+ x * CLOUD_WEATHER_CELLS + 5,
251
+ y * CLOUD_VERTICAL_NOISE_CELLS - 7,
252
+ z * CLOUD_WEATHER_CELLS,
253
+ seed + 41023,
254
+ CLOUD_WEATHER_CELLS,
255
+ 2,
256
+ CLOUD_WEATHER_CELLS
257
+ ) - 0.5;
258
+ const bodyX = x + macroWarpX * 0.3;
259
+ const bodyZ = z + macroWarpZ * 0.3;
260
+ let amplitude = 0.5;
261
+ let frequency = 1;
262
+ let sum = 0;
263
+ let weight = 0;
264
+ for (let octave = 0; octave < 4; octave += 1) {
265
+ const tilePeriod = CLOUD_SHAPE_CELLS * frequency;
266
+ sum += valueNoise(
267
+ bodyX * tilePeriod,
268
+ y * frequency * CLOUD_VERTICAL_NOISE_CELLS,
269
+ bodyZ * tilePeriod,
270
+ seed + octave * 1013,
271
+ tilePeriod,
272
+ frequency * CLOUD_VERTICAL_NOISE_CELLS,
273
+ tilePeriod
274
+ ) * amplitude;
275
+ weight += amplitude;
276
+ amplitude *= 0.4;
277
+ frequency *= 2;
278
+ }
279
+ const broad = sum / weight;
280
+ const cells = cellularNoise(
281
+ bodyX * CLOUD_SHAPE_CELLS,
282
+ y * CLOUD_VERTICAL_NOISE_CELLS,
283
+ bodyZ * CLOUD_SHAPE_CELLS,
284
+ seed + 5011,
285
+ CLOUD_SHAPE_CELLS,
286
+ CLOUD_VERTICAL_NOISE_CELLS
287
+ );
288
+ const body = saturate(broad * 0.85 + cells * 0.15 + 0.15);
289
+ const weather = smoothstep(
290
+ 0.34,
291
+ 0.66,
292
+ valueNoise(
293
+ x * CLOUD_WEATHER_CELLS,
294
+ 0.37,
295
+ z * CLOUD_WEATHER_CELLS,
296
+ seed + 17041,
297
+ CLOUD_WEATHER_CELLS,
298
+ 1,
299
+ CLOUD_WEATHER_CELLS
300
+ )
301
+ );
302
+ const detailX = x * detailRepeat;
303
+ const detailY = y * detailRepeat;
304
+ const detailZ = z * detailRepeat;
305
+ const warpX = valueNoise(
306
+ detailX * CLOUD_WEATHER_CELLS,
307
+ detailY * 2 + 11,
308
+ detailZ * CLOUD_WEATHER_CELLS,
309
+ seed + 29011,
310
+ CLOUD_WEATHER_CELLS,
311
+ 2,
312
+ CLOUD_WEATHER_CELLS
313
+ ) - 0.5;
314
+ const warpZ = valueNoise(
315
+ detailX * CLOUD_WEATHER_CELLS + 7,
316
+ detailY * 2 - 5,
317
+ detailZ * CLOUD_WEATHER_CELLS,
318
+ seed + 29011,
319
+ CLOUD_WEATHER_CELLS,
320
+ 2,
321
+ CLOUD_WEATHER_CELLS
322
+ ) - 0.5;
323
+ const erosion = cellularNoise(
324
+ (detailX + warpX * 0.22) * CLOUD_DETAIL_CELLS,
325
+ detailY * CLOUD_DETAIL_VERTICAL_CELLS,
326
+ (detailZ + warpZ * 0.22) * CLOUD_DETAIL_CELLS,
327
+ seed + 7919,
328
+ CLOUD_DETAIL_CELLS,
329
+ CLOUD_DETAIL_VERTICAL_CELLS
330
+ );
331
+ const noise = body;
332
+ return { weather, body, erosion, noise };
333
+ }
334
+ function evaluateCloudFormation(params, position, timeSeconds = 0) {
335
+ const windX = (params.wind[0] ?? 0) * timeSeconds;
336
+ const windY = (params.wind[1] ?? 0) * timeSeconds;
337
+ const windZ = (params.wind[2] ?? 0) * timeSeconds;
338
+ const relativeHeight = ((position[1] ?? 0) - params.baseHeight) / Math.max(1e-6, params.thickness);
339
+ const advectedHeight = relativeHeight + windY * params.scale / Math.max(1e-6, CLOUD_VERTICAL_CELLS);
340
+ return formationField(
341
+ ((position[0] ?? 0) + windX) * params.scale,
342
+ fract(advectedHeight) * CLOUD_VERTICAL_CELLS,
343
+ ((position[2] ?? 0) + windZ) * params.scale,
344
+ params.seed
345
+ );
346
+ }
347
+ function evaluateCloudDensity(params, position, timeSeconds = 0) {
348
+ const relativeHeight = ((position[1] ?? 0) - params.baseHeight) / Math.max(1e-6, params.thickness);
349
+ if (relativeHeight <= 0 || relativeHeight >= 1 || params.density <= 0 || params.coverage <= 0) {
350
+ return { density: 0, heightFraction: saturate(relativeHeight), noise: 0 };
351
+ }
352
+ const windX = (params.wind[0] ?? 0) * timeSeconds;
353
+ const windY = (params.wind[1] ?? 0) * timeSeconds;
354
+ const windZ = (params.wind[2] ?? 0) * timeSeconds;
355
+ const x = ((position[0] ?? 0) + windX) * params.scale;
356
+ const advectedHeight = relativeHeight + windY * params.scale / Math.max(1e-6, CLOUD_VERTICAL_CELLS);
357
+ const y = fract(advectedHeight) * CLOUD_VERTICAL_CELLS;
358
+ const z = ((position[2] ?? 0) + windZ) * params.scale;
359
+ const formation = formationField(x, y, z, params.seed, 3);
360
+ const noise = formation.noise;
361
+ return {
362
+ density: composeCloudDensity(relativeHeight, formation, params.coverage) * params.density,
363
+ heightFraction: relativeHeight,
364
+ noise,
365
+ weather: formation.weather,
366
+ body: formation.body,
367
+ erosion: formation.erosion
368
+ };
369
+ }
370
+ function cacheIndex(resolution, x, y, z) {
371
+ return z * resolution * resolution + y * resolution + x;
372
+ }
373
+ function digestBytes(data) {
374
+ let hash = 2166136261;
375
+ for (const byte of data) {
376
+ hash ^= byte;
377
+ hash = Math.imul(hash, 16777619);
378
+ }
379
+ return (hash >>> 0).toString(16).padStart(8, "0");
380
+ }
381
+ function buildCloudDensityCache(params, profile = CLOUD_QUALITY_PROFILES[params.quality]) {
382
+ const resolution = Math.max(4, Math.floor(profile.cacheResolution));
383
+ const data = new Uint8Array(resolution * resolution * resolution);
384
+ const formationData = new Uint8Array(data.byteLength * 3);
385
+ const position = [0, 0, 0];
386
+ for (let z = 0; z < resolution; z += 1) {
387
+ for (let y = 0; y < resolution; y += 1) {
388
+ position[1] = params.baseHeight + (y + 0.5) / resolution * params.thickness;
389
+ for (let x = 0; x < resolution; x += 1) {
390
+ position[0] = (x + 0.5) / resolution / params.scale;
391
+ position[2] = (z + 0.5) / resolution / params.scale;
392
+ const index = cacheIndex(resolution, x, y, z);
393
+ const formation = evaluateCloudFormation(params, position, 0);
394
+ formationData[index] = Math.round(saturate(formation.weather) * 255);
395
+ formationData[data.byteLength + index] = Math.round(saturate(formation.body) * 255);
396
+ formationData[data.byteLength * 2 + index] = Math.round(saturate(formation.erosion) * 255);
397
+ }
398
+ }
399
+ }
400
+ const cache = {
401
+ sourceKey: cloudLayerFormationKey(params),
402
+ resolution,
403
+ data,
404
+ formationData,
405
+ formationByteLength: formationData.byteLength,
406
+ byteLength: formationData.byteLength,
407
+ digest: digestBytes(formationData)
408
+ };
409
+ for (let z = 0; z < resolution; z++) {
410
+ for (let y = 0; y < resolution; y++) {
411
+ for (let x = 0; x < resolution; x++) {
412
+ position[0] = (x + 0.5) / resolution / params.scale;
413
+ position[1] = params.baseHeight + (y + 0.5) / resolution * params.thickness;
414
+ position[2] = (z + 0.5) / resolution / params.scale;
415
+ data[cacheIndex(resolution, x, y, z)] = Math.round(
416
+ saturate(sampleCloudDensity(cache, params, position)) * 255
417
+ );
418
+ }
419
+ }
420
+ }
421
+ return Object.freeze(cache);
422
+ }
423
+ function snapshotCloudDensityCache(cache) {
424
+ return Object.freeze({
425
+ sourceKey: cache.sourceKey,
426
+ resolution: cache.resolution,
427
+ data: new Uint8Array(cache.data),
428
+ formationData: new Uint8Array(cache.formationData),
429
+ digest: cache.digest
430
+ });
431
+ }
432
+ function reconstructCloudDensityCache(params, snapshot) {
433
+ const expectedKey = cloudLayerFormationKey(params);
434
+ if (snapshot.sourceKey !== expectedKey) {
435
+ return err(new CloudLayerCacheInvalidError(snapshot.sourceKey, "source key mismatch"));
436
+ }
437
+ if (!Number.isInteger(snapshot.resolution) || snapshot.resolution < 4 || snapshot.resolution > 256 || snapshot.data.byteLength !== snapshot.resolution ** 3 || snapshot.formationData !== void 0 && snapshot.formationData.byteLength !== snapshot.resolution ** 3 * 3) {
438
+ return err(
439
+ new CloudLayerCacheInvalidError(snapshot.sourceKey, "resolution and payload length mismatch")
440
+ );
441
+ }
442
+ const formationData = snapshot.formationData === void 0 ? new Uint8Array(snapshot.data.length * 3).fill(0) : new Uint8Array(snapshot.formationData);
443
+ const digest = digestBytes(formationData);
444
+ if (digest !== snapshot.digest) {
445
+ return err(new CloudLayerCacheInvalidError(snapshot.sourceKey, "payload digest mismatch"));
446
+ }
447
+ const data = new Uint8Array(snapshot.data);
448
+ return ok(
449
+ Object.freeze({
450
+ sourceKey: snapshot.sourceKey,
451
+ resolution: snapshot.resolution,
452
+ data,
453
+ formationData,
454
+ formationByteLength: formationData.byteLength,
455
+ byteLength: formationData.byteLength,
456
+ digest
457
+ })
458
+ );
459
+ }
460
+ function wrapped(value, resolution) {
461
+ const result = value - Math.floor(value);
462
+ return Math.min(resolution - 1e-6, Math.max(0, result * resolution));
463
+ }
464
+ function sampleCloudDensity(cache, params, position, timeSeconds = 0) {
465
+ const height = ((position[1] ?? 0) - params.baseHeight) / Math.max(1e-6, params.thickness);
466
+ if (height <= 0 || height >= 1 || params.coverage <= 0 || params.density <= 0) return 0;
467
+ const period = 1 / Math.max(1e-6, params.scale);
468
+ const x = wrapped(
469
+ ((position[0] ?? 0) + (params.wind[0] ?? 0) * timeSeconds) / period - 0.5 / cache.resolution,
470
+ cache.resolution
471
+ );
472
+ const z = wrapped(
473
+ ((position[2] ?? 0) + (params.wind[2] ?? 0) * timeSeconds) / period - 0.5 / cache.resolution,
474
+ cache.resolution
475
+ );
476
+ const advectedHeight = height + (params.wind[1] ?? 0) * timeSeconds * params.scale / Math.max(1e-6, CLOUD_VERTICAL_CELLS);
477
+ const y = wrapped(advectedHeight - 0.5 / cache.resolution, cache.resolution);
478
+ const x0 = Math.floor(x);
479
+ const y0 = Math.floor(y);
480
+ const z0 = Math.floor(z);
481
+ const x1 = (x0 + 1) % cache.resolution;
482
+ const y1 = (y0 + 1) % cache.resolution;
483
+ const z1 = (z0 + 1) % cache.resolution;
484
+ const fx = x - x0;
485
+ const fy = y - y0;
486
+ const fz = z - z0;
487
+ const stride = cache.resolution ** 3;
488
+ if (cache.formationData === void 0 || cache.formationData.byteLength < stride * 3) {
489
+ const sampleLegacy = (sx, sy, sz) => (cache.data[cacheIndex(cache.resolution, sx, sy, sz)] ?? 0) / 255;
490
+ const c00 = sampleLegacy(x0, y0, z0) + (sampleLegacy(x1, y0, z0) - sampleLegacy(x0, y0, z0)) * fx;
491
+ const c10 = sampleLegacy(x0, y1, z0) + (sampleLegacy(x1, y1, z0) - sampleLegacy(x0, y1, z0)) * fx;
492
+ const c01 = sampleLegacy(x0, y0, z1) + (sampleLegacy(x1, y0, z1) - sampleLegacy(x0, y0, z1)) * fx;
493
+ const c11 = sampleLegacy(x0, y1, z1) + (sampleLegacy(x1, y1, z1) - sampleLegacy(x0, y1, z1)) * fx;
494
+ return (c00 + (c10 - c00) * fy + (c01 + (c11 - c01) * fy - (c00 + (c10 - c00) * fy)) * fz) * heightEnvelope(height, 1, 0.5) * params.density;
495
+ }
496
+ const samplePlane = (plane, sx, sy, sz) => {
497
+ const payload = cache.formationData;
498
+ const offset = plane * stride + cacheIndex(cache.resolution, sx, sy, sz);
499
+ return (payload[offset] ?? 0) / 255;
500
+ };
501
+ const sample = (plane) => {
502
+ const repeat = plane === 2 ? 3 : 1;
503
+ const sx = wrapped(
504
+ (x + 0.5) / cache.resolution * repeat - 0.5 / cache.resolution,
505
+ cache.resolution
506
+ );
507
+ const sy = wrapped(
508
+ (y + 0.5) / cache.resolution * repeat - 0.5 / cache.resolution,
509
+ cache.resolution
510
+ );
511
+ const sz = wrapped(
512
+ (z + 0.5) / cache.resolution * repeat - 0.5 / cache.resolution,
513
+ cache.resolution
514
+ );
515
+ const x02 = Math.floor(sx), y02 = Math.floor(sy), z02 = Math.floor(sz);
516
+ const x12 = (x02 + 1) % cache.resolution, y12 = (y02 + 1) % cache.resolution, z12 = (z02 + 1) % cache.resolution;
517
+ const fx2 = sx - x02, fy2 = sy - y02, fz2 = sz - z02;
518
+ const c00 = samplePlane(plane, x02, y02, z02) + (samplePlane(plane, x12, y02, z02) - samplePlane(plane, x02, y02, z02)) * fx2;
519
+ const c10 = samplePlane(plane, x02, y12, z02) + (samplePlane(plane, x12, y12, z02) - samplePlane(plane, x02, y12, z02)) * fx2;
520
+ const c01 = samplePlane(plane, x02, y02, z12) + (samplePlane(plane, x12, y02, z12) - samplePlane(plane, x02, y02, z12)) * fx2;
521
+ const c11 = samplePlane(plane, x02, y12, z12) + (samplePlane(plane, x12, y12, z12) - samplePlane(plane, x02, y12, z12)) * fx2;
522
+ return c00 + (c10 - c00) * fy2 + (c01 + (c11 - c01) * fy2 - (c00 + (c10 - c00) * fy2)) * fz2;
523
+ };
524
+ const weather = sample(0);
525
+ const body = sample(1);
526
+ const erosion = sample(2);
527
+ return composeCloudDensity(height, { weather, body, erosion}, params.coverage) * params.density;
528
+ }
529
+
530
+ // src/cloud/inspection.ts
531
+ function resourceStage(input) {
532
+ if (!input.authored || input.generation === void 0) return "none";
533
+ if (input.candidateGeneration !== void 0) return "candidate";
534
+ if (input.degraded === true || input.lkgGeneration !== void 0) return "lkg";
535
+ return "accepted";
536
+ }
537
+ function inspectCloudLayer(input) {
538
+ const capability = {
539
+ compute: input.capability?.compute ?? false,
540
+ storageBuffer: input.capability?.storageBuffer ?? false,
541
+ rgba16floatRenderable: input.capability?.rgba16floatRenderable ?? false,
542
+ physicalGpu: input.capability?.physicalGpu ?? "unknown",
543
+ timestampQuery: input.capability?.timestampQuery ?? false
544
+ };
545
+ const budget = {
546
+ measured: input.budget?.measured ?? false,
547
+ physicalGpu: input.budget?.physicalGpu ?? capability.physicalGpu,
548
+ gpuTimestamp: input.budget?.gpuTimestamp ?? capability.timestampQuery,
549
+ quality: input.budget?.quality,
550
+ warmupFrames: input.budget?.warmupFrames ?? 0,
551
+ sampleFrames: input.budget?.sampleFrames ?? 0,
552
+ p50GpuMs: input.budget?.p50GpuMs,
553
+ p95GpuMs: input.budget?.p95GpuMs,
554
+ coldGenerationMs: input.budget?.coldGenerationMs,
555
+ frameBudgetMs: input.budget?.frameBudgetMs,
556
+ ...input.budget?.reason === void 0 ? {} : { reason: input.budget.reason }
557
+ };
558
+ const available = capability.compute && capability.storageBuffer && capability.rgba16floatRenderable;
559
+ return Object.freeze({
560
+ status: !input.authored ? "off" : !available ? "unavailable" : input.degraded === true ? "degraded" : "available",
561
+ resourceStage: resourceStage(input),
562
+ sourceKey: input.sourceKey,
563
+ generation: input.generation,
564
+ candidateGeneration: input.candidateGeneration,
565
+ lkgGeneration: input.lkgGeneration,
566
+ shadowRevision: input.shadowRevision,
567
+ temporalResets: input.temporalResets ?? 0,
568
+ lastTemporalReset: input.lastTemporalReset,
569
+ resourceFacts: input.resourceFacts,
570
+ capability,
571
+ budget
572
+ });
573
+ }
574
+ function cloudCapabilitiesFromRhi(caps) {
575
+ return Object.freeze({
576
+ compute: caps.compute === true,
577
+ storageBuffer: caps.storageBuffer === true,
578
+ rgba16floatRenderable: caps.rgba16floatRenderable === true,
579
+ // RHI caps intentionally do not claim a physical adapter. A browser or
580
+ // Dawn receipt must promote this fact to available/unavailable.
581
+ physicalGpu: "unknown",
582
+ timestampQuery: caps.timestampQuery === true
583
+ });
584
+ }
585
+
586
+ // src/cloud/resources.ts
587
+ var CLOUD_RGBA16FLOAT_BYTES_PER_TEXEL = 8;
588
+ var CLOUD_HISTORY_SURFACE_COUNT = 6;
589
+ function inspectCloudLayerResources(input) {
590
+ const cacheBytes = input.cache?.byteLength ?? 0;
591
+ const shadowResolution = input.shadow?.resolution ?? 0;
592
+ const declaredShadowBytes = shadowResolution * shadowResolution * CLOUD_RGBA16FLOAT_BYTES_PER_TEXEL;
593
+ const historyWidth = input.history?.width ?? 0;
594
+ const historyHeight = input.history?.height ?? 0;
595
+ const declaredHistoryBytes = historyWidth * historyHeight * CLOUD_HISTORY_SURFACE_COUNT * CLOUD_RGBA16FLOAT_BYTES_PER_TEXEL;
596
+ const shadowBytes = input.shadowBytes ?? 0;
597
+ const historyBytes = input.historyBytes ?? 0;
598
+ const inFlightBytes = Math.max(0, input.inFlightBytes ?? 0);
599
+ const residentBytes = cacheBytes + shadowBytes + historyBytes;
600
+ const measuredGpuResourceCount = Number(input.shadowBytes !== void 0) + Number(input.historyBytes !== void 0);
601
+ const gpuEvidence = measuredGpuResourceCount > 0 || input.inFlightBytes !== void 0 ? "measured" : input.shadow !== void 0 || input.history !== void 0 ? "declared" : "unavailable";
602
+ const resourceCount = Number(input.cache !== void 0) + Number(input.shadow !== void 0) + Number(input.history !== void 0);
603
+ return Object.freeze({
604
+ generation: input.generation,
605
+ cacheBytes,
606
+ shadowBytes,
607
+ historyBytes,
608
+ inFlightBytes,
609
+ residentBytes,
610
+ resourceCount,
611
+ gpuEvidence,
612
+ declaredShadowBytes,
613
+ declaredHistoryBytes,
614
+ declaredResidentBytes: cacheBytes + declaredShadowBytes + declaredHistoryBytes,
615
+ declaredResourceCount: resourceCount,
616
+ measuredGpuResourceCount,
617
+ cacheResolution: input.cache?.resolution ?? 0,
618
+ shadowResolution
619
+ });
620
+ }
621
+ function acceptCloudLayerGeneration(candidate, previous) {
622
+ return Object.freeze({
623
+ active: Object.freeze({ ...candidate, status: "active" }),
624
+ lkg: previous === void 0 ? void 0 : Object.freeze({ ...previous, status: "lkg" })
625
+ });
626
+ }
627
+ function retainCloudLayerAfterFailure(active, candidate) {
628
+ return Object.freeze({
629
+ active,
630
+ retiring: Object.freeze({ ...candidate, status: "retiring" })
631
+ });
632
+ }
633
+
634
+ // src/cloud/optics.ts
635
+ var EMPTY_SCATTERING = [0, 0, 0];
636
+ function normalize(vector) {
637
+ const x = vector[0] ?? 0;
638
+ const y = vector[1] ?? 0;
639
+ const z = vector[2] ?? 0;
640
+ const length = Math.hypot(x, y, z);
641
+ return length <= 1e-8 ? [0, 1, 0] : [x / length, y / length, z / length];
642
+ }
643
+ function rayBoxInterval(ray, params) {
644
+ const min = [Number.NEGATIVE_INFINITY, params.baseHeight, Number.NEGATIVE_INFINITY];
645
+ const max = [
646
+ Number.POSITIVE_INFINITY,
647
+ params.baseHeight + params.thickness,
648
+ Number.POSITIVE_INFINITY
649
+ ];
650
+ let start = 0;
651
+ let end = Math.max(0, ray.maxDistance);
652
+ for (let axis = 0; axis < 3; axis += 1) {
653
+ const origin = ray.origin[axis] ?? 0;
654
+ const direction = ray.direction[axis] ?? 0;
655
+ const lower = min[axis] ?? 0;
656
+ const upper = max[axis] ?? 0;
657
+ if (Math.abs(direction) < 1e-8) {
658
+ if (origin < lower || origin > upper) return void 0;
659
+ continue;
660
+ }
661
+ const a = (lower - origin) / direction;
662
+ const b = (upper - origin) / direction;
663
+ start = Math.max(start, Math.min(a, b));
664
+ end = Math.min(end, Math.max(a, b));
665
+ if (end <= start) return void 0;
666
+ }
667
+ return end > start ? { start, end } : void 0;
668
+ }
669
+ function intersectCloudLayer(ray, params) {
670
+ return rayBoxInterval(ray, params);
671
+ }
672
+ function phaseFunction(cosTheta, g) {
673
+ const boundedG = Math.max(-0.95, Math.min(0.95, g));
674
+ const denominator = Math.max(1e-4, 1 + boundedG * boundedG - 2 * boundedG * cosTheta);
675
+ return (1 - boundedG * boundedG) / denominator ** 1.5 * (1 / (4 * Math.PI));
676
+ }
677
+ function cloudPhase(cosTheta, forwardG) {
678
+ return phaseFunction(cosTheta, forwardG) * 0.8 + phaseFunction(cosTheta, -0.2) * 0.2;
679
+ }
680
+ function smoothstep2(edge0, edge1, value) {
681
+ const t = Math.max(0, Math.min(1, (value - edge0) / Math.max(1e-6, edge1 - edge0)));
682
+ return t * t * (3 - 2 * t);
683
+ }
684
+ function cloudIncidentLight(solarT, cosTheta, height, forwardG = 0.55) {
685
+ const direct = solarT * cloudPhase(cosTheta, forwardG);
686
+ const multiple = (0.5 * Math.sqrt(solarT) + 0.25 * solarT ** 0.25) / (4 * Math.PI);
687
+ const energy = (direct + multiple) * (4 * Math.PI);
688
+ const skyFill = 0.025 + 0.05 * smoothstep2(0.1, 0.85, height);
689
+ return [energy + 0.72 * skyFill, energy + 0.84 * skyFill, energy + skyFill];
690
+ }
691
+ function densityAt(input, position) {
692
+ return input.cache === void 0 ? evaluateCloudDensity(input.params, position, input.timeSeconds ?? 0).density : sampleCloudDensity(input.cache, input.params, position, input.timeSeconds ?? 0);
693
+ }
694
+ function integrateCloudPath(input) {
695
+ const direction = normalize(input.ray.direction);
696
+ const interval = rayBoxInterval({ ...input.ray, direction }, input.params);
697
+ if (interval === void 0 || input.params.density <= 0) {
698
+ return {
699
+ path: input.path,
700
+ transmittance: 1,
701
+ scattering: EMPTY_SCATTERING,
702
+ opticalDepth: 0,
703
+ representativeDepth: input.ray.maxDistance,
704
+ steps: 0,
705
+ entered: false,
706
+ exited: false
707
+ };
708
+ }
709
+ const steps = Math.max(1, Math.floor(input.steps ?? 32));
710
+ const length = interval.end - interval.start;
711
+ const stepLength = length / steps;
712
+ let opticalDepth = 0;
713
+ const scattering = [0, 0, 0];
714
+ let weightedDepth = 0;
715
+ let scatteringWeight = 0;
716
+ const lighting = input.lighting;
717
+ const lightDirection = lighting === void 0 ? void 0 : normalize(lighting.sunDirection);
718
+ const viewDirection = normalize(input.ray.direction);
719
+ for (let index = 0; index < steps; index += 1) {
720
+ const distance = interval.start + (index + 0.5) * stepLength;
721
+ const position = [
722
+ (input.ray.origin[0] ?? 0) + direction[0] * distance,
723
+ (input.ray.origin[1] ?? 0) + direction[1] * distance,
724
+ (input.ray.origin[2] ?? 0) + direction[2] * distance
725
+ ];
726
+ const localDensity = Math.max(0, densityAt(input, position));
727
+ const extinction = localDensity * CLOUD_EXTINCTION_COEFFICIENT * stepLength;
728
+ const transmittanceBefore = Math.exp(-opticalDepth);
729
+ opticalDepth += extinction;
730
+ if (lighting !== void 0 && lightDirection !== void 0 && localDensity > 0) {
731
+ const localShadow = integrateCloudPath({
732
+ params: input.params,
733
+ path: "solar-column",
734
+ ray: {
735
+ origin: position,
736
+ direction: lightDirection,
737
+ maxDistance: input.params.shadowRange
738
+ },
739
+ steps: Math.max(4, Math.ceil((input.steps ?? 32) * 0.5)),
740
+ ...input.timeSeconds === void 0 ? {} : { timeSeconds: input.timeSeconds },
741
+ ...input.cache === void 0 ? {} : { cache: input.cache }
742
+ });
743
+ const cosTheta = viewDirection[0] * lightDirection[0] + viewDirection[1] * lightDirection[1] + viewDirection[2] * lightDirection[2];
744
+ const segmentWeight = transmittanceBefore * (1 - Math.exp(-extinction));
745
+ const heightFraction = Math.max(
746
+ 0,
747
+ Math.min(1, ((position[1] ?? 0) - input.params.baseHeight) / input.params.thickness)
748
+ );
749
+ const incident = cloudIncidentLight(
750
+ localShadow.transmittance,
751
+ cosTheta,
752
+ heightFraction,
753
+ lighting.phaseG
754
+ );
755
+ for (let channel = 0; channel < 3; channel++) {
756
+ scattering[channel] = (scattering[channel] ?? 0) + (lighting.sunRadiance[channel] ?? 0) * (incident[channel] ?? 0) * segmentWeight;
757
+ }
758
+ const weight = segmentWeight;
759
+ scatteringWeight += weight;
760
+ weightedDepth += distance * weight;
761
+ }
762
+ }
763
+ const transmittance = Math.exp(-opticalDepth);
764
+ return {
765
+ path: input.path,
766
+ transmittance,
767
+ scattering,
768
+ opticalDepth,
769
+ representativeDepth: scatteringWeight > 1e-6 ? weightedDepth / scatteringWeight : interval.start,
770
+ steps,
771
+ entered: true,
772
+ exited: interval.end < input.ray.maxDistance
773
+ };
774
+ }
775
+ function integrateCloudCameraPath(params, ray, options = {}) {
776
+ return integrateCloudPath({ ...options, params, ray, path: "camera" });
777
+ }
778
+ function integrateCloudSolarColumn(params, origin, sunDirection, options = {}) {
779
+ return integrateCloudPath({
780
+ ...options,
781
+ params,
782
+ path: "solar-column",
783
+ ray: {
784
+ origin,
785
+ direction: sunDirection,
786
+ maxDistance: options.maxDistance ?? params.shadowRange
787
+ }
788
+ });
789
+ }
790
+ function integrateCloudInterior(params, ray, lighting, options = {}) {
791
+ return integrateCloudPath({ ...options, params, ray, path: "cloud-interior", lighting });
792
+ }
793
+ function applyCloudSolarTransmittance(directSunRadiance, cloudTransmittance, meshVisibility = 1) {
794
+ const factor = Math.min(1, Math.max(0, cloudTransmittance)) * Math.min(1, Math.max(0, meshVisibility));
795
+ return [
796
+ (directSunRadiance[0] ?? 0) * factor,
797
+ (directSunRadiance[1] ?? 0) * factor,
798
+ (directSunRadiance[2] ?? 0) * factor
799
+ ];
800
+ }
801
+ function compositeCloudRadiance(sceneLinearHdr, cloud, sceneDepth, cloudDepth = cloud.representativeDepth) {
802
+ if (!Number.isFinite(sceneDepth) || cloudDepth > sceneDepth) return sceneLinearHdr;
803
+ const transmittance = Math.min(1, Math.max(0, cloud.transmittance));
804
+ return [
805
+ (sceneLinearHdr[0] ?? 0) * transmittance + (cloud.scattering[0] ?? 0),
806
+ (sceneLinearHdr[1] ?? 0) * transmittance + (cloud.scattering[1] ?? 0),
807
+ (sceneLinearHdr[2] ?? 0) * transmittance + (cloud.scattering[2] ?? 0),
808
+ 1
809
+ ];
810
+ }
811
+
812
+ // src/cloud/shadow.ts
813
+ function normalize2(vector) {
814
+ const x = vector[0] ?? 0;
815
+ const y = vector[1] ?? 0;
816
+ const z = vector[2] ?? 0;
817
+ const length = Math.hypot(x, y, z);
818
+ return length <= 1e-8 ? [0, 1, 0] : [x / length, y / length, z / length];
819
+ }
820
+ function cross(a, b) {
821
+ return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
822
+ }
823
+ function dot(a, b) {
824
+ return (a[0] ?? 0) * (b[0] ?? 0) + (a[1] ?? 0) * (b[1] ?? 0) + (a[2] ?? 0) * (b[2] ?? 0);
825
+ }
826
+ function stableRevision(input) {
827
+ let hash = 2166136261;
828
+ const values = [...input.center, ...input.sunDirection, input.range, input.resolution];
829
+ for (const value of values) {
830
+ const bits = new Float32Array([value]);
831
+ const bytes = new Uint8Array(bits.buffer);
832
+ for (const byte of bytes) hash = Math.imul(hash ^ byte, 16777619);
833
+ }
834
+ return hash >>> 0;
835
+ }
836
+ function createCloudShadowProjection(input) {
837
+ const sunDirection = normalize2(input.sunDirection);
838
+ const resolution = Math.max(16, Math.floor(input.resolution ?? 1024));
839
+ const range = Math.max(1, input.range);
840
+ const texelSize = range / resolution;
841
+ const reference = Math.abs(sunDirection[1]) < 0.95 ? [0, 1, 0] : [1, 0, 0];
842
+ const right = normalize2(cross(reference, sunDirection));
843
+ const up = normalize2(cross(sunDirection, right));
844
+ const center = input.center;
845
+ const lightX = dot(center, right);
846
+ const lightY = dot(center, up);
847
+ const lightZ = dot(center, sunDirection);
848
+ const snappedX = Math.floor(lightX / texelSize + 0.5) * texelSize;
849
+ const snappedY = Math.floor(lightY / texelSize + 0.5) * texelSize;
850
+ const origin = [
851
+ right[0] * snappedX + up[0] * snappedY + sunDirection[0] * lightZ,
852
+ right[1] * snappedX + up[1] * snappedY + sunDirection[1] * lightZ,
853
+ right[2] * snappedX + up[2] * snappedY + sunDirection[2] * lightZ
854
+ ];
855
+ return Object.freeze({
856
+ revision: stableRevision({ center, sunDirection, range, resolution }),
857
+ resolution,
858
+ range,
859
+ texelSize,
860
+ center: Object.freeze([...center]),
861
+ origin: Object.freeze(origin),
862
+ right,
863
+ up,
864
+ sunDirection,
865
+ lowSun: sunDirection[1] <= 0.08
866
+ });
867
+ }
868
+ function projectCloudShadowUv(projection, position) {
869
+ const relative = [
870
+ position[0] - projection.origin[0],
871
+ position[1] - projection.origin[1],
872
+ position[2] - projection.origin[2]
873
+ ];
874
+ const x = dot(relative, projection.right);
875
+ const y = dot(relative, projection.up);
876
+ const uv = [0.5 + x / projection.range, 0.5 + y / projection.range];
877
+ return { uv, inRange: uv[0] >= 0 && uv[0] <= 1 && uv[1] >= 0 && uv[1] <= 1 };
878
+ }
879
+ function sampleCloudShadow(params, projection, worldPosition, options = {}) {
880
+ const projected = projectCloudShadowUv(projection, worldPosition);
881
+ if (projection.lowSun) {
882
+ return {
883
+ transmittance: 1,
884
+ uv: projected.uv,
885
+ valid: false,
886
+ fallback: "low-sun",
887
+ revision: projection.revision
888
+ };
889
+ }
890
+ if (!projected.inRange) {
891
+ return {
892
+ transmittance: 1,
893
+ uv: projected.uv,
894
+ valid: false,
895
+ fallback: "outside-range",
896
+ revision: projection.revision
897
+ };
898
+ }
899
+ if (params.coverage >= 1 || params.density <= 0) {
900
+ return {
901
+ transmittance: 1,
902
+ uv: projected.uv,
903
+ valid: false,
904
+ fallback: "empty-coverage",
905
+ revision: projection.revision
906
+ };
907
+ }
908
+ const integrated = integrateCloudSolarColumn(params, worldPosition, projection.sunDirection, {
909
+ steps: 20,
910
+ maxDistance: projection.range,
911
+ ...options.cache === void 0 ? {} : { cache: options.cache },
912
+ ...options.timeSeconds === void 0 ? {} : { timeSeconds: options.timeSeconds }
913
+ });
914
+ return {
915
+ transmittance: integrated.transmittance,
916
+ uv: projected.uv,
917
+ valid: integrated.entered,
918
+ fallback: "none",
919
+ revision: projection.revision
920
+ };
921
+ }
922
+
923
+ // src/cloud/temporal.ts
924
+ function cloudTemporalSignature(input) {
925
+ return [
926
+ "cloud-temporal-v1",
927
+ input.viewId,
928
+ input.sourceKey,
929
+ input.authoringGeneration,
930
+ input.cloudShadowRevision,
931
+ input.cameraRevision,
932
+ input.deviceGeneration,
933
+ input.width,
934
+ input.height,
935
+ input.timeSeconds,
936
+ input.quality,
937
+ input.sceneDepthVersion ?? 0
938
+ ].join(":");
939
+ }
940
+ function sameLayout(left, right) {
941
+ return left.viewId === right.viewId && left.width === right.width && left.height === right.height;
942
+ }
943
+ function cloudTemporalResetReasons(previous, next, options = {}) {
944
+ if (previous === void 0) return ["history-missing"];
945
+ const reasons = [];
946
+ if (previous.sourceKey !== next.sourceKey || previous.authoringGeneration !== next.authoringGeneration) {
947
+ reasons.push("authoring-generation");
948
+ }
949
+ if (previous.cloudShadowRevision !== next.cloudShadowRevision)
950
+ reasons.push("cloud-shadow-revision");
951
+ if (previous.deviceGeneration !== next.deviceGeneration) reasons.push("device-generation");
952
+ if (previous.sceneDepthVersion !== void 0 && next.sceneDepthVersion !== void 0 && previous.sceneDepthVersion !== next.sceneDepthVersion) {
953
+ reasons.push("scene-depth-disocclusion");
954
+ }
955
+ if (!sameLayout(previous, next)) reasons.push("resize");
956
+ if (options.cameraCut === true || previous.cameraRevision !== next.cameraRevision)
957
+ reasons.push("camera-cut");
958
+ const dt = next.timeSeconds - previous.timeSeconds;
959
+ if (!Number.isFinite(dt) || dt < 0 || dt > 0.5) reasons.push("time-discontinuity");
960
+ if (options.recovery === true) reasons.push("recovery");
961
+ return Object.freeze(reasons);
962
+ }
963
+ function createCloudHistory(signature, generation, valid = false) {
964
+ return Object.freeze({
965
+ width: Math.max(1, Math.floor(signature.width)),
966
+ height: Math.max(1, Math.floor(signature.height)),
967
+ signature: cloudTemporalSignature(signature),
968
+ temporal: Object.freeze({ ...signature }),
969
+ generation,
970
+ valid
971
+ });
972
+ }
973
+ var CloudHistoryStore = class {
974
+ maxViews;
975
+ entries = /* @__PURE__ */ new Map();
976
+ constructor(maxViews = 4) {
977
+ this.maxViews = Math.max(1, Math.floor(maxViews));
978
+ }
979
+ begin(signature, options = {}) {
980
+ const key = signature.viewId;
981
+ const previousEntry = this.entries.get(key);
982
+ const previous = previousEntry?.history;
983
+ const reasons = cloudTemporalResetReasons(previousEntry?.signature, signature, options);
984
+ const reset = reasons.length > 0;
985
+ return Object.freeze({
986
+ reset,
987
+ reasons,
988
+ signature: cloudTemporalSignature(signature),
989
+ history: reset ? void 0 : previous
990
+ });
991
+ }
992
+ commit(signature, history) {
993
+ if (history.width !== Math.max(1, Math.floor(signature.width)) || history.height !== Math.max(1, Math.floor(signature.height))) {
994
+ this.entries.delete(signature.viewId);
995
+ return;
996
+ }
997
+ this.entries.set(signature.viewId, { history, signature: Object.freeze({ ...signature }) });
998
+ while (this.entries.size > this.maxViews) {
999
+ const oldest = this.entries.keys().next().value;
1000
+ if (oldest === void 0) break;
1001
+ this.entries.delete(oldest);
1002
+ }
1003
+ }
1004
+ reset(viewId) {
1005
+ if (viewId === void 0) this.entries.clear();
1006
+ else this.entries.delete(viewId);
1007
+ }
1008
+ get(viewId) {
1009
+ return this.entries.get(viewId)?.history;
1010
+ }
1011
+ dispose() {
1012
+ this.entries.clear();
1013
+ }
1014
+ };
1015
+ function reprojectCloudHistory(input, historyWeight = 0.9) {
1016
+ const width = Math.max(1, input.historySize[0] ?? 1);
1017
+ const height = Math.max(1, input.historySize[1] ?? 1);
1018
+ const uv = [
1019
+ ((input.currentPixel[0] ?? 0) - (input.motion[0] ?? 0)) / width,
1020
+ ((input.currentPixel[1] ?? 0) - (input.motion[1] ?? 0)) / height
1021
+ ];
1022
+ if (uv[0] < 0 || uv[0] > 1 || uv[1] < 0 || uv[1] > 1) {
1023
+ return { accepted: false, historyUv: uv, weight: 0, reason: "outside" };
1024
+ }
1025
+ const depthTolerance = input.depthTolerance ?? Math.max(0.01, Math.abs(input.currentDepth) * 0.02);
1026
+ if (!Number.isFinite(input.currentDepth) || !Number.isFinite(input.historyDepth) || Math.abs(input.currentDepth - input.historyDepth) > depthTolerance) {
1027
+ return { accepted: false, historyUv: uv, weight: 0, reason: "depth-disocclusion" };
1028
+ }
1029
+ return {
1030
+ accepted: true,
1031
+ historyUv: uv,
1032
+ weight: Math.min(0.99, Math.max(0, historyWeight)),
1033
+ reason: "accepted"
1034
+ };
1035
+ }
1036
+ var CLOUD_LAYER_FEATURE_IDENTITY = "forgeax.cloud-layer";
1037
+ var CLOUD_LAYER_DENSITY_PROGRAM = "cloud-layer-density";
1038
+ var CLOUD_LAYER_DENSITY_BINDINGS = "cloud-layer-density-bindings";
1039
+ var CLOUD_LAYER_DENSITY_CACHE = "cloud-layer-density-cache";
1040
+ var CLOUD_LAYER_DENSITY_OUTPUT = "cloud-layer-density-output";
1041
+ var COMPUTE_STAGE = 4;
1042
+ var CLOUD_WORKGROUP_SIZE = 64;
1043
+ var CLOUD_VIEW_PARAMS_BYTES = 176;
1044
+ function packCloudDensityCache(cache) {
1045
+ const packed = new Uint32Array(Math.max(1, Math.ceil(cache.formationData.byteLength / 4)));
1046
+ for (let index = 0; index < cache.formationData.byteLength; index += 1) {
1047
+ const wordIndex = index >>> 2;
1048
+ packed[wordIndex] = (packed[wordIndex] ?? 0) | (cache.formationData[index] ?? 0) << (index & 3) * 8;
1049
+ }
1050
+ return packed;
1051
+ }
1052
+ var CLOUD_DENSITY_COMPUTE_WGSL = (
1053
+ /* wgsl */
1054
+ `
1055
+ struct CloudCacheParams { count: u32, }
1056
+ @group(0) @binding(0) var<storage, read> sourceDensity: array<u32>;
1057
+ @group(0) @binding(1) var<storage, read_write> cachedDensity: array<u32>;
1058
+ @group(0) @binding(2) var<uniform> params: CloudCacheParams;
1059
+ @compute @workgroup_size(64)
1060
+ fn cloud_density_cache(@builtin(global_invocation_id) id: vec3<u32>) {
1061
+ if (id.x >= params.count) { return; }
1062
+ cachedDensity[id.x] = sourceDensity[id.x];
1063
+ }
1064
+ `
1065
+ );
1066
+ var CLOUD_VIEW_FULLSCREEN_WGSL = (
1067
+ /* wgsl */
1068
+ `
1069
+ const CLOUD_VERTICAL_CELLS: f32 = ${CLOUD_VERTICAL_CELLS};
1070
+ const CLOUD_VERTICAL_NOISE_CELLS: f32 = 2.0;
1071
+
1072
+ struct CloudCameraView {
1073
+ worldViewProj: mat4x4<f32>,
1074
+ lightDir: vec3<f32>,
1075
+ lightColor: vec3<f32>,
1076
+ cameraPos: vec3<f32>,
1077
+ lightViewProj_A: mat4x4<f32>,
1078
+ inverseViewProj: mat4x4<f32>,
1079
+ lightViewProj_B: mat4x4<f32>,
1080
+ lightViewProj_C: mat4x4<f32>,
1081
+ lightViewProj_D: mat4x4<f32>,
1082
+ splitPlanes: array<vec4<f32>, 4>,
1083
+ cascadeCount: f32,
1084
+ cascadeBlend: f32,
1085
+ depthBias: f32,
1086
+ normalBias: f32,
1087
+ directionalShadowFilter: vec4<f32>,
1088
+ spotLightViewProj: array<mat4x4<f32>, 4>,
1089
+ temporalCurrentViewProj: mat4x4<f32>,
1090
+ temporalPreviousViewProj: mat4x4<f32>,
1091
+ temporalProjection: vec4<f32>,
1092
+ temporalPreviousCameraPos: vec4<f32>,
1093
+ ssrParams: vec4<f32>,
1094
+ };
1095
+
1096
+ struct CloudViewParams {
1097
+ layer: vec4<f32>, // baseHeight, thickness, scale, density
1098
+ field: vec4<f32>, // coverage, timeSeconds, maxDistance, seed
1099
+ wind: vec4<f32>, // xyz m/s
1100
+ sunDirection: vec4<f32>,
1101
+ sunRadiance: vec4<f32>,
1102
+ integration: vec4<u32>, // cacheResolution, viewSteps, solarSteps, reserved
1103
+ shadowOrigin: vec4<f32>,
1104
+ shadowRight: vec4<f32>,
1105
+ shadowUp: vec4<f32>,
1106
+ shadowProjection: vec4<f32>, // range, valid, lowSun, texelSize
1107
+ temporal: vec4<f32>, // valid, reset, historyWeight, reserved
1108
+ };
1109
+
1110
+ @group(0) @binding(0) var<uniform> view: CloudCameraView;
1111
+ @group(1) @binding(0) var sceneColor: texture_2d<f32>;
1112
+ @group(1) @binding(1) var sceneSampler: sampler;
1113
+ @group(1) @binding(2) var<uniform> cloud: CloudViewParams;
1114
+ @group(1) @binding(3) var sceneDepth: texture_depth_2d;
1115
+ @group(1) @binding(4) var depthSampler: sampler;
1116
+ @group(1) @binding(5) var previousRadiance: texture_2d<f32>;
1117
+ @group(1) @binding(6) var previousTransmittance: texture_2d<f32>;
1118
+ @group(1) @binding(7) var previousDepth: texture_2d<f32>;
1119
+ @group(1) @binding(8) var<storage, read> densityCache: array<u32>;
1120
+
1121
+ struct CloudViewOut {
1122
+ @builtin(position) position: vec4<f32>,
1123
+ @location(0) uv: vec2<f32>,
1124
+ };
1125
+
1126
+ fn cloud_hash3(cell: vec3<i32>, seed: u32) -> f32 {
1127
+ var h = bitcast<u32>(cell.x) * 0x45d9f3bu;
1128
+ h = (h ^ (bitcast<u32>(cell.y) * 0x119de1f3u)) * 0x45d9f3bu;
1129
+ h = (h ^ (bitcast<u32>(cell.z) * 0x3449fu)) * 0x45d9f3bu;
1130
+ h = h ^ seed;
1131
+ h = (h ^ (h >> 16u)) * 0x45d9f3bu;
1132
+ h = (h ^ (h >> 13u)) * 0x27d4eb2du;
1133
+ return f32(h ^ (h >> 16u)) / 4294967296.0;
1134
+ }
1135
+
1136
+ fn cloud_wrap_cell(value: i32, period: i32) -> i32 {
1137
+ let wrapped = value % period;
1138
+ return select(wrapped, wrapped + period, wrapped < 0);
1139
+ }
1140
+
1141
+ fn cloud_hash3_periodic(
1142
+ cell: vec3<i32>,
1143
+ seed: u32,
1144
+ periodX: i32,
1145
+ periodY: i32,
1146
+ periodZ: i32,
1147
+ ) -> f32 {
1148
+ return cloud_hash3(
1149
+ vec3<i32>(
1150
+ cloud_wrap_cell(cell.x, periodX),
1151
+ cloud_wrap_cell(cell.y, periodY),
1152
+ cloud_wrap_cell(cell.z, periodZ),
1153
+ ),
1154
+ seed,
1155
+ );
1156
+ }
1157
+
1158
+ fn cloud_value_noise(
1159
+ position: vec3<f32>,
1160
+ seed: u32,
1161
+ periodX: i32,
1162
+ periodY: i32,
1163
+ periodZ: i32,
1164
+ ) -> f32 {
1165
+ let cell = vec3<i32>(floor(position));
1166
+ let fraction = fract(position);
1167
+ let smoothFraction = fraction * fraction * (3.0 - 2.0 * fraction);
1168
+ let c000 = cloud_hash3_periodic(cell + vec3<i32>(0, 0, 0), seed, periodX, periodY, periodZ);
1169
+ let c100 = cloud_hash3_periodic(cell + vec3<i32>(1, 0, 0), seed, periodX, periodY, periodZ);
1170
+ let c010 = cloud_hash3_periodic(cell + vec3<i32>(0, 1, 0), seed, periodX, periodY, periodZ);
1171
+ let c110 = cloud_hash3_periodic(cell + vec3<i32>(1, 1, 0), seed, periodX, periodY, periodZ);
1172
+ let c001 = cloud_hash3_periodic(cell + vec3<i32>(0, 0, 1), seed, periodX, periodY, periodZ);
1173
+ let c101 = cloud_hash3_periodic(cell + vec3<i32>(1, 0, 1), seed, periodX, periodY, periodZ);
1174
+ let c011 = cloud_hash3_periodic(cell + vec3<i32>(0, 1, 1), seed, periodX, periodY, periodZ);
1175
+ let c111 = cloud_hash3_periodic(cell + vec3<i32>(1, 1, 1), seed, periodX, periodY, periodZ);
1176
+ let x00 = mix(c000, c100, smoothFraction.x);
1177
+ let x10 = mix(c010, c110, smoothFraction.x);
1178
+ let x01 = mix(c001, c101, smoothFraction.x);
1179
+ let x11 = mix(c011, c111, smoothFraction.x);
1180
+ return mix(mix(x00, x10, smoothFraction.y), mix(x01, x11, smoothFraction.y), smoothFraction.z);
1181
+ }
1182
+
1183
+ fn cloud_cellular_noise(position: vec3<f32>, seed: u32, periodXZ: i32, periodY: i32) -> f32 {
1184
+ let cell = vec3<i32>(floor(position));
1185
+ let fraction = fract(position);
1186
+ var nearest = 1e9;
1187
+ var dz: i32 = -1;
1188
+ loop {
1189
+ var dy: i32 = -1;
1190
+ loop {
1191
+ var dx: i32 = -1;
1192
+ loop {
1193
+ let random = cloud_hash3_periodic(
1194
+ cell + vec3<i32>(dx, dy, dz),
1195
+ seed ^ 0x9e3779b9u,
1196
+ periodXZ,
1197
+ periodY,
1198
+ periodXZ,
1199
+ );
1200
+ let point = vec3<f32>(
1201
+ f32(dx) + fract(random * 17.0) - fraction.x,
1202
+ f32(dy) + fract(random * 31.0) - fraction.y,
1203
+ f32(dz) + fract(random * 47.0) - fraction.z,
1204
+ );
1205
+ nearest = min(nearest, dot(point, point));
1206
+ if (dx >= 1) { break; }
1207
+ dx = dx + 1;
1208
+ }
1209
+ if (dy >= 1) { break; }
1210
+ dy = dy + 1;
1211
+ }
1212
+ if (dz >= 1) { break; }
1213
+ dz = dz + 1;
1214
+ }
1215
+ return 1.0 - clamp(sqrt(nearest) * 1.25, 0.0, 1.0);
1216
+ }
1217
+
1218
+ fn cloud_weather_field(position: vec3<f32>, seed: u32) -> f32 {
1219
+ return smoothstep(
1220
+ 0.34,
1221
+ 0.66,
1222
+ cloud_value_noise(
1223
+ vec3<f32>(position.x * 2.0, 0.37, position.z * 2.0),
1224
+ seed + 17041u,
1225
+ 2,
1226
+ 1,
1227
+ 2,
1228
+ ),
1229
+ );
1230
+ }
1231
+
1232
+ fn cloud_formation_field(position: vec3<f32>, seed: u32) -> vec3<f32> {
1233
+ let macroWarpX = cloud_value_noise(
1234
+ vec3<f32>(position.x * 2.0, position.y * CLOUD_VERTICAL_NOISE_CELLS + 3.0, position.z * 2.0),
1235
+ seed + 41023u,
1236
+ 2,
1237
+ 2,
1238
+ 2,
1239
+ ) - 0.5;
1240
+ let macroWarpZ = cloud_value_noise(
1241
+ vec3<f32>(position.x * 2.0 + 5.0, position.y * CLOUD_VERTICAL_NOISE_CELLS - 7.0, position.z * 2.0),
1242
+ seed + 41023u,
1243
+ 2,
1244
+ 2,
1245
+ 2,
1246
+ ) - 0.5;
1247
+ let bodyX = position.x + macroWarpX * 0.3;
1248
+ let bodyZ = position.z + macroWarpZ * 0.3;
1249
+ var noise = 0.0;
1250
+ var weight = 0.0;
1251
+ var amplitude = 0.5;
1252
+ var frequency = 1.0;
1253
+ for (var octave = 0u; octave < 4u; octave = octave + 1u) {
1254
+ let tilePeriod = 4.0 * frequency;
1255
+ noise = noise + cloud_value_noise(
1256
+ vec3<f32>(bodyX * tilePeriod, position.y * frequency * CLOUD_VERTICAL_NOISE_CELLS, bodyZ * tilePeriod),
1257
+ seed + octave * 1013u,
1258
+ i32(tilePeriod),
1259
+ i32(frequency * CLOUD_VERTICAL_NOISE_CELLS),
1260
+ i32(tilePeriod),
1261
+ ) * amplitude;
1262
+ weight = weight + amplitude;
1263
+ amplitude = amplitude * 0.4;
1264
+ frequency = frequency * 2.0;
1265
+ }
1266
+ let broad = noise / max(0.0001, weight);
1267
+ let cells = cloud_cellular_noise(
1268
+ vec3<f32>(bodyX * 4.0, position.y * CLOUD_VERTICAL_NOISE_CELLS, bodyZ * 4.0),
1269
+ seed + 5011u,
1270
+ 4,
1271
+ i32(CLOUD_VERTICAL_NOISE_CELLS),
1272
+ );
1273
+ // Keep the broad field responsible for the cloud silhouette and use the
1274
+ // cellular term as bounded breakup. A broad-dominant mix reads as fewer,
1275
+ // wider formations instead of evenly spaced foam balls.
1276
+ let base = clamp(broad * 0.85 + cells * 0.15 + 0.15, 0.0, 1.0);
1277
+ let weather = cloud_weather_field(position, seed);
1278
+ let detailPosition = position * 3.0;
1279
+ let warpX = cloud_value_noise(
1280
+ vec3<f32>(detailPosition.x * 2.0, detailPosition.y * 2.0 + 11.0, detailPosition.z * 2.0),
1281
+ seed + 29011u,
1282
+ 2,
1283
+ 2,
1284
+ 2,
1285
+ ) - 0.5;
1286
+ let warpZ = cloud_value_noise(
1287
+ vec3<f32>(detailPosition.x * 2.0 + 7.0, detailPosition.y * 2.0 - 5.0, detailPosition.z * 2.0),
1288
+ seed + 29011u,
1289
+ 2,
1290
+ 2,
1291
+ 2,
1292
+ ) - 0.5;
1293
+ let erosion = cloud_cellular_noise(
1294
+ vec3<f32>(
1295
+ (detailPosition.x + warpX * 0.22) * 12.0,
1296
+ detailPosition.y * 3.0,
1297
+ (detailPosition.z + warpZ * 0.22) * 12.0,
1298
+ ),
1299
+ seed + 7919u,
1300
+ 12,
1301
+ 3,
1302
+ );
1303
+ return vec3<f32>(weather, base, erosion);
1304
+ }
1305
+
1306
+ fn cloud_height_profile(height: f32, body: f32, weather: f32) -> f32 {
1307
+ // A body-dependent crown breaks the fixed horizontal slab silhouette while
1308
+ // keeping the profile smooth enough for the bounded ray step budget.
1309
+ let lowerEdge = 0.05 + (1.0 - body) * 0.1;
1310
+ let crownEdge = min(0.96, 0.6 + body * 0.3 + weather * 0.08);
1311
+ let lower = smoothstep(lowerEdge, min(1.0, lowerEdge + 0.12), height);
1312
+ let upper = 1.0 - smoothstep(crownEdge, min(1.0, crownEdge + 0.14), height);
1313
+ return lower * upper;
1314
+ }
1315
+
1316
+ fn cloud_compose_density(height: f32, formation: vec3<f32>, coverage: f32) -> f32 {
1317
+ let threshold = 1.0 - coverage * (0.45 + formation.x * 0.55);
1318
+ let covered = smoothstep(0.0, 0.4, (formation.y - threshold) / max(0.001, 1.0 - threshold));
1319
+ let shaped = covered * cloud_height_profile(height, formation.y, formation.x);
1320
+ let erosion = (1.0 - formation.z) * 0.18;
1321
+ return clamp((shaped - erosion) / (1.0 - erosion), 0.0, 1.0);
1322
+ }
1323
+
1324
+ fn cloud_analytic_density(position: vec3<f32>) -> f32 {
1325
+ let height = (position.y - cloud.layer.x) / max(0.000001, cloud.layer.y);
1326
+ if (height <= 0.0 || height >= 1.0 || cloud.layer.w <= 0.0 || cloud.field.x <= 0.0) { return 0.0; }
1327
+ let advected = position + cloud.wind.xyz * cloud.field.y;
1328
+ let advectedHeight = height +
1329
+ cloud.wind.y * cloud.field.y * cloud.layer.z / max(CLOUD_VERTICAL_CELLS, 0.000001);
1330
+ let p = vec3<f32>(
1331
+ advected.x * cloud.layer.z,
1332
+ fract(advectedHeight) * CLOUD_VERTICAL_CELLS,
1333
+ advected.z * cloud.layer.z,
1334
+ );
1335
+ let formation = cloud_formation_field(p, u32(round(cloud.field.w)));
1336
+ return cloud_compose_density(height, formation, cloud.field.x) * cloud.layer.w;
1337
+ }
1338
+
1339
+ fn cloud_cache_byte(index: u32) -> f32 {
1340
+ let word = densityCache[index >> 2u];
1341
+ let shift = (index & 3u) * 8u;
1342
+ return f32((word >> shift) & 255u) / 255.0;
1343
+ }
1344
+
1345
+ fn cloud_cache_component(plane: u32, index: u32, stride: u32) -> f32 {
1346
+ return cloud_cache_byte(plane * stride + index);
1347
+ }
1348
+
1349
+ fn cloud_cache_trilinear(
1350
+ plane: u32,
1351
+ resolution: u32,
1352
+ x0: u32,
1353
+ x1: u32,
1354
+ y0: u32,
1355
+ y1: u32,
1356
+ z0: u32,
1357
+ z1: u32,
1358
+ fx: f32,
1359
+ fy: f32,
1360
+ fz: f32,
1361
+ ) -> f32 {
1362
+ let stride = resolution * resolution * resolution;
1363
+ let layer = resolution * resolution;
1364
+ let c000 = cloud_cache_component(plane, z0 * layer + y0 * resolution + x0, stride);
1365
+ let c100 = cloud_cache_component(plane, z0 * layer + y0 * resolution + x1, stride);
1366
+ let c010 = cloud_cache_component(plane, z0 * layer + y1 * resolution + x0, stride);
1367
+ let c110 = cloud_cache_component(plane, z0 * layer + y1 * resolution + x1, stride);
1368
+ let c001 = cloud_cache_component(plane, z1 * layer + y0 * resolution + x0, stride);
1369
+ let c101 = cloud_cache_component(plane, z1 * layer + y0 * resolution + x1, stride);
1370
+ let c011 = cloud_cache_component(plane, z1 * layer + y1 * resolution + x0, stride);
1371
+ let c111 = cloud_cache_component(plane, z1 * layer + y1 * resolution + x1, stride);
1372
+ let x00 = mix(c000, c100, fx);
1373
+ let x10 = mix(c010, c110, fx);
1374
+ let x01 = mix(c001, c101, fx);
1375
+ let x11 = mix(c011, c111, fx);
1376
+ return mix(mix(x00, x10, fy), mix(x01, x11, fy), fz);
1377
+ }
1378
+
1379
+ fn cloud_density(position: vec3<f32>) -> f32 {
1380
+ let height = (position.y - cloud.layer.x) / max(0.000001, cloud.layer.y);
1381
+ if (height <= 0.0 || height >= 1.0 || cloud.layer.w <= 0.0 || cloud.field.x <= 0.0) { return 0.0; }
1382
+ let resolution = cloud.integration.x;
1383
+ if (resolution < 4u) { return max(cloud_analytic_density(position), 0.0); }
1384
+ let periodCoordinate = fract((position.xz + cloud.wind.xz * cloud.field.y) * cloud.layer.z);
1385
+ let x = fract(periodCoordinate.x - 0.5 / f32(resolution)) * f32(resolution);
1386
+ let z = fract(periodCoordinate.y - 0.5 / f32(resolution)) * f32(resolution);
1387
+ let advectedHeight = height +
1388
+ cloud.wind.y * cloud.field.y * cloud.layer.z / max(CLOUD_VERTICAL_CELLS, 0.000001);
1389
+ let y = fract(advectedHeight - 0.5 / f32(resolution)) * f32(resolution);
1390
+ let x0 = u32(floor(x));
1391
+ let y0 = u32(floor(y));
1392
+ let z0 = u32(floor(z));
1393
+ let x1 = (x0 + 1u) % resolution;
1394
+ let y1 = (y0 + 1u) % resolution;
1395
+ let z1 = (z0 + 1u) % resolution;
1396
+ let fx = x - f32(x0);
1397
+ let fy = y - f32(y0);
1398
+ let fz = z - f32(z0);
1399
+ let weather = cloud_cache_trilinear(0u, resolution, x0, x1, y0, y1, z0, z1, fx, fy, fz);
1400
+ let body = cloud_cache_trilinear(1u, resolution, x0, x1, y0, y1, z0, z1, fx, fy, fz);
1401
+ // Repeat only the independent erosion plane: fine boundary structure with
1402
+ // the same eight packed reads and the same cache allocation.
1403
+ let detail = fract(vec3<f32>(periodCoordinate.x, advectedHeight, periodCoordinate.y) * 3.0 - vec3<f32>(0.5 / f32(resolution))) * f32(resolution);
1404
+ let d0 = vec3<u32>(floor(detail));
1405
+ let d1 = (d0 + vec3<u32>(1u)) % vec3<u32>(resolution);
1406
+ let df = fract(detail);
1407
+ let erosion = cloud_cache_trilinear(2u, resolution, d0.x, d1.x, d0.y, d1.y, d0.z, d1.z, df.x, df.y, df.z);
1408
+ return cloud_compose_density(height, vec3<f32>(weather, body, erosion), cloud.field.x) * cloud.layer.w;
1409
+ }
1410
+
1411
+ fn cloud_layer_interval(origin: vec3<f32>, direction: vec3<f32>, maxDistance: f32) -> vec2<f32> {
1412
+ if (abs(direction.y) < 0.000001) {
1413
+ if (origin.y < cloud.layer.x || origin.y > cloud.layer.x + cloud.layer.y) {
1414
+ return vec2<f32>(1.0, 0.0);
1415
+ }
1416
+ return vec2<f32>(0.0, maxDistance);
1417
+ }
1418
+ let lower = (cloud.layer.x - origin.y) / direction.y;
1419
+ let upper = (cloud.layer.x + cloud.layer.y - origin.y) / direction.y;
1420
+ return vec2<f32>(
1421
+ max(0.0, min(lower, upper)),
1422
+ min(maxDistance, max(lower, upper)),
1423
+ );
1424
+ }
1425
+
1426
+ fn cloud_solar_transmittance(position: vec3<f32>) -> f32 {
1427
+ let sunDirection = normalize(-view.lightDir);
1428
+ let solarDistance = select(cloud.field.z, cloud.shadowProjection.x, cloud.shadowProjection.x > 0.0);
1429
+ let interval = cloud_layer_interval(position, sunDirection, solarDistance);
1430
+ if (interval.y <= interval.x) { return 1.0; }
1431
+ let steps = min(cloud.integration.z, 64u);
1432
+ let stepLength = (interval.y - interval.x) / max(1.0, f32(steps));
1433
+ var opticalDepth = 0.0;
1434
+ let solarJitter = cloud_solar_jitter(position);
1435
+ for (var index = 0u; index < 64u; index = index + 1u) {
1436
+ if (index >= steps) { break; }
1437
+ let samplePosition = position + sunDirection * (interval.x + (f32(index) + solarJitter) * stepLength);
1438
+ opticalDepth = opticalDepth + cloud_density(samplePosition) * ${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
1439
+ }
1440
+ return exp(-max(opticalDepth, 0.0));
1441
+ }
1442
+
1443
+ fn cloud_henyey_greenstein(cosTheta: f32, g: f32) -> f32 {
1444
+ let denominator = max(0.0001, 1.0 + g * g - 2.0 * g * cosTheta);
1445
+ return (1.0 - g * g) / (denominator * sqrt(denominator)) * 0.0795774715;
1446
+ }
1447
+
1448
+ fn cloud_phase(cosTheta: f32) -> f32 {
1449
+ // A bounded dual lobe gives a readable forward highlight and a small
1450
+ // back-scatter rim without another solar march.
1451
+ return cloud_henyey_greenstein(cosTheta, 0.55) * 0.8 +
1452
+ cloud_henyey_greenstein(cosTheta, -0.2) * 0.2;
1453
+ }
1454
+
1455
+ // Reuse the same remaining-path optical depth for two bounded scattering
1456
+ // orders. This adds arithmetic only; no extra density or light-ray samples.
1457
+ fn cloud_incident_light(solarT: f32, cosTheta: f32, height: f32) -> vec3<f32> {
1458
+ let direct = solarT * cloud_phase(cosTheta);
1459
+ let multiple = (0.5 * pow(solarT, 0.5) + 0.25 * pow(solarT, 0.25)) * 0.0795774715;
1460
+ let skyFill = mix(0.025, 0.075, smoothstep(0.1, 0.85, height));
1461
+ // Match volume-integrate: the shared DirectionalLight value uses the
1462
+ // unnormalized phase convention. Convert the normalized HG basis once.
1463
+ return cloud.sunRadiance.xyz *
1464
+ (vec3<f32>((direct + multiple) * 12.5663706144) + vec3<f32>(0.72, 0.84, 1.0) * skyFill);
1465
+ }
1466
+
1467
+ fn cloud_ray_jitter(pixel: vec2<f32>) -> f32 {
1468
+ // A stable per-pixel offset removes view-step contour bands without adding
1469
+ // a frame-varying noise source that would fight temporal reprojection.
1470
+ return fract(sin(dot(pixel, vec2<f32>(12.9898, 78.233))) * 43758.5453);
1471
+ }
1472
+
1473
+ fn cloud_solar_jitter(position: vec3<f32>) -> f32 {
1474
+ // Dither the light-column samples in world space so low shadow-step
1475
+ // profiles do not paint parallel contour bands across cloud bases.
1476
+ return fract(sin(dot(position.xz, vec2<f32>(19.193, 47.117))) * 15731.743);
1477
+ }
1478
+
1479
+ fn reconstruct_world(uv: vec2<f32>, depth: f32) -> vec3<f32> {
1480
+ let ndc = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
1481
+ let world = view.inverseViewProj * ndc;
1482
+ return world.xyz / max(abs(world.w), 0.00001);
1483
+ }
1484
+
1485
+ @vertex
1486
+ fn vs_main(@builtin(vertex_index) index: u32) -> CloudViewOut {
1487
+ var positions = array<vec2<f32>, 3>(
1488
+ vec2<f32>(-1.0, -1.0),
1489
+ vec2<f32>(3.0, -1.0),
1490
+ vec2<f32>(-1.0, 3.0),
1491
+ );
1492
+ var output: CloudViewOut;
1493
+ output.position = vec4<f32>(positions[index], 0.0, 1.0);
1494
+ // WebGPU clip-space Y grows upward while sampled framebuffer rows and the
1495
+ // depth texture use a top-left origin. Keep this one top-left UV for color,
1496
+ // depth and inverse-projection reconstruction; flipping only the final
1497
+ // color would leave the camera ray and depth cut mirrored.
1498
+ output.uv = vec2<f32>(
1499
+ positions[index].x * 0.5 + 0.5,
1500
+ 1.0 - (positions[index].y * 0.5 + 0.5),
1501
+ );
1502
+ return output;
1503
+ }
1504
+
1505
+ @fragment
1506
+ fn fs_main(input: CloudViewOut) -> @location(0) vec4<f32> {
1507
+ // The shadow producer uses this same production density/cache contract but
1508
+ // rasterizes a camera-independent, texel-snapped light-space map. It does
1509
+ // not consume scene colour; the generic fullscreen host still supplies the
1510
+ // input binding to keep the feature binding ABI single and validated.
1511
+ if (cloud.integration.w == 1u) {
1512
+ let projection = cloud.shadowProjection;
1513
+ if (projection.y < 0.5 || projection.z > 0.5 || projection.x <= 0.0) {
1514
+ return vec4<f32>(1.0);
1515
+ }
1516
+ let offset = (input.uv - vec2<f32>(0.5)) * projection.x;
1517
+ let receiver = cloud.shadowOrigin.xyz +
1518
+ cloud.shadowRight.xyz * offset.x + cloud.shadowUp.xyz * offset.y;
1519
+ let sunDirection = normalize(-view.lightDir);
1520
+ let interval = cloud_layer_interval(receiver, sunDirection, projection.x);
1521
+ if (interval.y <= interval.x) { return vec4<f32>(1.0); }
1522
+ let steps = min(cloud.integration.z, 64u);
1523
+ let stepLength = (interval.y - interval.x) / max(1.0, f32(steps));
1524
+ var opticalDepth = 0.0;
1525
+ var firstHeight = 1.0;
1526
+ var lastHeight = 0.0;
1527
+ var occupied = false;
1528
+ let solarJitter = cloud_solar_jitter(receiver);
1529
+ for (var index = 0u; index < 64u; index = index + 1u) {
1530
+ if (index >= steps) { break; }
1531
+ let samplePosition = receiver + sunDirection *
1532
+ (interval.x + (f32(index) + solarJitter) * stepLength);
1533
+ let localDensity = cloud_density(samplePosition);
1534
+ opticalDepth = opticalDepth + localDensity * ${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
1535
+ if (localDensity > 0.0001) {
1536
+ let sampleHeight = clamp(
1537
+ (samplePosition.y - cloud.layer.x) / max(0.0001, cloud.layer.y),
1538
+ 0.0,
1539
+ 1.0,
1540
+ );
1541
+ if (!occupied) { firstHeight = sampleHeight; }
1542
+ lastHeight = sampleHeight;
1543
+ occupied = true;
1544
+ }
1545
+ }
1546
+ let transmittance = exp(-max(opticalDepth, 0.0));
1547
+ // R stores full-column transmittance. G/B preserve the first/last
1548
+ // occupied normalized layer heights so interior samples can estimate the
1549
+ // remaining segment without pretending optical depth is uniform through
1550
+ // the whole layer. A stores the integrated optical depth and distinguishes
1551
+ // an empty projected column from a valid, nearly-clear one.
1552
+ return vec4<f32>(
1553
+ transmittance,
1554
+ select(1.0, firstHeight, occupied),
1555
+ select(0.0, lastHeight, occupied),
1556
+ select(0.0, opticalDepth, occupied),
1557
+ );
1558
+ }
1559
+ // The shadow producer branch above is per-fragment (its light-space
1560
+ // interval depends on the rasterized receiver), so derivative-based
1561
+ // sampling here would violate WGSL uniform-control-flow rules. The cloud
1562
+ // scene path already owns an explicit level-0 sampler contract.
1563
+ // Keep point semantics for the current full-resolution scene path. The
1564
+ // half-resolution transport contract is introduced with its own resolve
1565
+ // shader so this path retains the established depth convention.
1566
+ let depthPixel = vec2<i32>(input.position.xy);
1567
+ let depth = textureLoad(sceneDepth, depthPixel, 0);
1568
+ let farWorld = reconstruct_world(input.uv, 1.0);
1569
+ let sceneWorld = reconstruct_world(input.uv, clamp(depth, 0.0, 1.0));
1570
+ let ray = normalize(farWorld - view.cameraPos);
1571
+ let rayJitter = cloud_ray_jitter(input.position.xy);
1572
+ let sceneDistance = select(cloud.field.z, length(sceneWorld - view.cameraPos), depth < 0.999999);
1573
+ let interval = cloud_layer_interval(view.cameraPos, ray, sceneDistance);
1574
+
1575
+ // The depth producer shares the same camera ray, interval and density
1576
+ // contract as the display path. It writes a cloud representative world
1577
+ // position and a validity bit so history remains comparable after camera
1578
+ // translation or rotation and never uses an opaque scene distance as a
1579
+ // proxy for a participating medium.
1580
+ if (cloud.integration.w == 2u) {
1581
+ if (interval.y <= interval.x) { return vec4<f32>(sceneWorld, 0.0); }
1582
+ let depthSteps = min(cloud.integration.y, 64u);
1583
+ let depthStepLength = (interval.y - interval.x) / max(1.0, f32(depthSteps));
1584
+ var depthTransmittance = 1.0;
1585
+ var weightedDistance = 0.0;
1586
+ var opticalWeight = 0.0;
1587
+ for (var depthIndex = 0u; depthIndex < 64u; depthIndex = depthIndex + 1u) {
1588
+ if (depthIndex >= depthSteps) { break; }
1589
+ let sampleDistance = interval.x + (f32(depthIndex) + rayJitter) * depthStepLength;
1590
+ let samplePosition = view.cameraPos + ray * sampleDistance;
1591
+ let localDensity = cloud_density(samplePosition);
1592
+ let localTransmittance = exp(-max(localDensity * ${CLOUD_EXTINCTION_COEFFICIENT} * depthStepLength, 0.0));
1593
+ let weight = depthTransmittance * (1.0 - localTransmittance);
1594
+ weightedDistance = weightedDistance + sampleDistance * weight;
1595
+ opticalWeight = opticalWeight + weight;
1596
+ depthTransmittance = depthTransmittance * localTransmittance;
1597
+ }
1598
+ let hasCloud = opticalWeight > 0.00001;
1599
+ let representativeDistance = select(sceneDistance, weightedDistance / opticalWeight, hasCloud);
1600
+ let representativeWorld = view.cameraPos + ray * representativeDistance;
1601
+ return vec4<f32>(representativeWorld, select(0.0, 1.0, hasCloud));
1602
+ }
1603
+
1604
+ let scene = textureSampleLevel(sceneColor, sceneSampler, input.uv, 0.0);
1605
+ if (interval.y <= interval.x) { return scene; }
1606
+ let steps = min(cloud.integration.y, 64u);
1607
+ let stepLength = (interval.y - interval.x) / max(1.0, f32(steps));
1608
+ var transmittance = 1.0;
1609
+ var scattering = vec3<f32>(0.0);
1610
+ var weightedDistance = 0.0;
1611
+ var opticalWeight = 0.0;
1612
+ let sunDirection = normalize(-view.lightDir);
1613
+ for (var index = 0u; index < 64u; index = index + 1u) {
1614
+ if (index >= steps) { break; }
1615
+ let distance = interval.x + (f32(index) + rayJitter) * stepLength;
1616
+ let samplePosition = view.cameraPos + ray * distance;
1617
+ let localDensity = cloud_density(samplePosition);
1618
+ let extinction = localDensity * ${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
1619
+ let localTransmittance = exp(-max(extinction, 0.0));
1620
+ let depthWeight = transmittance * (1.0 - localTransmittance);
1621
+ weightedDistance = weightedDistance + distance * depthWeight;
1622
+ opticalWeight = opticalWeight + depthWeight;
1623
+ // Empty samples are common in the separated-cloud field. Avoid a nested
1624
+ // solar march for them and stop once the camera path is effectively opaque.
1625
+ if (localDensity > 0.0001 && transmittance > 0.01) {
1626
+ let solarTransmittance = cloud_solar_transmittance(samplePosition);
1627
+ let cloudHeight = clamp(
1628
+ (samplePosition.y - cloud.layer.x) / max(0.0001, cloud.layer.y), 0.0, 1.0,
1629
+ );
1630
+ scattering = scattering + cloud_incident_light(
1631
+ solarTransmittance, dot(ray, sunDirection), cloudHeight,
1632
+ ) * depthWeight;
1633
+ }
1634
+ transmittance = transmittance * localTransmittance;
1635
+ if (transmittance < 0.01) { break; }
1636
+ }
1637
+ let hasCloud = opticalWeight > 0.00001;
1638
+ let representativeDistance = select(sceneDistance, weightedDistance / opticalWeight, hasCloud);
1639
+ let representativeWorld = view.cameraPos + ray * representativeDistance;
1640
+ // Wind is an advective coordinate change. Reproject the cloud point into
1641
+ // the previous frame's world before applying the previous camera matrix;
1642
+ // authored field values remain immutable.
1643
+ let windDelta = cloud.wind.xyz * max(0.0, cloud.field.y - cloud.temporal.w);
1644
+ // cloud_density evaluates the authored field at position + wind*time.
1645
+ // Keeping the same field sample in the previous frame therefore advances
1646
+ // the previous world point by the elapsed wind delta as well.
1647
+ let previousCloudWorld = representativeWorld + windDelta;
1648
+ let previousCloudClip = view.temporalPreviousViewProj * vec4<f32>(previousCloudWorld, 1.0);
1649
+ let previousCloudNdc = previousCloudClip.xyz / max(abs(previousCloudClip.w), 0.00001);
1650
+ let previousCloudUv = vec2<f32>(
1651
+ previousCloudNdc.x * 0.5 + 0.5,
1652
+ 1.0 - (previousCloudNdc.y * 0.5 + 0.5),
1653
+ );
1654
+ let cloudInBounds = all(previousCloudUv >= vec2<f32>(0.0)) && all(previousCloudUv <= vec2<f32>(1.0));
1655
+ let cloudHistoryUv = clamp(previousCloudUv, vec2<f32>(0.0), vec2<f32>(1.0));
1656
+ let cloudHistoryDepthSample = textureSampleLevel(previousDepth, depthSampler, cloudHistoryUv, 0.0);
1657
+ let cloudHistoryWorld = cloudHistoryDepthSample.xyz;
1658
+ let previous = textureSampleLevel(previousRadiance, sceneSampler, cloudHistoryUv, 0.0);
1659
+ let previousTransport = textureSampleLevel(
1660
+ previousTransmittance,
1661
+ sceneSampler,
1662
+ cloudHistoryUv,
1663
+ 0.0,
1664
+ );
1665
+ let worldTolerance = max(0.05, length(previousCloudWorld - view.cameraPos) * 0.02);
1666
+ let depthAccepted = cloudHistoryDepthSample.a > 0.5 &&
1667
+ distance(cloudHistoryWorld, previousCloudWorld) <= worldTolerance;
1668
+ let accepted =
1669
+ hasCloud && cloud.temporal.x > 0.5 && cloud.temporal.y < 0.5 && cloudInBounds &&
1670
+ depthAccepted && previousTransport.a > 0.0;
1671
+ let historyWeight = select(0.0, clamp(cloud.temporal.z, 0.0, 0.95), accepted);
1672
+ let cloudRadiance = mix(scattering, previous.rgb, historyWeight);
1673
+ // Preserve scene-linear HDR output for the downstream material lane. The
1674
+ // history owner receives cloudRadiance separately through the history pass.
1675
+ return vec4<f32>(scene.rgb * transmittance + cloudRadiance, transmittance);
1676
+ }
1677
+
1678
+ // The light-space shadow producer already paid for a bounded column integral.
1679
+ // Reuse that texel for camera samples instead of starting a full solar march
1680
+ // for every occupied camera step. Outside the occupied interval the cached
1681
+ // full-column transmittance is exact for the projected column. Inside the
1682
+ // interval, the cache supplies the stable projection/bounds and a short
1683
+ // residual march resolves the non-uniform density that a linear optical-depth
1684
+ // interpolation would lose. Invalid/out-of-range projections retain the
1685
+ // analytic path as an explicit fallback.
1686
+ fn cloud_solar_cached_transmittance(position: vec3<f32>) -> f32 {
1687
+ let projection = cloud.shadowProjection;
1688
+ if (projection.y < 0.5 || projection.x <= 0.0) {
1689
+ return cloud_solar_transmittance(position);
1690
+ }
1691
+ let relative = position - cloud.shadowOrigin.xyz;
1692
+ let uv = vec2<f32>(
1693
+ dot(relative, cloud.shadowRight.xyz) / projection.x + 0.5,
1694
+ dot(relative, cloud.shadowUp.xyz) / projection.x + 0.5,
1695
+ );
1696
+ if (any(uv < vec2<f32>(0.0)) || any(uv > vec2<f32>(1.0))) {
1697
+ return cloud_solar_transmittance(position);
1698
+ }
1699
+ let cachedShadow = textureSampleLevel(previousRadiance, sceneSampler, uv, 0.0);
1700
+ let cachedOpticalDepth = max(cachedShadow.a, 0.0);
1701
+ let layerHeight = clamp(
1702
+ (position.y - cloud.layer.x) / max(0.0001, cloud.layer.y),
1703
+ 0.0,
1704
+ 1.0,
1705
+ );
1706
+ if (cachedOpticalDepth <= 0.0001) { return 1.0; }
1707
+ let firstHeight = clamp(cachedShadow.g, 0.0, 1.0);
1708
+ let lastHeight = clamp(cachedShadow.b, 0.0, 1.0);
1709
+ let occupiedMin = min(firstHeight, lastHeight);
1710
+ let occupiedMax = max(firstHeight, lastHeight);
1711
+ let sunDirection = normalize(-view.lightDir);
1712
+ // G/B are ordered along the light ray, not necessarily bottom-to-top. Only
1713
+ // use the cached full/empty fast paths when the light has enough vertical
1714
+ // component to make that ordering meaningful; grazing light stays on the
1715
+ // residual path so a low-sun column cannot invert the cache bounds.
1716
+ if (abs(sunDirection.y) > 0.2) {
1717
+ if (sunDirection.y > 0.0) {
1718
+ if (layerHeight <= occupiedMin) { return exp(-cachedOpticalDepth); }
1719
+ if (layerHeight >= occupiedMax) { return 1.0; }
1720
+ } else {
1721
+ if (layerHeight >= occupiedMax) { return exp(-cachedOpticalDepth); }
1722
+ if (layerHeight <= occupiedMin) { return 1.0; }
1723
+ }
1724
+ }
1725
+
1726
+ // A short residual march is only paid for samples inside the projected
1727
+ // cloud span. Vertical light uses twelve taps, oblique light sixteen, and
1728
+ // grazing light up to thirty-two because the same layer projects across a
1729
+ // much longer solar segment. This removes the large error caused by
1730
+ // assuming optical depth is uniform between the first and last occupied
1731
+ // heights while keeping the normal camera path far below a second full
1732
+ // solar integration.
1733
+ let solarDistance = select(cloud.field.z, projection.x, projection.x > 0.0);
1734
+ let interval = cloud_layer_interval(position, sunDirection, solarDistance);
1735
+ if (interval.y <= interval.x) { return 1.0; }
1736
+ let absLightY = abs(sunDirection.y);
1737
+ let directionBudget = select(
1738
+ 32u,
1739
+ select(16u, 12u, absLightY > 0.5),
1740
+ absLightY > 0.2,
1741
+ );
1742
+ let residualSteps = min(directionBudget, max(8u, cloud.integration.z * 3u));
1743
+ let stepLength = (interval.y - interval.x) / max(1.0, f32(residualSteps));
1744
+ var opticalDepth = 0.0;
1745
+ let solarJitter = cloud_solar_jitter(position);
1746
+ for (var index = 0u; index < 32u; index = index + 1u) {
1747
+ if (index >= residualSteps) { break; }
1748
+ let samplePosition = position + sunDirection *
1749
+ (interval.x + (f32(index) + solarJitter) * stepLength);
1750
+ opticalDepth = opticalDepth + cloud_density(samplePosition) *
1751
+ ${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
1752
+ }
1753
+ return exp(-max(opticalDepth, 0.0));
1754
+ }
1755
+
1756
+ struct CloudTransportOutput {
1757
+ @location(0) radiance: vec4<f32>,
1758
+ @location(1) transmittance: vec4<f32>,
1759
+ @location(2) depth: vec4<f32>,
1760
+ };
1761
+
1762
+ // One half-resolution transport pass writes all three reusable cloud fields.
1763
+ // The full-resolution resolve below reconstructs colour and applies temporal
1764
+ // history; no second full raymarch exists solely to obtain cloud depth.
1765
+ @fragment
1766
+ fn fs_transport(input: CloudViewOut) -> CloudTransportOutput {
1767
+ let depth = textureSampleLevel(sceneDepth, depthSampler, input.uv, 0u);
1768
+ let farWorld = reconstruct_world(input.uv, 1.0);
1769
+ let sceneWorld = reconstruct_world(input.uv, clamp(depth, 0.0, 1.0));
1770
+ let ray = normalize(farWorld - view.cameraPos);
1771
+ let rayJitter = cloud_ray_jitter(input.position.xy);
1772
+ let sceneDistance = select(cloud.field.z, length(sceneWorld - view.cameraPos), depth < 0.999999);
1773
+ let interval = cloud_layer_interval(view.cameraPos, ray, sceneDistance);
1774
+ var output: CloudTransportOutput;
1775
+ output.radiance = vec4<f32>(0.0, 0.0, 0.0, 0.0);
1776
+ output.transmittance = vec4<f32>(1.0, 1.0, 1.0, 1.0);
1777
+ output.depth = vec4<f32>(sceneWorld, 0.0);
1778
+ if (interval.y <= interval.x) { return output; }
1779
+
1780
+ let steps = min(cloud.integration.y, 64u);
1781
+ let stepLength = (interval.y - interval.x) / max(1.0, f32(steps));
1782
+ var transmittance = 1.0;
1783
+ var scattering = vec3<f32>(0.0);
1784
+ var weightedDistance = 0.0;
1785
+ var opticalWeight = 0.0;
1786
+ let sunDirection = normalize(-view.lightDir);
1787
+ for (var index = 0u; index < 64u; index = index + 1u) {
1788
+ if (index >= steps) { break; }
1789
+ let distance = interval.x + (f32(index) + rayJitter) * stepLength;
1790
+ let samplePosition = view.cameraPos + ray * distance;
1791
+ let localDensity = cloud_density(samplePosition);
1792
+ let extinction = localDensity * ${CLOUD_EXTINCTION_COEFFICIENT} * stepLength;
1793
+ let localTransmittance = exp(-max(extinction, 0.0));
1794
+ let depthWeight = transmittance * (1.0 - localTransmittance);
1795
+ weightedDistance = weightedDistance + distance * depthWeight;
1796
+ opticalWeight = opticalWeight + depthWeight;
1797
+ if (localDensity > 0.0001 && transmittance > 0.01) {
1798
+ let solarTransmittance = cloud_solar_cached_transmittance(samplePosition);
1799
+ let cloudHeight = clamp(
1800
+ (samplePosition.y - cloud.layer.x) / max(0.0001, cloud.layer.y), 0.0, 1.0,
1801
+ );
1802
+ scattering = scattering + cloud_incident_light(
1803
+ solarTransmittance, dot(ray, sunDirection), cloudHeight,
1804
+ ) * depthWeight;
1805
+ }
1806
+ transmittance = transmittance * localTransmittance;
1807
+ if (transmittance < 0.01) { break; }
1808
+ }
1809
+ let hasCloud = opticalWeight > 0.00001;
1810
+ let representativeDistance = select(sceneDistance, weightedDistance / max(opticalWeight, 0.00001), hasCloud);
1811
+ output.radiance = vec4<f32>(scattering, select(0.0, 1.0, hasCloud));
1812
+ output.transmittance = vec4<f32>(transmittance, transmittance, transmittance, 1.0);
1813
+ output.depth = vec4<f32>(view.cameraPos + ray * representativeDistance, select(0.0, 1.0, hasCloud));
1814
+ return output;
1815
+ }
1816
+ `
1817
+ );
1818
+ var CLOUD_TRANSPORT_FULLSCREEN_WGSL = CLOUD_VIEW_FULLSCREEN_WGSL;
1819
+ var CLOUD_TRANSPORT_ANALYTIC_FULLSCREEN_WGSL = CLOUD_VIEW_FULLSCREEN_WGSL.replace(
1820
+ "cloud_solar_cached_transmittance(samplePosition)",
1821
+ "cloud_solar_transmittance(samplePosition)"
1822
+ );
1823
+ var CLOUD_HISTORY_FULLSCREEN_WGSL = (
1824
+ /* wgsl */
1825
+ `
1826
+ struct CloudCameraView {
1827
+ worldViewProj: mat4x4<f32>,
1828
+ lightDir: vec3<f32>,
1829
+ lightColor: vec3<f32>,
1830
+ cameraPos: vec3<f32>,
1831
+ lightViewProj_A: mat4x4<f32>,
1832
+ inverseViewProj: mat4x4<f32>,
1833
+ lightViewProj_B: mat4x4<f32>,
1834
+ lightViewProj_C: mat4x4<f32>,
1835
+ lightViewProj_D: mat4x4<f32>,
1836
+ splitPlanes: array<vec4<f32>, 4>,
1837
+ cascadeCount: f32,
1838
+ cascadeBlend: f32,
1839
+ depthBias: f32,
1840
+ normalBias: f32,
1841
+ directionalShadowFilter: vec4<f32>,
1842
+ spotLightViewProj: array<mat4x4<f32>, 4>,
1843
+ temporalCurrentViewProj: mat4x4<f32>,
1844
+ temporalPreviousViewProj: mat4x4<f32>,
1845
+ temporalProjection: vec4<f32>,
1846
+ temporalPreviousCameraPos: vec4<f32>,
1847
+ ssrParams: vec4<f32>,
1848
+ };
1849
+
1850
+ struct CloudHistoryParams {
1851
+ temporal: vec4<f32>, // valid, reset, historyWeight, reserved
1852
+ windTime: vec4<f32>, // wind.xyz, elapsed seconds since previous submit
1853
+ };
1854
+
1855
+ @group(0) @binding(0) var<uniform> view: CloudCameraView;
1856
+ @group(1) @binding(0) var currentRadiance: texture_2d<f32>;
1857
+ @group(1) @binding(1) var sceneSampler: sampler;
1858
+ @group(1) @binding(2) var<uniform> params: CloudHistoryParams;
1859
+ @group(1) @binding(3) var sceneDepth: texture_depth_2d;
1860
+ @group(1) @binding(4) var depthSampler: sampler;
1861
+ @group(1) @binding(5) var currentCloudDepth: texture_2d<f32>;
1862
+ @group(1) @binding(6) var sceneBackground: texture_2d<f32>;
1863
+
1864
+ struct CloudHistoryInput {
1865
+ @builtin(position) position: vec4<f32>,
1866
+ @location(0) uv: vec2<f32>,
1867
+ };
1868
+
1869
+ struct CloudHistoryOutput {
1870
+ @location(0) radiance: vec4<f32>,
1871
+ @location(1) transmittance: vec4<f32>,
1872
+ };
1873
+
1874
+ fn reconstruct_world(uv: vec2<f32>, depth: f32) -> vec3<f32> {
1875
+ let ndc = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
1876
+ let world = view.inverseViewProj * ndc;
1877
+ return world.xyz / max(abs(world.w), 0.00001);
1878
+ }
1879
+
1880
+ @vertex
1881
+ fn vs_main(@builtin(vertex_index) index: u32) -> CloudHistoryInput {
1882
+ var positions = array<vec2<f32>, 3>(
1883
+ vec2<f32>(-1.0, -1.0),
1884
+ vec2<f32>(3.0, -1.0),
1885
+ vec2<f32>(-1.0, 3.0),
1886
+ );
1887
+ var output: CloudHistoryInput;
1888
+ output.position = vec4<f32>(positions[index], 0.0, 1.0);
1889
+ output.uv = vec2<f32>(
1890
+ positions[index].x * 0.5 + 0.5,
1891
+ 1.0 - (positions[index].y * 0.5 + 0.5),
1892
+ );
1893
+ return output;
1894
+ }
1895
+
1896
+ @fragment
1897
+ fn fs_main(input: CloudHistoryInput) -> CloudHistoryOutput {
1898
+ let current = textureSampleLevel(currentRadiance, sceneSampler, input.uv, 0.0);
1899
+ let background = textureSampleLevel(sceneBackground, sceneSampler, input.uv, 0.0);
1900
+ let cloudDepth = textureSampleLevel(currentCloudDepth, depthSampler, input.uv, 0.0);
1901
+ let currentValid = cloudDepth.a > 0.5;
1902
+ // currentRadiance is the scene-linear cloud composite. Remove the
1903
+ // unchanged scene background before writing history so the persistent owner
1904
+ // stores raw cloud radiance only. The view pass is the sole temporal blend
1905
+ // owner; blending here as well would accumulate history twice per frame.
1906
+ let currentCloudRadiance = max(current.rgb - background.rgb * current.a, vec3<f32>(0.0));
1907
+ let currentTransmittance = vec4<f32>(current.a, current.a, current.a, 1.0);
1908
+ var output: CloudHistoryOutput;
1909
+ output.radiance = vec4<f32>(currentCloudRadiance, select(0.0, 1.0, currentValid));
1910
+ output.transmittance = currentTransmittance;
1911
+ return output;
1912
+ }
1913
+ `
1914
+ );
1915
+ var CLOUD_RESOLVE_FULLSCREEN_WGSL = (
1916
+ /* wgsl */
1917
+ `
1918
+ struct CloudCameraView {
1919
+ worldViewProj: mat4x4<f32>,
1920
+ lightDir: vec3<f32>,
1921
+ lightColor: vec3<f32>,
1922
+ cameraPos: vec3<f32>,
1923
+ lightViewProj_A: mat4x4<f32>,
1924
+ inverseViewProj: mat4x4<f32>,
1925
+ lightViewProj_B: mat4x4<f32>,
1926
+ lightViewProj_C: mat4x4<f32>,
1927
+ lightViewProj_D: mat4x4<f32>,
1928
+ splitPlanes: array<vec4<f32>, 4>,
1929
+ cascadeCount: f32,
1930
+ cascadeBlend: f32,
1931
+ depthBias: f32,
1932
+ normalBias: f32,
1933
+ directionalShadowFilter: vec4<f32>,
1934
+ spotLightViewProj: array<mat4x4<f32>, 4>,
1935
+ temporalCurrentViewProj: mat4x4<f32>,
1936
+ temporalPreviousViewProj: mat4x4<f32>,
1937
+ temporalProjection: vec4<f32>,
1938
+ temporalPreviousCameraPos: vec4<f32>,
1939
+ ssrParams: vec4<f32>,
1940
+ };
1941
+
1942
+ struct CloudResolveParams {
1943
+ layer: vec4<f32>,
1944
+ field: vec4<f32>,
1945
+ wind: vec4<f32>,
1946
+ sunDirection: vec4<f32>,
1947
+ sunRadiance: vec4<f32>,
1948
+ integration: vec4<u32>,
1949
+ shadowOrigin: vec4<f32>,
1950
+ shadowRight: vec4<f32>,
1951
+ shadowUp: vec4<f32>,
1952
+ shadowProjection: vec4<f32>,
1953
+ temporal: vec4<f32>,
1954
+ };
1955
+
1956
+ @group(0) @binding(0) var<uniform> view: CloudCameraView;
1957
+ @group(1) @binding(0) var sceneColor: texture_2d<f32>;
1958
+ @group(1) @binding(1) var sceneSampler: sampler;
1959
+ @group(1) @binding(2) var<uniform> cloud: CloudResolveParams;
1960
+ @group(1) @binding(3) var sceneDepth: texture_depth_2d;
1961
+ @group(1) @binding(4) var depthSampler: sampler;
1962
+ @group(1) @binding(5) var currentRadiance: texture_2d<f32>;
1963
+ @group(1) @binding(6) var currentTransmittance: texture_2d<f32>;
1964
+ @group(1) @binding(7) var currentDepth: texture_2d<f32>;
1965
+ @group(1) @binding(8) var previousRadiance: texture_2d<f32>;
1966
+ @group(1) @binding(9) var previousTransmittance: texture_2d<f32>;
1967
+ @group(1) @binding(10) var previousDepth: texture_2d<f32>;
1968
+
1969
+ struct CloudResolveInput {
1970
+ @builtin(position) position: vec4<f32>,
1971
+ @location(0) uv: vec2<f32>,
1972
+ };
1973
+
1974
+ fn reconstruct_world(uv: vec2<f32>, depth: f32) -> vec3<f32> {
1975
+ let ndc = vec4<f32>(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, depth, 1.0);
1976
+ let world = view.inverseViewProj * ndc;
1977
+ return world.xyz / max(abs(world.w), 0.00001);
1978
+ }
1979
+
1980
+ @vertex
1981
+ fn vs_main(@builtin(vertex_index) index: u32) -> CloudResolveInput {
1982
+ var positions = array<vec2<f32>, 3>(
1983
+ vec2<f32>(-1.0, -1.0),
1984
+ vec2<f32>(3.0, -1.0),
1985
+ vec2<f32>(-1.0, 3.0),
1986
+ );
1987
+ var output: CloudResolveInput;
1988
+ output.position = vec4<f32>(positions[index], 0.0, 1.0);
1989
+ output.uv = vec2<f32>(
1990
+ positions[index].x * 0.5 + 0.5,
1991
+ 1.0 - (positions[index].y * 0.5 + 0.5),
1992
+ );
1993
+ return output;
1994
+ }
1995
+
1996
+ @fragment
1997
+ fn fs_main(input: CloudResolveInput) -> @location(0) vec4<f32> {
1998
+ let scene = textureSampleLevel(sceneColor, sceneSampler, input.uv, 0.0);
1999
+ // The resolve owns the full-resolution foreground test. Reject the current
2000
+ // low-resolution transport before any history or radiance is composited;
2001
+ // gating history alone still lets a newly sampled cloud bleed over geometry.
2002
+ let sceneDepthValue = textureLoad(sceneDepth, vec2<i32>(input.position.xy), 0);
2003
+ let sceneWorld = reconstruct_world(input.uv, clamp(sceneDepthValue, 0.0, 1.0));
2004
+ let sceneDistance = length(sceneWorld - view.cameraPos);
2005
+ let transport = textureSampleLevel(currentRadiance, sceneSampler, input.uv, 0.0);
2006
+ let transportT = textureSampleLevel(currentTransmittance, sceneSampler, input.uv, 0.0);
2007
+ // The transport depth is an rgba16float color attachment, not the scene's
2008
+ // depth texture. Sample it with the filtering color sampler; binding the
2009
+ // non-filtering scene-depth sampler here makes the shader contract depend on
2010
+ // an incompatible sampler/texture pair on strict WebGPU implementations.
2011
+ let cloudDepth = textureSampleLevel(currentDepth, sceneSampler, input.uv, 0.0);
2012
+ let currentValid = transport.a > 0.5 && cloudDepth.a > 0.5;
2013
+ let cloudDistance = length(cloudDepth.xyz - view.cameraPos);
2014
+ let foregroundAccepted = sceneDepthValue >= 0.999999 ||
2015
+ cloudDistance <= sceneDistance + max(0.05, sceneDistance * 0.01);
2016
+ if (!currentValid || !foregroundAccepted) { return scene; }
2017
+
2018
+ let previousClip = view.temporalPreviousViewProj * vec4<f32>(
2019
+ cloudDepth.xyz + cloud.wind.xyz * max(0.0, cloud.field.y - cloud.temporal.w),
2020
+ 1.0,
2021
+ );
2022
+ let previousNdc = previousClip.xyz / max(abs(previousClip.w), 0.00001);
2023
+ let previousUv = vec2<f32>(
2024
+ previousNdc.x * 0.5 + 0.5,
2025
+ 1.0 - (previousNdc.y * 0.5 + 0.5),
2026
+ );
2027
+ let inBounds = all(previousUv >= vec2<f32>(0.0)) && all(previousUv <= vec2<f32>(1.0));
2028
+ let historyUv = clamp(previousUv, vec2<f32>(0.0), vec2<f32>(1.0));
2029
+ let historyDepth = textureSampleLevel(previousDepth, sceneSampler, historyUv, 0.0);
2030
+ let previous = textureSampleLevel(previousRadiance, sceneSampler, historyUv, 0.0);
2031
+ let previousTransport = textureSampleLevel(previousTransmittance, sceneSampler, historyUv, 0.0);
2032
+ let dims = vec2<f32>(textureDimensions(previousRadiance));
2033
+ let texel = 1.0 / max(dims, vec2<f32>(1.0));
2034
+ // Clamp reprojected history against the current low-resolution transport
2035
+ // neighborhood. Using only previous-frame neighbors (and including the
2036
+ // previous center in both extrema) is a no-op and lets stale bright cloud
2037
+ // pixels leak across a newly exposed edge.
2038
+ let c0 = transport.rgb;
2039
+ let c1 = textureSampleLevel(currentRadiance, sceneSampler, clamp(input.uv + vec2<f32>(-texel.x, 0.0), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).rgb;
2040
+ let c2 = textureSampleLevel(currentRadiance, sceneSampler, clamp(input.uv + vec2<f32>(texel.x, 0.0), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).rgb;
2041
+ let c3 = textureSampleLevel(currentRadiance, sceneSampler, clamp(input.uv + vec2<f32>(0.0, -texel.y), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).rgb;
2042
+ let c4 = textureSampleLevel(currentRadiance, sceneSampler, clamp(input.uv + vec2<f32>(0.0, texel.y), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).rgb;
2043
+ let minRadiance = min(c0, min(min(c1, c2), min(c3, c4)));
2044
+ let maxRadiance = max(c0, max(max(c1, c2), max(c3, c4)));
2045
+ let t1 = textureSampleLevel(currentTransmittance, sceneSampler, clamp(input.uv + vec2<f32>(-texel.x, 0.0), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).r;
2046
+ let t2 = textureSampleLevel(currentTransmittance, sceneSampler, clamp(input.uv + vec2<f32>(texel.x, 0.0), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).r;
2047
+ let t3 = textureSampleLevel(currentTransmittance, sceneSampler, clamp(input.uv + vec2<f32>(0.0, -texel.y), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).r;
2048
+ let t4 = textureSampleLevel(currentTransmittance, sceneSampler, clamp(input.uv + vec2<f32>(0.0, texel.y), vec2<f32>(0.0), vec2<f32>(1.0)), 0.0).r;
2049
+ let minTransport = min(transportT.r, min(min(t1, t2), min(t3, t4)));
2050
+ let maxTransport = max(transportT.r, max(max(t1, t2), max(t3, t4)));
2051
+ let worldTolerance = max(0.05, distance(cloudDepth.xyz, view.cameraPos) * 0.02);
2052
+ let depthAccepted = historyDepth.a > 0.5 && distance(historyDepth.xyz, cloudDepth.xyz) <= worldTolerance;
2053
+ let accepted = cloud.temporal.x > 0.5 && cloud.temporal.y < 0.5 && inBounds && depthAccepted;
2054
+ let historyWeight = select(0.0, clamp(cloud.temporal.z, 0.0, 0.92), accepted);
2055
+ let stableHistory = clamp(previous.rgb, minRadiance, maxRadiance);
2056
+ let stableTransport = clamp(previousTransport.r, minTransport, maxTransport);
2057
+ let radiance = mix(transport.rgb, stableHistory, historyWeight);
2058
+ let transmittance = mix(transportT.r, stableTransport, historyWeight);
2059
+ return vec4<f32>(scene.rgb * transmittance + radiance, transmittance);
2060
+ }
2061
+ `
2062
+ );
2063
+ function makeCloudViewParams(frame, cache, mode = 0) {
2064
+ const params = new ArrayBuffer(CLOUD_VIEW_PARAMS_BYTES);
2065
+ const floats = new Float32Array(params);
2066
+ const integers = new Uint32Array(params);
2067
+ const profile = CLOUD_QUALITY_PROFILES[frame.params?.quality ?? "medium"];
2068
+ const authored = frame.params;
2069
+ floats[0] = authored?.baseHeight ?? 0;
2070
+ floats[1] = authored?.thickness ?? 1;
2071
+ floats[2] = authored?.scale ?? 1;
2072
+ floats[3] = authored?.density ?? 0;
2073
+ floats[4] = authored?.coverage ?? 1;
2074
+ floats[5] = frame.timeSeconds;
2075
+ floats[6] = mode === 1 ? authored?.shadowRange ?? 0 : cloudViewDistanceForQuality(authored?.quality ?? "medium");
2076
+ floats[7] = authored?.seed ?? 0;
2077
+ floats[8] = authored?.wind[0] ?? 0;
2078
+ floats[9] = authored?.wind[1] ?? 0;
2079
+ floats[10] = authored?.wind[2] ?? 0;
2080
+ floats[12] = frame.sunDirection?.[0] ?? 0;
2081
+ floats[13] = frame.sunDirection?.[1] ?? -1;
2082
+ floats[14] = frame.sunDirection?.[2] ?? 0;
2083
+ floats[15] = 0.2;
2084
+ floats[16] = frame.sunRadiance?.[0] ?? 0;
2085
+ floats[17] = frame.sunRadiance?.[1] ?? 0;
2086
+ floats[18] = frame.sunRadiance?.[2] ?? 0;
2087
+ integers[20] = cache.resolution;
2088
+ integers[21] = Math.max(1, Math.min(64, Math.floor(profile.viewSteps)));
2089
+ integers[22] = Math.max(1, Math.min(64, Math.floor(profile.shadowSteps)));
2090
+ integers[23] = mode;
2091
+ const projection = frame.shadowProjection;
2092
+ if (projection !== void 0) {
2093
+ floats[24] = projection.origin[0];
2094
+ floats[25] = projection.origin[1];
2095
+ floats[26] = projection.origin[2];
2096
+ floats[28] = projection.right[0];
2097
+ floats[29] = projection.right[1];
2098
+ floats[30] = projection.right[2];
2099
+ floats[32] = projection.up[0];
2100
+ floats[33] = projection.up[1];
2101
+ floats[34] = projection.up[2];
2102
+ floats[36] = projection.range;
2103
+ floats[37] = projection.lowSun ? 0 : 1;
2104
+ floats[38] = projection.lowSun ? 1 : 0;
2105
+ floats[39] = projection.texelSize;
2106
+ }
2107
+ const temporal = frame.temporal;
2108
+ floats[40] = temporal !== void 0 && temporal.reset === false ? 1 : 0;
2109
+ floats[41] = temporal?.reset === true ? 1 : 0;
2110
+ floats[42] = temporal === void 0 ? 0 : profile.historyWeight;
2111
+ floats[43] = temporal?.history?.temporal.timeSeconds ?? frame.timeSeconds;
2112
+ return new Uint8Array(params);
2113
+ }
2114
+ function planCloudDensity(frame, packedCache, context) {
2115
+ const cacheCount = packedCache.length;
2116
+ const params = new Uint32Array([cacheCount]);
2117
+ const inputTarget = context.targets.find((target) => target.name === "motion-input");
2118
+ const outputTarget = context.targets.find((target) => target.name === "motion-output");
2119
+ const historyRadiancePrevious = context.targets.find(
2120
+ (target) => target.name === "cloud-history-radiance-previous"
2121
+ );
2122
+ const historyTransmittancePrevious = context.targets.find(
2123
+ (target) => target.name === "cloud-history-transmittance-previous"
2124
+ );
2125
+ const historyDepthPrevious = context.targets.find(
2126
+ (target) => target.name === "cloud-history-depth-previous"
2127
+ );
2128
+ const historyRadianceCurrent = context.targets.find(
2129
+ (target) => target.name === "cloud-history-radiance-current"
2130
+ );
2131
+ const historyTransmittanceCurrent = context.targets.find(
2132
+ (target) => target.name === "cloud-history-transmittance-current"
2133
+ );
2134
+ const historyDepthCurrent = context.targets.find(
2135
+ (target) => target.name === "cloud-history-depth-current"
2136
+ );
2137
+ const hasHistoryTargets = historyRadiancePrevious !== void 0 && historyTransmittancePrevious !== void 0 && historyDepthPrevious !== void 0 && historyRadianceCurrent !== void 0 && historyTransmittanceCurrent !== void 0 && historyDepthCurrent !== void 0;
2138
+ const shadowTarget = context.targets.find((target) => target.name === "cloud-shadow");
2139
+ const canShadow = shadowTarget !== void 0 && shadowTarget.sampleCount === 1 && frame.shadowProjection !== void 0 && hasHistoryTargets;
2140
+ const canCompose = inputTarget !== void 0 && outputTarget !== void 0 && inputTarget.sampleCount === 1 && outputTarget.sampleCount === 1 && hasHistoryTargets;
2141
+ const transportReads = ["motion-input", { key: "depth", sampleType: "depth" }];
2142
+ const transportCacheReads = [
2143
+ "motion-input",
2144
+ "cloud-shadow",
2145
+ { key: "depth", sampleType: "depth" }
2146
+ ];
2147
+ const historyReads = [
2148
+ "motion-input",
2149
+ "cloud-history-radiance-previous",
2150
+ "cloud-history-transmittance-previous",
2151
+ "cloud-history-depth-previous",
2152
+ { key: "depth", sampleType: "depth" }
2153
+ ];
2154
+ const historyAdditionalTextures = [
2155
+ "cloud-history-radiance-previous",
2156
+ "cloud-history-transmittance-previous",
2157
+ "cloud-history-depth-previous"
2158
+ ];
2159
+ const viewParams = makeCloudViewParams(frame, frame.cache);
2160
+ const shadowParams = canShadow ? makeCloudViewParams(frame, frame.cache, 1) : void 0;
2161
+ const transportUsesShadowCache = canCompose && canShadow;
2162
+ return {
2163
+ resources: [
2164
+ {
2165
+ kind: "compute-program",
2166
+ name: CLOUD_LAYER_DENSITY_PROGRAM,
2167
+ program: {
2168
+ wgsl: CLOUD_DENSITY_COMPUTE_WGSL,
2169
+ entryPoints: ["cloud_density_cache"],
2170
+ bindings: [
2171
+ {
2172
+ label: "forgeax.cloud-layer.cache",
2173
+ entries: [
2174
+ { binding: 0, visibility: COMPUTE_STAGE, buffer: { type: "read-only-storage" } },
2175
+ { binding: 1, visibility: COMPUTE_STAGE, buffer: { type: "storage" } },
2176
+ { binding: 2, visibility: COMPUTE_STAGE, buffer: { type: "uniform" } }
2177
+ ]
2178
+ }
2179
+ ]
2180
+ }
2181
+ },
2182
+ {
2183
+ kind: "buffer",
2184
+ name: CLOUD_LAYER_DENSITY_CACHE,
2185
+ size: packedCache.byteLength,
2186
+ usage: ["storage", "copy-src"],
2187
+ data: packedCache
2188
+ },
2189
+ {
2190
+ kind: "buffer",
2191
+ name: CLOUD_LAYER_DENSITY_OUTPUT,
2192
+ size: packedCache.byteLength,
2193
+ usage: ["storage", "copy-src"]
2194
+ },
2195
+ {
2196
+ kind: "buffer",
2197
+ name: "cloud-layer-density-params",
2198
+ size: params.byteLength,
2199
+ usage: ["uniform"],
2200
+ data: params
2201
+ },
2202
+ {
2203
+ kind: "compute-bindings",
2204
+ name: CLOUD_LAYER_DENSITY_BINDINGS,
2205
+ program: CLOUD_LAYER_DENSITY_PROGRAM,
2206
+ entries: [
2207
+ { binding: 0, resource: CLOUD_LAYER_DENSITY_CACHE },
2208
+ { binding: 1, resource: CLOUD_LAYER_DENSITY_OUTPUT },
2209
+ { binding: 2, resource: "cloud-layer-density-params" }
2210
+ ]
2211
+ },
2212
+ {
2213
+ kind: "fullscreen-program",
2214
+ name: "cloud-layer-view",
2215
+ source: CLOUD_VIEW_FULLSCREEN_WGSL,
2216
+ usesView: true,
2217
+ storageBindings: [8],
2218
+ reads: historyReads,
2219
+ params: {
2220
+ byteSize: viewParams.byteLength,
2221
+ defaultValue: viewParams
2222
+ }
2223
+ },
2224
+ ...shadowParams === void 0 ? [] : [
2225
+ {
2226
+ kind: "fullscreen-program",
2227
+ name: "cloud-layer-shadow",
2228
+ source: CLOUD_VIEW_FULLSCREEN_WGSL,
2229
+ usesView: true,
2230
+ storageBindings: [8],
2231
+ reads: historyReads,
2232
+ params: {
2233
+ byteSize: shadowParams.byteLength,
2234
+ defaultValue: shadowParams
2235
+ }
2236
+ }
2237
+ ],
2238
+ ...canCompose ? [
2239
+ {
2240
+ kind: "fullscreen-program",
2241
+ name: "cloud-layer-transport",
2242
+ source: transportUsesShadowCache ? CLOUD_TRANSPORT_FULLSCREEN_WGSL : CLOUD_TRANSPORT_ANALYTIC_FULLSCREEN_WGSL,
2243
+ fragmentEntryPoint: "fs_transport",
2244
+ usesView: true,
2245
+ storageBindings: [8],
2246
+ reads: transportUsesShadowCache ? transportCacheReads : transportReads,
2247
+ params: {
2248
+ byteSize: viewParams.byteLength,
2249
+ defaultValue: viewParams
2250
+ }
2251
+ },
2252
+ {
2253
+ kind: "fullscreen-program",
2254
+ name: "cloud-layer-resolve",
2255
+ source: CLOUD_RESOLVE_FULLSCREEN_WGSL,
2256
+ usesView: true,
2257
+ reads: [
2258
+ "motion-input",
2259
+ "cloud-history-radiance-current",
2260
+ "cloud-history-transmittance-current",
2261
+ "cloud-history-depth-current",
2262
+ "cloud-history-radiance-previous",
2263
+ "cloud-history-transmittance-previous",
2264
+ "cloud-history-depth-previous",
2265
+ { key: "depth", sampleType: "depth" }
2266
+ ],
2267
+ params: {
2268
+ byteSize: viewParams.byteLength,
2269
+ defaultValue: viewParams
2270
+ }
2271
+ }
2272
+ ] : [],
2273
+ ...canCompose ? [
2274
+ {
2275
+ kind: "graphics-program",
2276
+ name: "cloud-layer-transport-pipeline",
2277
+ program: {
2278
+ shader: "cloud-layer-transport",
2279
+ vertexLayout: "none",
2280
+ colorFormats: [
2281
+ historyRadianceCurrent.format,
2282
+ historyTransmittanceCurrent.format,
2283
+ historyDepthCurrent.format
2284
+ ],
2285
+ sampleCount: 1,
2286
+ renderState: {
2287
+ depthWriteEnabled: false,
2288
+ depthCompare: "always"
2289
+ }
2290
+ }
2291
+ },
2292
+ {
2293
+ kind: "graphics-bindings",
2294
+ name: "cloud-layer-transport-view-bindings",
2295
+ program: "cloud-layer-transport-pipeline",
2296
+ values: { group: 0, view: true }
2297
+ },
2298
+ {
2299
+ kind: "graphics-bindings",
2300
+ name: "cloud-layer-transport-bindings",
2301
+ program: "cloud-layer-transport-pipeline",
2302
+ values: {
2303
+ group: 1,
2304
+ fullscreen: true,
2305
+ shader: "cloud-layer-transport",
2306
+ input: "motion-input",
2307
+ depth: "depth",
2308
+ additionalTextures: transportUsesShadowCache ? ["cloud-shadow"] : [],
2309
+ storageBuffers: [CLOUD_LAYER_DENSITY_OUTPUT]
2310
+ },
2311
+ logicalTargets: { input: "motion-input" }
2312
+ },
2313
+ {
2314
+ kind: "graphics-program",
2315
+ name: "cloud-layer-resolve-pipeline",
2316
+ program: {
2317
+ shader: "cloud-layer-resolve",
2318
+ vertexLayout: "none",
2319
+ colorFormats: [outputTarget.format],
2320
+ sampleCount: 1,
2321
+ renderState: {
2322
+ depthWriteEnabled: false,
2323
+ depthCompare: "always"
2324
+ }
2325
+ }
2326
+ },
2327
+ {
2328
+ kind: "graphics-bindings",
2329
+ name: "cloud-layer-resolve-view-bindings",
2330
+ program: "cloud-layer-resolve-pipeline",
2331
+ values: { group: 0, view: true }
2332
+ },
2333
+ {
2334
+ kind: "graphics-bindings",
2335
+ name: "cloud-layer-resolve-bindings",
2336
+ program: "cloud-layer-resolve-pipeline",
2337
+ values: {
2338
+ group: 1,
2339
+ fullscreen: true,
2340
+ shader: "cloud-layer-resolve",
2341
+ input: "motion-input",
2342
+ depth: "depth",
2343
+ additionalTextures: [
2344
+ "cloud-history-radiance-current",
2345
+ "cloud-history-transmittance-current",
2346
+ "cloud-history-depth-current",
2347
+ "cloud-history-radiance-previous",
2348
+ "cloud-history-transmittance-previous",
2349
+ "cloud-history-depth-previous"
2350
+ ]
2351
+ }
2352
+ }
2353
+ ] : [],
2354
+ ...canShadow ? [
2355
+ {
2356
+ kind: "graphics-program",
2357
+ name: "cloud-layer-shadow-pipeline",
2358
+ program: {
2359
+ shader: "cloud-layer-shadow",
2360
+ vertexLayout: "none",
2361
+ colorFormats: [shadowTarget.format],
2362
+ sampleCount: 1,
2363
+ renderState: {
2364
+ depthWriteEnabled: false,
2365
+ depthCompare: "always",
2366
+ blend: {
2367
+ color: {
2368
+ srcFactor: "one",
2369
+ dstFactor: "zero",
2370
+ operation: "add"
2371
+ },
2372
+ alpha: {
2373
+ srcFactor: "one",
2374
+ dstFactor: "zero",
2375
+ operation: "add"
2376
+ }
2377
+ }
2378
+ }
2379
+ }
2380
+ },
2381
+ {
2382
+ kind: "graphics-bindings",
2383
+ name: "cloud-layer-shadow-view-bindings",
2384
+ program: "cloud-layer-shadow-pipeline",
2385
+ values: { group: 0, view: true }
2386
+ },
2387
+ {
2388
+ kind: "graphics-bindings",
2389
+ name: "cloud-layer-shadow-bindings",
2390
+ program: "cloud-layer-shadow-pipeline",
2391
+ values: {
2392
+ group: 1,
2393
+ fullscreen: true,
2394
+ shader: "cloud-layer-shadow",
2395
+ input: false,
2396
+ depth: "depth",
2397
+ // The shadow branch's WGSL keeps the shared depth binding
2398
+ // shape, but its light-space path never samples scene depth.
2399
+ // Bind the renderer-owned far-depth fallback instead of
2400
+ // introducing a read-before-main graph dependency.
2401
+ depthFallback: true,
2402
+ additionalTextures: historyAdditionalTextures,
2403
+ storageBuffers: [CLOUD_LAYER_DENSITY_OUTPUT]
2404
+ }
2405
+ }
2406
+ ] : []
2407
+ ],
2408
+ passes: context.caps.compute && context.caps.storageBuffer ? [
2409
+ {
2410
+ kind: "compute",
2411
+ name: "cloud-layer-density-cache",
2412
+ program: CLOUD_LAYER_DENSITY_PROGRAM,
2413
+ bindings: CLOUD_LAYER_DENSITY_BINDINGS,
2414
+ dispatches: [
2415
+ {
2416
+ kind: "direct",
2417
+ entryPoint: "cloud_density_cache",
2418
+ workgroups: [Math.max(1, Math.ceil(cacheCount / CLOUD_WORKGROUP_SIZE))]
2419
+ }
2420
+ ]
2421
+ },
2422
+ ...canShadow ? [
2423
+ {
2424
+ kind: "raster",
2425
+ name: "cloud-layer-shadow",
2426
+ colorAttachments: [
2427
+ {
2428
+ target: "cloud-shadow",
2429
+ loadOp: "clear",
2430
+ storeOp: "store"
2431
+ }
2432
+ ],
2433
+ sampledTargets: [
2434
+ "cloud-history-radiance-previous",
2435
+ "cloud-history-transmittance-previous",
2436
+ "cloud-history-depth-previous"
2437
+ ],
2438
+ draws: [
2439
+ {
2440
+ program: "cloud-layer-shadow-pipeline",
2441
+ bindings: [
2442
+ "cloud-layer-shadow-view-bindings",
2443
+ "cloud-layer-shadow-bindings"
2444
+ ],
2445
+ vertexData: [],
2446
+ vertexLayout: "none",
2447
+ draw: { kind: "draw", vertexCount: 3, instanceCount: 1 }
2448
+ }
2449
+ ]
2450
+ }
2451
+ ] : [],
2452
+ ...canCompose ? [
2453
+ {
2454
+ kind: "raster",
2455
+ name: "cloud-layer-transport",
2456
+ colorAttachments: [
2457
+ {
2458
+ target: "cloud-history-radiance-current",
2459
+ loadOp: "clear",
2460
+ storeOp: "store"
2461
+ },
2462
+ {
2463
+ target: "cloud-history-transmittance-current",
2464
+ loadOp: "clear",
2465
+ storeOp: "store"
2466
+ },
2467
+ {
2468
+ target: "cloud-history-depth-current",
2469
+ loadOp: "clear",
2470
+ storeOp: "store"
2471
+ }
2472
+ ],
2473
+ sampledTargets: [
2474
+ "motion-input",
2475
+ "depth",
2476
+ ...transportUsesShadowCache ? ["cloud-shadow"] : []
2477
+ ],
2478
+ draws: [
2479
+ {
2480
+ program: "cloud-layer-transport-pipeline",
2481
+ bindings: [
2482
+ "cloud-layer-transport-view-bindings",
2483
+ "cloud-layer-transport-bindings"
2484
+ ],
2485
+ vertexData: [],
2486
+ vertexLayout: "none",
2487
+ draw: { kind: "draw", vertexCount: 3, instanceCount: 1 }
2488
+ }
2489
+ ]
2490
+ },
2491
+ {
2492
+ kind: "raster",
2493
+ name: "cloud-layer-resolve",
2494
+ colorAttachments: [
2495
+ {
2496
+ target: "motion-output",
2497
+ loadOp: "clear",
2498
+ storeOp: "store"
2499
+ }
2500
+ ],
2501
+ sampledTargets: [
2502
+ "motion-input",
2503
+ "depth",
2504
+ "cloud-history-radiance-current",
2505
+ "cloud-history-transmittance-current",
2506
+ "cloud-history-depth-current",
2507
+ "cloud-history-radiance-previous",
2508
+ "cloud-history-transmittance-previous",
2509
+ "cloud-history-depth-previous"
2510
+ ],
2511
+ draws: [
2512
+ {
2513
+ program: "cloud-layer-resolve-pipeline",
2514
+ bindings: [
2515
+ "cloud-layer-resolve-view-bindings",
2516
+ "cloud-layer-resolve-bindings"
2517
+ ],
2518
+ vertexData: [],
2519
+ vertexLayout: "none",
2520
+ draw: { kind: "draw", vertexCount: 3, instanceCount: 1 }
2521
+ }
2522
+ ]
2523
+ }
2524
+ ] : []
2525
+ ] : []
2526
+ };
2527
+ }
2528
+ function createCloudLayerFeature(options = {}) {
2529
+ let cachedSourceKey;
2530
+ let cached;
2531
+ let packedCache;
2532
+ let generation = 0;
2533
+ const histories = new CloudHistoryStore();
2534
+ let temporalResetCount = 0;
2535
+ let lastTemporalReset;
2536
+ return Object.freeze({
2537
+ identity: CLOUD_LAYER_FEATURE_IDENTITY,
2538
+ placement: "scene",
2539
+ requiredCapabilities: ["compute", "storageBuffer", "rgba16floatRenderable"],
2540
+ // Cloud programs are generated by the feature and have no user-authored
2541
+ // manifest entry to await. Use the immediate module path so first-frame
2542
+ // feature admission does not publish a transient pending-shader failure;
2543
+ // pipeline creation remains the validation boundary for this WGSL.
2544
+ shaderModuleMode: "immediate",
2545
+ extract: (context) => {
2546
+ if (options.enabled === false) {
2547
+ histories.reset();
2548
+ return ok({
2549
+ params: void 0,
2550
+ cache: void 0,
2551
+ sourceKey: void 0,
2552
+ timeSeconds: 0,
2553
+ sunDirection: void 0,
2554
+ sunRadiance: void 0,
2555
+ shadowProjection: void 0,
2556
+ temporal: void 0,
2557
+ generation,
2558
+ inspection: inspectCloudLayer({ authored: false })
2559
+ });
2560
+ }
2561
+ const world = context.worlds[context.owner];
2562
+ if (world === void 0) {
2563
+ histories.reset();
2564
+ return ok({
2565
+ params: void 0,
2566
+ cache: void 0,
2567
+ sourceKey: void 0,
2568
+ timeSeconds: 0,
2569
+ sunDirection: void 0,
2570
+ sunRadiance: void 0,
2571
+ shadowProjection: void 0,
2572
+ temporal: void 0,
2573
+ generation,
2574
+ inspection: inspectCloudLayer({ authored: false })
2575
+ });
2576
+ }
2577
+ const cloud = context.frame?.cloudLayer;
2578
+ const capability = context.caps === void 0 ? void 0 : cloudCapabilitiesFromRhi(context.caps);
2579
+ if (cloud === void 0) {
2580
+ histories.reset();
2581
+ return ok({
2582
+ params: void 0,
2583
+ cache: void 0,
2584
+ sourceKey: void 0,
2585
+ timeSeconds: 0,
2586
+ sunDirection: void 0,
2587
+ sunRadiance: void 0,
2588
+ shadowProjection: void 0,
2589
+ temporal: void 0,
2590
+ generation,
2591
+ inspection: inspectCloudLayer({
2592
+ authored: false,
2593
+ ...capability === void 0 ? {} : { capability }
2594
+ })
2595
+ });
2596
+ }
2597
+ const formationKey = cloudLayerFormationKey(cloud.params);
2598
+ if (cached === void 0 || cachedSourceKey !== formationKey) {
2599
+ cached = buildCloudDensityCache(cloud.params);
2600
+ packedCache = packCloudDensityCache(cached);
2601
+ cachedSourceKey = formationKey;
2602
+ generation += 1;
2603
+ }
2604
+ const view = context.frame?.view;
2605
+ const shadowProjection = view === void 0 || cloud.sunDirection === void 0 ? void 0 : createCloudShadowProjection({
2606
+ center: view.shadowAnchor ?? view.cameraPosition,
2607
+ sunDirection: cloud.sunDirection,
2608
+ range: cloud.params.shadowRange,
2609
+ resolution: cloudShadowResolutionForQuality(cloud.params.quality)
2610
+ });
2611
+ const temporalSignature = view === void 0 ? void 0 : {
2612
+ sourceKey: cloud.sourceKey,
2613
+ viewId: view.identity,
2614
+ authoringGeneration: generation,
2615
+ cloudShadowRevision: shadowProjection?.revision ?? 0,
2616
+ cameraRevision: view.cameraRevision,
2617
+ deviceGeneration: view.deviceGeneration,
2618
+ width: view.width,
2619
+ height: view.height,
2620
+ timeSeconds: cloud.worldTimeSeconds,
2621
+ quality: cloud.params.quality,
2622
+ ...view.sceneDepthVersion === void 0 ? {} : { sceneDepthVersion: view.sceneDepthVersion }
2623
+ };
2624
+ const temporal = temporalSignature === void 0 ? void 0 : (() => {
2625
+ const decision = histories.begin(temporalSignature, {
2626
+ ...view?.cameraCut === void 0 ? {} : { cameraCut: view.cameraCut },
2627
+ ...view?.recovery === void 0 ? {} : { recovery: view.recovery }
2628
+ });
2629
+ const history = decision.history ?? createCloudHistory(temporalSignature, generation, false);
2630
+ return Object.freeze({
2631
+ signature: temporalSignature,
2632
+ history,
2633
+ reset: decision.reset,
2634
+ reasons: decision.reasons
2635
+ });
2636
+ })();
2637
+ const resourceFacts = inspectCloudLayerResources({
2638
+ generation,
2639
+ cache: cached,
2640
+ ...shadowProjection === void 0 ? {} : { shadow: shadowProjection },
2641
+ ...temporal?.history === void 0 ? {} : { history: temporal.history }
2642
+ });
2643
+ const temporalReset = temporal?.reasons.at(-1);
2644
+ if (temporal?.reset === true) temporalResetCount += 1;
2645
+ if (temporalReset !== void 0) lastTemporalReset = temporalReset;
2646
+ return ok({
2647
+ params: cloud.params,
2648
+ cache: cached,
2649
+ sourceKey: cloud.sourceKey,
2650
+ timeSeconds: cloud.worldTimeSeconds,
2651
+ sunDirection: cloud.sunDirection,
2652
+ sunRadiance: cloud.sunRadiance,
2653
+ shadowProjection,
2654
+ temporal,
2655
+ generation,
2656
+ inspection: inspectCloudLayer({
2657
+ authored: true,
2658
+ sourceKey: cloud.sourceKey,
2659
+ generation,
2660
+ resourceFacts,
2661
+ ...shadowProjection === void 0 ? {} : { shadowRevision: shadowProjection.revision },
2662
+ ...temporal === void 0 ? {} : {
2663
+ temporalResets: temporalResetCount,
2664
+ ...lastTemporalReset === void 0 ? {} : { lastTemporalReset }
2665
+ },
2666
+ ...capability === void 0 ? {} : { capability },
2667
+ budget: {
2668
+ quality: cloud.params.quality,
2669
+ reason: "physical GPU timing and residency receipts are unavailable until a prepared adapter submits them"
2670
+ }
2671
+ })
2672
+ });
2673
+ },
2674
+ onFrameSubmitted: (frame) => {
2675
+ if (frame.temporal?.history !== void 0) {
2676
+ histories.commit(
2677
+ frame.temporal.signature,
2678
+ createCloudHistory(frame.temporal.signature, frame.generation, true)
2679
+ );
2680
+ }
2681
+ },
2682
+ onFrameAborted: (frame) => {
2683
+ },
2684
+ plan: (frame, context) => {
2685
+ if (frame.cache === void 0 || packedCache === void 0 || frame.params === void 0) {
2686
+ return ok({ resources: [], passes: [] });
2687
+ }
2688
+ return ok(planCloudDensity(frame, packedCache, context));
2689
+ }
2690
+ });
2691
+ }
2692
+ var ERROR_POLICY = {
2693
+ "surface-abi-missing": {
2694
+ expected: "a producer-published single-layer-medium ABI is present",
2695
+ hint: "compose, reflect, and publish the Surface model before preparing the draw"
2696
+ },
2697
+ "surface-pass-missing": {
2698
+ expected: "the published model contains both nearest-layer and color passes",
2699
+ hint: "recook the complete model publication; do not synthesize a missing pass"
2700
+ },
2701
+ "surface-producer-not-ready": {
2702
+ expected: "scene-index, resources, and dynamic input facts are ready for the published model",
2703
+ hint: "repair the owning producer publication and retry; capability fallback does not cover stale assets"
2704
+ },
2705
+ "surface-generation-stale": {
2706
+ expected: "prepared Surface facts belong to the current device generation",
2707
+ hint: "rebuild the prepared surface resources for the current device generation"
2708
+ }
2709
+ };
2710
+ function failure(code, pass, owner, actual) {
2711
+ return {
2712
+ code,
2713
+ expected: ERROR_POLICY[code].expected,
2714
+ hint: ERROR_POLICY[code].hint,
2715
+ pass,
2716
+ owner,
2717
+ ...actual === void 0 ? {} : { actual }
2718
+ };
2719
+ }
2720
+ function capabilityFallback(caps) {
2721
+ if (!caps.compute) return "compute";
2722
+ if (!caps.storageBuffer) return "storageBuffer";
2723
+ if (!caps.indirectDrawing) return "indirectDrawing";
2724
+ return void 0;
2725
+ }
2726
+ function admitSingleLayerMediumSubmission(input) {
2727
+ const firstPass = "nearest-layer";
2728
+ const surface = input.abi.surface;
2729
+ if (surface === void 0 || surface.model !== "single-layer-medium") {
2730
+ return err(failure("surface-abi-missing", firstPass, "surface-producer", surface));
2731
+ }
2732
+ if (!surface.passes.includes("nearest-layer") || !surface.passes.includes("color")) {
2733
+ return err(failure("surface-pass-missing", firstPass, "surface-producer", surface.passes));
2734
+ }
2735
+ if (input.deviceGeneration !== input.preparedDeviceGeneration) {
2736
+ return err(
2737
+ failure("surface-generation-stale", firstPass, "render-generation", {
2738
+ deviceGeneration: input.deviceGeneration,
2739
+ preparedDeviceGeneration: input.preparedDeviceGeneration
2740
+ })
2741
+ );
2742
+ }
2743
+ const dynamicRequired = surface.dynamicInput !== void 0;
2744
+ if (!input.sceneIndexReady || !input.resourcesReady || dynamicRequired && !input.dynamicInputReady) {
2745
+ return err(
2746
+ failure("surface-producer-not-ready", firstPass, "surface-producer", {
2747
+ sceneIndexReady: input.sceneIndexReady,
2748
+ resourcesReady: input.resourcesReady,
2749
+ dynamicInputReady: input.dynamicInputReady
2750
+ })
2751
+ );
2752
+ }
2753
+ const fallback = capabilityFallback(input.caps);
2754
+ const admitted = fallback === void 0 ? [
2755
+ { pass: firstPass, lane: "gpu-driven" },
2756
+ { pass: "color", lane: "gpu-driven" }
2757
+ ] : [
2758
+ { pass: firstPass, lane: "direct", reason: fallback },
2759
+ { pass: "color", lane: "direct", reason: fallback }
2760
+ ];
2761
+ return ok({
2762
+ model: "single-layer-medium",
2763
+ lane: fallback === void 0 ? "gpu-driven" : "direct",
2764
+ passes: admitted,
2765
+ deviceGeneration: input.deviceGeneration
2766
+ });
2767
+ }
2768
+ function invalidStyle(input, component, field, value, expected) {
2769
+ return err(new PointsLinesInvalidStyleError({ entity: input.entity, component, field, value, expected }));
2770
+ }
2771
+ function topologyMismatch(input, submesh, expected, actual) {
2772
+ return err(new PointsLinesTopologyMismatchError({ entity: input.entity, submesh, expected, actual }));
2773
+ }
2774
+ function checkStyle(input) {
2775
+ const hasPoints = input.points !== void 0;
2776
+ const hasLines = input.lines !== void 0;
2777
+ if (hasPoints && hasLines) {
2778
+ return invalidStyle(
2779
+ input,
2780
+ "Points/Lines",
2781
+ "components",
2782
+ "Points + Lines",
2783
+ "exactly one style component"
2784
+ );
2785
+ }
2786
+ if (!hasPoints && !hasLines) {
2787
+ return invalidStyle(input, "Points/Lines", "components", void 0, "one style component");
2788
+ }
2789
+ if (hasPoints) {
2790
+ const sizePx = input.points?.sizePx ?? 4;
2791
+ const shapeValue = input.points?.shape ?? PointShapeValue.square;
2792
+ const shape = pointShapeFromU32(shapeValue);
2793
+ if (!Number.isFinite(sizePx) || sizePx <= 0) {
2794
+ return invalidStyle(input, "Points", "sizePx", sizePx, "finite sizePx > 0");
2795
+ }
2796
+ if (shape === void 0) {
2797
+ return invalidStyle(input, "Points", "shape", shapeValue, "shape is 'square' or 'circle'");
2798
+ }
2799
+ return ok({ component: "Points", shape, sizePx });
2800
+ }
2801
+ const widthPx = input.lines?.widthPx ?? 1;
2802
+ if (!Number.isFinite(widthPx) || widthPx <= 0) {
2803
+ return invalidStyle(input, "Lines", "widthPx", widthPx, "finite widthPx > 0");
2804
+ }
2805
+ return ok({ component: "Lines", widthPx });
2806
+ }
2807
+ function checkTopology(input, component) {
2808
+ const expected = component === "Points" ? "point-list" : "line-list";
2809
+ let pointCount = 0;
2810
+ let segmentCount = 0;
2811
+ const acceptedSubmeshes = [];
2812
+ let sawNonEmpty = false;
2813
+ for (const [submeshIndex, submesh] of input.mesh.submeshes.entries()) {
2814
+ const indexed = input.mesh.indices !== void 0 && submesh.indexCount > 0;
2815
+ const elementCount = indexed ? submesh.indexCount : submesh.vertexCount;
2816
+ if (elementCount === 0) continue;
2817
+ sawNonEmpty = true;
2818
+ if (component === "Lines" && submesh.topology === "line-strip") {
2819
+ return err(
2820
+ new PointsLinesStyleUnsupportedError({
2821
+ lane: "admission",
2822
+ field: "topology",
2823
+ member: "line-strip",
2824
+ supported: ["line-list"]
2825
+ })
2826
+ );
2827
+ }
2828
+ if (submesh.topology !== expected) {
2829
+ return topologyMismatch(input, submeshIndex, expected, submesh.topology);
2830
+ }
2831
+ if (component === "Lines" && elementCount % 2 !== 0) {
2832
+ return topologyMismatch(input, submeshIndex, "line-list pairs", "line-list odd tail");
2833
+ }
2834
+ acceptedSubmeshes.push(submeshIndex);
2835
+ if (component === "Points") pointCount += elementCount;
2836
+ else segmentCount += elementCount / 2;
2837
+ }
2838
+ if (!sawNonEmpty) return topologyMismatch(input, 0, expected, "empty-range");
2839
+ return ok({ pointCount, segmentCount, submeshes: acceptedSubmeshes });
2840
+ }
2841
+ function checkMaterial(input) {
2842
+ const passes = input.material.passes ?? [];
2843
+ const unlitModules = /* @__PURE__ */ new Set(["forgeax_material::unlit", "forgeax::default-unlit"]);
2844
+ if (passes.length !== 1 || passes[0] === void 0) {
2845
+ const pass2 = passes.find(
2846
+ (candidate) => candidate.name === "deferred" || candidate.name === "shadow-caster"
2847
+ ) ?? passes[0];
2848
+ return err(
2849
+ new PointsLinesMaterialUnsupportedError({
2850
+ entity: input.entity,
2851
+ material: input.materialId ?? "<anonymous>",
2852
+ pass: pass2?.name ?? "<empty>",
2853
+ module: pass2?.program.module ?? "<empty>",
2854
+ reason: "points-lines admission requires one unlit forward pass"
2855
+ })
2856
+ );
2857
+ }
2858
+ const pass = passes[0];
2859
+ const lightMode = pass.renderState?.tags?.LightMode;
2860
+ if (!unlitModules.has(pass.program.module) || pass.name !== "forward" || lightMode === "ShadowCaster") {
2861
+ return err(
2862
+ new PointsLinesMaterialUnsupportedError({
2863
+ entity: input.entity,
2864
+ material: input.materialId ?? "<anonymous>",
2865
+ pass: pass.name,
2866
+ module: pass.program.module,
2867
+ reason: "points-lines admission requires an engine-owned unlit forward pass"
2868
+ })
2869
+ );
2870
+ }
2871
+ return ok(void 0);
2872
+ }
2873
+ function admitPointsLines(input) {
2874
+ const style = checkStyle(input);
2875
+ if (!style.ok) return style;
2876
+ const topology = checkTopology(input, style.value.component);
2877
+ if (!topology.ok) return topology;
2878
+ const material = checkMaterial(input);
2879
+ if (!material.ok) return material;
2880
+ if (style.value.component === "Points" && input.limits?.maxPoints !== void 0) {
2881
+ if (topology.value.pointCount > input.limits.maxPoints) {
2882
+ return err(
2883
+ new PointsLinesBudgetExceededError({
2884
+ lane: "admission",
2885
+ requested: topology.value.pointCount,
2886
+ limit: input.limits.maxPoints,
2887
+ unit: "points"
2888
+ })
2889
+ );
2890
+ }
2891
+ }
2892
+ if (style.value.component === "Lines" && input.limits?.maxSegments !== void 0) {
2893
+ if (topology.value.segmentCount > input.limits.maxSegments) {
2894
+ return err(
2895
+ new PointsLinesBudgetExceededError({
2896
+ lane: "admission",
2897
+ requested: topology.value.segmentCount,
2898
+ limit: input.limits.maxSegments,
2899
+ unit: "segments"
2900
+ })
2901
+ );
2902
+ }
2903
+ }
2904
+ return ok({ ...style.value, ...topology.value });
2905
+ }
2906
+
2907
+ // src/point-shadow-inspection.ts
2908
+ function inspectPointShadow(snapshots, shadowAtlasCapacity) {
2909
+ const requested = snapshots.length;
2910
+ const admitted = snapshots.reduce(
2911
+ (count, snapshot) => snapshot.shadowAtlasLayer >= 0 && snapshot.shadowAtlasLayer < shadowAtlasCapacity ? count + 1 : count,
2912
+ 0
2913
+ );
2914
+ const status = requested === 0 ? "inactive" : requested > shadowAtlasCapacity || admitted !== requested ? "over-budget" : "ready";
2915
+ return Object.freeze({
2916
+ status,
2917
+ requested,
2918
+ admitted,
2919
+ // A point shadow only samples an atlas after it owns an atlas layer. Keep
2920
+ // these names explicit for AI diagnostics while deriving both from the
2921
+ // one layer projection above.
2922
+ shadowed: admitted,
2923
+ shadowAtlasOccupancy: admitted,
2924
+ shadowAtlasCapacity
2925
+ });
2926
+ }
2927
+ var SSR_FORMAT_PROFILE = "r32float-mip-sampled-storage";
2928
+ var SSR_FORMAT_STAGES = Object.freeze([
2929
+ "texture-create",
2930
+ "mip-view",
2931
+ "sampled-storage-bind-group",
2932
+ "pipeline-bind",
2933
+ "finish",
2934
+ "submit",
2935
+ "completion",
2936
+ "readback"
2937
+ ]);
2938
+ var ZERO_WORK = Object.freeze({
2939
+ attachmentCount: 0,
2940
+ passCount: 0,
2941
+ bindingCount: 0,
2942
+ resourceCount: 0,
2943
+ historyCount: 0,
2944
+ temporalDemand: 0
2945
+ });
2946
+ var ADMITTED_WORK = Object.freeze({
2947
+ attachmentCount: 1,
2948
+ passCount: 1,
2949
+ bindingCount: 1,
2950
+ resourceCount: 1,
2951
+ historyCount: 0,
2952
+ temporalDemand: 0
2953
+ });
2954
+ var ZERO_BUDGET = Object.freeze({
2955
+ probeExecutions: 0,
2956
+ resetCount: 0,
2957
+ rebuildCount: 0
2958
+ });
2959
+ var ADMITTED_BUDGET = Object.freeze({
2960
+ probeExecutions: 1,
2961
+ resetCount: 0,
2962
+ rebuildCount: 0
2963
+ });
2964
+ function identityFieldMismatch(expected, actual) {
2965
+ for (const field of ["sourceHead", "sourceTree", "lockSha256", "buildSha256"]) {
2966
+ if (expected[field] !== actual[field]) return field;
2967
+ }
2968
+ return void 0;
2969
+ }
2970
+ function blocked(identity, failure3, generation = 0) {
2971
+ return Object.freeze({
2972
+ status: "fallback-only",
2973
+ identity,
2974
+ generation,
2975
+ work: ZERO_WORK,
2976
+ budget: ZERO_BUDGET,
2977
+ failure: failure3
2978
+ });
2979
+ }
2980
+ function failure2(code, owner, hint, detail = {}) {
2981
+ const action = recoveryActionFor(code);
2982
+ return Object.freeze({
2983
+ code,
2984
+ expected: "all producer, format, and temporal receipts share one admitted identity",
2985
+ hint,
2986
+ detail: Object.freeze({ owner, action, ...detail })
2987
+ });
2988
+ }
2989
+ function recoveryActionFor(code) {
2990
+ switch (code) {
2991
+ case "ssr-reflection-fallback-unavailable":
2992
+ case "ssr-receipt-identity-mismatch":
2993
+ return "rebuild";
2994
+ case "ssr-not-requested":
2995
+ case "ssr-format-unavailable":
2996
+ case "ssr-temporal-unavailable":
2997
+ case "ssr-receipt-stale":
2998
+ return "retry";
2999
+ }
3000
+ }
3001
+ function receiptFailure(input, receipt, owner) {
3002
+ const field = identityFieldMismatch(input.identity, receipt.identity);
3003
+ if (field === void 0) return void 0;
3004
+ return blocked(
3005
+ input.identity,
3006
+ failure2("ssr-receipt-identity-mismatch", owner, "rebuild the owner receipt for this identity", {
3007
+ identityField: field
3008
+ })
3009
+ );
3010
+ }
3011
+ function validFallback(receipt) {
3012
+ return receipt !== void 0 && receipt.candidateVisible === false && Number.isFinite(receipt.coverage) && receipt.coverage >= 0 && receipt.coverage <= 1 && (receipt.sourceKey === void 0 || receipt.sourceKey.length > 0) && (receipt.extent === void 0 || receipt.extent.length === 3 && receipt.extent.every((value) => Number.isFinite(value) && value > 0)) && Number.isInteger(receipt.sourceGeneration) && receipt.sourceGeneration >= 0 && Number.isInteger(receipt.projectionGeneration) && receipt.projectionGeneration >= 0 && Number.isInteger(receipt.deviceGeneration) && receipt.deviceGeneration >= 0 && receipt.brdfSignature === "standard-pbr-ibl-v1" && (receipt.source === "probe" && (receipt.state === "active" || receipt.state === "lkg") || receipt.source === "skylight" && (receipt.state === "active" || receipt.state === "lkg") || receipt.source === "neutral" && receipt.state === "neutral");
3013
+ }
3014
+ function validFormat(receipt) {
3015
+ if (receipt?.profile !== SSR_FORMAT_PROFILE || receipt.verdict !== "admitted" || receipt.evidence !== "real" || !Number.isInteger(receipt.deviceGeneration) || receipt.deviceGeneration < 0) {
3016
+ return false;
3017
+ }
3018
+ if (!SSR_FORMAT_STAGES.every(
3019
+ (stage) => receipt.stages.some((entry) => entry.stage === stage && entry.verdict === "admitted")
3020
+ )) {
3021
+ return false;
3022
+ }
3023
+ return validateR32FloatReceipt(receipt).ok;
3024
+ }
3025
+ function validTemporal(receipt) {
3026
+ return receipt?.successfulSubmit === true && Number.isInteger(receipt.generation) && receipt.generation >= 0;
3027
+ }
3028
+ function admitSsrM0(input) {
3029
+ if (!input.requested) {
3030
+ return blocked(
3031
+ input.identity,
3032
+ failure2("ssr-not-requested", "consumer", "request SSR before evaluating M0 admission")
3033
+ );
3034
+ }
3035
+ if (input.reflectionFallback === void 0) {
3036
+ return blocked(
3037
+ input.identity,
3038
+ failure2("ssr-reflection-fallback-unavailable", "producer", "use LKG, Skylight, or neutral")
3039
+ );
3040
+ }
3041
+ const fallbackIdentityFailure = receiptFailure(input, input.reflectionFallback, "producer");
3042
+ if (fallbackIdentityFailure !== void 0) return fallbackIdentityFailure;
3043
+ if (!validFallback(input.reflectionFallback)) {
3044
+ return blocked(
3045
+ input.identity,
3046
+ failure2(
3047
+ "ssr-reflection-fallback-unavailable",
3048
+ "producer",
3049
+ "inspect and rebuild the producer receipt"
3050
+ ),
3051
+ input.reflectionFallback.projectionGeneration
3052
+ );
3053
+ }
3054
+ if (input.format === void 0) {
3055
+ return blocked(
3056
+ input.identity,
3057
+ failure2("ssr-format-unavailable", "format", "probe the exact format profile for this device"),
3058
+ input.reflectionFallback.projectionGeneration
3059
+ );
3060
+ }
3061
+ const formatIdentityFailure = receiptFailure(input, input.format, "format");
3062
+ if (formatIdentityFailure !== void 0) return formatIdentityFailure;
3063
+ if (!validFormat(input.format)) {
3064
+ return blocked(
3065
+ input.identity,
3066
+ failure2("ssr-format-unavailable", "format", "complete the full r32float profile probe"),
3067
+ input.reflectionFallback.projectionGeneration
3068
+ );
3069
+ }
3070
+ const temporalIdentityFailure = input.temporal === void 0 ? void 0 : receiptFailure(input, input.temporal, "temporal");
3071
+ if (temporalIdentityFailure !== void 0) return temporalIdentityFailure;
3072
+ if (input.temporal !== void 0 && !validTemporal(input.temporal)) {
3073
+ return blocked(
3074
+ input.identity,
3075
+ failure2("ssr-temporal-unavailable", "temporal", "inspect and rebuild the temporal receipt"),
3076
+ input.reflectionFallback.projectionGeneration
3077
+ );
3078
+ }
3079
+ if (input.previousGeneration !== void 0 && input.previousGeneration !== input.reflectionFallback.projectionGeneration) {
3080
+ return blocked(
3081
+ input.identity,
3082
+ failure2(
3083
+ "ssr-receipt-stale",
3084
+ "consumer",
3085
+ "discard stale completion and reinspect the owners",
3086
+ {
3087
+ generation: input.reflectionFallback.projectionGeneration
3088
+ }
3089
+ ),
3090
+ input.reflectionFallback.projectionGeneration
3091
+ );
3092
+ }
3093
+ return Object.freeze({
3094
+ status: "admitted",
3095
+ identity: input.identity,
3096
+ generation: input.reflectionFallback.projectionGeneration,
3097
+ work: ADMITTED_WORK,
3098
+ budget: ADMITTED_BUDGET
3099
+ });
3100
+ }
3101
+ function projectSsrDependencies(input) {
3102
+ const identity = input.identity;
3103
+ const reflectionFallback = identity === void 0 || input.reflectionFallback?.identity === void 0 ? void 0 : input.reflectionFallback;
3104
+ const format = identity === void 0 || input.format === void 0 ? void 0 : { ...input.format, identity };
3105
+ const temporal = identity === void 0 || input.temporal === void 0 ? void 0 : { identity, ...input.temporal };
3106
+ const admission = identity === void 0 ? blocked(
3107
+ void 0,
3108
+ failure2(
3109
+ input.requested ? "ssr-receipt-identity-mismatch" : "ssr-not-requested",
3110
+ "consumer",
3111
+ input.requested ? "bind the exact source, tree, lock, and build identity before admission" : "request SSR before evaluating M0 admission",
3112
+ input.requested ? { identityField: "sourceHead" } : {}
3113
+ )
3114
+ ) : admitSsrM0({
3115
+ requested: input.requested,
3116
+ identity,
3117
+ ...reflectionFallback === void 0 ? {} : { reflectionFallback },
3118
+ ...format === void 0 ? {} : { format },
3119
+ ...temporal === void 0 ? {} : { temporal },
3120
+ ...input.previousGeneration === void 0 ? {} : { previousGeneration: input.previousGeneration }
3121
+ });
3122
+ return Object.freeze({
3123
+ status: admission.status,
3124
+ identity,
3125
+ requested: input.requested,
3126
+ reflectionFallback: input.reflectionFallback,
3127
+ format,
3128
+ temporal,
3129
+ admission,
3130
+ work: admission.work,
3131
+ budget: admission.budget,
3132
+ failure: admission.failure
3133
+ });
3134
+ }
3135
+ function zeroSsrAdmissionWork() {
3136
+ return ZERO_WORK;
3137
+ }
3138
+ function resolveSsrAdmissionGeneration(previousGeneration, nextGeneration) {
3139
+ return {
3140
+ changed: previousGeneration !== void 0 && nextGeneration !== void 0 && previousGeneration !== nextGeneration,
3141
+ generation: nextGeneration
3142
+ };
3143
+ }
3144
+ var ZERO_SPATIAL_WORK = Object.freeze({
3145
+ attachmentCount: 0,
3146
+ passCount: 0,
3147
+ bindingCount: 0,
3148
+ resourceCount: 0,
3149
+ historyCount: 0,
3150
+ temporalDemand: 0
3151
+ });
3152
+ var ADMITTED_SPATIAL_WORK = Object.freeze({
3153
+ attachmentCount: 3,
3154
+ passCount: 4,
3155
+ bindingCount: 4,
3156
+ resourceCount: 3,
3157
+ historyCount: 0,
3158
+ temporalDemand: 0
3159
+ });
3160
+ function configRange(field, viewRange, maxDistance) {
3161
+ switch (field) {
3162
+ case "maxDistance":
3163
+ return { min: 0, minInclusive: false, max: viewRange, maxInclusive: true };
3164
+ case "thickness":
3165
+ return { min: 0, minInclusive: false, max: maxDistance, maxInclusive: true };
3166
+ case "maxRoughness":
3167
+ return { min: 0, minInclusive: true, max: 1, maxInclusive: true };
3168
+ }
3169
+ }
3170
+ function configError(field, value, range) {
3171
+ const left = range.minInclusive ? "[" : "(";
3172
+ const right = range.maxInclusive ? "]" : ")";
3173
+ return Object.freeze({
3174
+ code: "ssr-config-invalid",
3175
+ expected: `finite ${field} in ${left}${range.min}, ${range.max}${right}`,
3176
+ hint: `set ${field} to a finite value inside the reported range before retrying`,
3177
+ detail: Object.freeze({ field, value, range: Object.freeze(range) })
3178
+ });
3179
+ }
3180
+ function validateScreenSpaceReflection(config, input) {
3181
+ const viewRange = input.viewRange;
3182
+ if (!Number.isFinite(viewRange) || viewRange <= 0) {
3183
+ return err(
3184
+ configError("maxDistance", config.maxDistance, configRange("maxDistance", viewRange, 0))
3185
+ );
3186
+ }
3187
+ if (!Number.isFinite(config.maxDistance) || config.maxDistance <= 0 || config.maxDistance > viewRange) {
3188
+ return err(
3189
+ configError(
3190
+ "maxDistance",
3191
+ config.maxDistance,
3192
+ configRange("maxDistance", viewRange, config.maxDistance)
3193
+ )
3194
+ );
3195
+ }
3196
+ if (!Number.isFinite(config.thickness) || config.thickness <= 0 || config.thickness > config.maxDistance) {
3197
+ return err(
3198
+ configError(
3199
+ "thickness",
3200
+ config.thickness,
3201
+ configRange("thickness", viewRange, config.maxDistance)
3202
+ )
3203
+ );
3204
+ }
3205
+ if (!Number.isFinite(config.maxRoughness) || config.maxRoughness < 0 || config.maxRoughness > 1) {
3206
+ return err(
3207
+ configError(
3208
+ "maxRoughness",
3209
+ config.maxRoughness,
3210
+ configRange("maxRoughness", viewRange, config.maxDistance)
3211
+ )
3212
+ );
3213
+ }
3214
+ return ok(
3215
+ Object.freeze({
3216
+ maxDistance: config.maxDistance,
3217
+ thickness: config.thickness,
3218
+ maxRoughness: config.maxRoughness
3219
+ })
3220
+ );
3221
+ }
3222
+ function unavailable(lane, reason, required, actual) {
3223
+ const recovery = reason === "lane-unsupported" || reason === "projection-unsupported" ? "select a perspective clustered or deferred Standard lane" : reason === "reflection-fallback-unavailable" ? "repair the submitted reflection fallback owner receipt" : reason === "temporal-unavailable" ? "submit the matching temporal-v1 producer receipt" : reason === "recovery-unavailable" ? "complete device and producer recovery before retrying" : "restore the required scene, capability, or format input before retrying";
3224
+ return Object.freeze({
3225
+ code: "ssr-unavailable",
3226
+ expected: "SSR spatial admission has a complete Standard input and capability set",
3227
+ hint: recovery,
3228
+ detail: Object.freeze({
3229
+ lane,
3230
+ reason,
3231
+ required: Object.freeze([...required]),
3232
+ actual: Object.freeze({ ...actual }),
3233
+ recovery
3234
+ })
3235
+ });
3236
+ }
3237
+ function unavailableResult(lane, viewRange, failure3) {
3238
+ return Object.freeze({
3239
+ status: "fallback-only",
3240
+ lane,
3241
+ config: void 0,
3242
+ viewRange,
3243
+ work: ZERO_SPATIAL_WORK,
3244
+ failure: failure3
3245
+ });
3246
+ }
3247
+ function admitSsrSpatial(input) {
3248
+ const camera = input.camera;
3249
+ const environment = input.environment;
3250
+ const viewRange = camera.far - camera.near;
3251
+ const config = camera.screenSpaceReflection;
3252
+ if (config === void 0) {
3253
+ return Object.freeze({
3254
+ status: "not-requested",
3255
+ lane: environment.lane,
3256
+ config: void 0,
3257
+ viewRange,
3258
+ work: ZERO_SPATIAL_WORK
3259
+ });
3260
+ }
3261
+ const valid = validateScreenSpaceReflection(config, { viewRange });
3262
+ if (!valid.ok) {
3263
+ return Object.freeze({
3264
+ status: "fallback-only",
3265
+ lane: environment.lane,
3266
+ config,
3267
+ viewRange,
3268
+ work: ZERO_SPATIAL_WORK,
3269
+ failure: valid.error
3270
+ });
3271
+ }
3272
+ if (environment.m0.status !== "admitted") {
3273
+ return unavailableResult(
3274
+ environment.lane,
3275
+ viewRange,
3276
+ unavailable(
3277
+ environment.lane,
3278
+ "reflection-fallback-unavailable",
3279
+ ["admitted M0 dependency receipt"],
3280
+ { m0: false }
3281
+ )
3282
+ );
3283
+ }
3284
+ if (environment.lane !== "clustered" && environment.lane !== "deferred") {
3285
+ return unavailableResult(
3286
+ environment.lane,
3287
+ viewRange,
3288
+ unavailable(environment.lane, "lane-unsupported", ["clustered or deferred Standard lane"], {
3289
+ lane: environment.lane
3290
+ })
3291
+ );
3292
+ }
3293
+ if (camera.projection !== "perspective") {
3294
+ return unavailableResult(
3295
+ environment.lane,
3296
+ viewRange,
3297
+ unavailable(environment.lane, "projection-unsupported", ["perspective camera"], {
3298
+ projection: camera.projection
3299
+ })
3300
+ );
3301
+ }
3302
+ if (!environment.sceneInputs) {
3303
+ return unavailableResult(
3304
+ environment.lane,
3305
+ viewRange,
3306
+ unavailable(
3307
+ environment.lane,
3308
+ "scene-input-unavailable",
3309
+ ["deferred Standard scene/material/fallback inputs"],
3310
+ { sceneInputs: false }
3311
+ )
3312
+ );
3313
+ }
3314
+ if (!environment.temporal) {
3315
+ return unavailableResult(
3316
+ environment.lane,
3317
+ viewRange,
3318
+ unavailable(environment.lane, "temporal-unavailable", ["temporal-v1"], { temporal: false })
3319
+ );
3320
+ }
3321
+ if (!environment.reflectionFallback) {
3322
+ return unavailableResult(
3323
+ environment.lane,
3324
+ viewRange,
3325
+ unavailable(
3326
+ environment.lane,
3327
+ "reflection-fallback-unavailable",
3328
+ ["submitted BRDF-projected fallback"],
3329
+ { reflectionFallback: false }
3330
+ )
3331
+ );
3332
+ }
3333
+ if (environment.recovery === false) {
3334
+ return unavailableResult(
3335
+ environment.lane,
3336
+ viewRange,
3337
+ unavailable(environment.lane, "recovery-unavailable", ["ready device and owners"], {
3338
+ recovery: false
3339
+ })
3340
+ );
3341
+ }
3342
+ const capabilities = environment.capabilities;
3343
+ if (!capabilities.compute || !capabilities.storageTexture || !capabilities.rgba16floatRenderable) {
3344
+ return unavailableResult(
3345
+ environment.lane,
3346
+ viewRange,
3347
+ unavailable(
3348
+ environment.lane,
3349
+ "capability-unavailable",
3350
+ ["compute", "storageTexture", "rgba16floatRenderable"],
3351
+ {
3352
+ compute: capabilities.compute,
3353
+ storageTexture: capabilities.storageTexture,
3354
+ rgba16floatRenderable: capabilities.rgba16floatRenderable
3355
+ }
3356
+ )
3357
+ );
3358
+ }
3359
+ if (!capabilities.r32floatSampledStorage) {
3360
+ return unavailableResult(
3361
+ environment.lane,
3362
+ viewRange,
3363
+ unavailable(
3364
+ environment.lane,
3365
+ "format-unavailable",
3366
+ ["r32float sampled and storage mip chain"],
3367
+ { r32floatSampledStorage: false }
3368
+ )
3369
+ );
3370
+ }
3371
+ return Object.freeze({
3372
+ status: "admitted",
3373
+ lane: environment.lane,
3374
+ config: valid.value,
3375
+ viewRange,
3376
+ work: ADMITTED_SPATIAL_WORK
3377
+ });
3378
+ }
3379
+
3380
+ // src/volume/inspection.ts
3381
+ function resourceStage2(input) {
3382
+ if (!input.authored || input.recovery === void 0) return "none";
3383
+ if (input.recovery.status === "candidate") return "candidate";
3384
+ if (input.recovery.status === "recovering") return "recovering";
3385
+ if (input.recovery.status === "degraded") return "lkg";
3386
+ return "accepted";
3387
+ }
3388
+ function inspectVolumetricFog(input) {
3389
+ const recovery = input.recovery;
3390
+ const status = !input.authored ? "off" : input.capability === "unavailable" ? "unavailable" : input.degraded === true || recovery?.status === "degraded" ? "degraded" : "available";
3391
+ return Object.freeze({
3392
+ ownerCount: 0,
3393
+ status,
3394
+ resourceStage: resourceStage2(input),
3395
+ guid: recovery?.guid,
3396
+ generation: recovery?.generation,
3397
+ digest: input.acceptedDigest,
3398
+ candidateGeneration: recovery?.candidateGeneration,
3399
+ candidateDigest: recovery?.candidateDigest,
3400
+ lkgGeneration: recovery?.lkgGeneration,
3401
+ candidateFailure: recovery?.candidateFailure,
3402
+ deviceEpoch: recovery?.deviceEpoch ?? 0,
3403
+ format: input.format,
3404
+ passCount: input.passCount ?? 0,
3405
+ sampleCount: input.sampleCount ?? 0,
3406
+ memoryBytes: input.memoryBytes ?? 0,
3407
+ resourceFacts: input.resourceFacts,
3408
+ selectedLight: recovery?.selectedLight,
3409
+ candidateSelectedLight: recovery?.candidateSelectedLight
3410
+ });
3411
+ }
3412
+ var ERROR_POLICY2 = {
3413
+ "invalid-schema": {
3414
+ expected: "the dynamic input schema has one derived bounded record layout",
3415
+ hint: "repair the root MaterialSurface dynamicInput declaration and recook it"
3416
+ },
3417
+ "invalid-record": {
3418
+ expected: "each dynamic record matches the derived scalar/vector fields",
3419
+ hint: "write finite values using the field names and types declared by the material"
3420
+ },
3421
+ "range-overflow": {
3422
+ expected: "the instance range stays within the declared read-only page",
3423
+ hint: "reduce the range or increase the producer page budget before publication"
3424
+ },
3425
+ "domain-overflow": {
3426
+ expected: "the page stays within its declared number of producer domains",
3427
+ hint: "reuse an existing domain or publish a larger bounded declaration"
3428
+ },
3429
+ "not-uploaded": {
3430
+ expected: "the current page revision was uploaded before frame consumption",
3431
+ hint: "submit the page upload receipt and retry the same frame"
3432
+ },
3433
+ "stale-generation": {
3434
+ expected: "the range, upload, and device use one current page generation",
3435
+ hint: "discard stale addresses and rebuild the producer projection for the current device"
3436
+ },
3437
+ released: {
3438
+ expected: "the read-only page is still attached to its owning Render lifecycle",
3439
+ hint: "create a new page after releasing the old owner"
3440
+ }
3441
+ };
3442
+ var DynamicInputError = class extends Error {
3443
+ code;
3444
+ expected;
3445
+ hint;
3446
+ detail;
3447
+ constructor(code, detail) {
3448
+ super(`${code}: ${ERROR_POLICY2[code].expected}`);
3449
+ this.name = "DynamicInputError";
3450
+ this.code = code;
3451
+ this.expected = ERROR_POLICY2[code].expected;
3452
+ this.hint = ERROR_POLICY2[code].hint;
3453
+ this.detail = Object.freeze({ ...detail });
3454
+ }
3455
+ };
3456
+ function alignRange(ranges, start, end) {
3457
+ if (start >= end) return;
3458
+ let index = 0;
3459
+ while (index < ranges.length && (ranges[index]?.byteEnd ?? 0) < start) index += 1;
3460
+ while (index < ranges.length) {
3461
+ const existing = ranges[index];
3462
+ if (existing === void 0 || existing.byteStart > end) break;
3463
+ start = Math.min(start, existing.byteStart);
3464
+ end = Math.max(end, existing.byteEnd);
3465
+ ranges.splice(index, 1);
3466
+ }
3467
+ ranges.splice(index, 0, { byteStart: start, byteEnd: end });
3468
+ }
3469
+ function fieldWidth(type) {
3470
+ switch (type) {
3471
+ case "f32":
3472
+ case "u32":
3473
+ return 1;
3474
+ case "vec2<f32>":
3475
+ return 2;
3476
+ case "vec3<f32>":
3477
+ return 3;
3478
+ case "vec4<f32>":
3479
+ return 4;
3480
+ }
3481
+ }
3482
+ function finiteNumber(value) {
3483
+ return typeof value === "number" && Number.isFinite(value);
3484
+ }
3485
+ function invalid(code, operation, expected, actual, field, page) {
3486
+ return new DynamicInputError(code, {
3487
+ operation,
3488
+ expected,
3489
+ ...actual === void 0 ? {} : { actual },
3490
+ ...field === void 0 ? {} : { field },
3491
+ ...page === void 0 ? {} : page
3492
+ });
3493
+ }
3494
+ var ReadonlyDynamicInputPage = class _ReadonlyDynamicInputPage {
3495
+ sourceId;
3496
+ pageId;
3497
+ schema;
3498
+ layout;
3499
+ bytes;
3500
+ contentRevisionValue = 1;
3501
+ bufferGenerationValue = 1;
3502
+ deviceGenerationValue = 1;
3503
+ uploadedRevisionValue = 0;
3504
+ lastUploadedBytesValue = 0;
3505
+ released = false;
3506
+ ownedRanges = /* @__PURE__ */ new WeakSet();
3507
+ ownedUploads = /* @__PURE__ */ new WeakSet();
3508
+ dirty = [
3509
+ {
3510
+ byteStart: 0,
3511
+ byteEnd: 0
3512
+ }
3513
+ ];
3514
+ domains = /* @__PURE__ */ new Map();
3515
+ constructor(sourceId, pageId, schema, layout) {
3516
+ this.sourceId = sourceId;
3517
+ this.pageId = pageId;
3518
+ this.schema = schema;
3519
+ this.layout = layout;
3520
+ this.bytes = new Uint8Array(schema.maxPageBytes);
3521
+ this.dirty.length = 0;
3522
+ }
3523
+ static create(input) {
3524
+ if (input.sourceId.length === 0 || !Number.isSafeInteger(input.pageId) || input.pageId < 0) {
3525
+ return err(
3526
+ invalid(
3527
+ "invalid-schema",
3528
+ "create",
3529
+ "sourceId is non-empty and pageId is non-negative",
3530
+ input.pageId
3531
+ )
3532
+ );
3533
+ }
3534
+ const layout = deriveMaterialDynamicInputLayout(input.schema);
3535
+ if (!layout.ok) {
3536
+ return err(
3537
+ invalid(
3538
+ "invalid-schema",
3539
+ "create",
3540
+ layout.error.expected,
3541
+ layout.error.actual,
3542
+ layout.error.field
3543
+ )
3544
+ );
3545
+ }
3546
+ return ok(
3547
+ new _ReadonlyDynamicInputPage(input.sourceId, input.pageId, input.schema, layout.value)
3548
+ );
3549
+ }
3550
+ get contentRevision() {
3551
+ return this.contentRevisionValue;
3552
+ }
3553
+ get bufferGeneration() {
3554
+ return this.bufferGenerationValue;
3555
+ }
3556
+ get deviceGeneration() {
3557
+ return this.deviceGenerationValue;
3558
+ }
3559
+ get uploadedRevision() {
3560
+ return this.uploadedRevisionValue;
3561
+ }
3562
+ get lastUploadedBytes() {
3563
+ return this.lastUploadedBytesValue;
3564
+ }
3565
+ /** Write one complete record and mark only its byte interval dirty. */
3566
+ writeRecord(index, values) {
3567
+ const live = this.assertLive("write");
3568
+ if (!live.ok) return live;
3569
+ if (!Number.isSafeInteger(index) || index < 0 || index >= this.schema.maxRecords) {
3570
+ return err(
3571
+ invalid(
3572
+ "range-overflow",
3573
+ "write",
3574
+ `record index is within [0, ${this.schema.maxRecords})`,
3575
+ index,
3576
+ "index",
3577
+ this
3578
+ )
3579
+ );
3580
+ }
3581
+ const recordOffset = index * this.layout.stride;
3582
+ const encoded = new Uint8Array(this.layout.stride);
3583
+ const view = new DataView(encoded.buffer, encoded.byteOffset, encoded.byteLength);
3584
+ for (const field of this.layout.fields) {
3585
+ const value = values[field.name];
3586
+ if (value === void 0) {
3587
+ return err(
3588
+ invalid(
3589
+ "invalid-record",
3590
+ "write",
3591
+ `record contains field ${field.name}`,
3592
+ value,
3593
+ field.name,
3594
+ this
3595
+ )
3596
+ );
3597
+ }
3598
+ const width = fieldWidth(field.type);
3599
+ const numbers = typeof value === "number" ? [value] : value;
3600
+ if (numbers.length !== width || numbers.some((candidate) => !finiteNumber(candidate))) {
3601
+ return err(
3602
+ invalid(
3603
+ "invalid-record",
3604
+ "write",
3605
+ `${field.name} is a finite ${field.type}`,
3606
+ value,
3607
+ field.name,
3608
+ this
3609
+ )
3610
+ );
3611
+ }
3612
+ if (field.type === "u32" && (!Number.isInteger(numbers[0]) || numbers[0] < 0 || numbers[0] > 4294967295)) {
3613
+ return err(
3614
+ invalid(
3615
+ "invalid-record",
3616
+ "write",
3617
+ `${field.name} is an unsigned 32-bit integer`,
3618
+ value,
3619
+ field.name,
3620
+ this
3621
+ )
3622
+ );
3623
+ }
3624
+ for (const [component, number] of numbers.entries()) {
3625
+ const offset = field.offset + component * 4;
3626
+ if (field.type === "u32") view.setUint32(offset, number, true);
3627
+ else view.setFloat32(offset, number, true);
3628
+ }
3629
+ }
3630
+ this.bytes.set(encoded, recordOffset);
3631
+ this.contentRevisionValue += 1;
3632
+ alignRange(this.dirty, recordOffset, recordOffset + this.layout.stride);
3633
+ return ok(this.contentRevisionValue);
3634
+ }
3635
+ /** Reserve one explicit instance range; no skin address field is involved. */
3636
+ reserveRange(input) {
3637
+ const live = this.assertLive("reserve");
3638
+ if (!live.ok) return live;
3639
+ if (input.domain.length === 0) {
3640
+ return err(
3641
+ invalid(
3642
+ "domain-overflow",
3643
+ "reserve",
3644
+ "domain is a non-empty identity",
3645
+ input.domain,
3646
+ "domain",
3647
+ this
3648
+ )
3649
+ );
3650
+ }
3651
+ if (!this.domains.has(input.domain) && this.domains.size >= this.schema.maxDomains) {
3652
+ return err(
3653
+ invalid(
3654
+ "domain-overflow",
3655
+ "reserve",
3656
+ `at most ${this.schema.maxDomains} domains are active`,
3657
+ this.domains.size + 1,
3658
+ "domain",
3659
+ this
3660
+ )
3661
+ );
3662
+ }
3663
+ if (!Number.isSafeInteger(input.instanceIndex) || input.instanceIndex < 0 || input.instanceIndex > 4294967295) {
3664
+ return err(
3665
+ invalid(
3666
+ "range-overflow",
3667
+ "reserve",
3668
+ "instance index is an exact unsigned 32-bit integer",
3669
+ input.instanceIndex,
3670
+ "instanceIndex",
3671
+ this
3672
+ )
3673
+ );
3674
+ }
3675
+ if (input.member.worldIdentity.length === 0 || !Number.isSafeInteger(input.member.entityKey) || input.member.entityKey < 0 || input.member.entityKey > 4294967295 || !Number.isSafeInteger(input.member.drawItemIndex) || input.member.drawItemIndex < 0 || input.member.drawItemIndex > 4294967295 || !Number.isSafeInteger(input.member.instanceOrdinal) || input.member.instanceOrdinal < 0 || input.member.instanceOrdinal > 4294967295) {
3676
+ return err(
3677
+ invalid(
3678
+ "range-overflow",
3679
+ "reserve",
3680
+ "member is a stable World identity plus exact unsigned 32-bit entity, draw-item, and instance identities",
3681
+ input.member,
3682
+ "member",
3683
+ this
3684
+ )
3685
+ );
3686
+ }
3687
+ if (!Number.isSafeInteger(input.recordStart) || !Number.isSafeInteger(input.recordCount) || input.recordStart < 0 || input.recordCount <= 0 || input.recordStart + input.recordCount > this.schema.maxRecords) {
3688
+ return err(
3689
+ invalid(
3690
+ "range-overflow",
3691
+ "reserve",
3692
+ "record range and instance index fit the declared page",
3693
+ input,
3694
+ "recordStart",
3695
+ this
3696
+ )
3697
+ );
3698
+ }
3699
+ const range = {
3700
+ pageId: this.pageId,
3701
+ sourceId: this.sourceId,
3702
+ domain: input.domain,
3703
+ byteOffset: input.recordStart * this.layout.stride,
3704
+ recordStart: input.recordStart,
3705
+ recordCount: input.recordCount,
3706
+ instanceIndex: input.instanceIndex,
3707
+ member: Object.freeze({ ...input.member }),
3708
+ contentRevision: this.contentRevisionValue,
3709
+ bufferGeneration: this.bufferGenerationValue,
3710
+ deviceGeneration: this.deviceGenerationValue
3711
+ };
3712
+ this.ownedRanges.add(range);
3713
+ this.domains.set(input.domain, {
3714
+ name: input.domain,
3715
+ start: input.recordStart,
3716
+ end: input.recordStart + input.recordCount
3717
+ });
3718
+ return ok(range);
3719
+ }
3720
+ /** Return detached dirty bytes for the existing resource owner to upload. */
3721
+ beginUpload() {
3722
+ const live = this.assertLive("upload");
3723
+ if (!live.ok) return live;
3724
+ const ranges = this.dirty.map((range) => ({ ...range }));
3725
+ const bytes = ranges.reduce((sum, range) => sum + range.byteEnd - range.byteStart, 0);
3726
+ const receipt = {
3727
+ sourceId: this.sourceId,
3728
+ pageId: this.pageId,
3729
+ contentRevision: this.contentRevisionValue,
3730
+ bufferGeneration: this.bufferGenerationValue,
3731
+ deviceGeneration: this.deviceGenerationValue,
3732
+ ranges,
3733
+ bytes
3734
+ };
3735
+ this.ownedUploads.add(receipt);
3736
+ return ok(receipt);
3737
+ }
3738
+ /**
3739
+ * Commit an upload only after the owning queue write has succeeded. A
3740
+ * producer that changed the page while a write was in flight leaves the
3741
+ * dirty bytes pending and must retry the new revision.
3742
+ */
3743
+ commitUpload(receipt) {
3744
+ const live = this.assertLive("upload");
3745
+ if (!live.ok) return live;
3746
+ if (!this.ownedUploads.has(receipt) || receipt.sourceId !== this.sourceId || receipt.pageId !== this.pageId || receipt.bufferGeneration !== this.bufferGenerationValue || receipt.deviceGeneration !== this.deviceGenerationValue || receipt.contentRevision !== this.contentRevisionValue) {
3747
+ return err(
3748
+ invalid(
3749
+ "stale-generation",
3750
+ "upload",
3751
+ "the upload receipt still describes the current page revision and generation",
3752
+ receipt,
3753
+ "receipt",
3754
+ this
3755
+ )
3756
+ );
3757
+ }
3758
+ this.uploadedRevisionValue = receipt.contentRevision;
3759
+ this.lastUploadedBytesValue = receipt.bytes;
3760
+ for (const range of receipt.ranges) {
3761
+ const index = this.dirty.findIndex(
3762
+ (candidate) => candidate.byteStart === range.byteStart && candidate.byteEnd === range.byteEnd
3763
+ );
3764
+ if (index >= 0) this.dirty.splice(index, 1);
3765
+ }
3766
+ return ok(true);
3767
+ }
3768
+ /** CPU-only convenience: begin and commit are still explicit in production. */
3769
+ upload() {
3770
+ const receipt = this.beginUpload();
3771
+ if (!receipt.ok) return receipt;
3772
+ const committed = this.commitUpload(receipt.value);
3773
+ if (!committed.ok) return committed;
3774
+ return receipt;
3775
+ }
3776
+ /** Consume an uploaded range after graph submission has completed. */
3777
+ consume(range, frameNumber) {
3778
+ const live = this.assertLive("consume");
3779
+ if (!live.ok) return live;
3780
+ if (!this.ownedRanges.has(range) || range.sourceId !== this.sourceId || range.pageId !== this.pageId || range.bufferGeneration !== this.bufferGenerationValue || range.deviceGeneration !== this.deviceGenerationValue || range.recordStart < 0 || range.recordCount <= 0 || range.recordStart + range.recordCount > this.schema.maxRecords) {
3781
+ return err(
3782
+ invalid(
3783
+ "stale-generation",
3784
+ "consume",
3785
+ "range belongs to this current page generation",
3786
+ range,
3787
+ "generation",
3788
+ this
3789
+ )
3790
+ );
3791
+ }
3792
+ if (this.uploadedRevisionValue < range.contentRevision) {
3793
+ return err(
3794
+ invalid(
3795
+ "not-uploaded",
3796
+ "consume",
3797
+ "range content revision was uploaded before consumption",
3798
+ this.uploadedRevisionValue,
3799
+ "contentRevision",
3800
+ this
3801
+ )
3802
+ );
3803
+ }
3804
+ return ok({ ...range, frameNumber, uploadedRevision: this.uploadedRevisionValue });
3805
+ }
3806
+ /** Device loss/rebuild changes physical generations while source identity stays stable. */
3807
+ reconfigureDevice(deviceGeneration) {
3808
+ const live = this.assertLive("reconfigure");
3809
+ if (!live.ok) return live;
3810
+ if (!Number.isSafeInteger(deviceGeneration) || deviceGeneration < 0) {
3811
+ return err(
3812
+ invalid(
3813
+ "stale-generation",
3814
+ "reconfigure",
3815
+ "deviceGeneration is a non-negative safe integer",
3816
+ deviceGeneration,
3817
+ "deviceGeneration",
3818
+ this
3819
+ )
3820
+ );
3821
+ }
3822
+ const rendererInitialGeneration = this.bufferGenerationValue === 1 && this.deviceGenerationValue === 1 && deviceGeneration === 0;
3823
+ if (deviceGeneration < this.deviceGenerationValue && !rendererInitialGeneration) {
3824
+ return err(
3825
+ invalid(
3826
+ "stale-generation",
3827
+ "reconfigure",
3828
+ "deviceGeneration does not precede the current device generation",
3829
+ deviceGeneration,
3830
+ "deviceGeneration",
3831
+ this
3832
+ )
3833
+ );
3834
+ }
3835
+ this.deviceGenerationValue = deviceGeneration;
3836
+ this.bufferGenerationValue += 1;
3837
+ this.uploadedRevisionValue = 0;
3838
+ alignRange(this.dirty, 0, this.schema.maxRecords * this.layout.stride);
3839
+ return ok(this.bufferGenerationValue);
3840
+ }
3841
+ release() {
3842
+ if (this.released) return ok(true);
3843
+ this.released = true;
3844
+ this.domains.clear();
3845
+ this.dirty.length = 0;
3846
+ return ok(true);
3847
+ }
3848
+ assertLive(operation) {
3849
+ return this.released ? err(
3850
+ invalid(
3851
+ "released",
3852
+ operation,
3853
+ ERROR_POLICY2.released.expected,
3854
+ void 0,
3855
+ void 0,
3856
+ this
3857
+ )
3858
+ ) : ok(true);
3859
+ }
3860
+ };
3861
+
3862
+ // src/reflection/filter.ts
3863
+ function createProbeFilterState(input) {
3864
+ return {
3865
+ probeIndex: input.probeIndex,
3866
+ faceCount: input.faceCount ?? 6,
3867
+ mipCount: input.mipCount,
3868
+ cursor: 0,
3869
+ activeGeneration: input.activeGeneration ?? 0
3870
+ };
3871
+ }
3872
+ function advanceProbeFilter(state) {
3873
+ const total = state.faceCount * state.mipCount;
3874
+ if (state.cursor >= total) return void 0;
3875
+ return {
3876
+ probeIndex: state.probeIndex,
3877
+ faceIndex: state.cursor % state.faceCount,
3878
+ mipLevel: Math.floor(state.cursor / state.faceCount)
3879
+ };
3880
+ }
3881
+ function commitProbeFilterStep(state, success) {
3882
+ if (!success) return state;
3883
+ const nextCursor = state.cursor + 1;
3884
+ const complete = nextCursor >= state.faceCount * state.mipCount;
3885
+ return {
3886
+ ...state,
3887
+ cursor: nextCursor,
3888
+ activeGeneration: complete ? state.activeGeneration + 1 : state.activeGeneration
3889
+ };
3890
+ }
3891
+ function probeFilterIsSteady(state) {
3892
+ return state.cursor >= state.faceCount * state.mipCount;
3893
+ }
3894
+ function normalize3(vector) {
3895
+ const length = Math.hypot(vector[0], vector[1], vector[2]);
3896
+ if (length === 0) return [0, 0, 1];
3897
+ return [vector[0] / length, vector[1] / length, vector[2] / length];
3898
+ }
3899
+ function boxProjectReflectionDirection(direction, boxCenter, halfExtents, point) {
3900
+ const ray = normalize3(direction);
3901
+ for (const axis of [0, 1, 2]) {
3902
+ if (Math.abs(point[axis] - boxCenter[axis]) >= halfExtents[axis]) return ray;
3903
+ }
3904
+ let distance = Number.POSITIVE_INFINITY;
3905
+ for (let axis = 0; axis < 3; axis += 1) {
3906
+ const component = ray[axis] ?? 0;
3907
+ if (component === 0) continue;
3908
+ const extent = halfExtents[axis] ?? 0;
3909
+ const edge = component > 0 ? (boxCenter[axis] ?? 0) + extent : (boxCenter[axis] ?? 0) - extent;
3910
+ const candidate = (edge - (point[axis] ?? 0)) / component;
3911
+ if (candidate > 0) distance = Math.min(distance, candidate);
3912
+ }
3913
+ if (!Number.isFinite(distance)) return ray;
3914
+ return normalize3([
3915
+ (point[0] ?? 0) + ray[0] * distance - (boxCenter[0] ?? 0),
3916
+ (point[1] ?? 0) + ray[1] * distance - (boxCenter[1] ?? 0),
3917
+ (point[2] ?? 0) + ray[2] * distance - (boxCenter[2] ?? 0)
3918
+ ]);
3919
+ }
3920
+
3921
+ // src/reflection/gpu-table.ts
3922
+ function buildReflectionProbeTable(rows) {
3923
+ const ordered = rows.slice().sort((a, b) => a.index - b.index);
3924
+ const indices = /* @__PURE__ */ new Map();
3925
+ for (const row of ordered) {
3926
+ if (row.primitiveKey !== void 0) indices.set(row.primitiveKey, row.index);
3927
+ }
3928
+ const indexFor = (primitiveKey2) => indices.get(primitiveKey2);
3929
+ return {
3930
+ rows: Object.freeze(ordered),
3931
+ cpuIndexByPrimitive: indexFor,
3932
+ gpuIndexByPrimitive: indexFor
3933
+ };
3934
+ }
3935
+ function reflectionProbeTableBytes(table) {
3936
+ return table.rows.length * 48;
3937
+ }
3938
+
3939
+ // src/reflection/inspection.ts
3940
+ function recoveryFor(receipt, failure3) {
3941
+ if (failure3 !== void 0) {
3942
+ const code = failure3.code.toLowerCase();
3943
+ if (code.includes("device") || code.includes("format") || code.includes("capability")) {
3944
+ return "rebuild";
3945
+ }
3946
+ if (failure3.stage === "prepare" || failure3.stage === "filter") return "recapture";
3947
+ if (failure3.stage === "build" || failure3.stage === "encode") return "rebuild";
3948
+ if (failure3.stage === "finish" || failure3.stage === "submit") return "retry";
3949
+ if (failure3.stage === "completion") {
3950
+ if (receipt.source === "probe" && (receipt.state === "active" || receipt.state === "lkg")) {
3951
+ return "use-LKG";
3952
+ }
3953
+ if (receipt.source === "skylight" && (receipt.state === "active" || receipt.state === "lkg")) {
3954
+ return "use-Skylight";
3955
+ }
3956
+ if (receipt.source === "neutral" && receipt.state === "neutral") return "use-neutral";
3957
+ return "retry";
3958
+ }
3959
+ }
3960
+ if (receipt.source === "probe" && (receipt.state === "active" || receipt.state === "lkg")) {
3961
+ return "use-LKG";
3962
+ }
3963
+ if (receipt.source === "skylight" || receipt.state === "lkg") return "use-Skylight";
3964
+ return "use-neutral";
3965
+ }
3966
+ function inspectReflectionFallback(receipt, failure3) {
3967
+ return {
3968
+ receipt,
3969
+ recoveryAction: recoveryFor(receipt, failure3),
3970
+ ...failure3 === void 0 ? {} : {
3971
+ failureStage: failure3.stage,
3972
+ failureCode: failure3.code,
3973
+ expected: failure3.expected,
3974
+ ...failure3.detail === void 0 ? {} : { detail: failure3.detail }
3975
+ }
3976
+ };
3977
+ }
3978
+
3979
+ // src/reflection/projection.ts
3980
+ var REFLECTION_PROBE_BYTES_PER_TEXEL = 8;
3981
+ var DEFAULT_REFLECTION_PROBE_LIMITS = Object.freeze({
3982
+ maxProbes: 16,
3983
+ maxResolution: 256,
3984
+ maxBytes: 64 * 1024 * 1024
3985
+ });
3986
+ function validateReflectionProbeInput(input) {
3987
+ for (let index = 0; index < input.halfExtents.length; index += 1) {
3988
+ const value = input.halfExtents[index];
3989
+ if (value === void 0 || !Number.isFinite(value) || value <= 0) {
3990
+ return {
3991
+ ok: false,
3992
+ error: {
3993
+ code: "reflection-probe-input-invalid",
3994
+ field: `halfExtents[${index}]`,
3995
+ value,
3996
+ expected: "a finite number greater than zero"
3997
+ }
3998
+ };
3999
+ }
4000
+ }
4001
+ if (!Number.isFinite(input.priority)) {
4002
+ return {
4003
+ ok: false,
4004
+ error: {
4005
+ code: "reflection-probe-input-invalid",
4006
+ field: "priority",
4007
+ value: input.priority,
4008
+ expected: "a finite number"
4009
+ }
4010
+ };
4011
+ }
4012
+ if (!Number.isFinite(input.intensity) || input.intensity < 0) {
4013
+ return {
4014
+ ok: false,
4015
+ error: {
4016
+ code: "reflection-probe-input-invalid",
4017
+ field: "intensity",
4018
+ value: input.intensity,
4019
+ expected: "a finite number greater than or equal to zero"
4020
+ }
4021
+ };
4022
+ }
4023
+ if (!Number.isInteger(input.resolution) || input.resolution < 16 || (input.resolution & input.resolution - 1) !== 0) {
4024
+ return {
4025
+ ok: false,
4026
+ error: {
4027
+ code: "reflection-probe-input-invalid",
4028
+ field: "resolution",
4029
+ value: input.resolution,
4030
+ expected: "a power of two of at least 16 pixels"
4031
+ }
4032
+ };
4033
+ }
4034
+ return { ok: true };
4035
+ }
4036
+ function estimateReflectionProbeBytes(resolution, limits = DEFAULT_REFLECTION_PROBE_LIMITS) {
4037
+ const maxResolution = limits.maxResolution ?? DEFAULT_REFLECTION_PROBE_LIMITS.maxResolution;
4038
+ const clamped = Math.min(resolution, maxResolution);
4039
+ let filteredTexels = 0;
4040
+ for (let mip = 0; mip < 5; mip++) {
4041
+ const size = Math.max(1, Math.floor(clamped / 2 ** mip));
4042
+ filteredTexels += size * size;
4043
+ }
4044
+ return (clamped * clamped + 2 * filteredTexels) * 6 * REFLECTION_PROBE_BYTES_PER_TEXEL + 3 * clamped * clamped * 4;
4045
+ }
4046
+ function admitReflectionProbe(input, current, limits = DEFAULT_REFLECTION_PROBE_LIMITS) {
4047
+ const maxProbes = limits.maxProbes ?? DEFAULT_REFLECTION_PROBE_LIMITS.maxProbes;
4048
+ const maxBytes = limits.maxBytes ?? DEFAULT_REFLECTION_PROBE_LIMITS.maxBytes;
4049
+ const maxResolution = limits.maxResolution ?? DEFAULT_REFLECTION_PROBE_LIMITS.maxResolution;
4050
+ const requested = estimateReflectionProbeBytes(input.resolution, { maxResolution });
4051
+ if (input.resolution > maxResolution || current.acceptedCount >= maxProbes || current.acceptedBytes + requested > maxBytes) {
4052
+ return {
4053
+ ok: false,
4054
+ error: {
4055
+ code: "reflection-probe-budget-exceeded",
4056
+ requested,
4057
+ activeCount: current.acceptedCount,
4058
+ activeBytes: current.acceptedBytes,
4059
+ maxProbes,
4060
+ maxBytes
4061
+ }
4062
+ };
4063
+ }
4064
+ return { ok: true, acceptedBytes: current.acceptedBytes + requested };
4065
+ }
4066
+ function resolveReflectionFallbackSource(selection, probeReady, skylightAvailable = true) {
4067
+ if (selection.kind === "probe" && probeReady) return "probe";
4068
+ if (skylightAvailable) return "skylight";
4069
+ return "neutral";
4070
+ }
4071
+ function reflectionFallbackCoverage(source, fact, selection) {
4072
+ if (source === "neutral") return 0;
4073
+ if (source === "skylight") return 1;
4074
+ if (fact === void 0 || selection.kind !== "probe") return 0;
4075
+ const normalized = Math.sqrt(Math.max(0, selection.normalizedDistance));
4076
+ return Math.max(0, Math.min(1, 1 - normalized / Math.sqrt(3)));
4077
+ }
4078
+ function reflectionFallbackSourceKey(source, selection, skylight) {
4079
+ if (source === "probe" && selection.kind === "probe") {
4080
+ return `probe:${selection.worldId}:${selection.entityKey}`;
4081
+ }
4082
+ if (source === "skylight" && skylight !== void 0) {
4083
+ return `skylight:${skylight.entityHandle}:${skylight.equirectHandle}`;
4084
+ }
4085
+ return source;
4086
+ }
4087
+ function reflectionFallbackProjectionSignature(input) {
4088
+ return JSON.stringify({
4089
+ renderableKey: input.renderableKey,
4090
+ sourceKey: input.sourceKey,
4091
+ source: input.source,
4092
+ coverage: input.coverage,
4093
+ extent: input.extent,
4094
+ brdfSignature: input.brdfSignature
4095
+ });
4096
+ }
4097
+ function deriveReflectionFallbackProjection(input) {
4098
+ if (!Number.isFinite(input.coverage) || input.coverage < 0 || input.coverage > 1) {
4099
+ return {
4100
+ ok: false,
4101
+ error: {
4102
+ code: "reflection-fallback-input-invalid",
4103
+ field: "coverage",
4104
+ expected: "a finite number between zero and one"
4105
+ }
4106
+ };
4107
+ }
4108
+ if (input.extent?.some((value) => !Number.isFinite(value) || value <= 0)) {
4109
+ return {
4110
+ ok: false,
4111
+ error: {
4112
+ code: "reflection-fallback-input-invalid",
4113
+ field: "extent",
4114
+ expected: "three finite numbers greater than zero"
4115
+ }
4116
+ };
4117
+ }
4118
+ if (input.linearHdr.some((value) => !Number.isFinite(value))) {
4119
+ return {
4120
+ ok: false,
4121
+ error: {
4122
+ code: "reflection-fallback-input-invalid",
4123
+ field: "linearHdr",
4124
+ expected: "four finite linear HDR values"
4125
+ }
4126
+ };
4127
+ }
4128
+ if (input.brdfSignature.length === 0) {
4129
+ return {
4130
+ ok: false,
4131
+ error: {
4132
+ code: "reflection-fallback-input-invalid",
4133
+ field: "brdfSignature",
4134
+ expected: "a non-empty BRDF signature"
4135
+ }
4136
+ };
4137
+ }
4138
+ const linearHdr = input.source === "neutral" ? [0, 0, 0, 0] : [...input.linearHdr];
4139
+ return {
4140
+ ok: true,
4141
+ value: Object.freeze({
4142
+ ...input.renderableKey === void 0 ? {} : { renderableKey: input.renderableKey },
4143
+ ...input.sourceKey === void 0 ? {} : { sourceKey: input.sourceKey },
4144
+ source: input.source,
4145
+ coverage: input.coverage,
4146
+ ...input.extent === void 0 ? {} : {
4147
+ extent: Object.freeze([...input.extent])
4148
+ },
4149
+ linearHdr: Object.freeze(linearHdr),
4150
+ brdfSignature: input.brdfSignature
4151
+ })
4152
+ };
4153
+ }
4154
+ function contains(fact, point) {
4155
+ return fact.halfExtents.every((extent, axis) => {
4156
+ const center = fact.center[axis] ?? 0;
4157
+ const coordinate = point[axis] ?? 0;
4158
+ return Math.abs(coordinate - center) <= extent;
4159
+ });
4160
+ }
4161
+ function normalizedDistance(fact, point) {
4162
+ let squared = 0;
4163
+ for (let axis = 0; axis < 3; axis += 1) {
4164
+ const delta = (point[axis] ?? 0) - (fact.center[axis] ?? 0);
4165
+ const extent = fact.halfExtents[axis] ?? 1;
4166
+ squared += (delta / extent) ** 2;
4167
+ }
4168
+ return squared;
4169
+ }
4170
+ function isBetter(candidate, current, facts) {
4171
+ const candidateFact = facts.find(
4172
+ (fact) => fact.worldId === candidate.worldId && fact.entityKey === candidate.entityKey
4173
+ );
4174
+ const currentFact = facts.find(
4175
+ (fact) => fact.worldId === current.worldId && fact.entityKey === current.entityKey
4176
+ );
4177
+ if (candidateFact === void 0 || currentFact === void 0) return false;
4178
+ if (candidateFact.priority !== currentFact.priority)
4179
+ return candidateFact.priority > currentFact.priority;
4180
+ if (candidate.normalizedDistance !== current.normalizedDistance) {
4181
+ return candidate.normalizedDistance < current.normalizedDistance;
4182
+ }
4183
+ return candidate.worldId < current.worldId || candidate.worldId === current.worldId && candidate.entityKey < current.entityKey;
4184
+ }
4185
+ function selectReflectionProbe(facts, primitiveCenter) {
4186
+ let selected;
4187
+ for (const fact of facts) {
4188
+ if (!contains(fact, primitiveCenter)) continue;
4189
+ const candidate = {
4190
+ kind: "probe",
4191
+ worldId: fact.worldId,
4192
+ entityKey: fact.entityKey,
4193
+ normalizedDistance: normalizedDistance(fact, primitiveCenter)
4194
+ };
4195
+ if (selected === void 0 || isBetter(candidate, selected, facts)) selected = candidate;
4196
+ }
4197
+ return selected ?? { kind: "skylight" };
4198
+ }
4199
+ function primitiveKey(worldId, entityKey) {
4200
+ return `${worldId}:${entityKey}`;
4201
+ }
4202
+ var ReflectionProbeProjection = class {
4203
+ revision = 0;
4204
+ facts = [];
4205
+ selected = /* @__PURE__ */ new Map();
4206
+ signature = "";
4207
+ scannedPrimitives = 0;
4208
+ update(facts, primitives) {
4209
+ const nextFacts = facts.slice().sort((a, b) => a.worldId - b.worldId || a.entityKey - b.entityKey);
4210
+ const signature = JSON.stringify({ facts: nextFacts, primitives });
4211
+ if (signature === this.signature) return this.snapshot();
4212
+ this.signature = signature;
4213
+ this.facts = nextFacts;
4214
+ this.selected = /* @__PURE__ */ new Map();
4215
+ for (const primitive of primitives) {
4216
+ this.selected.set(
4217
+ primitive.renderableKey ?? primitiveKey(primitive.worldId, primitive.entityKey),
4218
+ selectReflectionProbe(nextFacts, primitive.center)
4219
+ );
4220
+ }
4221
+ this.scannedPrimitives = primitives.length;
4222
+ this.revision += 1;
4223
+ return this.snapshot();
4224
+ }
4225
+ snapshot() {
4226
+ return {
4227
+ revision: this.revision,
4228
+ facts: this.facts,
4229
+ selected: this.selected,
4230
+ scannedPrimitives: this.scannedPrimitives
4231
+ };
4232
+ }
4233
+ selection(worldId, entityKey) {
4234
+ return this.selected.get(primitiveKey(worldId, entityKey)) ?? { kind: "skylight" };
4235
+ }
4236
+ };
4237
+
4238
+ // src/scene/visibility/inspection.ts
4239
+ var LOD_OCCLUSION_INSPECTION_SCHEMA = "forgeax::lod-occlusion-inspection::v2";
4240
+ var LOD_OCCLUSION_INSPECTION_MAX_BYTES = 16 * 1024;
4241
+ var MAX_INSPECTION_SAMPLES = 64;
4242
+ function nonNegativeInteger(name, value) {
4243
+ if (!Number.isInteger(value) || value < 0)
4244
+ throw new Error(`${name} must be a non-negative integer`);
4245
+ return value;
4246
+ }
4247
+ function copyError(error) {
4248
+ return Object.freeze({
4249
+ code: error.code,
4250
+ expected: error.expected,
4251
+ hint: error.hint,
4252
+ ...error.detail === void 0 ? {} : { detail: Object.freeze({ ...error.detail }) }
4253
+ });
4254
+ }
4255
+ function copyFallback(fallback) {
4256
+ return fallback.active ? Object.freeze({ ...fallback, error: copyError(fallback.error) }) : Object.freeze({ active: false });
4257
+ }
4258
+ function copySamples(samples) {
4259
+ return Object.freeze(
4260
+ [...samples].sort((left, right) => left.primitiveSlot - right.primitiveSlot).slice(0, MAX_INSPECTION_SAMPLES).map(
4261
+ (sample) => Object.freeze({
4262
+ primitiveSlot: nonNegativeInteger("sample.primitiveSlot", sample.primitiveSlot),
4263
+ level: nonNegativeInteger("sample.level", sample.level),
4264
+ visible: sample.visible
4265
+ })
4266
+ )
4267
+ );
4268
+ }
4269
+ function copyWorldAttribution(attribution) {
4270
+ if (attribution === void 0) {
4271
+ return Object.freeze({ status: "unavailable", reason: "projection-only" });
4272
+ }
4273
+ if (attribution.status === "same-submit") {
4274
+ if (typeof attribution.submit.build !== "string" || attribution.submit.build.length === 0) {
4275
+ throw new Error("world.attribution.submit.build must be a non-empty string");
4276
+ }
4277
+ nonNegativeInteger("world.attribution.submit.frameId", attribution.submit.frameId);
4278
+ nonNegativeInteger(
4279
+ "world.attribution.submit.deviceGeneration",
4280
+ attribution.submit.deviceGeneration
4281
+ );
4282
+ return Object.freeze({
4283
+ status: "same-submit",
4284
+ submit: Object.freeze({ ...attribution.submit })
4285
+ });
4286
+ }
4287
+ if (attribution.status !== "unavailable" || attribution.reason !== "projection-only") {
4288
+ throw new Error("world.attribution must declare same-submit or projection-only");
4289
+ }
4290
+ return Object.freeze({ status: "unavailable", reason: "projection-only" });
4291
+ }
4292
+ function copyRow(row) {
4293
+ for (const [name, value] of Object.entries({
4294
+ generation: row.generation,
4295
+ candidates: row.count.candidates,
4296
+ visible: row.count.visible,
4297
+ occluded: row.count.occluded,
4298
+ median: row.queryLatencyUs.median,
4299
+ p95: row.queryLatencyUs.p95,
4300
+ last: row.queryLatencyUs.last,
4301
+ pageUsed: row.pagePressure.used,
4302
+ pageCapacity: row.pagePressure.capacity
4303
+ }))
4304
+ nonNegativeInteger(`inspection.${name}`, value);
4305
+ if (row.view.cameraEntity < 0 || row.view.viewGeneration < 0 || row.slot.primitiveSlot < 0 || row.slot.slotGeneration < 0) {
4306
+ throw new Error("inspection identity values must be non-negative");
4307
+ }
4308
+ if (row.pagePressure.used > row.pagePressure.capacity) {
4309
+ throw new Error("inspection page pressure exceeds capacity");
4310
+ }
4311
+ return Object.freeze({
4312
+ root: Object.freeze({ ...row.root }),
4313
+ view: Object.freeze({ ...row.view }),
4314
+ slot: Object.freeze({ ...row.slot }),
4315
+ generation: row.generation,
4316
+ count: Object.freeze({ ...row.count }),
4317
+ lodHistogram: Object.freeze(
4318
+ row.lodHistogram.map(
4319
+ (histogram) => Object.freeze({
4320
+ level: nonNegativeInteger("histogram.level", histogram.level),
4321
+ count: nonNegativeInteger("histogram.count", histogram.count)
4322
+ })
4323
+ )
4324
+ ),
4325
+ queryLatencyUs: Object.freeze({ ...row.queryLatencyUs }),
4326
+ pagePressure: Object.freeze({ ...row.pagePressure }),
4327
+ fallback: copyFallback(row.fallback),
4328
+ degradation: Object.freeze({ ...row.degradation }),
4329
+ samples: copySamples(row.samples)
4330
+ });
4331
+ }
4332
+ function copyWorlds(worlds, fallback) {
4333
+ const source = worlds ?? [{ attachmentId: fallback.view.attachmentId, rows: [fallback] }];
4334
+ return Object.freeze(
4335
+ source.map(
4336
+ (world) => Object.freeze({
4337
+ attachmentId: world.attachmentId,
4338
+ rows: Object.freeze(world.rows.map((row) => copyRow(row))),
4339
+ attribution: copyWorldAttribution(world.attribution)
4340
+ })
4341
+ )
4342
+ );
4343
+ }
4344
+ function inspectLodOcclusion(input) {
4345
+ const row = copyRow(input);
4346
+ const defaultBudget = createVisibilityBudget();
4347
+ const budget = input.budget ?? {
4348
+ configuredQueryBudget: defaultBudget.configuredQueryBudget,
4349
+ effectiveQueryBudget: defaultBudget.effectiveQueryBudget,
4350
+ settleSubmits: defaultBudget.settleSubmits,
4351
+ retestSubmits: defaultBudget.retestSubmits,
4352
+ expirySubmits: defaultBudget.expirySubmits
4353
+ };
4354
+ for (const [name, value] of Object.entries(budget))
4355
+ nonNegativeInteger(`inspection.budget.${name}`, value);
4356
+ const submit = input.submit ?? {
4357
+ frameId: input.generation,
4358
+ build: "unknown",
4359
+ deviceGeneration: 0
4360
+ };
4361
+ nonNegativeInteger("inspection.submit.frameId", submit.frameId);
4362
+ nonNegativeInteger("inspection.submit.deviceGeneration", submit.deviceGeneration);
4363
+ if (typeof submit.build !== "string" || submit.build.length === 0) {
4364
+ throw new Error("inspection.submit.build must be a non-empty string");
4365
+ }
4366
+ const worlds = copyWorlds(input.worlds, row);
4367
+ for (const world of worlds) {
4368
+ if (world.attribution.status === "same-submit" && (world.attribution.submit.frameId !== submit.frameId || world.attribution.submit.build !== submit.build || world.attribution.submit.deviceGeneration !== submit.deviceGeneration)) {
4369
+ throw new Error("world attribution must reference the parent inspection submit");
4370
+ }
4371
+ }
4372
+ const inspection = Object.freeze({
4373
+ schema: LOD_OCCLUSION_INSPECTION_SCHEMA,
4374
+ ...row,
4375
+ submit: Object.freeze({ ...submit }),
4376
+ budget: Object.freeze({ ...budget }),
4377
+ worlds
4378
+ });
4379
+ if (JSON.stringify(inspection).length > LOD_OCCLUSION_INSPECTION_MAX_BYTES) {
4380
+ throw new Error(`LOD occlusion inspection exceeds ${LOD_OCCLUSION_INSPECTION_MAX_BYTES} bytes`);
4381
+ }
4382
+ return inspection;
4383
+ }
4384
+ function serializeLodOcclusionInspection(inspection) {
4385
+ const serialized = JSON.stringify(inspection);
4386
+ if (serialized.length > LOD_OCCLUSION_INSPECTION_MAX_BYTES) {
4387
+ throw new Error(`LOD occlusion inspection exceeds ${LOD_OCCLUSION_INSPECTION_MAX_BYTES} bytes`);
4388
+ }
4389
+ return serialized;
4390
+ }
4391
+ function consumeLodOcclusionInspection(inspection) {
4392
+ if (!inspection.fallback.active) return { action: "none" };
4393
+ const { reason, error } = inspection.fallback;
4394
+ const action = error.code.startsWith("asset-") || reason === "producer-failed" ? "rebuild" : reason === "query-failed" || reason === "page-exhausted" || reason === "device-loss" ? "retry" : "cold-cook";
4395
+ return { action, sourceKey: inspection.root.sourceKey, reason };
4396
+ }
4397
+
4398
+ // src/ssr/inspection.ts
4399
+ var SSR_INSPECTION_MAX_PASSES = 64;
4400
+ function nonNegativeSafeInteger(name, value) {
4401
+ if (!Number.isSafeInteger(value) || value < 0) {
4402
+ throw new Error(`${name} must be a non-negative safe integer`);
4403
+ }
4404
+ return value;
4405
+ }
4406
+ function copyHistory(history) {
4407
+ nonNegativeSafeInteger("SSR inspection history.bytes", history.bytes);
4408
+ nonNegativeSafeInteger("SSR inspection history.resetCount", history.resetCount);
4409
+ return Object.freeze({
4410
+ state: history.state,
4411
+ bytes: history.bytes,
4412
+ resetCount: history.resetCount
4413
+ });
4414
+ }
4415
+ function copyCoverage(coverage) {
4416
+ const result = {
4417
+ hitCount: coverage?.hitCount ?? null,
4418
+ fallbackCount: coverage?.fallbackCount ?? null,
4419
+ excludedCount: coverage?.excludedCount ?? null
4420
+ };
4421
+ for (const [key, value] of Object.entries(result)) {
4422
+ if (value !== null) nonNegativeSafeInteger(`SSR inspection coverage.${key}`, value);
4423
+ }
4424
+ return Object.freeze(result);
4425
+ }
4426
+ function copyPassRoster(passRoster) {
4427
+ return Object.freeze(
4428
+ [...passRoster ?? []].slice(0, SSR_INSPECTION_MAX_PASSES).map((name, index) => {
4429
+ if (typeof name !== "string" || name.length === 0) {
4430
+ throw new Error(`SSR inspection passRoster[${index}] must be a non-empty string`);
4431
+ }
4432
+ return name;
4433
+ })
4434
+ );
4435
+ }
4436
+ function projectSsrSpatialInspection(admission, projection = {}) {
4437
+ const config = admission.config;
4438
+ return Object.freeze({
4439
+ status: projection.status ?? admission.status,
4440
+ lane: admission.lane,
4441
+ config: config === void 0 ? void 0 : Object.freeze({
4442
+ maxDistance: config.maxDistance,
4443
+ thickness: config.thickness,
4444
+ maxRoughness: config.maxRoughness
4445
+ }),
4446
+ viewRange: admission.viewRange,
4447
+ coverage: copyCoverage(projection.coverage),
4448
+ history: copyHistory(
4449
+ projection.history ?? {
4450
+ state: "not-owned",
4451
+ bytes: 0,
4452
+ resetCount: 0
4453
+ }
4454
+ ),
4455
+ work: Object.freeze({ ...admission.work }),
4456
+ passRoster: copyPassRoster(projection.passRoster),
4457
+ fallbackSource: projection.fallbackSource,
4458
+ failure: projection.failure ?? admission.failure
4459
+ });
4460
+ }
4461
+ function serializeSsrSpatialInspection(inspection) {
4462
+ return JSON.stringify(inspection);
4463
+ }
4464
+ var DynamicGeometryError = class extends Error {
4465
+ code;
4466
+ expected;
4467
+ hint;
4468
+ detail;
4469
+ constructor(code, expected, hint, detail = {}) {
4470
+ super(`${code}: ${expected}`);
4471
+ this.name = "DynamicGeometryError";
4472
+ this.code = code;
4473
+ this.expected = expected;
4474
+ this.hint = hint;
4475
+ this.detail = Object.freeze({ code, ...detail });
4476
+ }
4477
+ };
4478
+ function sameCandidateCredential(candidate, committed) {
4479
+ return candidate.candidateId === committed.candidateId && candidate.owner === committed.owner && candidate.world === committed.world && candidate.generation === committed.generation && candidate.entity === committed.entity && candidate.physicsEntity === committed.physicsEntity && candidate.gpuReady === committed.gpuReady && candidate.meshBytes === committed.meshBytes && candidate.revision === committed.revision && candidate.topologyRevision === committed.topologyRevision && candidate.materialIdentity === committed.materialIdentity && candidate.meshHandle === committed.meshHandle && candidate.fixedStep === committed.fixedStep;
4480
+ }
4481
+ function cloneTypedArray(value) {
4482
+ return new value.constructor(value);
4483
+ }
4484
+ function cloneMesh(mesh) {
4485
+ const attributes = Object.fromEntries(
4486
+ Object.entries(mesh.attributes).map(([key, value]) => [
4487
+ key,
4488
+ value instanceof ArrayBuffer ? value.slice(0) : value === void 0 ? void 0 : cloneTypedArray(value)
4489
+ ])
4490
+ );
4491
+ return Object.freeze({
4492
+ ...mesh,
4493
+ vertices: new Float32Array(mesh.vertices),
4494
+ ...mesh.indices === void 0 ? {} : { indices: cloneTypedArray(mesh.indices) },
4495
+ attributes,
4496
+ ...mesh.aabb === void 0 ? {} : { aabb: new Float32Array(mesh.aabb) },
4497
+ submeshes: Object.freeze(mesh.submeshes.map((submesh) => Object.freeze({ ...submesh }))),
4498
+ materialSlots: Object.freeze(mesh.materialSlots.map((slot) => Object.freeze({ ...slot })))
4499
+ });
4500
+ }
4501
+ function cloneCandidate(candidate) {
4502
+ return Object.freeze({ ...candidate, mesh: cloneMesh(candidate.mesh) });
4503
+ }
4504
+ function validateMesh(mesh) {
4505
+ if (mesh.kind !== "mesh") return "MeshAsset.kind must be mesh";
4506
+ if (!(mesh.vertices instanceof Float32Array) || mesh.vertices.length === 0) {
4507
+ return "MeshAsset.vertices must be a non-empty Float32Array";
4508
+ }
4509
+ if (mesh.indices !== void 0 && !(mesh.indices instanceof Uint16Array || mesh.indices instanceof Uint32Array)) {
4510
+ return "MeshAsset.indices must be Uint16Array or Uint32Array";
4511
+ }
4512
+ if (mesh.submeshes.length === 0)
4513
+ return "MeshAsset.submeshes must contain at least one topology range";
4514
+ if (mesh.submeshes.some(
4515
+ (submesh) => !Number.isSafeInteger(submesh.materialSlot) || submesh.materialSlot < 0 || submesh.materialSlot >= mesh.materialSlots.length
4516
+ )) {
4517
+ return "MeshAsset.materialSlots must cover every referenced materialSlot";
4518
+ }
4519
+ if (mesh.vertices.some((value) => !Number.isFinite(value)))
4520
+ return "MeshAsset.vertices must be finite";
4521
+ return void 0;
4522
+ }
4523
+ function meshByteSize(mesh) {
4524
+ let bytes = mesh.vertices.byteLength + (mesh.indices?.byteLength ?? 0);
4525
+ for (const value of Object.values(mesh.attributes)) {
4526
+ if (value instanceof ArrayBuffer) bytes += value.byteLength;
4527
+ else if (value !== void 0 && ArrayBuffer.isView(value)) bytes += value.byteLength;
4528
+ }
4529
+ if (mesh.aabb !== void 0) bytes += mesh.aabb.byteLength;
4530
+ return bytes;
4531
+ }
4532
+ function createDynamicGeometryLifecycle(maxCandidates = 32, maxMeshBytes = 64 * 1024 * 1024) {
4533
+ const records = /* @__PURE__ */ new Map();
4534
+ const owner = {};
4535
+ let sequence = 0;
4536
+ let currentGeneration = 0;
4537
+ let disposed = false;
4538
+ let historyInvalidations = 0;
4539
+ let latestRevision;
4540
+ let latestFrameId;
4541
+ let retired = 0;
4542
+ let invalidated = 0;
4543
+ let meshBytes = 0;
4544
+ const latestRevisionByWorld = /* @__PURE__ */ new WeakMap();
4545
+ const latestTopologyRevisionByWorld = /* @__PURE__ */ new WeakMap();
4546
+ const publishedReceipts = /* @__PURE__ */ new Map();
4547
+ const failure3 = (code, expected, hint, detail = {}) => err(new DynamicGeometryError(code, expected, hint, detail));
4548
+ const current = (candidate) => {
4549
+ const record = records.get(candidate.candidateId);
4550
+ if (record === void 0 || candidate.owner !== owner || record.candidate.owner !== candidate.owner || record.candidate.world !== candidate.world || record.candidate.generation !== candidate.generation)
4551
+ return void 0;
4552
+ return record;
4553
+ };
4554
+ return {
4555
+ prepare(input, generation) {
4556
+ if (disposed)
4557
+ return failure3("dynamic-geometry-disposed", "renderer is alive", "rebuild the Renderer");
4558
+ if (input.world === null || typeof input.world !== "object") {
4559
+ return failure3(
4560
+ "dynamic-geometry-world-not-attached",
4561
+ "candidate belongs to an attached World",
4562
+ "attach the World before preparing geometry"
4563
+ );
4564
+ }
4565
+ if (!Number.isInteger(input.revision) || input.revision < 0) {
4566
+ return failure3(
4567
+ "dynamic-geometry-invalid",
4568
+ "geometry revision is a non-negative integer",
4569
+ "advance the consumer revision monotonically",
4570
+ { revision: input.revision }
4571
+ );
4572
+ }
4573
+ if (generation !== currentGeneration && currentGeneration !== 0) {
4574
+ return failure3(
4575
+ "dynamic-geometry-generation-mismatch",
4576
+ "candidate uses the active renderer generation",
4577
+ "discard stale generation work and prepare again",
4578
+ { generation, actual: currentGeneration }
4579
+ );
4580
+ }
4581
+ if (records.size >= maxCandidates) {
4582
+ return failure3(
4583
+ "dynamic-geometry-budget-exceeded",
4584
+ "candidate count remains within the bounded renderer budget",
4585
+ "cancel or retire an earlier candidate before retrying",
4586
+ { actual: records.size }
4587
+ );
4588
+ }
4589
+ const invalid2 = validateMesh(input.mesh);
4590
+ if (invalid2 !== void 0)
4591
+ return failure3(
4592
+ "dynamic-geometry-invalid",
4593
+ "MeshAsset satisfies the standard geometry contract",
4594
+ invalid2
4595
+ );
4596
+ const candidateBytes = meshByteSize(input.mesh);
4597
+ if (!Number.isSafeInteger(candidateBytes) || candidateBytes < 0 || meshBytes > maxMeshBytes - candidateBytes) {
4598
+ return failure3(
4599
+ "dynamic-geometry-budget-exceeded",
4600
+ "candidate mesh bytes remain within the bounded renderer resource budget",
4601
+ "cancel or retire an earlier candidate before retrying",
4602
+ { actual: { meshBytes, candidateBytes, maxMeshBytes } }
4603
+ );
4604
+ }
4605
+ const candidateId = `mesh:${generation}:${++sequence}:${input.revision}`;
4606
+ const candidate = Object.freeze({
4607
+ candidateId,
4608
+ generation,
4609
+ owner,
4610
+ world: input.world,
4611
+ ...input.entity === void 0 ? {} : { entity: input.entity },
4612
+ ...input.physicsEntity === void 0 ? {} : { physicsEntity: input.physicsEntity },
4613
+ gpuReady: input.meshHandle !== void 0,
4614
+ meshBytes: candidateBytes,
4615
+ revision: input.revision,
4616
+ topologyRevision: input.topologyRevision ?? input.revision,
4617
+ ...input.materialIdentity === void 0 ? {} : { materialIdentity: input.materialIdentity },
4618
+ ...input.meshHandle === void 0 ? {} : { meshHandle: input.meshHandle },
4619
+ ...input.fixedStep === void 0 ? {} : { fixedStep: input.fixedStep },
4620
+ mesh: cloneMesh(input.mesh),
4621
+ state: "prepared"
4622
+ });
4623
+ records.set(candidateId, {
4624
+ world: input.world,
4625
+ candidate,
4626
+ historyInvalidated: false
4627
+ });
4628
+ meshBytes += candidateBytes;
4629
+ currentGeneration = generation;
4630
+ return ok(cloneCandidate(candidate));
4631
+ },
4632
+ accept(candidate, ordering) {
4633
+ if (disposed)
4634
+ return failure3("dynamic-geometry-disposed", "renderer is alive", "rebuild the Renderer");
4635
+ const record = current(candidate);
4636
+ if (record === void 0)
4637
+ return failure3(
4638
+ "dynamic-geometry-candidate-not-found",
4639
+ "candidate is owned by this renderer",
4640
+ "discard the stale credential"
4641
+ );
4642
+ if (candidate.generation !== currentGeneration) {
4643
+ return failure3(
4644
+ "dynamic-geometry-generation-mismatch",
4645
+ "candidate generation matches the active renderer generation",
4646
+ "prepare against the current renderer generation",
4647
+ { candidateId: candidate.candidateId }
4648
+ );
4649
+ }
4650
+ if (record.candidate.state !== "prepared") {
4651
+ return failure3(
4652
+ "dynamic-geometry-candidate-state",
4653
+ "candidate is prepared exactly once before acceptance",
4654
+ "retain the accepted or published receipt",
4655
+ { candidateId: candidate.candidateId }
4656
+ );
4657
+ }
4658
+ if (!sameCandidateCredential(candidate, record.candidate)) {
4659
+ return failure3(
4660
+ "dynamic-geometry-receipt-mismatch",
4661
+ "acceptance uses the exact prepared candidate credential",
4662
+ "discard the altered credential and use the value returned by prepare",
4663
+ { candidateId: candidate.candidateId }
4664
+ );
4665
+ }
4666
+ if (ordering !== void 0) {
4667
+ if (ordering.world !== record.world || !Number.isInteger(ordering.fixedStep) || ordering.fixedStep < 0) {
4668
+ return failure3(
4669
+ "dynamic-geometry-ordering-required",
4670
+ "acceptance ordering names the candidate World and a non-negative fixed step",
4671
+ "submit the PhysicsWorld publication ordering for the same attached World",
4672
+ { candidateId: candidate.candidateId, actual: ordering }
4673
+ );
4674
+ }
4675
+ }
4676
+ const newest = [...records.values()].filter(
4677
+ (entry) => entry.candidate.candidateId !== candidate.candidateId && entry.world === record.world && entry.candidate.entity === record.candidate.entity && (entry.candidate.state === "prepared" || entry.candidate.state === "accepted")
4678
+ ).map((entry) => entry.candidate.revision).concat(latestRevisionByWorld.get(record.world)?.get(record.candidate.entity) ?? -1).sort((a, b) => b - a)[0];
4679
+ if (newest !== void 0 && record.candidate.revision <= newest) {
4680
+ return failure3(
4681
+ "dynamic-geometry-stale",
4682
+ "accepted geometry revision is newer than the active candidate",
4683
+ "advance topology revision before accepting",
4684
+ { candidateId: candidate.candidateId }
4685
+ );
4686
+ }
4687
+ const accepted = Object.freeze({
4688
+ ...record.candidate,
4689
+ ...ordering === void 0 ? {} : { fixedStep: ordering.fixedStep },
4690
+ state: "accepted"
4691
+ });
4692
+ const previousTopologies = [...records.values()].filter(
4693
+ (entry) => entry.candidate.candidateId !== candidate.candidateId && entry.world === record.world && entry.candidate.entity === record.candidate.entity
4694
+ ).map((entry) => entry.candidate.topologyRevision);
4695
+ const previousLatestTopology = latestTopologyRevisionByWorld.get(record.world)?.get(record.candidate.entity);
4696
+ const newestTopology = previousTopologies.concat(previousLatestTopology === void 0 ? [] : [previousLatestTopology]).sort((a, b) => b - a)[0];
4697
+ if (newestTopology !== void 0 && accepted.topologyRevision < newestTopology) {
4698
+ return failure3(
4699
+ "dynamic-geometry-stale",
4700
+ "accepted topology revision is not older than the active geometry history",
4701
+ "advance topology revision before accepting a replacement candidate",
4702
+ { candidateId: candidate.candidateId }
4703
+ );
4704
+ }
4705
+ if (newestTopology !== void 0 && accepted.topologyRevision > newestTopology) {
4706
+ historyInvalidations += 1;
4707
+ for (const previous of records.values()) {
4708
+ if (previous.world !== record.world || previous.candidate.entity !== record.candidate.entity || previous.candidate.candidateId === candidate.candidateId || previous.candidate.topologyRevision >= accepted.topologyRevision)
4709
+ continue;
4710
+ if (!previous.historyInvalidated) {
4711
+ previous.historyInvalidated = true;
4712
+ invalidated += 1;
4713
+ }
4714
+ }
4715
+ }
4716
+ record.candidate = accepted;
4717
+ return ok(cloneCandidate(accepted));
4718
+ },
4719
+ cancel(candidate, completion) {
4720
+ const record = current(candidate);
4721
+ if (record === void 0)
4722
+ return failure3(
4723
+ "dynamic-geometry-candidate-not-found",
4724
+ "candidate is owned by this renderer",
4725
+ "discard the stale credential"
4726
+ );
4727
+ if (!sameCandidateCredential(candidate, record.candidate))
4728
+ return failure3(
4729
+ "dynamic-geometry-receipt-mismatch",
4730
+ "cancellation uses the exact prepared candidate credential",
4731
+ "discard the altered credential and use the value returned by prepare",
4732
+ { candidateId: candidate.candidateId }
4733
+ );
4734
+ if (record.candidate.state !== "prepared" && record.candidate.state !== "accepted") {
4735
+ return failure3(
4736
+ "dynamic-geometry-candidate-state",
4737
+ "only prepared or accepted geometry can be cancelled once",
4738
+ "retire published geometry or await the existing terminal cleanup"
4739
+ );
4740
+ }
4741
+ record.candidate = Object.freeze({ ...record.candidate, state: "cancelled" });
4742
+ const release = () => {
4743
+ if (records.get(candidate.candidateId) !== record) return;
4744
+ records.delete(candidate.candidateId);
4745
+ meshBytes = Math.max(0, meshBytes - record.candidate.meshBytes);
4746
+ };
4747
+ if (completion === void 0) release();
4748
+ else void completion.then(release, release);
4749
+ return ok(void 0);
4750
+ },
4751
+ retire(candidate) {
4752
+ const record = current(candidate);
4753
+ if (record === void 0)
4754
+ return failure3(
4755
+ "dynamic-geometry-candidate-not-found",
4756
+ "retirement uses a published candidate owned by this renderer",
4757
+ "discard the stale credential and do not evict any unrelated mesh",
4758
+ { candidateId: candidate.candidateId }
4759
+ );
4760
+ if (!sameCandidateCredential(candidate, record.candidate))
4761
+ return failure3(
4762
+ "dynamic-geometry-receipt-mismatch",
4763
+ "retirement uses the exact published candidate credential",
4764
+ "discard the altered credential and use the value returned by publish",
4765
+ { candidateId: candidate.candidateId }
4766
+ );
4767
+ if (record.candidate.state !== "published") {
4768
+ return failure3(
4769
+ "dynamic-geometry-candidate-state",
4770
+ "only published geometry can enter receipt-bound retirement",
4771
+ "publish the candidate from a submitted frame first"
4772
+ );
4773
+ }
4774
+ record.candidate = Object.freeze({ ...record.candidate, state: "retired" });
4775
+ publishedReceipts.delete(candidate.candidateId);
4776
+ retired += 1;
4777
+ return ok(void 0);
4778
+ },
4779
+ finalizeRetirement(candidate) {
4780
+ const record = current(candidate);
4781
+ if (record === void 0)
4782
+ return failure3(
4783
+ "dynamic-geometry-candidate-not-found",
4784
+ "retirement finalization uses a candidate retained by this lifecycle",
4785
+ "discard the stale credential or finish the current receipt-bound retirement"
4786
+ );
4787
+ if (!sameCandidateCredential(candidate, record.candidate))
4788
+ return failure3(
4789
+ "dynamic-geometry-receipt-mismatch",
4790
+ "retirement finalization uses the exact published candidate credential",
4791
+ "discard the altered credential and use the value returned by publication",
4792
+ { candidateId: candidate.candidateId }
4793
+ );
4794
+ if (record.candidate.state !== "retired")
4795
+ return failure3(
4796
+ "dynamic-geometry-candidate-state",
4797
+ "only a retired candidate can release its deferred budget",
4798
+ "retire the published candidate before finalizing its receipt-bound cleanup",
4799
+ { candidateId: candidate.candidateId }
4800
+ );
4801
+ records.delete(candidate.candidateId);
4802
+ meshBytes = Math.max(0, meshBytes - record.candidate.meshBytes);
4803
+ return ok(void 0);
4804
+ },
4805
+ publishFrame(frame, worlds, fixedStep, canPublish, recordStageLane) {
4806
+ if (disposed || frame.deviceGeneration !== currentGeneration) return [];
4807
+ const receipts = [];
4808
+ for (const record of records.values()) {
4809
+ const canRepeatPublished = record.candidate.state === "published" && // Attached Renderer publication revalidates the entity binding in
4810
+ // `canPublish`, so a history-invalidated candidate can still record
4811
+ // a real draw for its live entity. Detached lifecycle publication
4812
+ // remains one-shot once that history is invalidated.
4813
+ (canPublish !== void 0 || !record.historyInvalidated);
4814
+ if (record.candidate.state !== "accepted" && !canRepeatPublished) continue;
4815
+ if (worlds !== void 0 && !worlds.includes(record.world)) continue;
4816
+ if (record.candidate.fixedStep !== void 0 && (fixedStep === void 0 || record.candidate.fixedStep > fixedStep)) {
4817
+ continue;
4818
+ }
4819
+ if (canPublish !== void 0 && !canPublish(record.candidate)) continue;
4820
+ const consumedLane = canPublish === void 0 ? void 0 : recordStageLane?.(record.candidate);
4821
+ record.candidate = Object.freeze({ ...record.candidate, state: "published" });
4822
+ const receipt = Object.freeze({
4823
+ candidateId: record.candidate.candidateId,
4824
+ generation: record.candidate.generation,
4825
+ revision: record.candidate.revision,
4826
+ topologyRevision: record.candidate.topologyRevision,
4827
+ ...record.candidate.fixedStep === void 0 ? {} : { fixedStep: record.candidate.fixedStep },
4828
+ frameId: frame.frameId,
4829
+ deviceGeneration: frame.deviceGeneration,
4830
+ recordStageConsumed: canPublish !== void 0,
4831
+ ...consumedLane === void 0 ? {} : { recordStageLane: consumedLane },
4832
+ ...fixedStep === void 0 ? {} : { publicationFixedStep: fixedStep },
4833
+ frame: Object.freeze({ ...frame })
4834
+ });
4835
+ receipts.push(receipt);
4836
+ publishedReceipts.set(record.candidate.candidateId, receipt);
4837
+ latestRevision = record.candidate.revision;
4838
+ latestFrameId = frame.frameId;
4839
+ if (!latestRevisionByWorld.has(record.world))
4840
+ latestRevisionByWorld.set(record.world, /* @__PURE__ */ new Map());
4841
+ latestRevisionByWorld.get(record.world)?.set(record.candidate.entity, record.candidate.revision);
4842
+ if (!latestTopologyRevisionByWorld.has(record.world))
4843
+ latestTopologyRevisionByWorld.set(record.world, /* @__PURE__ */ new Map());
4844
+ latestTopologyRevisionByWorld.get(record.world)?.set(record.candidate.entity, record.candidate.topologyRevision);
4845
+ }
4846
+ return receipts;
4847
+ },
4848
+ invalidateWorld(world, completion) {
4849
+ for (const [id, record] of records) {
4850
+ if (record.world !== world) continue;
4851
+ if (record.candidate.state === "cancelled" || record.candidate.state === "retired")
4852
+ continue;
4853
+ publishedReceipts.delete(id);
4854
+ record.candidate = Object.freeze({ ...record.candidate, state: "cancelled" });
4855
+ const release = () => {
4856
+ if (records.get(id) !== record) return;
4857
+ records.delete(id);
4858
+ meshBytes = Math.max(0, meshBytes - record.candidate.meshBytes);
4859
+ };
4860
+ if (completion === void 0) release();
4861
+ else void completion.then(release, release);
4862
+ invalidated += 1;
4863
+ }
4864
+ latestRevisionByWorld.delete(world);
4865
+ latestTopologyRevisionByWorld.delete(world);
4866
+ },
4867
+ invalidateGeneration(generation) {
4868
+ if (generation === currentGeneration) return;
4869
+ currentGeneration = generation;
4870
+ const invalidatedWorlds = /* @__PURE__ */ new Set();
4871
+ for (const [id, record] of records) {
4872
+ if (record.candidate.generation !== generation) {
4873
+ records.delete(id);
4874
+ publishedReceipts.delete(id);
4875
+ meshBytes = Math.max(0, meshBytes - record.candidate.meshBytes);
4876
+ invalidatedWorlds.add(record.world);
4877
+ invalidated += 1;
4878
+ }
4879
+ }
4880
+ for (const world of invalidatedWorlds) {
4881
+ latestRevisionByWorld.delete(world);
4882
+ latestTopologyRevisionByWorld.delete(world);
4883
+ }
4884
+ },
4885
+ receipt(candidate) {
4886
+ const receipt = publishedReceipts.get(candidate.candidateId);
4887
+ const record = records.get(candidate.candidateId);
4888
+ if (record === void 0 || !sameCandidateCredential(candidate, record.candidate)) {
4889
+ return void 0;
4890
+ }
4891
+ return receipt?.generation === candidate.generation ? receipt : void 0;
4892
+ },
4893
+ inspect() {
4894
+ let prepared = 0;
4895
+ let accepted = 0;
4896
+ let published = 0;
4897
+ for (const record of records.values()) {
4898
+ if (record.candidate.state === "prepared") prepared += 1;
4899
+ else if (record.candidate.state === "accepted") accepted += 1;
4900
+ else if (record.candidate.state === "published") published += 1;
4901
+ }
4902
+ return Object.freeze({
4903
+ generation: currentGeneration,
4904
+ prepared,
4905
+ accepted,
4906
+ published,
4907
+ retired,
4908
+ ...latestRevision === void 0 ? {} : { latestRevision },
4909
+ ...latestFrameId === void 0 ? {} : { latestFrameId },
4910
+ historyInvalidations,
4911
+ invalidated,
4912
+ meshBytes,
4913
+ maxMeshBytes
4914
+ });
4915
+ },
4916
+ dispose() {
4917
+ disposed = true;
4918
+ records.clear();
4919
+ publishedReceipts.clear();
4920
+ meshBytes = 0;
4921
+ }
4922
+ };
4923
+ }
4924
+
4925
+ export { BARREL_DISTORTION_FEATURE_IDENTITY, BARREL_DISTORTION_POST_PROCESS_ID, BARREL_DISTORTION_WGSL, BARREL_DISTORTION_WGSL_COORDINATE, CLOUD_DENSITY_COMPUTE_WGSL, CLOUD_HISTORY_FULLSCREEN_WGSL, CLOUD_HISTORY_SURFACE_COUNT, CLOUD_LAYER_DENSITY_BINDINGS, CLOUD_LAYER_DENSITY_CACHE, CLOUD_LAYER_DENSITY_OUTPUT, CLOUD_LAYER_DENSITY_PROGRAM, CLOUD_LAYER_FEATURE_IDENTITY, CLOUD_RGBA16FLOAT_BYTES_PER_TEXEL, CLOUD_VIEW_FULLSCREEN_WGSL, CLOUD_VIEW_PARAMS_BYTES, CloudHistoryStore, DEFAULT_REFLECTION_PROBE_LIMITS, DynamicGeometryError, DynamicInputError, LOD_OCCLUSION_INSPECTION_MAX_BYTES, LOD_OCCLUSION_INSPECTION_SCHEMA, ReadonlyDynamicInputPage, ReflectionProbeProjection, SSR_FORMAT_PROFILE, SSR_FORMAT_STAGES, acceptCloudLayerGeneration, admitPointsLines, admitReflectionProbe, admitSingleLayerMediumSubmission, admitSsrM0, admitSsrSpatial, advanceProbeFilter, applyCloudSolarTransmittance, boxProjectReflectionDirection, buildCloudDensityCache, buildReflectionProbeTable, cloudCapabilitiesFromRhi, cloudTemporalResetReasons, cloudTemporalSignature, commitProbeFilterStep, compositeCloudRadiance, consumeLodOcclusionInspection, createBarrelDistortionRenderFeature, createCloudHistory, createCloudLayerFeature, createCloudShadowProjection, createDynamicGeometryLifecycle, createProbeFilterState, deriveReflectionFallbackProjection, estimateReflectionProbeBytes, evaluateCloudDensity, inspectCloudLayer, inspectCloudLayerResources, inspectLodOcclusion, inspectPointShadow, inspectReflectionFallback, inspectVolumetricFog, integrateCloudCameraPath, integrateCloudInterior, integrateCloudPath, integrateCloudSolarColumn, intersectCloudLayer, probeFilterIsSteady, projectCloudShadowUv, projectSsrDependencies, projectSsrSpatialInspection, reconstructCloudDensityCache, reflectionFallbackCoverage, reflectionFallbackProjectionSignature, reflectionFallbackSourceKey, reflectionProbeTableBytes, reprojectCloudHistory, resolveReflectionFallbackSource, resolveSsrAdmissionGeneration, retainCloudLayerAfterFailure, sameCandidateCredential, sampleCloudDensity, sampleCloudShadow, selectReflectionProbe, serializeLodOcclusionInspection, serializeSsrSpatialInspection, snapshotCloudDensityCache, validateReflectionProbeInput, validateScreenSpaceReflection, zeroSsrAdmissionWork };
4926
+ //# sourceMappingURL=chunk-ODZWR6JQ.mjs.map
4927
+ //# sourceMappingURL=chunk-ODZWR6JQ.mjs.map