@forgeax/engine-render 0.1.32 → 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 (624) hide show
  1. package/README.md +1374 -142
  2. package/dist/assembly/analytic-fog-registration.d.ts +3 -0
  3. package/dist/assembly/analytic-fog-registration.d.ts.map +1 -0
  4. package/dist/assembly/backend-contract.d.ts +2 -0
  5. package/dist/assembly/backend-contract.d.ts.map +1 -1
  6. package/dist/assembly/barrel-distortion-inspection.d.ts +3 -0
  7. package/dist/assembly/barrel-distortion-inspection.d.ts.map +1 -0
  8. package/dist/assembly/canvas-draw-guard.d.ts +3 -0
  9. package/dist/assembly/canvas-draw-guard.d.ts.map +1 -0
  10. package/dist/assembly/cloud-shadow-entry.d.ts +4 -0
  11. package/dist/assembly/cloud-shadow-entry.d.ts.map +1 -0
  12. package/dist/assembly/dynamic-geometry-host.d.ts +4 -1
  13. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  14. package/dist/assembly/dynamic-geometry-runtime.d.ts +1 -1
  15. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -1
  16. package/dist/assembly/factory.d.ts +1 -1
  17. package/dist/assembly/factory.d.ts.map +1 -1
  18. package/dist/assembly/fxaa-registration.d.ts +4 -0
  19. package/dist/assembly/fxaa-registration.d.ts.map +1 -0
  20. package/dist/assembly/host-contract.d.ts +3 -3
  21. package/dist/assembly/host-contract.d.ts.map +1 -1
  22. package/dist/assembly/lod-observation.d.ts +4 -0
  23. package/dist/assembly/lod-observation.d.ts.map +1 -0
  24. package/dist/assembly/material/assembly.d.ts.map +1 -1
  25. package/dist/assembly/material/pipeline-helpers.d.ts +5 -0
  26. package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -1
  27. package/dist/assembly/material-shader-policy.d.ts +16 -2
  28. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  29. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -1
  30. package/dist/assembly/motion-blur-registration.d.ts +3 -0
  31. package/dist/assembly/motion-blur-registration.d.ts.map +1 -0
  32. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
  33. package/dist/assembly/recovery/renderer-recover.d.ts +2 -0
  34. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  35. package/dist/assembly/render-feature-post-processes.d.ts +6 -0
  36. package/dist/assembly/render-feature-post-processes.d.ts.map +1 -0
  37. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  38. package/dist/assembly/renderer-feature-inspection.d.ts +1 -1
  39. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -1
  40. package/dist/assembly/renderer-frame-transaction.d.ts +12 -0
  41. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  42. package/dist/assembly/renderer-inspection.d.ts +11 -0
  43. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  44. package/dist/assembly/temporal-post-process-prewarm.d.ts +13 -0
  45. package/dist/assembly/temporal-post-process-prewarm.d.ts.map +1 -0
  46. package/dist/assembly/webgpu-pbr-ready.d.ts +1 -0
  47. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -1
  48. package/dist/assembly/webgpu-ready-contract.d.ts +4 -3
  49. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -1
  50. package/dist/assembly/webgpu-ready-per-pass.d.ts +39 -0
  51. package/dist/assembly/webgpu-ready-per-pass.d.ts.map +1 -0
  52. package/dist/assembly/webgpu-ready.d.ts +2 -2
  53. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  54. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -1
  55. package/dist/assembly/webgpu-renderer-observation-owner.d.ts +35 -0
  56. package/dist/assembly/webgpu-renderer-observation-owner.d.ts.map +1 -0
  57. package/dist/assembly/webgpu-renderer.d.ts +1 -1
  58. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  59. package/dist/authoring.mjs +1 -1
  60. package/dist/barrel-distortion.d.ts +47 -0
  61. package/dist/barrel-distortion.d.ts.map +1 -0
  62. package/dist/bloom-admission.d.ts +13 -0
  63. package/dist/bloom-admission.d.ts.map +1 -0
  64. package/dist/{chunk-PA5DRJNI.mjs → chunk-ELJFJ2BU.mjs} +2 -2
  65. package/dist/chunk-ELJFJ2BU.mjs.map +1 -0
  66. package/dist/{chunk-JCIRYQ4V.mjs → chunk-HEB564WF.mjs} +10 -4
  67. package/dist/chunk-HEB564WF.mjs.map +1 -0
  68. package/dist/{chunk-JRIPCMYC.mjs → chunk-MKDIT2EM.mjs} +1584 -375
  69. package/dist/chunk-MKDIT2EM.mjs.map +1 -0
  70. package/dist/{chunk-S665NJNF.mjs → chunk-OCDLEI6I.mjs} +3 -3
  71. package/dist/{chunk-S665NJNF.mjs.map → chunk-OCDLEI6I.mjs.map} +1 -1
  72. package/dist/chunk-ODZWR6JQ.mjs +4927 -0
  73. package/dist/chunk-ODZWR6JQ.mjs.map +1 -0
  74. package/dist/{chunk-DRR7D3KX.mjs → chunk-SHFUFJ5N.mjs} +192 -18
  75. package/dist/chunk-SHFUFJ5N.mjs.map +1 -0
  76. package/dist/{chunk-4NNZQCL3.mjs → chunk-UI5SFODX.mjs} +223 -277
  77. package/dist/chunk-UI5SFODX.mjs.map +1 -0
  78. package/dist/{chunk-GWCEMDVR.mjs → chunk-UINHLCZA.mjs} +787 -80
  79. package/dist/chunk-UINHLCZA.mjs.map +1 -0
  80. package/dist/{chunk-6TFBXLRJ.mjs → chunk-WEX45HKH.mjs} +222 -337
  81. package/dist/chunk-WEX45HKH.mjs.map +1 -0
  82. package/dist/cloud/density.d.ts +55 -0
  83. package/dist/cloud/density.d.ts.map +1 -0
  84. package/dist/cloud/extract.d.ts +25 -0
  85. package/dist/cloud/extract.d.ts.map +1 -0
  86. package/dist/cloud/feature.d.ts +57 -0
  87. package/dist/cloud/feature.d.ts.map +1 -0
  88. package/dist/cloud/frame-context.d.ts +8 -0
  89. package/dist/cloud/frame-context.d.ts.map +1 -0
  90. package/dist/cloud/inspection.d.ts +59 -0
  91. package/dist/cloud/inspection.d.ts.map +1 -0
  92. package/dist/cloud/optics.d.ts +56 -0
  93. package/dist/cloud/optics.d.ts.map +1 -0
  94. package/dist/cloud/parameters.d.ts +73 -0
  95. package/dist/cloud/parameters.d.ts.map +1 -0
  96. package/dist/cloud/resources.d.ts +60 -0
  97. package/dist/cloud/resources.d.ts.map +1 -0
  98. package/dist/cloud/shadow.d.ts +42 -0
  99. package/dist/cloud/shadow.d.ts.map +1 -0
  100. package/dist/cloud/temporal.d.ts +74 -0
  101. package/dist/cloud/temporal.d.ts.map +1 -0
  102. package/dist/components/atmosphere.d.ts +2 -0
  103. package/dist/components/atmosphere.d.ts.map +1 -1
  104. package/dist/components/barrel-distortion.d.ts +13 -0
  105. package/dist/components/barrel-distortion.d.ts.map +1 -0
  106. package/dist/components/camera.d.ts +20 -4
  107. package/dist/components/camera.d.ts.map +1 -1
  108. package/dist/components/cloud-layer.d.ts +29 -0
  109. package/dist/components/cloud-layer.d.ts.map +1 -0
  110. package/dist/components/index.d.ts +2 -0
  111. package/dist/components/index.d.ts.map +1 -1
  112. package/dist/components/instances.d.ts +20 -6
  113. package/dist/components/instances.d.ts.map +1 -1
  114. package/dist/components/mesh-filter.d.ts +6 -5
  115. package/dist/components/mesh-filter.d.ts.map +1 -1
  116. package/dist/components/motion-blur.d.ts +1 -0
  117. package/dist/components/motion-blur.d.ts.map +1 -1
  118. package/dist/construct-renderer.d.ts +1 -1
  119. package/dist/construct-renderer.d.ts.map +1 -1
  120. package/dist/construct-renderer.mjs +9751 -3528
  121. package/dist/construct-renderer.mjs.map +1 -1
  122. package/dist/device/gpu-residency.d.ts +6 -0
  123. package/dist/device/gpu-residency.d.ts.map +1 -1
  124. package/dist/dynamic-geometry.d.ts +5 -1
  125. package/dist/dynamic-geometry.d.ts.map +1 -1
  126. package/dist/environment/analytic-fog.d.ts +6 -0
  127. package/dist/environment/analytic-fog.d.ts.map +1 -0
  128. package/dist/errors/cloud.d.ts +59 -0
  129. package/dist/errors/cloud.d.ts.map +1 -0
  130. package/dist/errors/render.d.ts +23 -5
  131. package/dist/errors/render.d.ts.map +1 -1
  132. package/dist/examples/single-layer-medium-surface.d.ts +47 -0
  133. package/dist/examples/single-layer-medium-surface.d.ts.map +1 -0
  134. package/dist/extract/camera.d.ts.map +1 -1
  135. package/dist/extract/contracts.d.ts +12 -1
  136. package/dist/extract/contracts.d.ts.map +1 -1
  137. package/dist/extract/environment.d.ts +2 -0
  138. package/dist/extract/environment.d.ts.map +1 -1
  139. package/dist/features/barrel-distortion.d.ts +29 -0
  140. package/dist/features/barrel-distortion.d.ts.map +1 -0
  141. package/dist/features/error-reporting.d.ts +17 -0
  142. package/dist/features/error-reporting.d.ts.map +1 -0
  143. package/dist/features/host.d.ts +15 -0
  144. package/dist/features/host.d.ts.map +1 -1
  145. package/dist/features/motion-blur/motion-blur-feature.d.ts +20 -2
  146. package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -1
  147. package/dist/features/motion-blur/motion-blur-params.d.ts +21 -0
  148. package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -1
  149. package/dist/features/motion-blur/motion-blur-runtime.d.ts +54 -0
  150. package/dist/features/motion-blur/motion-blur-runtime.d.ts.map +1 -0
  151. package/dist/features/plan.d.ts +15 -4
  152. package/dist/features/plan.d.ts.map +1 -1
  153. package/dist/features/prepared-gpu-work.d.ts +9 -5
  154. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  155. package/dist/features/render-graph-compute.d.ts.map +1 -1
  156. package/dist/features/render-graph-contribution.d.ts +5 -0
  157. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  158. package/dist/features/render-graph-raster.d.ts +1 -0
  159. package/dist/features/render-graph-raster.d.ts.map +1 -1
  160. package/dist/features/targets.d.ts +2 -0
  161. package/dist/features/targets.d.ts.map +1 -1
  162. package/dist/features/types.d.ts +44 -0
  163. package/dist/features/types.d.ts.map +1 -1
  164. package/dist/fullscreen-post-process-pass.d.ts +15 -7
  165. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  166. package/dist/gpu-dirty-ranges.d.ts +11 -0
  167. package/dist/gpu-dirty-ranges.d.ts.map +1 -0
  168. package/dist/gpu-driven/batch-topology.d.ts +17 -0
  169. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  170. package/dist/gpu-driven/material-artifacts.d.ts +6 -0
  171. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
  172. package/dist/gpu-driven/prepared-draw.d.ts +7 -0
  173. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  174. package/dist/gpu-driven/production-raster-material.d.ts +178 -0
  175. package/dist/gpu-driven/production-raster-material.d.ts.map +1 -0
  176. package/dist/gpu-driven/production-raster.d.ts +50 -4
  177. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  178. package/dist/gpu-driven/resource-allocation.d.ts +31 -0
  179. package/dist/gpu-driven/resource-allocation.d.ts.map +1 -0
  180. package/dist/gpu-driven/shadow-ownership.d.ts +14 -0
  181. package/dist/gpu-driven/shadow-ownership.d.ts.map +1 -0
  182. package/dist/gpu-driven/view-gpu.d.ts +35 -2
  183. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  184. package/dist/gpu-scene.d.ts +11 -1
  185. package/dist/gpu-scene.d.ts.map +1 -1
  186. package/dist/hdrp-buffers.d.ts +2 -2
  187. package/dist/hdrp-buffers.d.ts.map +1 -1
  188. package/dist/ibl/skylight-bind-group.d.ts +10 -1
  189. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  190. package/dist/index.d.ts +31 -8
  191. package/dist/index.d.ts.map +1 -1
  192. package/dist/index.mjs +258 -8
  193. package/dist/index.mjs.map +1 -1
  194. package/dist/inspection-types.d.ts +178 -3
  195. package/dist/inspection-types.d.ts.map +1 -1
  196. package/dist/instance-buffer-cache.d.ts +4 -4
  197. package/dist/instance-buffer-cache.d.ts.map +1 -1
  198. package/dist/instances.d.ts +49 -94
  199. package/dist/instances.d.ts.map +1 -1
  200. package/dist/internal.mjs +7 -7
  201. package/dist/pbr-pipeline.d.ts +19 -1
  202. package/dist/pbr-pipeline.d.ts.map +1 -1
  203. package/dist/pipeline/single-layer-medium-passes.d.ts +73 -0
  204. package/dist/pipeline/single-layer-medium-passes.d.ts.map +1 -0
  205. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  206. package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
  207. package/dist/pipeline/standard-output/types.d.ts +4 -2
  208. package/dist/pipeline/standard-output/types.d.ts.map +1 -1
  209. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  210. package/dist/pipeline/standard-post.d.ts +28 -19
  211. package/dist/pipeline/standard-post.d.ts.map +1 -1
  212. package/dist/pipeline-builder.d.ts +1 -1
  213. package/dist/pipeline-builder.d.ts.map +1 -1
  214. package/dist/pipeline-spec.d.ts +2 -2
  215. package/dist/plugin.d.ts.map +1 -1
  216. package/dist/prepare/prepared-graphics-resolver.d.ts +2 -0
  217. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  218. package/dist/record/barrel-distortion-frame.d.ts +15 -0
  219. package/dist/record/barrel-distortion-frame.d.ts.map +1 -0
  220. package/dist/record/dynamic-geometry-consumption.d.ts +10 -2
  221. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -1
  222. package/dist/record/fold-buckets.d.ts +95 -0
  223. package/dist/record/fold-buckets.d.ts.map +1 -0
  224. package/dist/record/fold-instance-buffer.d.ts +6 -0
  225. package/dist/record/fold-instance-buffer.d.ts.map +1 -0
  226. package/dist/record/frame-lighting.d.ts +1 -0
  227. package/dist/record/frame-lighting.d.ts.map +1 -1
  228. package/dist/record/frame-snapshot.d.ts +40 -3
  229. package/dist/record/frame-snapshot.d.ts.map +1 -1
  230. package/dist/record/frame-targets.d.ts.map +1 -1
  231. package/dist/record/frame.d.ts +14 -62
  232. package/dist/record/frame.d.ts.map +1 -1
  233. package/dist/record/gpu-draw-receipts.d.ts +11 -0
  234. package/dist/record/gpu-draw-receipts.d.ts.map +1 -0
  235. package/dist/record/gpu-timing.d.ts +8 -0
  236. package/dist/record/gpu-timing.d.ts.map +1 -1
  237. package/dist/record/main-pass-geometry.d.ts +21 -5
  238. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  239. package/dist/record/main-pass-material.d.ts +7 -1
  240. package/dist/record/main-pass-material.d.ts.map +1 -1
  241. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  242. package/dist/record/main-pass.d.ts +13 -2
  243. package/dist/record/main-pass.d.ts.map +1 -1
  244. package/dist/record/mesh-ssbo.d.ts +6 -5
  245. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  246. package/dist/record/motion-blur-receipt.d.ts +11 -0
  247. package/dist/record/motion-blur-receipt.d.ts.map +1 -0
  248. package/dist/record/probe-blend-buffer.d.ts +16 -0
  249. package/dist/record/probe-blend-buffer.d.ts.map +1 -0
  250. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  251. package/dist/record/render-context.d.ts +34 -31
  252. package/dist/record/render-context.d.ts.map +1 -1
  253. package/dist/record/shadow-pass.d.ts +2 -0
  254. package/dist/record/shadow-pass.d.ts.map +1 -1
  255. package/dist/record/skybox-post-pass.d.ts +6 -4
  256. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  257. package/dist/record/typed-frame-graph.d.ts +22 -2
  258. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  259. package/dist/record/view-ubo.d.ts +11 -3
  260. package/dist/record/view-ubo.d.ts.map +1 -1
  261. package/dist/record/volume-params.d.ts +9 -0
  262. package/dist/record/volume-params.d.ts.map +1 -0
  263. package/dist/recovery/render-system-candidate.d.ts +10 -0
  264. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  265. package/dist/reflection/record-owner.d.ts.map +1 -1
  266. package/dist/render-contract.d.ts +66 -8
  267. package/dist/render-contract.d.ts.map +1 -1
  268. package/dist/render-graph-primitives.d.ts.map +1 -1
  269. package/dist/render-pipeline.d.ts +106 -3
  270. package/dist/render-pipeline.d.ts.map +1 -1
  271. package/dist/render-system-extract-tail.d.ts +3 -2
  272. package/dist/render-system-extract-tail.d.ts.map +1 -1
  273. package/dist/render-system-extract.d.ts +24 -10
  274. package/dist/render-system-extract.d.ts.map +1 -1
  275. package/dist/render-system-presentation.d.ts +23 -0
  276. package/dist/render-system-presentation.d.ts.map +1 -0
  277. package/dist/render-system-producer-roots.d.ts +10 -0
  278. package/dist/render-system-producer-roots.d.ts.map +1 -0
  279. package/dist/render-system-residency.d.ts +12 -0
  280. package/dist/render-system-residency.d.ts.map +1 -0
  281. package/dist/render-system.d.ts +26 -15
  282. package/dist/render-system.d.ts.map +1 -1
  283. package/dist/scene/probe-blend.d.ts +27 -0
  284. package/dist/scene/probe-blend.d.ts.map +1 -1
  285. package/dist/scene/render-scene.d.ts +14 -5
  286. package/dist/scene/render-scene.d.ts.map +1 -1
  287. package/dist/ssao-buffers.d.ts +9 -0
  288. package/dist/ssao-buffers.d.ts.map +1 -1
  289. package/dist/surface/dynamic-input.d.ts +133 -0
  290. package/dist/surface/dynamic-input.d.ts.map +1 -0
  291. package/dist/surface/gpu-driven.d.ts +41 -0
  292. package/dist/surface/gpu-driven.d.ts.map +1 -0
  293. package/dist/surface/submission-observation.d.ts +136 -0
  294. package/dist/surface/submission-observation.d.ts.map +1 -0
  295. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  296. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  297. package/dist/temporal/frame.d.ts +7 -0
  298. package/dist/temporal/frame.d.ts.map +1 -1
  299. package/dist/temporal/gpu.d.ts +13 -1
  300. package/dist/temporal/gpu.d.ts.map +1 -1
  301. package/dist/temporal/index.mjs +5 -5
  302. package/dist/temporal/inspection.d.ts.map +1 -1
  303. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  304. package/dist/temporal/target-coverage-attachment.d.ts.map +1 -1
  305. package/dist/temporal/view.d.ts +6 -1
  306. package/dist/temporal/view.d.ts.map +1 -1
  307. package/dist/transmission/single-layer-medium.d.ts +89 -0
  308. package/dist/transmission/single-layer-medium.d.ts.map +1 -0
  309. package/dist/typed-render-graph-primitives.d.ts +24 -9
  310. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  311. package/dist/volume/component.d.ts +9 -5
  312. package/dist/volume/component.d.ts.map +1 -1
  313. package/dist/volume/extract.d.ts +1 -1
  314. package/dist/volume/extract.d.ts.map +1 -1
  315. package/dist/volume/inspection.d.ts +2 -0
  316. package/dist/volume/inspection.d.ts.map +1 -1
  317. package/dist/volume/passes.d.ts +4 -1
  318. package/dist/volume/passes.d.ts.map +1 -1
  319. package/dist/volume/resources.d.ts.map +1 -1
  320. package/dist/volume/temporal.d.ts +3 -1
  321. package/dist/volume/temporal.d.ts.map +1 -1
  322. package/package.json +22 -21
  323. package/src/__tests__/analytic-fog-graph.unit.test.ts +199 -0
  324. package/src/__tests__/analytic-fog-optics.unit.test.ts +38 -0
  325. package/src/__tests__/atmosphere-daylight.dawn.test.ts +72 -0
  326. package/src/__tests__/barrel-distortion-lkg.integration.test.ts +353 -0
  327. package/src/__tests__/barrel-distortion.dawn.test.ts +41 -0
  328. package/src/__tests__/barrel-distortion.unit.test.ts +364 -0
  329. package/src/__tests__/bloom-exact-zero.unit.test.ts +112 -18
  330. package/src/__tests__/bloom-lifecycle.integration.test.ts +17 -4
  331. package/src/__tests__/construct-renderer-identity.unit.test.ts +49 -0
  332. package/src/__tests__/detached-recovery-graph.unit.test.ts +277 -0
  333. package/src/__tests__/device-loss-fanout.unit.test.ts +36 -1
  334. package/src/__tests__/docs-gate-green.unit.test.ts +112 -1
  335. package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +20 -0
  336. package/src/__tests__/environment-fog-lanes.integration.test.ts +14 -0
  337. package/src/__tests__/environment-selection.unit.test.ts +12 -0
  338. package/src/__tests__/factory-contract.integration.test.ts +626 -113
  339. package/src/__tests__/feature-depth-input.dawn.test.ts +129 -2
  340. package/src/__tests__/frame-camera-selection.unit.test.ts +39 -1
  341. package/src/__tests__/frame-plan-contract.unit.test.ts +2 -0
  342. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +14 -3
  343. package/src/__tests__/gpu-dirty-ranges.unit.test.ts +22 -0
  344. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +16 -3
  345. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +12 -0
  346. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +2 -1
  347. package/src/__tests__/gpu-driven-material-artifacts.unit.test.ts +143 -0
  348. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +2 -2
  349. package/src/__tests__/gpu-driven-production.integration.test.ts +482 -3
  350. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +7 -6
  351. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +1 -1
  352. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +103 -4
  353. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +18 -2
  354. package/src/__tests__/gpu-driven-view.browser.test.ts +5 -8
  355. package/src/__tests__/gpu-driven-view.dawn.test.ts +6 -2
  356. package/src/__tests__/gpu-driven-view.unit.test.ts +1 -1
  357. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +1 -0
  358. package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -4
  359. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +1 -0
  360. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +12 -0
  361. package/src/__tests__/gpu-scene.unit.test.ts +210 -9
  362. package/src/__tests__/instance-allocation-failure.unit.test.ts +57 -0
  363. package/src/__tests__/instances-culling.integration.test.ts +10 -36
  364. package/src/__tests__/instances-store.unit.test.ts +105 -238
  365. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  366. package/src/__tests__/main-pass-material-medium-selection.unit.test.ts +146 -0
  367. package/src/__tests__/material-shader-prewarm.unit.test.ts +59 -0
  368. package/src/__tests__/material-snapshot-mutation.integration.test.ts +39 -1
  369. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +31 -0
  370. package/src/__tests__/morph-culling-reentry.dawn.test.ts +5 -5
  371. package/src/__tests__/morph-visual-evidence.integration.test.ts +8 -7
  372. package/src/__tests__/motion-blur-compute.dawn.test.ts +589 -0
  373. package/src/__tests__/motion-blur.pixel.integration.test.ts +2 -2
  374. package/src/__tests__/nineslice-handle-surface.unit.test.ts +4 -4
  375. package/src/__tests__/pbr-view-layout.unit.test.ts +9 -0
  376. package/src/__tests__/physical-clearcoat.integration.test.ts +58 -0
  377. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +21 -0
  378. package/src/__tests__/prepared-graphics-capability.unit.test.ts +66 -2
  379. package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +86 -0
  380. package/src/__tests__/probe-bindings.unit.test.ts +582 -1
  381. package/src/__tests__/probe-scene-dirty.integration.test.ts +28 -1
  382. package/src/__tests__/public-surface.test-d.ts +8 -0
  383. package/src/__tests__/receipt-observation-retirement.unit.test.ts +65 -0
  384. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +69 -0
  385. package/src/__tests__/render-error-code-owner.test-d.ts +9 -0
  386. package/src/__tests__/render-feature-diagnostics.unit.test.ts +15 -2
  387. package/src/__tests__/render-feature-error-reporting.unit.test.ts +44 -0
  388. package/src/__tests__/render-feature-prepared-diagnostics.unit.test.ts +10 -1
  389. package/src/__tests__/render-feature-retry.unit.test.ts +12 -1
  390. package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +23 -0
  391. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +145 -19
  392. package/src/__tests__/render-scene-projection.unit.test.ts +36 -11
  393. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +37 -6
  394. package/src/__tests__/render-target-probe-soak.perf.test.ts +2 -2
  395. package/src/__tests__/render-temporal-environment-consumer.browser.test.ts +3 -3
  396. package/src/__tests__/render-temporal-environment-consumer.dawn.test.ts +5 -2
  397. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +41 -7
  398. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +16 -0
  399. package/src/__tests__/renderer-frame-transaction.integration.test.ts +33 -11
  400. package/src/__tests__/resource-allocation.unit.test.ts +123 -0
  401. package/src/__tests__/shader-manifest-fixture.ts +4 -4
  402. package/src/__tests__/single-layer-medium-passes.unit.test.ts +275 -0
  403. package/src/__tests__/skin-motion-regression.unit.test.ts +70 -1
  404. package/src/__tests__/skin-palette-recycling.dawn.test.ts +97 -0
  405. package/src/__tests__/skinned-shadow-caster.test.ts +17 -0
  406. package/src/__tests__/ssr-carrier-public-contract.unit.test.ts +21 -0
  407. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +4 -4
  408. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +2 -2
  409. package/src/__tests__/ssr-gpu-dispatch.ts +1 -1
  410. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +6 -2
  411. package/src/__tests__/standard-lighting-write.unit.test.ts +2 -1
  412. package/src/__tests__/standard-output-chain-contract.unit.test.ts +28 -0
  413. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +46 -1
  414. package/src/__tests__/standard-pipeline.dawn.test.ts +1 -1
  415. package/src/__tests__/standard-pipeline.integration.test.ts +200 -9
  416. package/src/__tests__/surface-dynamic-buffer-lifecycle.unit.test.ts +506 -0
  417. package/src/__tests__/surface-submission-observation.unit.test.ts +812 -0
  418. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +32 -0
  419. package/src/__tests__/temporal-performance.integration.test.ts +3 -3
  420. package/src/__tests__/temporal-reset-matrix.unit.test.ts +10 -0
  421. package/src/__tests__/temporal-view.unit.test.ts +2 -1
  422. package/src/__tests__/typed-pipeline-topology.unit.test.ts +20 -4
  423. package/src/__tests__/visibility-instances.integration.test.ts +7 -19
  424. package/src/__tests__/visibility-producer-matrix.integration.test.ts +9 -20
  425. package/src/__tests__/volume-optics-oracle.unit.test.ts +7 -3
  426. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +189 -15
  427. package/src/__tests__/volumetric-fog-errors.unit.test.ts +4 -3
  428. package/src/__tests__/volumetric-fog-multiple.unit.test.ts +55 -0
  429. package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +1 -1
  430. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +558 -68
  431. package/src/__tests__/volumetric-fog-temporal.unit.test.ts +27 -0
  432. package/src/__tests__/volumetric-fog-topology.unit.test.ts +23 -5
  433. package/src/assembly/analytic-fog-registration.ts +14 -0
  434. package/src/assembly/backend-contract.ts +30 -0
  435. package/src/assembly/barrel-distortion-inspection.ts +22 -0
  436. package/src/assembly/canvas-draw-guard.ts +11 -0
  437. package/src/assembly/cloud-shadow-entry.ts +22 -0
  438. package/src/assembly/dynamic-geometry-host.ts +65 -37
  439. package/src/assembly/dynamic-geometry-runtime.ts +7 -1
  440. package/src/assembly/factory.ts +2 -30
  441. package/src/assembly/fxaa-registration.ts +17 -0
  442. package/src/assembly/host-contract.ts +3 -3
  443. package/src/assembly/lod-observation.ts +12 -0
  444. package/src/assembly/material/assembly.ts +25 -13
  445. package/src/assembly/material/pipeline-helpers.ts +53 -12
  446. package/src/assembly/material-shader-policy.ts +55 -0
  447. package/src/assembly/material-shader-prewarm.ts +21 -10
  448. package/src/assembly/motion-blur-registration.ts +17 -0
  449. package/src/assembly/recovery/device-loss-fanout.ts +13 -0
  450. package/src/assembly/recovery/renderer-recover.ts +41 -18
  451. package/src/assembly/render-feature-post-processes.ts +54 -0
  452. package/src/assembly/renderer-facade.ts +6 -1
  453. package/src/assembly/renderer-feature-inspection.ts +8 -2
  454. package/src/assembly/renderer-frame-transaction.ts +23 -0
  455. package/src/assembly/renderer-inspection.ts +28 -0
  456. package/src/assembly/skin-palette-owner.ts +1 -1
  457. package/src/assembly/temporal-post-process-prewarm.ts +40 -0
  458. package/src/assembly/webgpu-pbr-ready.ts +9 -1
  459. package/src/assembly/webgpu-ready-contract.ts +8 -3
  460. package/src/assembly/webgpu-ready-per-pass.ts +104 -0
  461. package/src/assembly/webgpu-ready.ts +296 -306
  462. package/src/assembly/webgpu-renderer-bootstrap.ts +0 -12
  463. package/src/assembly/webgpu-renderer-observation-owner.ts +222 -0
  464. package/src/assembly/webgpu-renderer.ts +228 -281
  465. package/src/assembly/webgpu-vertex-layouts.ts +1 -1
  466. package/src/barrel-distortion.ts +247 -0
  467. package/src/bloom-admission.ts +21 -0
  468. package/src/cloud/__tests__/cloud-density-parity.dawn.test.ts +173 -0
  469. package/src/cloud/__tests__/cloud-layer-framebuffer-y.dawn.test.ts +265 -0
  470. package/src/cloud/__tests__/cloud-layer.browser.test.ts +432 -0
  471. package/src/cloud/__tests__/cloud-layer.unit.test.ts +634 -0
  472. package/src/cloud/density.ts +600 -0
  473. package/src/cloud/extract.ts +110 -0
  474. package/src/cloud/feature.ts +1810 -0
  475. package/src/cloud/frame-context.ts +42 -0
  476. package/src/cloud/inspection.ts +130 -0
  477. package/src/cloud/optics.ts +285 -0
  478. package/src/cloud/parameters.ts +255 -0
  479. package/src/cloud/resources.ts +122 -0
  480. package/src/cloud/shadow.ts +167 -0
  481. package/src/cloud/temporal.ts +226 -0
  482. package/src/components/__tests__/motion-blur.unit.test.ts +1 -0
  483. package/src/components/atmosphere.ts +4 -0
  484. package/src/components/barrel-distortion.ts +46 -0
  485. package/src/components/camera.ts +66 -10
  486. package/src/components/cloud-layer.ts +47 -0
  487. package/src/components/index.ts +2 -0
  488. package/src/components/instances.ts +18 -45
  489. package/src/components/mesh-filter.ts +11 -9
  490. package/src/components/motion-blur.ts +1 -0
  491. package/src/components/sprite-instances.ts +1 -1
  492. package/src/construct-renderer.ts +1 -1
  493. package/src/device/gpu-residency.ts +56 -12
  494. package/src/dynamic-geometry.ts +12 -1
  495. package/src/environment/analytic-fog.ts +44 -0
  496. package/src/environment/background.ts +2 -2
  497. package/src/errors/cloud.ts +103 -0
  498. package/src/errors/render.ts +58 -8
  499. package/src/examples/single-layer-medium-surface.ts +59 -0
  500. package/src/extract/camera.ts +35 -8
  501. package/src/extract/contracts.ts +12 -0
  502. package/src/extract/environment.ts +4 -0
  503. package/src/features/barrel-distortion.ts +172 -0
  504. package/src/features/error-reporting.ts +98 -0
  505. package/src/features/host.ts +138 -24
  506. package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +111 -1
  507. package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +58 -3
  508. package/src/features/motion-blur/motion-blur-feature.ts +1519 -15
  509. package/src/features/motion-blur/motion-blur-params.ts +70 -2
  510. package/src/features/motion-blur/motion-blur-runtime.ts +176 -0
  511. package/src/features/plan.ts +33 -8
  512. package/src/features/prepared-gpu-work.ts +86 -35
  513. package/src/features/render-graph-compute.ts +13 -5
  514. package/src/features/render-graph-contribution.ts +11 -0
  515. package/src/features/render-graph-raster.ts +45 -3
  516. package/src/features/targets.ts +66 -0
  517. package/src/features/types.ts +47 -0
  518. package/src/fullscreen-post-process-pass.ts +41 -9
  519. package/src/gpu-dirty-ranges.ts +48 -0
  520. package/src/gpu-driven/batch-topology.ts +53 -0
  521. package/src/gpu-driven/material-artifacts.ts +35 -32
  522. package/src/gpu-driven/prepared-draw.ts +27 -14
  523. package/src/gpu-driven/production-raster-material.ts +1894 -0
  524. package/src/gpu-driven/production-raster.ts +904 -787
  525. package/src/gpu-driven/resource-allocation.ts +142 -0
  526. package/src/gpu-driven/shadow-ownership.ts +71 -0
  527. package/src/gpu-driven/view-gpu.ts +181 -10
  528. package/src/gpu-scene.ts +259 -101
  529. package/src/hdrp-buffers.ts +4 -3
  530. package/src/ibl/skylight-bind-group.ts +49 -7
  531. package/src/index.ts +228 -9
  532. package/src/inspection-types.ts +222 -2
  533. package/src/instance-buffer-cache.ts +4 -4
  534. package/src/instances.ts +151 -492
  535. package/src/pbr-pipeline.ts +219 -13
  536. package/src/pipeline/single-layer-medium-passes.ts +267 -0
  537. package/src/pipeline/standard-forward-lane.ts +313 -57
  538. package/src/pipeline/standard-output/graph.ts +8 -2
  539. package/src/pipeline/standard-output/types.ts +12 -1
  540. package/src/pipeline/standard-pipeline.ts +291 -18
  541. package/src/pipeline/standard-post.ts +397 -102
  542. package/src/pipeline-builder.ts +1 -1
  543. package/src/pipeline-spec.ts +5 -5
  544. package/src/plugin.ts +4 -0
  545. package/src/points-lines/__tests__/points-lines.bench.ts +17 -8
  546. package/src/points-lines/__tests__/view-ubo.unit.test.ts +2 -1
  547. package/src/prepare/prepared-graphics-resolver.ts +2 -0
  548. package/src/record/__tests__/main-pass-geometry.unit.test.ts +30 -0
  549. package/src/record/__tests__/sprite-material-abi.unit.test.ts +1 -1
  550. package/src/record/__tests__/temporal-payload.unit.test.ts +3 -2
  551. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +11 -0
  552. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -0
  553. package/src/record/__tests__/view-ubo-layout.unit.test.ts +4 -3
  554. package/src/record/barrel-distortion-frame.ts +110 -0
  555. package/src/record/dynamic-geometry-consumption.ts +39 -9
  556. package/src/record/fold-buckets.ts +852 -0
  557. package/src/record/fold-instance-buffer.ts +51 -0
  558. package/src/record/frame-lighting.ts +36 -2
  559. package/src/record/frame-snapshot.ts +44 -4
  560. package/src/record/frame-targets.ts +10 -6
  561. package/src/record/frame.ts +373 -1099
  562. package/src/record/gpu-draw-receipts.ts +38 -0
  563. package/src/record/gpu-timing.ts +48 -5
  564. package/src/record/main-pass-geometry.ts +281 -83
  565. package/src/record/main-pass-material.ts +66 -4
  566. package/src/record/main-pass-sprite-draws.ts +13 -66
  567. package/src/record/main-pass.ts +156 -20
  568. package/src/record/mesh-ssbo.ts +43 -6
  569. package/src/record/motion-blur-receipt.ts +91 -0
  570. package/src/record/probe-blend-buffer.ts +180 -0
  571. package/src/record/recovery-pipeline.ts +35 -3
  572. package/src/record/render-context.ts +51 -31
  573. package/src/record/shadow-pass.ts +36 -2
  574. package/src/record/skybox-post-pass.ts +150 -200
  575. package/src/record/typed-frame-graph.ts +824 -44
  576. package/src/record/view-ubo.ts +26 -3
  577. package/src/record/volume-params.ts +225 -0
  578. package/src/recovery/render-system-candidate.ts +257 -52
  579. package/src/reflection/record-owner.ts +7 -3
  580. package/src/render-contract.ts +71 -8
  581. package/src/render-graph-primitives.ts +7 -1
  582. package/src/render-pipeline.ts +136 -1
  583. package/src/render-system-extract-tail.ts +72 -60
  584. package/src/render-system-extract.ts +117 -87
  585. package/src/render-system-presentation.ts +56 -0
  586. package/src/render-system-producer-roots.ts +96 -0
  587. package/src/render-system-residency.ts +159 -0
  588. package/src/render-system.ts +601 -482
  589. package/src/scene/probe-blend.ts +96 -4
  590. package/src/scene/render-scene.ts +431 -281
  591. package/src/ssao-buffers.ts +95 -2
  592. package/src/surface/__tests__/dynamic-input.unit.test.ts +374 -0
  593. package/src/surface/__tests__/gpu-driven.unit.test.ts +87 -0
  594. package/src/surface/dynamic-input.ts +658 -0
  595. package/src/surface/gpu-driven.ts +147 -0
  596. package/src/surface/submission-observation.ts +517 -0
  597. package/src/systems/skin-palette-allocator.ts +4 -1
  598. package/src/temporal/__tests__/temporal-frame-transaction.unit.test.ts +36 -0
  599. package/src/temporal/frame-coordinator.ts +8 -1
  600. package/src/temporal/frame.ts +20 -0
  601. package/src/temporal/gpu.ts +101 -17
  602. package/src/temporal/inspection.ts +7 -1
  603. package/src/temporal/standard-scene-data.ts +5 -0
  604. package/src/temporal/target-coverage-attachment.ts +4 -0
  605. package/src/temporal/view.ts +13 -2
  606. package/src/transmission/__tests__/single-layer-medium.unit.test.ts +149 -0
  607. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +12 -11
  608. package/src/transmission/single-layer-medium.ts +322 -0
  609. package/src/typed-render-graph-primitives.ts +191 -50
  610. package/src/volume/component.ts +10 -10
  611. package/src/volume/extract.ts +11 -5
  612. package/src/volume/inspection.ts +3 -0
  613. package/src/volume/passes.ts +95 -8
  614. package/src/volume/resources.ts +2 -1
  615. package/src/volume/temporal.ts +11 -0
  616. package/dist/chunk-4NNZQCL3.mjs.map +0 -1
  617. package/dist/chunk-6TFBXLRJ.mjs.map +0 -1
  618. package/dist/chunk-DRR7D3KX.mjs.map +0 -1
  619. package/dist/chunk-GWCEMDVR.mjs.map +0 -1
  620. package/dist/chunk-JCIRYQ4V.mjs.map +0 -1
  621. package/dist/chunk-JRIPCMYC.mjs.map +0 -1
  622. package/dist/chunk-LULCA3OF.mjs +0 -1713
  623. package/dist/chunk-LULCA3OF.mjs.map +0 -1
  624. package/dist/chunk-PA5DRJNI.mjs.map +0 -1
@@ -3,6 +3,8 @@ import { createShaderModule, rhi } from '@forgeax/engine-rhi-webgpu';
3
3
  import { buildEngineShaderManifest } from '@forgeax/engine-vite-plugin-shader';
4
4
  import { describe, expect, it } from 'vitest';
5
5
  import { BYTES_PER_DIRECT_LIGHT_SLOT } from '../light-buffer-layout';
6
+ import { VIEW_UNIFORM_BYTES } from '../record/view-ubo';
7
+ import { VOLUMETRIC_FOG_PARAMS_BYTES } from '../volume/resources';
6
8
 
7
9
  const MAP_READ = 0x0001;
8
10
  const BUFFER_COPY_DST = 0x0008;
@@ -19,6 +21,13 @@ const BYTES_PER_PIXEL = 8;
19
21
  const MAX_CLUSTER_LIGHTS = 256;
20
22
  const CLUSTER_LIGHT_DATA_BYTES = MAX_CLUSTER_LIGHTS * BYTES_PER_DIRECT_LIGHT_SLOT;
21
23
  const CLUSTER_UNIFORM_BYTES = 32;
24
+ const VOLUME_BOUNDS_MIN_Z = 0.1;
25
+ const VOLUME_BOUNDS_MAX_Z = 0.9;
26
+ const NON_UNIFORM_BOUNDS_MAX_Z = 8.1;
27
+ const MAX_NON_UNIFORM_BEER_ERROR = 0.001;
28
+ const NON_UNIFORM_EXTINCTION = 0.05;
29
+ const MIN_NON_UNIFORM_REFERENCE_SEPARATION = 0.05;
30
+ const MIN_NON_UNIFORM_GPU_SEPARATION = 0.05;
22
31
  const engineManifest = await buildEngineShaderManifest();
23
32
 
24
33
  interface StageValues {
@@ -33,6 +42,110 @@ interface VolumeStageReadbacks {
33
42
  readonly integrate: StageValues;
34
43
  }
35
44
 
45
+ interface StageReadbackOptions {
46
+ readonly owners?: number;
47
+ readonly secondDensity?: number;
48
+ readonly thinSeparated?: boolean;
49
+ readonly reverse?: boolean;
50
+ readonly sceneDepthClear?: number;
51
+ readonly densityByte?: number;
52
+ /** Optional RGBA8 density field for the production integrate stage. */
53
+ readonly densityData?: Uint8Array;
54
+ readonly boundsMinZ?: number;
55
+ readonly boundsMaxZ?: number;
56
+ readonly maxDistance?: number;
57
+ readonly viewDepthScale?: number;
58
+ readonly extinction?: number;
59
+ readonly frameIndex?: number;
60
+ /** Depth clear used by the stable shadow receiver fixture. */
61
+ readonly shadowDepthClear?: number;
62
+ /** Unshadowed punctual light isolates ray integration from packed visibility. */
63
+ readonly localLight?: 'point' | 'spot';
64
+ /** Uniform-step diagnostic; omitted means the untouched production shader. */
65
+ readonly integratorStepCount?: number;
66
+ }
67
+
68
+ interface DensityField {
69
+ readonly data: Uint8Array;
70
+ readonly zRed: readonly number[];
71
+ }
72
+
73
+ function createNonUniformDensityField(redValues?: readonly number[]): DensityField {
74
+ const data = new Uint8Array(4 * SIZE * SIZE * SIZE);
75
+ const zRed: number[] = [];
76
+ for (let z = 0; z < SIZE; z += 1) {
77
+ // Keep X/Y constant. The default field changes only the first Z texel;
78
+ // the fine-density probe supplies a faster, seam-matched sequence.
79
+ // The exercised camera ray crosses the first-to-second texel transition
80
+ // under all three production density scales, so flattening the field to
81
+ // its first texel produces a measurable, independently falsifiable result.
82
+ const red = redValues?.[z] ?? (z === 0 ? 128 : 255);
83
+ zRed.push(red / 255);
84
+ for (let y = 0; y < SIZE; y += 1) {
85
+ for (let x = 0; x < SIZE; x += 1) {
86
+ const offset = ((z * SIZE + y) * SIZE + x) * 4;
87
+ data[offset] = red;
88
+ data[offset + 1] = 255;
89
+ data[offset + 2] = 255;
90
+ data[offset + 3] = 255;
91
+ }
92
+ }
93
+ }
94
+ return { data, zRed };
95
+ }
96
+
97
+ function createFlattenedDensityField(field: DensityField): DensityField {
98
+ const red = Math.round((field.zRed[0] ?? 0) * 255);
99
+ const data = new Uint8Array(4 * SIZE * SIZE * SIZE);
100
+ for (let z = 0; z < SIZE; z += 1) {
101
+ for (let y = 0; y < SIZE; y += 1) {
102
+ for (let x = 0; x < SIZE; x += 1) {
103
+ const offset = ((z * SIZE + y) * SIZE + x) * 4;
104
+ data[offset] = red;
105
+ data[offset + 1] = 255;
106
+ data[offset + 2] = 255;
107
+ data[offset + 3] = 255;
108
+ }
109
+ }
110
+ }
111
+ return { data, zRed: Array.from({ length: SIZE }, () => red / 255) };
112
+ }
113
+
114
+ function sampleClampedLinear(values: readonly number[], coordinate: number): number {
115
+ const scaled = coordinate * values.length - 0.5;
116
+ const lower = Math.floor(scaled);
117
+ const upper = lower + 1;
118
+ const interpolation = scaled - lower;
119
+ const clampIndex = (index: number): number => Math.max(0, Math.min(values.length - 1, index));
120
+ const lowerValue = values[clampIndex(lower)] ?? 0;
121
+ const upperValue = values[clampIndex(upper)] ?? 0;
122
+ return lowerValue + (upperValue - lowerValue) * interpolation;
123
+ }
124
+
125
+ function nonUniformScatteringDensity(field: DensityField, worldZ: number): number {
126
+ let grain = sampleClampedLinear(field.zRed, (worldZ * 0.1) % 1) + 0.5;
127
+ grain *= sampleClampedLinear(field.zRed, (worldZ * 0.05) % 1) + 0.5;
128
+ grain *= sampleClampedLinear(field.zRed, (worldZ * 0.02) % 1) + 0.5;
129
+ return Math.max(2 * grain - 1, 0);
130
+ }
131
+
132
+ function nonUniformBeerReference(
133
+ field: DensityField,
134
+ extinction = 1,
135
+ sampleCount = 16_384,
136
+ boundsMinZ = VOLUME_BOUNDS_MIN_Z,
137
+ boundsMaxZ = VOLUME_BOUNDS_MAX_Z,
138
+ ): number {
139
+ const interval = boundsMaxZ - boundsMinZ;
140
+ const segmentLength = interval / sampleCount;
141
+ let opticalDepth = 0;
142
+ for (let sample = 0; sample < sampleCount; sample += 1) {
143
+ const worldZ = boundsMinZ + (sample + 0.5) * segmentLength;
144
+ opticalDepth += nonUniformScatteringDensity(field, worldZ) * segmentLength;
145
+ }
146
+ return Math.exp(-extinction * opticalDepth);
147
+ }
148
+
36
149
  function halfToFloat(bits: number): number {
37
150
  const sign = (bits & 0x8000) === 0 ? 1 : -1;
38
151
  const exponent = (bits >>> 10) & 0x1f;
@@ -42,12 +155,50 @@ function halfToFloat(bits: number): number {
42
155
  return sign * 2 ** (exponent - 15) * (1 + fraction / 1024);
43
156
  }
44
157
 
158
+ function firstStageValue(values: readonly number[], label: string): number {
159
+ const value = values[0];
160
+ if (value === undefined) throw new Error(`missing stage readback value: ${label}`);
161
+ return value;
162
+ }
163
+
45
164
  function manifestShader(marker: string): string {
46
165
  const entry = engineManifest.entries.find(({ wgsl }) => wgsl.includes(marker));
47
166
  if (entry === undefined) throw new Error(`missing cooked shader entry: ${marker}`);
48
167
  return entry.wgsl;
49
168
  }
50
169
 
170
+ function productionIntegratorShader(stepCount?: number): string {
171
+ const source = manifestShader('fn volume_integrate');
172
+ if (stepCount === undefined) return source;
173
+ if (!Number.isInteger(stepCount) || stepCount < 1) {
174
+ throw new Error(`invalid production integrator step count: ${stepCount}`);
175
+ }
176
+ const functionStart = source.indexOf('fn volume_integrate');
177
+ if (functionStart < 0) throw new Error('production integrator function missing');
178
+ const prefix = source.slice(0, functionStart);
179
+ const body = source.slice(functionStart);
180
+ // The cooked manifest constant-folds the source-level loop count into the
181
+ // step-length divisor and loop guard. Replace both literals in that one
182
+ // production function to compile the same WGSL with a diagnostic schedule;
183
+ // an omitted override always exercises the unmodified production manifest.
184
+ // Diagnostic schedules disable local refinement to expose the old defect.
185
+ const productionCount = body.match(/< (\d+)u\b/)?.[1];
186
+ if (productionCount === undefined) throw new Error('production ray loop count missing');
187
+ const stepLengthMarker = new RegExp(`f32\\(${productionCount}u\\)`, 'g');
188
+ const loopGuardMarker = new RegExp(`< ${productionCount}u\\b`, 'g');
189
+ const stepLengthOccurrences = body.match(stepLengthMarker)?.length ?? 0;
190
+ const loopGuardOccurrences = body.match(loopGuardMarker)?.length ?? 0;
191
+ if (stepLengthOccurrences !== 1 || loopGuardOccurrences !== 1) {
192
+ throw new Error(
193
+ `production integrator step markers: length=${stepLengthOccurrences}, guard=${loopGuardOccurrences}`,
194
+ );
195
+ }
196
+ return `${prefix}${body
197
+ .replace(stepLengthMarker, `f32(${stepCount}u)`)
198
+ .replace(loopGuardMarker, `< ${stepCount}u`)
199
+ .replace(/select\(1u, 4u,/, 'select(1u, 1u,')} `;
200
+ }
201
+
51
202
  function identityMatrix(target: Float32Array, offset: number): void {
52
203
  target[offset] = 1;
53
204
  target[offset + 5] = 1;
@@ -74,12 +225,19 @@ async function requestDevice(): Promise<RhiDevice> {
74
225
  }
75
226
 
76
227
  async function runInjectReadback(
77
- sceneDepthClear = 1,
78
- densitySource: number | 'height-gradient' | 'depth-gradient' = 255,
79
- heightOffset = 0,
80
- frameIndex = 0,
81
- slopedShadow = false,
228
+ options: StageReadbackOptions = {},
82
229
  ): Promise<VolumeStageReadbacks> {
230
+ const sceneDepthClear = options.sceneDepthClear ?? 1;
231
+ const densityByte = options.densityByte ?? 255;
232
+ const boundsMinZ = options.boundsMinZ ?? VOLUME_BOUNDS_MIN_Z;
233
+ const boundsMaxZ = options.boundsMaxZ ?? VOLUME_BOUNDS_MAX_Z;
234
+ const maxDistance = options.maxDistance ?? 2;
235
+ const viewDepthScale = options.viewDepthScale ?? 1;
236
+ const extinction = options.extinction ?? 1;
237
+ const frameIndex = options.frameIndex ?? 0;
238
+ const shadowDepthClear = options.shadowDepthClear ?? 1;
239
+ const integratorStepCount = options.integratorStepCount;
240
+ const owners = options.owners ?? 1;
83
241
  const device = await requestDevice();
84
242
  const moduleResult = await createShaderModule(device, {
85
243
  code: manifestShader('fn volume_inject'),
@@ -91,7 +249,7 @@ async function runInjectReadback(
91
249
  label: 'stage-readback.volume-temporal',
92
250
  });
93
251
  const integrateModule = await createShaderModule(device, {
94
- code: manifestShader('fn volume_integrate'),
252
+ code: productionIntegratorShader(integratorStepCount),
95
253
  label: 'stage-readback.volume-integrate',
96
254
  });
97
255
  if (!temporalModule.ok) throw temporalModule.error;
@@ -119,9 +277,17 @@ async function runInjectReadback(
119
277
  usage: TEXTURE_BINDING | TEXTURE_COPY_DST,
120
278
  textureBindingViewDimension: undefined,
121
279
  });
280
+ const secondDensityTexture = device.createTexture({
281
+ label: 'stage-readback.second-density',
282
+ size: { width: SIZE, height: SIZE, depthOrArrayLayers: SIZE },
283
+ dimension: '3d',
284
+ format: 'rgba8unorm',
285
+ usage: TEXTURE_BINDING | TEXTURE_COPY_DST,
286
+ textureBindingViewDimension: undefined,
287
+ });
122
288
  const outputTexture = device.createTexture({
123
289
  label: 'stage-readback.froxel',
124
- size: { width: SIZE, height: SIZE, depthOrArrayLayers: PACKED_LAYERS },
290
+ size: { width: SIZE, height: SIZE, depthOrArrayLayers: PACKED_LAYERS * owners },
125
291
  dimension: '2d',
126
292
  format: 'rgba8unorm',
127
293
  usage: STORAGE_BINDING | TEXTURE_COPY_SRC | TEXTURE_BINDING,
@@ -166,6 +332,7 @@ async function runInjectReadback(
166
332
  !sceneDepthTexture.ok ||
167
333
  !shadowTexture.ok ||
168
334
  !densityTexture.ok ||
335
+ !secondDensityTexture.ok ||
169
336
  !outputTexture.ok ||
170
337
  !historyTexture.ok ||
171
338
  !temporalTexture.ok ||
@@ -179,6 +346,7 @@ async function runInjectReadback(
179
346
  if (!sceneDepthView.ok || !shadowView.ok)
180
347
  throw new Error('Dawn volume stage readback unavailable: depth view allocation failed');
181
348
  const densityView = createView(device, densityTexture.value, '3d');
349
+ const secondDensityView = createView(device, secondDensityTexture.value, '3d');
182
350
  const outputView = createView(device, outputTexture.value, '2d-array');
183
351
  const historyView = createView(device, historyTexture.value, '2d');
184
352
  const temporalView = createView(device, temporalTexture.value, '2d');
@@ -186,12 +354,12 @@ async function runInjectReadback(
186
354
  const projectorView = createView(device, projectorTexture.value, '2d');
187
355
  const viewBuffer = device.createBuffer({
188
356
  label: 'stage-readback.view',
189
- size: 960,
357
+ size: VIEW_UNIFORM_BYTES,
190
358
  usage: 0x0040 | BUFFER_COPY_DST,
191
359
  });
192
360
  const paramsBuffer = device.createBuffer({
193
361
  label: 'stage-readback.params',
194
- size: 128,
362
+ size: VOLUMETRIC_FOG_PARAMS_BYTES,
195
363
  usage: 0x0040 | BUFFER_COPY_DST,
196
364
  });
197
365
  const lightDataBuffer = device.createBuffer({
@@ -206,7 +374,7 @@ async function runInjectReadback(
206
374
  });
207
375
  const injectReadback = device.createBuffer({
208
376
  label: 'stage-readback.inject',
209
- size: BYTES_PER_ROW * SIZE * PACKED_LAYERS,
377
+ size: BYTES_PER_ROW * SIZE * PACKED_LAYERS * owners,
210
378
  usage: MAP_READ | BUFFER_COPY_DST,
211
379
  });
212
380
  const temporalReadback = device.createBuffer({
@@ -230,30 +398,63 @@ async function runInjectReadback(
230
398
  )
231
399
  throw new Error('Dawn volume stage readback unavailable: readback allocation failed');
232
400
 
233
- const viewData = new Float32Array(240);
401
+ const viewData = new Float32Array(VIEW_UNIFORM_BYTES / Float32Array.BYTES_PER_ELEMENT);
234
402
  for (const offset of [0, 28, 44, 60, 76, 92, 196, 212]) identityMatrix(viewData, offset);
403
+ viewData[44 + 10] = viewDepthScale;
235
404
  viewData[24] = 0;
236
- viewData[25] = heightOffset;
237
- viewData[44 + 13] = heightOffset;
405
+ viewData[25] = 0;
238
406
  viewData[26] = -1;
239
- viewData[124] = slopedShadow ? 1 : 0;
407
+ // Enable one real shadow cascade. Otherwise the stability falsifier sees
408
+ // only unshadowed texels (the old overlong readback supplied fake zeros).
240
409
  viewData[108] = 10;
241
- if (slopedShadow) viewData[28 + 2] = 0.5;
410
+ viewData[124] = 1;
242
411
  viewData[228] = 0.1;
243
412
  viewData[229] = 10;
244
413
  viewData[230] = 0;
245
414
  viewData[235] = 1;
246
- const paramsData = new Float32Array(32);
247
- paramsData.set([-1, heightOffset - 1, 0.1, 0], 0);
248
- paramsData.set([1, heightOffset + 1, 0.9, 0], 4);
249
- paramsData.set([1, 1, 1, 0], 8);
415
+ const paramsData = new Float32Array(VOLUMETRIC_FOG_PARAMS_BYTES / 4);
416
+ paramsData.set([-1, -1, boundsMinZ, 0], 0);
417
+ paramsData.set([1, 1, boundsMaxZ, 0], 4);
418
+ paramsData.set([extinction, extinction, extinction, 0], 8);
250
419
  paramsData.set([1, 1, 1, 0], 12);
251
420
  paramsData.set([0, 0, 0, 0], 16);
252
- paramsData.set([0, -1, 0, 0], 20);
421
+ paramsData.set([0, -1, 0, frameIndex], 20);
253
422
  paramsData.set([1, 1, 1, 0], 24);
254
- paramsData.set([2, 0, 0, 0], 28);
255
- paramsData[23] = frameIndex;
423
+ paramsData.set([maxDistance, 0, 0, 0], 28);
424
+ // `simulation.w` is the renderer-owned selected-sun cloud transmittance
425
+ // carrier. A fixture without a CloudLayer must retain the unshadowed
426
+ // #3197 baseline instead of reading the typed-array zero value.
427
+ paramsData[35] = 1;
256
428
  const lightData = new Uint8Array(CLUSTER_LIGHT_DATA_BYTES);
429
+ if (options.localLight !== undefined) {
430
+ paramsData[30] = options.localLight === 'point' ? 1 : 2;
431
+ const light = new Float32Array(lightData.buffer);
432
+ light.set([0, 0, 5.37, 0, 1, 1, 1, Math.cos(Math.PI / 8), 0, 0, -1, Math.cos(Math.PI / 6)]);
433
+ // Zero shadow intensity makes this a pure punctual-light integral. No
434
+ // projected image or shadow map can hide an undersampled light peak.
435
+ new Uint32Array(lightData.buffer).set(
436
+ [options.localLight === 'point' ? 0 : 1, 0xffffffff, 0xffffffff, 0xffffffff],
437
+ 16,
438
+ );
439
+ }
440
+ for (let index = 0; index < owners; index++)
441
+ paramsData.set(paramsData.slice(0, 36), 36 * (index + 1));
442
+ paramsData[3] = owners;
443
+ if (options.secondDensity !== undefined || options.thinSeparated) {
444
+ for (let index = 0; index < owners; index++) paramsData[36 * (index + 1) + 7] = 1;
445
+ }
446
+ if (options.thinSeparated) {
447
+ paramsData[38] = 0.1;
448
+ paramsData[42] = 0.101;
449
+ paramsData[74] = 0.899;
450
+ paramsData[78] = 0.9;
451
+ for (const offset of [44, 80]) paramsData.set([100, 100, 100], offset);
452
+ }
453
+ if (options.reverse) {
454
+ const first = paramsData.slice(36, 72);
455
+ paramsData.copyWithin(36, 72, 108);
456
+ paramsData.set(first, 72);
457
+ }
257
458
  const clusterUniformData = new Float32Array(8);
258
459
  const clusterGrid = new Uint32Array(clusterUniformData.buffer);
259
460
  clusterGrid[0] = 1;
@@ -270,22 +471,12 @@ async function runInjectReadback(
270
471
  );
271
472
  if (!viewWrite.ok || !paramsWrite.ok || !lightDataWrite.ok || !clusterUniformWrite.ok)
272
473
  throw new Error('Dawn volume stage readback unavailable: uniform upload failed');
273
- const densityData = new Uint8Array((BYTES_PER_PIXEL / 2) * SIZE * SIZE * SIZE);
274
- for (let z = 0; z < SIZE; z++)
275
- for (let y = 0; y < SIZE; y++)
276
- for (let x = 0; x < SIZE; x++) {
277
- const value =
278
- densitySource === 'height-gradient'
279
- ? Math.round((y / (SIZE - 1)) * 255)
280
- : densitySource === 'depth-gradient'
281
- ? Math.round((z / (SIZE - 1)) * 255)
282
- : densitySource;
283
- densityData.fill(
284
- value,
285
- ((z * SIZE + y) * SIZE + x) * 4,
286
- ((z * SIZE + y) * SIZE + x + 1) * 4,
287
- );
288
- }
474
+ const densityData =
475
+ options.densityData ?? new Uint8Array((BYTES_PER_PIXEL / 2) * SIZE * SIZE * SIZE);
476
+ if (options.densityData === undefined) densityData.fill(densityByte);
477
+ if (densityData.length !== (BYTES_PER_PIXEL / 2) * SIZE * SIZE * SIZE) {
478
+ throw new Error(`Dawn volume stage readback density field length: ${densityData.length}`);
479
+ }
289
480
  const densityWrite = device.queue.writeTexture(
290
481
  { texture: densityTexture.value },
291
482
  densityData,
@@ -294,6 +485,13 @@ async function runInjectReadback(
294
485
  );
295
486
  if (!densityWrite.ok)
296
487
  throw new Error('Dawn volume stage readback unavailable: density upload failed');
488
+ const secondDensityWrite = device.queue.writeTexture(
489
+ { texture: secondDensityTexture.value },
490
+ new Uint8Array(densityData.length).fill(options.secondDensity ?? 255),
491
+ { offset: 0, bytesPerRow: SIZE * 4, rowsPerImage: SIZE },
492
+ { width: SIZE, height: SIZE, depthOrArrayLayers: SIZE },
493
+ );
494
+ if (!secondDensityWrite.ok) throw secondDensityWrite.error;
297
495
  const projectorWrite = device.queue.writeTexture(
298
496
  { texture: projectorTexture.value },
299
497
  new Uint8Array([255, 255, 255, 255]),
@@ -317,6 +515,7 @@ async function runInjectReadback(
317
515
  },
318
516
  { binding: 7, visibility: 4, buffer: { type: 'read-only-storage' } },
319
517
  { binding: 8, visibility: 4, buffer: { type: 'uniform' } },
518
+ { binding: 9, visibility: 4, texture: { sampleType: 'depth', viewDimension: '2d' } },
320
519
  ],
321
520
  });
322
521
  if (!layout.ok) throw layout.error;
@@ -383,6 +582,13 @@ async function runInjectReadback(
383
582
  { binding: 11, visibility: 4, buffer: { type: 'uniform' } },
384
583
  { binding: 12, visibility: 4, texture: { sampleType: 'float', viewDimension: '2d' } },
385
584
  { binding: 13, visibility: 4, sampler: { type: 'filtering' } },
585
+ { binding: 16, visibility: 4, texture: { sampleType: 'float', viewDimension: '2d' } },
586
+ { binding: 17, visibility: 4, sampler: { type: 'filtering' } },
587
+ ...Array.from({ length: 7 }, (_, index) => ({
588
+ binding: 18 + index,
589
+ visibility: 4,
590
+ texture: { sampleType: 'float' as const, viewDimension: '3d' as const },
591
+ })),
386
592
  ],
387
593
  });
388
594
  if (!temporalLayout.ok) throw temporalLayout.error;
@@ -421,6 +627,7 @@ async function runInjectReadback(
421
627
  { binding: 6, resource: { kind: 'textureView', value: outputView } },
422
628
  { binding: 7, resource: { kind: 'buffer', value: { buffer: lightDataBuffer.value } } },
423
629
  { binding: 8, resource: { kind: 'buffer', value: { buffer: clusterUniformBuffer.value } } },
630
+ { binding: 9, resource: { kind: 'textureView', value: shadowView.value } },
424
631
  ],
425
632
  });
426
633
  if (!bindings.ok) throw bindings.error;
@@ -443,7 +650,10 @@ async function runInjectReadback(
443
650
  { binding: 3, resource: { kind: 'buffer', value: { buffer: paramsBuffer.value } } },
444
651
  { binding: 4, resource: { kind: 'buffer', value: { buffer: viewBuffer.value } } },
445
652
  { binding: 5, resource: { kind: 'sampler', value: densitySampler.value } },
446
- { binding: 6, resource: { kind: 'textureView', value: densityView } },
653
+ {
654
+ binding: 6,
655
+ resource: { kind: 'textureView', value: options.reverse ? secondDensityView : densityView },
656
+ },
447
657
  { binding: 7, resource: { kind: 'sampler', value: densitySampler.value } },
448
658
  { binding: 8, resource: { kind: 'textureView', value: historyView } },
449
659
  { binding: 9, resource: { kind: 'textureView', value: temporalView } },
@@ -451,6 +661,15 @@ async function runInjectReadback(
451
661
  { binding: 11, resource: { kind: 'buffer', value: { buffer: clusterUniformBuffer.value } } },
452
662
  { binding: 12, resource: { kind: 'textureView', value: projectorView } },
453
663
  { binding: 13, resource: { kind: 'sampler', value: densitySampler.value } },
664
+ { binding: 16, resource: { kind: 'textureView', value: projectorView } },
665
+ { binding: 17, resource: { kind: 'sampler', value: densitySampler.value } },
666
+ ...Array.from({ length: 7 }, (_, index) => ({
667
+ binding: 18 + index,
668
+ resource: {
669
+ kind: 'textureView' as const,
670
+ value: index === 0 && !options.reverse ? secondDensityView : densityView,
671
+ },
672
+ })),
454
673
  ],
455
674
  });
456
675
  if (!temporalBindings.ok) throw temporalBindings.error;
@@ -462,7 +681,7 @@ async function runInjectReadback(
462
681
  colorAttachments: [],
463
682
  depthStencilAttachment: {
464
683
  view,
465
- depthClearValue: index === 0 ? sceneDepthClear : slopedShadow ? 0.5 : 1,
684
+ depthClearValue: index === 0 ? sceneDepthClear : shadowDepthClear,
466
685
  depthLoadOp: 'clear',
467
686
  depthStoreOp: 'store',
468
687
  },
@@ -472,12 +691,12 @@ async function runInjectReadback(
472
691
  const compute = encoder.value.beginComputePass();
473
692
  compute.setPipeline(pipeline.value);
474
693
  compute.setBindGroup(0, bindings.value);
475
- compute.dispatchWorkgroups(1, 1, PACKED_LAYERS);
694
+ compute.dispatchWorkgroups(1, 1, PACKED_LAYERS * owners);
476
695
  compute.end();
477
696
  encoder.value.copyTextureToBuffer(
478
697
  { texture: outputTexture.value as never },
479
698
  { buffer: injectReadback.value as never, bytesPerRow: BYTES_PER_ROW, rowsPerImage: SIZE },
480
- { width: SIZE, height: SIZE, depthOrArrayLayers: PACKED_LAYERS },
699
+ { width: SIZE, height: SIZE, depthOrArrayLayers: PACKED_LAYERS * owners },
481
700
  );
482
701
  const integrateCompute = encoder.value.beginComputePass();
483
702
  integrateCompute.setPipeline(integratePipeline.value);
@@ -537,28 +756,152 @@ async function runInjectReadback(
537
756
  return { red, green, alpha };
538
757
  };
539
758
  return {
540
- inject: await decode(injectReadback.value, SIZE, 4, false),
759
+ inject: await decode(injectReadback.value, PACKED_LAYERS * owners, 4, false),
541
760
  temporal: await decode(temporalReadback.value, 1, BYTES_PER_PIXEL, true),
542
761
  integrate: await decode(integrateReadback.value, 1, BYTES_PER_PIXEL, true),
543
762
  };
544
763
  }
545
764
 
765
+ // Independent radiative-transfer quadrature: a homogeneous medium and an
766
+ // unshadowed punctual source. The near-light peak and spot cone make the old
767
+ // 24 midpoint schedule visibly wrong even though its Beer transmittance passes.
768
+ function punctualReference(kind: 'point' | 'spot', samples = 16_384): number[] {
769
+ const sigma = 0.01 * (2 * (128 / 255 + 0.5) ** 3 - 1);
770
+ return Array.from({ length: SIZE * SIZE }, (_, pixel) => {
771
+ const x = (((pixel % SIZE) + 0.5) / SIZE) * 2 - 1;
772
+ const y = 1 - ((Math.floor(pixel / SIZE) + 0.5) / SIZE) * 2;
773
+ const ds = 20 / samples;
774
+ let radiance = 0;
775
+ for (let i = 0; i < samples; i += 1) {
776
+ const s = (i + 0.5) * ds;
777
+ const z = 0.1 + s;
778
+ const distanceSquared = x * x + y * y + (5.37 - z) ** 2;
779
+ const cosine = (5.37 - z) / Math.sqrt(distanceSquared);
780
+ const t = Math.max(
781
+ 0,
782
+ Math.min(
783
+ 1,
784
+ (cosine - Math.cos(Math.PI / 6)) / (Math.cos(Math.PI / 8) - Math.cos(Math.PI / 6)),
785
+ ),
786
+ );
787
+ const cone = kind === 'point' ? 1 : t * t * (3 - 2 * t);
788
+ radiance += ((sigma * Math.exp(-sigma * s) * cone) / distanceSquared) * ds;
789
+ }
790
+ return radiance;
791
+ });
792
+ }
793
+
546
794
  describe('volumetric fog production stage readback on Dawn', () => {
547
795
  it('keeps static shadow visibility stable across renderer frame phases', async () => {
548
- const first = await runInjectReadback(1, 255, 0, 0, true);
549
- const next = await runInjectReadback(1, 255, 0, 1, true);
796
+ const first = await runInjectReadback({ frameIndex: 0, shadowDepthClear: 0.5 });
797
+ const next = await runInjectReadback({ frameIndex: 1, shadowDepthClear: 0.5 });
798
+ expect(first.inject.red).toHaveLength(SIZE * SIZE * PACKED_LAYERS);
799
+ expect(first.inject).toEqual(next.inject);
550
800
  expect(first.inject.red.some((value) => value === 0)).toBe(true);
551
801
  expect(first.inject.red.some((value) => value > 0.9)).toBe(true);
552
- expect(next.inject).toEqual(first.inject);
553
- });
554
- it('keeps a static depth-density field stable across renderer frame phases', async () => {
555
- const first = await runInjectReadback(1, 'depth-gradient', 0, 0);
556
- for (const frameIndex of [1, 7]) {
557
- const next = await runInjectReadback(1, 'depth-gradient', 0, frameIndex);
558
- expect(next.integrate.alpha).toEqual(first.integrate.alpha);
559
- expect(next.integrate.red).toEqual(first.integrate.red);
560
- }
561
- expect(first.integrate.alpha[0]).toBeCloseTo(Math.exp(-0.4), 2);
802
+ });
803
+
804
+ it.each([
805
+ 'point',
806
+ 'spot',
807
+ ] as const)('resolves the %s light peak without coherent ray bands', async (localLight) => {
808
+ const reference = punctualReference(localLight);
809
+ const refined = punctualReference(localLight, 32_768);
810
+ expect(Math.max(...reference.map((v, i) => Math.abs(v - (refined[i] ?? 0)) / v))).toBeLessThan(
811
+ 0.0001,
812
+ );
813
+ const options = {
814
+ localLight,
815
+ densityByte: 128,
816
+ extinction: 0.01,
817
+ boundsMaxZ: 20.1,
818
+ maxDistance: 20.1,
819
+ viewDepthScale: 21,
820
+ };
821
+ const maximumRelativeError = (values: VolumeStageReadbacks): number =>
822
+ Math.max(
823
+ ...values.integrate.red.map(
824
+ (v, i) => Math.abs(v - (reference[i] ?? 0)) / (reference[i] ?? 1),
825
+ ),
826
+ );
827
+ const production = await runInjectReadback(options);
828
+ const undersampled = await runInjectReadback({ ...options, integratorStepCount: 24 });
829
+ const resolved = await runInjectReadback({ ...options, integratorStepCount: 96 });
830
+ const refinedGpu = await runInjectReadback({ ...options, integratorStepCount: 384 });
831
+ const errors = {
832
+ refined: maximumRelativeError(refinedGpu),
833
+ production: maximumRelativeError(production),
834
+ undersampled: maximumRelativeError(undersampled),
835
+ resolved: maximumRelativeError(resolved),
836
+ };
837
+ // biome-ignore lint/suspicious/noConsole: production GPU spatial regression evidence.
838
+ console.info('[volumetric-fog-stage-readback] punctual-convergence', localLight, errors);
839
+ expect(errors.undersampled).toBeGreaterThan(0.1);
840
+ expect(errors.refined).toBeLessThan(0.003);
841
+ expect(errors.production).toBeLessThan(0.03);
842
+ });
843
+
844
+ it('retains fine density variation away from punctual light refinement', async () => {
845
+ // Matching end texels avoid an artificial discontinuity at the repeated
846
+ // texture seam. The interior stripes expose coarse-step density aliasing.
847
+ const field = createNonUniformDensityField([0, 255, 0, 255, 0, 255, 0, 0]);
848
+ const reference = nonUniformBeerReference(field, 0.05, 16_384, 0.1, 20.1);
849
+ const refined = nonUniformBeerReference(field, 0.05, 32_768, 0.1, 20.1);
850
+ expect(Math.abs(reference - refined)).toBeLessThan(0.0001);
851
+ const options = {
852
+ densityData: field.data,
853
+ extinction: 0.05,
854
+ boundsMaxZ: 20.1,
855
+ maxDistance: 20.1,
856
+ viewDepthScale: 21,
857
+ };
858
+ const production = firstStageValue(
859
+ (await runInjectReadback(options)).integrate.alpha,
860
+ 'fine density',
861
+ );
862
+ const coarse = firstStageValue(
863
+ (await runInjectReadback({ ...options, integratorStepCount: 24 })).integrate.alpha,
864
+ 'coarse density',
865
+ );
866
+ // biome-ignore lint/suspicious/noConsole: independent fine-density regression evidence.
867
+ console.info('[volumetric-fog-stage-readback] fine-density', { reference, production, coarse });
868
+ expect(Math.abs(coarse - reference)).toBeGreaterThan(0.005);
869
+ expect(Math.abs(production - reference)).toBeLessThan(0.002);
870
+ });
871
+
872
+ it('samples independent density textures and is invariant under owner permutation', async () => {
873
+ const transparent = await runInjectReadback({
874
+ sceneDepthClear: 0.57,
875
+ owners: 2,
876
+ secondDensity: 0,
877
+ });
878
+ const half = await runInjectReadback({ sceneDepthClear: 0.57, owners: 2, secondDensity: 128 });
879
+ const reversed = await runInjectReadback({
880
+ sceneDepthClear: 0.57,
881
+ owners: 2,
882
+ secondDensity: 128,
883
+ reverse: true,
884
+ });
885
+ expect(transparent.integrate.alpha[0]).toBeCloseTo(Math.exp(-0.47), 3);
886
+ expect(half.integrate.alpha[0]).toBeCloseTo(Math.exp(-0.47 * (1 + 128 / 255)), 3);
887
+ expect(reversed.integrate.alpha).toEqual(half.integrate.alpha);
888
+ expect(reversed.integrate.red).toEqual(half.integrate.red);
889
+ });
890
+ it('does not miss thin local volumes separated by empty space', async () => {
891
+ const values = await runInjectReadback({ owners: 2, thinSeparated: true });
892
+ expect(values.integrate.alpha[0]).toBeCloseTo(Math.exp(-0.002 * 100), 3);
893
+ });
894
+ it('adds overlapping optical depth instead of rejecting or selecting one volume', async () => {
895
+ const one = await runInjectReadback({ sceneDepthClear: 0.57 });
896
+ const two = await runInjectReadback({ sceneDepthClear: 0.57, owners: 2 });
897
+ const expected = Math.exp(-0.47 * (2 * 1.5 ** 3 - 1) * 2);
898
+ expect(two.integrate.alpha[0]).toBeCloseTo(expected, 3);
899
+ expect(two.integrate.alpha[0]).toBeLessThan(one.integrate.alpha[0] ?? 0);
900
+ expect(two.integrate.red[0]).toBeGreaterThan(one.integrate.red[0] ?? 0);
901
+ });
902
+ it('integrates all eight admitted owners through the production parameter layout', async () => {
903
+ const values = await runInjectReadback({ sceneDepthClear: 0.57, owners: 8, extinction: 0.1 });
904
+ expect(values.integrate.alpha[0]).toBeCloseTo(Math.exp(-0.47 * (2 * 1.5 ** 3 - 1) * 0.8), 3);
562
905
  });
563
906
  it('injects raw density then resolves a 2d integrated volume', async () => {
564
907
  const values = await runInjectReadback();
@@ -580,23 +923,170 @@ describe('volumetric fog production stage readback on Dawn', () => {
580
923
  });
581
924
 
582
925
  it('clips the final ray step to the scene-depth segment', async () => {
583
- const values = await runInjectReadback(0.57);
926
+ const values = await runInjectReadback({ sceneDepthClear: 0.57 });
584
927
  const transmittance = values.integrate.alpha[0];
585
- // White encodes density one; integrate only the visible 0.47-unit segment.
586
- expect(transmittance).toBeCloseTo(Math.exp(-0.47), 2);
928
+ // The fixture uploads an all-white source texture. The authored Three
929
+ // smoke expression therefore evaluates to 2 * 1.5^3 - 1 = 5.75 before
930
+ // Beer-Lambert consumes it; only the final 0.47-unit segment is allowed
931
+ // to contribute after the scene-depth clip.
932
+ const sourceDensity = 2 * 1.5 ** 3 - 1;
933
+ expect(transmittance).toBeCloseTo(Math.exp(-0.47 * sourceDensity), 2);
587
934
  expect(values.integrate.red[0]).toBeGreaterThan(0);
588
935
  });
589
- it.each([0, 64, 128])('preserves authored uniform density %s/255', async (densityByte) => {
590
- const values = await runInjectReadback(0.57, densityByte);
591
- expect(values.integrate.alpha[0]).toBeCloseTo(Math.exp((-0.47 * densityByte) / 255), 2);
592
- if (densityByte === 0) expect(values.integrate.red.every((value) => value === 0)).toBe(true);
936
+
937
+ it('keeps a zero medium transparent and applies Beer-Lambert monotonically', async () => {
938
+ const empty = await runInjectReadback({ densityByte: 0 });
939
+ const medium = await runInjectReadback({ densityByte: 128 });
940
+ const dense = await runInjectReadback({ densityByte: 255 });
941
+ const emptyScattering = firstStageValue(empty.integrate.red, 'empty scattering');
942
+ const denseScattering = firstStageValue(dense.integrate.red, 'dense scattering');
943
+ const mediumScattering = firstStageValue(medium.integrate.red, 'medium scattering');
944
+ const emptyTransmission = firstStageValue(empty.integrate.alpha, 'empty transmission');
945
+ const mediumTransmission = firstStageValue(medium.integrate.alpha, 'medium transmission');
946
+ const denseTransmission = firstStageValue(dense.integrate.alpha, 'dense transmission');
947
+ expect(emptyScattering).toBeCloseTo(0, 2);
948
+ expect(emptyTransmission).toBeCloseTo(1, 2);
949
+ expect(denseTransmission).toBeLessThan(mediumTransmission);
950
+ expect(mediumTransmission).toBeLessThan(emptyTransmission);
951
+ expect(denseScattering).toBeGreaterThan(mediumScattering);
593
952
  });
594
953
 
595
- it.each([
596
- 0, 20,
597
- ])('maps height density through world bounds translated by %s', async (heightOffset) => {
598
- const values = await runInjectReadback(0.57, 'height-gradient', heightOffset);
599
- expect(values.integrate.alpha[0]).toBeCloseTo(Math.exp(-0.47), 2);
600
- expect(values.integrate.alpha[(SIZE - 1) * SIZE]).toBeCloseTo(1, 3);
954
+ it('keeps extinction monotonic and bounds temporal jitter spread', async () => {
955
+ const lowExtinction = await runInjectReadback({ densityByte: 128, extinction: 0.25 });
956
+ const highExtinction = await runInjectReadback({ densityByte: 128, extinction: 1 });
957
+ const lowTransmission = firstStageValue(lowExtinction.integrate.alpha, 'low extinction');
958
+ const highTransmission = firstStageValue(highExtinction.integrate.alpha, 'high extinction');
959
+ expect(highTransmission).toBeLessThan(lowTransmission);
960
+
961
+ const samples: number[] = [];
962
+ for (let frameIndex = 0; frameIndex < 4; frameIndex += 1) {
963
+ const value = await runInjectReadback({ densityByte: 128, frameIndex });
964
+ const transmission = firstStageValue(value.integrate.alpha, `frame ${frameIndex}`);
965
+ expect(Number.isFinite(transmission)).toBe(true);
966
+ samples.push(transmission);
967
+ }
968
+ const spread = Math.max(...samples) - Math.min(...samples);
969
+ // Frame ordinals must not change a fixed World-time density integral.
970
+ // This remains separate from continuous-frame history and spatial accuracy.
971
+ expect(spread).toBeLessThan(0.2);
972
+ });
973
+
974
+ it('converges production step-count variants to the homogeneous Beer oracle', async () => {
975
+ const sceneDepthClear = 0.57;
976
+ const sourceDensity = 2 * 1.5 ** 3 - 1;
977
+ const oracle = Math.exp(-0.47 * sourceDensity);
978
+ const transmissions = await Promise.all(
979
+ [6, 12, 24].map(async (integratorStepCount) => {
980
+ const values = await runInjectReadback({
981
+ sceneDepthClear,
982
+ integratorStepCount,
983
+ });
984
+ return firstStageValue(values.integrate.alpha, `${integratorStepCount}-step transmission`);
985
+ }),
986
+ );
987
+ for (const transmission of transmissions) {
988
+ expect(transmission).toBeCloseTo(oracle, 2);
989
+ }
990
+ expect(Math.max(...transmissions) - Math.min(...transmissions)).toBeLessThan(0.01);
991
+ });
992
+
993
+ it('converges production steps against an independent non-uniform Beer reference', async () => {
994
+ const field = createNonUniformDensityField();
995
+ const flattenedField = createFlattenedDensityField(field);
996
+ const reference = nonUniformBeerReference(
997
+ field,
998
+ NON_UNIFORM_EXTINCTION,
999
+ 16_384,
1000
+ VOLUME_BOUNDS_MIN_Z,
1001
+ NON_UNIFORM_BOUNDS_MAX_Z,
1002
+ );
1003
+ const refinedReference = nonUniformBeerReference(
1004
+ field,
1005
+ NON_UNIFORM_EXTINCTION,
1006
+ 32_768,
1007
+ VOLUME_BOUNDS_MIN_Z,
1008
+ NON_UNIFORM_BOUNDS_MAX_Z,
1009
+ );
1010
+ const flattenedReference = nonUniformBeerReference(
1011
+ flattenedField,
1012
+ NON_UNIFORM_EXTINCTION,
1013
+ 16_384,
1014
+ VOLUME_BOUNDS_MIN_Z,
1015
+ NON_UNIFORM_BOUNDS_MAX_Z,
1016
+ );
1017
+ expect(Math.abs(reference - refinedReference)).toBeLessThan(MAX_NON_UNIFORM_BEER_ERROR / 10);
1018
+ expect(Math.abs(reference - flattenedReference)).toBeGreaterThan(
1019
+ MIN_NON_UNIFORM_REFERENCE_SEPARATION,
1020
+ );
1021
+ const stepCounts = [24, 48, 96] as const;
1022
+ const transmissions = await Promise.all(
1023
+ stepCounts.map(async (integratorStepCount) => {
1024
+ const values = await runInjectReadback({
1025
+ boundsMaxZ: NON_UNIFORM_BOUNDS_MAX_Z,
1026
+ densityData: field.data,
1027
+ extinction: NON_UNIFORM_EXTINCTION,
1028
+ integratorStepCount,
1029
+ maxDistance: NON_UNIFORM_BOUNDS_MAX_Z,
1030
+ viewDepthScale: 10,
1031
+ });
1032
+ return firstStageValue(
1033
+ values.integrate.alpha,
1034
+ `${integratorStepCount}-step non-uniform transmission`,
1035
+ );
1036
+ }),
1037
+ );
1038
+ const flattenedValues = await runInjectReadback({
1039
+ boundsMaxZ: NON_UNIFORM_BOUNDS_MAX_Z,
1040
+ densityData: flattenedField.data,
1041
+ extinction: NON_UNIFORM_EXTINCTION,
1042
+ integratorStepCount: 96,
1043
+ maxDistance: NON_UNIFORM_BOUNDS_MAX_Z,
1044
+ viewDepthScale: 10,
1045
+ });
1046
+ const flattenedTransmission = firstStageValue(
1047
+ flattenedValues.integrate.alpha,
1048
+ 'flattened-field transmission',
1049
+ );
1050
+ const defaultValues = await runInjectReadback({
1051
+ boundsMaxZ: NON_UNIFORM_BOUNDS_MAX_Z,
1052
+ densityData: field.data,
1053
+ extinction: NON_UNIFORM_EXTINCTION,
1054
+ maxDistance: NON_UNIFORM_BOUNDS_MAX_Z,
1055
+ viewDepthScale: 10,
1056
+ });
1057
+ const defaultTransmission = firstStageValue(
1058
+ defaultValues.integrate.alpha,
1059
+ 'production transmission',
1060
+ );
1061
+ const errors = transmissions.map((transmission) => Math.abs(transmission - reference));
1062
+ const flattenedError = Math.abs(flattenedTransmission - flattenedReference);
1063
+ const defaultError = Math.abs(defaultTransmission - reference);
1064
+ // biome-ignore lint/suspicious/noConsole: non-uniform production convergence receipt.
1065
+ console.info(
1066
+ '[volumetric-fog-stage-readback] non-uniform-convergence',
1067
+ JSON.stringify({
1068
+ reference,
1069
+ refinedReference,
1070
+ flattenedReference,
1071
+ stepCounts,
1072
+ transmissions,
1073
+ errors,
1074
+ flattenedTransmission,
1075
+ flattenedError,
1076
+ defaultTransmission,
1077
+ defaultError,
1078
+ }),
1079
+ );
1080
+ for (const error of errors) expect(error).toBeLessThan(MAX_NON_UNIFORM_BEER_ERROR);
1081
+ expect(flattenedError).toBeLessThan(MAX_NON_UNIFORM_BEER_ERROR);
1082
+ expect(defaultError).toBeLessThan(MAX_NON_UNIFORM_BEER_ERROR);
1083
+ expect(Math.abs((transmissions[2] ?? NaN) - flattenedTransmission)).toBeGreaterThan(
1084
+ MIN_NON_UNIFORM_GPU_SEPARATION,
1085
+ );
1086
+ const spread = Math.max(...transmissions) - Math.min(...transmissions);
1087
+ // rgba16float readback quantizes nearby step variants onto the same value,
1088
+ // so strict error ordering is not a stable production contract. Keep the
1089
+ // independent reference and require a bounded cross-step spread instead.
1090
+ expect(spread).toBeLessThan(MAX_NON_UNIFORM_BEER_ERROR);
601
1091
  });
602
1092
  });