@forgeax/engine-render 0.1.29 → 0.1.31

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 (350) hide show
  1. package/README.md +248 -21
  2. package/dist/assembly/atmosphere-shader-prewarm.d.ts +1 -1
  3. package/dist/assembly/atmosphere-shader-prewarm.d.ts.map +1 -1
  4. package/dist/assembly/device-feature-admission.d.ts +4 -3
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -1
  6. package/dist/assembly/dynamic-geometry-host.d.ts +6 -0
  7. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  8. package/dist/assembly/host-contract.d.ts +6 -0
  9. package/dist/assembly/host-contract.d.ts.map +1 -1
  10. package/dist/assembly/material/assembly.d.ts +26 -3
  11. package/dist/assembly/material/assembly.d.ts.map +1 -1
  12. package/dist/assembly/material/pipeline-helpers.d.ts +24 -0
  13. package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -0
  14. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  15. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
  16. package/dist/assembly/recovery/recovery-attempt.d.ts +1 -6
  17. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -1
  18. package/dist/assembly/recovery/renderer-recover.d.ts +1 -0
  19. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  20. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  21. package/dist/assembly/shader-prewarm-policy.d.ts +1 -1
  22. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -1
  23. package/dist/assembly/webgpu-ready.d.ts +1 -1
  24. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  25. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  26. package/dist/capture/scene-projection.d.ts +41 -0
  27. package/dist/capture/scene-projection.d.ts.map +1 -0
  28. package/dist/{chunk-BQVQOQJH.mjs → chunk-7MQ3XBSP.mjs} +20 -14
  29. package/dist/chunk-7MQ3XBSP.mjs.map +1 -0
  30. package/dist/{chunk-S4DNKQYQ.mjs → chunk-C3DDSXYK.mjs} +86 -33
  31. package/dist/chunk-C3DDSXYK.mjs.map +1 -0
  32. package/dist/{chunk-BTWH7BP4.mjs → chunk-FUUO4UND.mjs} +73 -29
  33. package/dist/chunk-FUUO4UND.mjs.map +1 -0
  34. package/dist/{chunk-NRNXBLIJ.mjs → chunk-JRBDO2KB.mjs} +46 -6
  35. package/dist/chunk-JRBDO2KB.mjs.map +1 -0
  36. package/dist/{chunk-NICXSHAT.mjs → chunk-MDQJPY3T.mjs} +8 -6
  37. package/dist/chunk-MDQJPY3T.mjs.map +1 -0
  38. package/dist/{chunk-JQTSEI3Z.mjs → chunk-NO4WZCAH.mjs} +3 -3
  39. package/dist/chunk-NO4WZCAH.mjs.map +1 -0
  40. package/dist/{chunk-FUODTTIA.mjs → chunk-PSEQ6QPH.mjs} +621 -108
  41. package/dist/chunk-PSEQ6QPH.mjs.map +1 -0
  42. package/dist/{chunk-33XZFTQU.mjs → chunk-S5UE2T5P.mjs} +48 -6
  43. package/dist/chunk-S5UE2T5P.mjs.map +1 -0
  44. package/dist/components/directional-light.d.ts +2 -2
  45. package/dist/components/dynamic-resolution.d.ts +15 -0
  46. package/dist/components/dynamic-resolution.d.ts.map +1 -0
  47. package/dist/components/index.d.ts +1 -0
  48. package/dist/components/index.d.ts.map +1 -1
  49. package/dist/components/light-helpers.d.ts.map +1 -1
  50. package/dist/components/reflection-probe.d.ts +1 -0
  51. package/dist/components/reflection-probe.d.ts.map +1 -1
  52. package/dist/components/skylight.d.ts +4 -1
  53. package/dist/components/skylight.d.ts.map +1 -1
  54. package/dist/construct-renderer.mjs +2172 -796
  55. package/dist/construct-renderer.mjs.map +1 -1
  56. package/dist/device/device-scope.d.ts +2 -0
  57. package/dist/device/device-scope.d.ts.map +1 -1
  58. package/dist/device/gpu-residency.d.ts +2 -2
  59. package/dist/device/gpu-residency.d.ts.map +1 -1
  60. package/dist/dynamic-geometry.d.ts.map +1 -1
  61. package/dist/environment/background.d.ts +3 -2
  62. package/dist/environment/background.d.ts.map +1 -1
  63. package/dist/environment/ibl.d.ts +11 -0
  64. package/dist/environment/ibl.d.ts.map +1 -0
  65. package/dist/environment/storage.d.ts +14 -0
  66. package/dist/environment/storage.d.ts.map +1 -0
  67. package/dist/errors/gpu-driven.d.ts +2 -2
  68. package/dist/errors/gpu-driven.d.ts.map +1 -1
  69. package/dist/errors/render.d.ts +35 -1
  70. package/dist/errors/render.d.ts.map +1 -1
  71. package/dist/extract/camera.d.ts.map +1 -1
  72. package/dist/extract/contracts.d.ts +7 -0
  73. package/dist/extract/contracts.d.ts.map +1 -1
  74. package/dist/extract/gpu-driven.d.ts +18 -1
  75. package/dist/extract/gpu-driven.d.ts.map +1 -1
  76. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  77. package/dist/gpu-driven/material-artifacts.d.ts +24 -0
  78. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -0
  79. package/dist/gpu-driven/material-bindings.d.ts +7 -0
  80. package/dist/gpu-driven/material-bindings.d.ts.map +1 -0
  81. package/dist/gpu-driven/prepared-draw.d.ts +2 -0
  82. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  83. package/dist/gpu-driven/production-raster.d.ts +27 -6
  84. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  85. package/dist/gpu-resource.d.ts +5 -2
  86. package/dist/gpu-resource.d.ts.map +1 -1
  87. package/dist/gpu-scene-schema.d.ts.map +1 -1
  88. package/dist/gpu-scene.d.ts.map +1 -1
  89. package/dist/hdrp-buffers.d.ts +3 -0
  90. package/dist/hdrp-buffers.d.ts.map +1 -1
  91. package/dist/ibl/IblPipelineCache.d.ts +2 -0
  92. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  93. package/dist/ibl/skylight-bind-group.d.ts +3 -1
  94. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  95. package/dist/index.d.ts +83 -81
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.mjs +7 -5
  98. package/dist/index.mjs.map +1 -1
  99. package/dist/inspection-types.d.ts +16 -1
  100. package/dist/inspection-types.d.ts.map +1 -1
  101. package/dist/internal.mjs +6 -6
  102. package/dist/material-row.d.ts +10 -0
  103. package/dist/material-row.d.ts.map +1 -1
  104. package/dist/pbr-pipeline.d.ts +3 -1
  105. package/dist/pbr-pipeline.d.ts.map +1 -1
  106. package/dist/pipeline/render-extent.d.ts +51 -0
  107. package/dist/pipeline/render-extent.d.ts.map +1 -0
  108. package/dist/pipeline/standard-build-context.d.ts +7 -0
  109. package/dist/pipeline/standard-build-context.d.ts.map +1 -0
  110. package/dist/pipeline/standard-forward-lane.d.ts +3 -2
  111. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  112. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  113. package/dist/pipeline/standard-post.d.ts +1 -1
  114. package/dist/pipeline/standard-post.d.ts.map +1 -1
  115. package/dist/pipeline/standard-profile.d.ts +2 -1
  116. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  117. package/dist/pipeline-builder.d.ts +3 -1
  118. package/dist/pipeline-builder.d.ts.map +1 -1
  119. package/dist/plugin.d.ts.map +1 -1
  120. package/dist/post-process-errors.d.ts +9 -2
  121. package/dist/post-process-errors.d.ts.map +1 -1
  122. package/dist/prepare/extended-lighting/resources.d.ts +3 -3
  123. package/dist/record/frame-lighting.d.ts.map +1 -1
  124. package/dist/record/frame-snapshot.d.ts +3 -8
  125. package/dist/record/frame-snapshot.d.ts.map +1 -1
  126. package/dist/record/frame.d.ts +3 -2
  127. package/dist/record/frame.d.ts.map +1 -1
  128. package/dist/record/main-pass-geometry.d.ts +7 -2
  129. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  130. package/dist/record/main-pass-material.d.ts +6 -3
  131. package/dist/record/main-pass-material.d.ts.map +1 -1
  132. package/dist/record/main-pass.d.ts +2 -0
  133. package/dist/record/main-pass.d.ts.map +1 -1
  134. package/dist/record/prepared-material-bindings.d.ts.map +1 -1
  135. package/dist/record/recovery-pipeline.d.ts +9 -1
  136. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  137. package/dist/record/render-context.d.ts +34 -3
  138. package/dist/record/render-context.d.ts.map +1 -1
  139. package/dist/record/shadow-pass.d.ts +2 -2
  140. package/dist/record/shadow-pass.d.ts.map +1 -1
  141. package/dist/record/typed-frame-graph.d.ts +5 -2
  142. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  143. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  144. package/dist/reflection/background.d.ts +14 -0
  145. package/dist/reflection/background.d.ts.map +1 -0
  146. package/dist/reflection/filter.d.ts.map +1 -1
  147. package/dist/reflection/projection.d.ts +1 -0
  148. package/dist/reflection/projection.d.ts.map +1 -1
  149. package/dist/reflection/record-owner.d.ts +4 -2
  150. package/dist/reflection/record-owner.d.ts.map +1 -1
  151. package/dist/render-contract.d.ts +47 -32
  152. package/dist/render-contract.d.ts.map +1 -1
  153. package/dist/render-graph-primitives.d.ts.map +1 -1
  154. package/dist/render-pipeline.d.ts +17 -1
  155. package/dist/render-pipeline.d.ts.map +1 -1
  156. package/dist/render-system-extract-tail.d.ts +9 -2
  157. package/dist/render-system-extract-tail.d.ts.map +1 -1
  158. package/dist/render-system-extract.d.ts +29 -2
  159. package/dist/render-system-extract.d.ts.map +1 -1
  160. package/dist/render-system.d.ts.map +1 -1
  161. package/dist/scene/render-scene.d.ts +2 -0
  162. package/dist/scene/render-scene.d.ts.map +1 -1
  163. package/dist/ssao-config.d.ts +8 -6
  164. package/dist/ssao-config.d.ts.map +1 -1
  165. package/dist/ssr/graph.d.ts.map +1 -1
  166. package/dist/temporal/gpu.d.ts.map +1 -1
  167. package/dist/temporal/index.mjs +5 -5
  168. package/dist/temporal/standard-scene-data.d.ts +2 -1
  169. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  170. package/dist/temporal/target-coverage-attachment.d.ts +15 -0
  171. package/dist/temporal/target-coverage-attachment.d.ts.map +1 -0
  172. package/dist/temporal/temporal-view.d.ts +6 -2
  173. package/dist/temporal/temporal-view.d.ts.map +1 -1
  174. package/dist/temporal/view.d.ts +6 -0
  175. package/dist/temporal/view.d.ts.map +1 -1
  176. package/dist/typed-render-graph-primitives.d.ts +6 -0
  177. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  178. package/dist/volume/component.d.ts +1 -0
  179. package/dist/volume/component.d.ts.map +1 -1
  180. package/package.json +21 -21
  181. package/src/__tests__/capture-scene-projection.unit.test.ts +24 -0
  182. package/src/__tests__/device-feature-admission.unit.test.ts +15 -24
  183. package/src/__tests__/device-loss-fanout.unit.test.ts +43 -0
  184. package/src/__tests__/dynamic-geometry-batch.unit.test.ts +157 -0
  185. package/src/__tests__/dynamic-geometry.unit.test.ts +36 -0
  186. package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +81 -0
  187. package/src/__tests__/factory-contract.integration.test.ts +164 -1
  188. package/src/__tests__/frame-observation.contract.test.ts +21 -0
  189. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +1 -1
  190. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +20 -1
  191. package/src/__tests__/gpu-driven-lod-cache.unit.test.ts +0 -3
  192. package/src/__tests__/gpu-driven-mesh-world-identity.unit.test.ts +0 -3
  193. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +11 -6
  194. package/src/__tests__/gpu-driven-production.integration.test.ts +13 -25
  195. package/src/__tests__/gpu-driven-view.unit.test.ts +1 -0
  196. package/src/__tests__/gpu-pass-timing-browser-runner.ts +15 -0
  197. package/src/__tests__/gpu-pass-timing.browser.test.ts +14 -1
  198. package/src/__tests__/gpu-resource-scope.unit.test.ts +71 -0
  199. package/src/__tests__/gpu-scene.unit.test.ts +55 -1
  200. package/src/__tests__/materials-standard-contract.unit.test.ts +5 -1
  201. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +8 -0
  202. package/src/__tests__/morph-neutral-admission.unit.test.ts +19 -0
  203. package/src/__tests__/physical-clearcoat.integration.test.ts +6 -1
  204. package/src/__tests__/post-process-error-policy-owner.unit.test.ts +6 -5
  205. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +216 -5
  206. package/src/__tests__/reflection-fallback-async-publication.unit.test.ts +68 -1
  207. package/src/__tests__/reflection-probe-authoring.unit.test.ts +12 -10
  208. package/src/__tests__/reflection-probe-box.dawn.test.ts +71 -0
  209. package/src/__tests__/reflection-probe-filter.unit.test.ts +9 -0
  210. package/src/__tests__/reflection-probe-orientation.dawn.test.ts +294 -0
  211. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +5 -15
  212. package/src/__tests__/reflection-probe-updates.unit.test.ts +129 -0
  213. package/src/__tests__/render-error-code-owner.test-d.ts +3 -0
  214. package/src/__tests__/render-error-detail-ssot.test-d.ts +5 -1
  215. package/src/__tests__/render-extent.unit.test.ts +63 -0
  216. package/src/__tests__/render-scene-projection.unit.test.ts +3 -2
  217. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +173 -9
  218. package/src/__tests__/shader-manifest-fixture.ts +6 -0
  219. package/src/__tests__/skinned-shadow-caster.test.ts +8 -1
  220. package/src/__tests__/specular-environment-scale.dawn.test.ts +4 -2
  221. package/src/__tests__/ssao-config.unit.test.ts +7 -0
  222. package/src/__tests__/ssao-surface.dawn.test.ts +252 -0
  223. package/src/__tests__/ssr-cube-silhouette.dawn.test.ts +270 -0
  224. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +51 -43
  225. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +4 -0
  226. package/src/__tests__/ssr-gpu-dispatch.ts +10 -5
  227. package/src/__tests__/ssr-graph-topology.unit.test.ts +2 -2
  228. package/src/__tests__/ssr-hiz.browser.test.ts +1 -1
  229. package/src/__tests__/ssr-hiz.dawn.test.ts +1 -1
  230. package/src/__tests__/ssr-spatial.integration.test.ts +1 -1
  231. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +40 -0
  232. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +238 -1
  233. package/src/__tests__/standard-profile-oracle.unit.test.ts +27 -0
  234. package/src/__tests__/taa-coverage-edge.dawn.test.ts +6 -0
  235. package/src/__tests__/taa-neighborhood-equivalence.dawn.test.ts +5 -1
  236. package/src/__tests__/taa-stability-feedback.ts +1 -0
  237. package/src/__tests__/temporal-reset-matrix.unit.test.ts +2 -2
  238. package/src/__tests__/temporal-view.unit.test.ts +17 -0
  239. package/src/__tests__/transmission-prewarm.unit.test.ts +57 -0
  240. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +62 -13
  241. package/src/__tests__/volumetric-fog-topology.unit.test.ts +7 -7
  242. package/src/assembly/atmosphere-shader-prewarm.ts +3 -2
  243. package/src/assembly/device-feature-admission.ts +9 -4
  244. package/src/assembly/dynamic-geometry-host.ts +236 -163
  245. package/src/assembly/host-contract.ts +9 -0
  246. package/src/assembly/material/assembly.ts +350 -6
  247. package/src/assembly/material/pipeline-helpers.ts +100 -0
  248. package/src/assembly/material-shader-policy.ts +39 -18
  249. package/src/assembly/recovery/device-loss-fanout.ts +5 -0
  250. package/src/assembly/recovery/recovery-attempt.ts +2 -29
  251. package/src/assembly/recovery/renderer-recover.ts +6 -9
  252. package/src/assembly/renderer-facade.ts +22 -1
  253. package/src/assembly/shader-prewarm-policy.ts +14 -1
  254. package/src/assembly/webgpu-ready.ts +28 -19
  255. package/src/assembly/webgpu-renderer.ts +95 -95
  256. package/src/capture/scene-projection.ts +37 -0
  257. package/src/components/__tests__/dynamic-resolution-schema.unit.test.ts +65 -0
  258. package/src/components/__tests__/light-radius.integration.test.ts +59 -0
  259. package/src/components/directional-light.ts +3 -3
  260. package/src/components/dynamic-resolution.ts +67 -0
  261. package/src/components/index.ts +1 -0
  262. package/src/components/light-helpers.ts +6 -1
  263. package/src/components/reflection-probe.ts +1 -0
  264. package/src/components/skylight.ts +4 -1
  265. package/src/device/device-scope.ts +13 -7
  266. package/src/device/gpu-residency.ts +11 -11
  267. package/src/dynamic-geometry.ts +13 -8
  268. package/src/environment/background.ts +30 -12
  269. package/src/environment/ibl.ts +211 -0
  270. package/src/environment/storage.ts +63 -0
  271. package/src/errors/gpu-driven.ts +10 -3
  272. package/src/errors/render.ts +64 -0
  273. package/src/extract/camera.ts +12 -1
  274. package/src/extract/contracts.ts +10 -0
  275. package/src/extract/gpu-driven.ts +72 -17
  276. package/src/gpu-driven/batch-topology.ts +33 -11
  277. package/src/gpu-driven/material-artifacts.ts +179 -0
  278. package/src/gpu-driven/material-bindings.ts +41 -0
  279. package/src/gpu-driven/prepared-draw.ts +13 -2
  280. package/src/gpu-driven/production-raster.ts +681 -164
  281. package/src/gpu-resource.ts +21 -2
  282. package/src/gpu-scene-schema.ts +11 -2
  283. package/src/gpu-scene.ts +14 -3
  284. package/src/hdrp-buffers.ts +69 -0
  285. package/src/ibl/IblPipelineCache.ts +5 -0
  286. package/src/ibl/skylight-bind-group.ts +27 -17
  287. package/src/index.ts +92 -81
  288. package/src/inspection-types.ts +16 -1
  289. package/src/material-row.ts +60 -11
  290. package/src/pbr-pipeline.ts +13 -2
  291. package/src/pipeline/render-extent.ts +111 -0
  292. package/src/pipeline/standard-build-context.ts +7 -0
  293. package/src/pipeline/standard-forward-lane.ts +41 -24
  294. package/src/pipeline/standard-pipeline.ts +47 -18
  295. package/src/pipeline/standard-post.ts +4 -0
  296. package/src/pipeline/standard-profile.ts +2 -1
  297. package/src/pipeline-builder.ts +12 -3
  298. package/src/plugin.ts +4 -0
  299. package/src/post-process-errors.ts +26 -18
  300. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +2 -2
  301. package/src/prepare/extended-lighting/resources.ts +3 -3
  302. package/src/record/__tests__/main-pass-geometry.unit.test.ts +68 -0
  303. package/src/record/__tests__/material-bg-cache.unit.test.ts +45 -0
  304. package/src/record/__tests__/material-slot-plan.unit.test.ts +9 -0
  305. package/src/record/__tests__/recovery-pipeline.unit.test.ts +47 -1
  306. package/src/record/__tests__/sprite-material-abi.unit.test.ts +7 -3
  307. package/src/record/__tests__/temporal-payload.unit.test.ts +2 -0
  308. package/src/record/frame-lighting.ts +3 -1
  309. package/src/record/frame-snapshot.ts +13 -11
  310. package/src/record/frame.ts +150 -74
  311. package/src/record/main-pass-geometry.ts +60 -9
  312. package/src/record/main-pass-material.ts +47 -19
  313. package/src/record/main-pass.ts +63 -4
  314. package/src/record/prepared-material-bindings.ts +4 -0
  315. package/src/record/recovery-pipeline.ts +56 -0
  316. package/src/record/render-context.ts +34 -3
  317. package/src/record/shadow-pass.ts +187 -50
  318. package/src/record/typed-frame-graph.ts +60 -3
  319. package/src/record/view-ubo.ts +1 -1
  320. package/src/recovery/render-system-candidate.ts +54 -11
  321. package/src/reflection/background.ts +151 -0
  322. package/src/reflection/filter.ts +3 -0
  323. package/src/reflection/projection.ts +16 -6
  324. package/src/reflection/record-owner.ts +282 -86
  325. package/src/render-contract.ts +50 -32
  326. package/src/render-graph-primitives.ts +28 -3
  327. package/src/render-pipeline.ts +20 -1
  328. package/src/render-system-extract-tail.ts +23 -3
  329. package/src/render-system-extract.ts +127 -42
  330. package/src/render-system.ts +35 -7
  331. package/src/scene/render-scene.ts +8 -0
  332. package/src/ssao-config.ts +25 -8
  333. package/src/ssr/graph.ts +78 -58
  334. package/src/temporal/gpu.ts +5 -0
  335. package/src/temporal/standard-scene-data.ts +7 -1
  336. package/src/temporal/target-coverage-attachment.ts +72 -0
  337. package/src/temporal/temporal-view.ts +10 -4
  338. package/src/temporal/view.ts +12 -2
  339. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +24 -9
  340. package/src/typed-render-graph-primitives.ts +38 -0
  341. package/src/volume/component.ts +1 -0
  342. package/src/volume/passes.ts +3 -3
  343. package/dist/chunk-33XZFTQU.mjs.map +0 -1
  344. package/dist/chunk-BQVQOQJH.mjs.map +0 -1
  345. package/dist/chunk-BTWH7BP4.mjs.map +0 -1
  346. package/dist/chunk-FUODTTIA.mjs.map +0 -1
  347. package/dist/chunk-JQTSEI3Z.mjs.map +0 -1
  348. package/dist/chunk-NICXSHAT.mjs.map +0 -1
  349. package/dist/chunk-NRNXBLIJ.mjs.map +0 -1
  350. package/dist/chunk-S4DNKQYQ.mjs.map +0 -1
@@ -1481,7 +1481,7 @@ describe('projected bounds query', () => {
1481
1481
  expect(scene.bounds(worldA, 99)).toBeUndefined();
1482
1482
  const first = scene.bounds(worldA, 7);
1483
1483
  if (first === undefined) throw new Error('expected projected bounds');
1484
- (first.min as number[])[0] = -999;
1484
+ Reflect.set(first.min, 0, -999);
1485
1485
  expect(scene.bounds(worldA, 7)?.min[0]).toBe(110);
1486
1486
  const instanceA = snapshot(1, 0).transform.world,
1487
1487
  instanceB = snapshot(2, 20).transform.world;
@@ -1506,8 +1506,9 @@ describe('projected bounds query', () => {
1506
1506
  );
1507
1507
  expect(scene.bounds(worldA, 7)?.max[0]).toBe(132);
1508
1508
  expect(scene.bounds(worldB, 7)?.max[0]).toBe(901);
1509
+ const { localAabb: _localAabb, ...withoutBounds } = a;
1509
1510
  scene.extractComposition([worldA], { cameraOwner: 0, resourceOwner: 0 }, 3, () =>
1510
- frame([{ ...a, localAabb: undefined }]),
1511
+ frame([withoutBounds]),
1511
1512
  );
1512
1513
  expect(scene.bounds(worldA, 7)).toBeUndefined();
1513
1514
  expect(scene.bounds(worldB, 7)).toBeUndefined();
@@ -1,7 +1,11 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { resolve } from 'node:path';
3
- import { RhiError } from '@forgeax/engine-rhi';
4
- import type { MaterialShaderManifestVariant } from '@forgeax/engine-shader';
3
+ import { type RhiDevice, RhiError } from '@forgeax/engine-rhi';
4
+ import type {
5
+ MaterialShaderManifestEntry,
6
+ MaterialShaderManifestVariant,
7
+ ShaderCatalog,
8
+ } from '@forgeax/engine-shader';
5
9
  import { describe, expect, it, vi } from 'vitest';
6
10
  import { deviceOptionsForAdapter } from '../assembly/device-feature-admission';
7
11
  import {
@@ -17,6 +21,7 @@ import {
17
21
  selectPipelineLayoutForVariant,
18
22
  shouldDeferMissingPreparedMaterialShader,
19
23
  } from '../assembly/factory';
24
+ import { prepareMaterialShaders } from '../assembly/material-shader-policy';
20
25
  import {
21
26
  buildPipelineForMaterialShader,
22
27
  type PipelineBuilderContext,
@@ -56,6 +61,56 @@ function noColorManifest() {
56
61
  }
57
62
 
58
63
  describe('material shader variant identity', () => {
64
+ it('defaults a declared coverage-only axis to the opaque boot variant', async () => {
65
+ const entry: MaterialShaderManifestEntry = {
66
+ identifier: 'forgeax::default-standard-pbr',
67
+ sourcePath: 'default-standard-pbr.wgsl',
68
+ composedWgsl: 'coverage-off',
69
+ paramSchema: '[]',
70
+ variants: [
71
+ {
72
+ definesKey: 'COVERAGE_ONLY=false+STORAGE_BUFFER_AVAILABLE=true',
73
+ defines: { COVERAGE_ONLY: false, STORAGE_BUFFER_AVAILABLE: true },
74
+ composedWgsl: 'coverage-off',
75
+ },
76
+ {
77
+ definesKey: 'COVERAGE_ONLY=true+STORAGE_BUFFER_AVAILABLE=true',
78
+ defines: { COVERAGE_ONLY: true, STORAGE_BUFFER_AVAILABLE: true },
79
+ composedWgsl: 'coverage-on',
80
+ },
81
+ ],
82
+ };
83
+ const installMaterialArtifact = vi.fn();
84
+ const registry = {
85
+ loadManifest: vi.fn(async () => ({ ok: true, value: undefined })),
86
+ materialShaderManifestEntries: () => [entry][Symbol.iterator](),
87
+ findMaterialArtifact: vi.fn(() => ({ ok: false })),
88
+ installMaterialArtifact,
89
+ } as unknown as ShaderCatalog;
90
+ const assets = { catalog: vi.fn(() => ({ ok: true, value: undefined })) } as never;
91
+ const device = {
92
+ caps: {
93
+ backendKind: 'webgpu',
94
+ storageBuffer: true,
95
+ rgba16floatRenderable: true,
96
+ samplerAliasing: true,
97
+ },
98
+ limits: {
99
+ maxStorageBuffersPerShaderStage: 8,
100
+ maxSampledTexturesPerShaderStage: 32,
101
+ maxTextureArrayLayers: 64,
102
+ maxUniformBuffersPerShaderStage: 4,
103
+ },
104
+ } as unknown as RhiDevice;
105
+
106
+ await prepareMaterialShaders(device, () => registry, assets, new Map());
107
+
108
+ expect(installMaterialArtifact).toHaveBeenCalledWith('forgeax::default-standard-pbr', {
109
+ source: 'coverage-off',
110
+ paramSchema: [],
111
+ });
112
+ });
113
+
59
114
  it('requests the extended-lighting sampled-texture limit when the adapter supports it', () => {
60
115
  const features = new Set<GPUFeatureName>();
61
116
  expect(
@@ -65,7 +120,7 @@ describe('material shader variant identity', () => {
65
120
  } as never),
66
121
  ).toEqual({
67
122
  requiredFeatures: [],
68
- requiredLimits: { maxSampledTexturesPerShaderStage: 21 },
123
+ requiredLimits: { maxSampledTexturesPerShaderStage: 22 },
69
124
  });
70
125
  expect(
71
126
  deviceOptionsForAdapter({
@@ -88,6 +143,29 @@ describe('material shader variant identity', () => {
88
143
  ).toBe(urpVariantKey);
89
144
  });
90
145
 
146
+ it('keeps coverage-only fallback selection explicit', () => {
147
+ const coverageKey = `COVERAGE_ONLY=true+${urpVariantKey}`;
148
+ const manifest = {
149
+ ...noColorManifest(),
150
+ variants: [
151
+ {
152
+ ...noColorVariant(coverageKey, false),
153
+ definesKey: coverageKey,
154
+ defines: {
155
+ ...noColorVariant(coverageKey, false).defines,
156
+ COVERAGE_ONLY: true,
157
+ },
158
+ composedWgsl: 'coverage-on',
159
+ },
160
+ { ...noColorVariant(urpVariantKey, false), composedWgsl: 'coverage-off' },
161
+ ],
162
+ };
163
+ expect(selectNoColorPbrVariant(manifest, true, undefined)?.definesKey).toBe(urpVariantKey);
164
+ expect(
165
+ selectNoColorPbrVariant(manifest, true, `COVERAGE_ONLY=true+${urpVariantKey}`)?.definesKey,
166
+ ).toBe(coverageKey);
167
+ });
168
+
91
169
  it('keeps physical root declarations out of capability variant selection', () => {
92
170
  const manifest = noColorManifest();
93
171
  expect(
@@ -207,6 +285,20 @@ describe('material shader variant identity', () => {
207
285
  );
208
286
  });
209
287
 
288
+ it('defaults coverage-only variants to the opaque path unless explicitly enabled', () => {
289
+ const variants = [{ defines: { COVERAGE_ONLY: true } }, { defines: { COVERAGE_ONLY: false } }];
290
+
291
+ expect(resolveMaterialShaderVariantSet(undefined, variants, 'webgpu', true)).toBe(
292
+ 'COVERAGE_ONLY=false',
293
+ );
294
+ expect(resolveMaterialShaderVariantSet('', variants, 'webgpu', true)).toBe(
295
+ 'COVERAGE_ONLY=false',
296
+ );
297
+ expect(resolveMaterialShaderVariantSet('COVERAGE_ONLY=true', variants, 'webgpu', true)).toBe(
298
+ '',
299
+ );
300
+ });
301
+
210
302
  it('keeps the negative skinning axis disabled when a shadow request omits it', () => {
211
303
  const variants = [
212
304
  {
@@ -260,7 +352,7 @@ describe('material shader variant identity', () => {
260
352
  variants,
261
353
  'webgpu',
262
354
  true,
263
- 19,
355
+ 20,
264
356
  ),
265
357
  ).toBe('EXTENDED_LIGHTING_AVAILABLE=false');
266
358
  expect(
@@ -269,11 +361,11 @@ describe('material shader variant identity', () => {
269
361
  variants,
270
362
  'webgpu',
271
363
  true,
272
- 20,
364
+ 21,
273
365
  ),
274
366
  ).toBe('');
275
367
  expect(
276
- resolveMaterialShaderVariantSet(undefined, variants, 'webgpu', true, 20),
368
+ resolveMaterialShaderVariantSet(undefined, variants, 'webgpu', true, 21),
277
369
  ).toBeUndefined();
278
370
  expect(
279
371
  resolveMaterialShaderVariantSet(
@@ -281,7 +373,7 @@ describe('material shader variant identity', () => {
281
373
  variants,
282
374
  'webgpu',
283
375
  true,
284
- 20,
376
+ 21,
285
377
  ),
286
378
  ).toBe('');
287
379
  expect(
@@ -290,11 +382,31 @@ describe('material shader variant identity', () => {
290
382
  variants,
291
383
  'webgpu',
292
384
  false,
293
- 19,
385
+ 20,
294
386
  ),
295
387
  ).toBe('EXTENDED_LIGHTING_AVAILABLE=false');
296
388
  });
297
389
 
390
+ it('preserves authored default axes while rewriting backend capabilities', () => {
391
+ const variants = [
392
+ { defines: { AUTHORED_TINT: true, STORAGE_BUFFER_AVAILABLE: true } },
393
+ { defines: { AUTHORED_TINT: false, STORAGE_BUFFER_AVAILABLE: true } },
394
+ { defines: { AUTHORED_TINT: true, STORAGE_BUFFER_AVAILABLE: false } },
395
+ { defines: { AUTHORED_TINT: false, STORAGE_BUFFER_AVAILABLE: false } },
396
+ ];
397
+ expect(resolveMaterialShaderVariantSet(undefined, variants, 'webgpu', true)).toBeUndefined();
398
+ expect(resolveMaterialShaderVariantSet('', variants, 'webgpu', true)).toBe('');
399
+ expect(
400
+ resolveMaterialShaderVariantSet('STORAGE_BUFFER_AVAILABLE=true', variants, 'webgpu', true),
401
+ ).toBe('');
402
+ expect(resolveMaterialShaderVariantSet(undefined, variants, 'wgpu-webgl2', false)).toBe(
403
+ 'AUTHORED_TINT=true+STORAGE_BUFFER_AVAILABLE=false',
404
+ );
405
+ expect(resolveMaterialShaderVariantSet('AUTHORED_TINT=false', variants, 'webgpu', true)).toBe(
406
+ 'AUTHORED_TINT=false+STORAGE_BUFFER_AVAILABLE=true',
407
+ );
408
+ });
409
+
298
410
  it('keeps sprite region variants on the explicit empty-key contract', () => {
299
411
  const variants = [
300
412
  {
@@ -474,6 +586,44 @@ describe('temporal material PSO creation contract', () => {
474
586
  });
475
587
  });
476
588
 
589
+ it('accepts r8unorm only for the output-domain coverage temporal variant', () => {
590
+ const coverageContext = {
591
+ ...context,
592
+ colorFormat: 'r8unorm' as GPUTextureFormat,
593
+ coverageOnly: true,
594
+ } satisfies Pick<
595
+ PipelineBuilderContext,
596
+ 'colorFormat' | 'depthFormat' | 'capabilities' | 'coverageOnly'
597
+ >;
598
+ expect(validateTemporalPipelineContract(source, coverageContext).ok).toBe(true);
599
+ expect(
600
+ validateTemporalPipelineContract(source, { ...coverageContext, coverageOnly: false }).ok,
601
+ ).toBe(false);
602
+
603
+ const createRenderPipeline = vi.fn((_descriptor: unknown) => ({ ok: true, value: {} }));
604
+ const result = buildPipelineForMaterialShader(
605
+ 'default-unlit-temporal-coverage',
606
+ { source, paramSchema: [] },
607
+ {
608
+ ...coverageContext,
609
+ device: { createRenderPipeline },
610
+ shaderModuleFactory: { createShaderModule: () => ({ ok: true, value: {} }) },
611
+ pipelineLayout: {},
612
+ vertexBuffers: [],
613
+ } as unknown as PipelineBuilderContext,
614
+ { depthWriteEnabled: true, depthCompare: 'less-equal' },
615
+ undefined,
616
+ 'vs_temporal',
617
+ 'fs_temporal',
618
+ undefined,
619
+ 'temporal',
620
+ );
621
+ expect(result.ok).toBe(true);
622
+ expect(createRenderPipeline.mock.calls[0]?.[0]).toMatchObject({
623
+ fragment: { entryPoint: 'fs_temporal', targets: [{ format: 'r8unorm' }] },
624
+ });
625
+ });
626
+
477
627
  it.each([
478
628
  { depthWriteEnabled: false, depthCompare: 'always' as const },
479
629
  { depthWriteEnabled: true, depthCompare: 'never' as const },
@@ -797,7 +947,7 @@ describe('material shader color-domain contract', () => {
797
947
  'utf8',
798
948
  );
799
949
  expect(source).toMatch(
800
- /if \(\s*!isStandardMapLayout\s*&&\s*isSharedMaterialUserRegionCompatible\(/u,
950
+ /if \(\s*!clustered\s*&&\s*!isStandardMapLayout\s*&&\s*isSharedMaterialUserRegionCompatible\(/u,
801
951
  );
802
952
  });
803
953
 
@@ -813,6 +963,20 @@ describe('material shader color-domain contract', () => {
813
963
  expect(pipelineSource).toContain('appendTextureInjection(afterTransmission, physicalFields)');
814
964
  expect(recordSource).toContain('BUILTIN_USER_REGION_TEXTURE_FIELDS');
815
965
  });
966
+
967
+ it('keeps scene-index entries and GPU-driven instance layouts on custom pipelines', () => {
968
+ const source = readFileSync(
969
+ resolve(import.meta.dirname, '../assembly/material/pipeline-helpers.ts'),
970
+ 'utf8',
971
+ );
972
+ expect(source).toContain(
973
+ "const sceneIndex = layoutKind === 'gpu-driven-pbr' || layoutKind === 'gpu-driven-skin';",
974
+ );
975
+ expect(source).toContain('? (vertexEntryPoint ?? authoredVertexEntry)');
976
+ expect(source).toMatch(
977
+ /const instancesLayout =\s+layoutKind === 'gpu-driven-pbr' \|\| layoutKind === 'gpu-driven-skin'\s+\? state\.gpuDrivenInstancesBindGroupLayout\s+: state\.instancesBindGroupLayout/u,
978
+ );
979
+ });
816
980
  });
817
981
 
818
982
  describe('material shader vertex input contract', () => {
@@ -30,6 +30,12 @@ const entries: readonly FixtureEntry[] = [
30
30
  glsl: '',
31
31
  bindings: '[]',
32
32
  },
33
+ {
34
+ hash: 'fixture-motion-blur',
35
+ wgsl: '// fixture marker: MotionBlurParams\n',
36
+ glsl: '',
37
+ bindings: '[]',
38
+ },
33
39
  {
34
40
  hash: 'fixture-fxaa',
35
41
  wgsl: '// fixture marker: rgb2luma\n',
@@ -52,6 +52,7 @@ describe('skinned shadow caster', () => {
52
52
  );
53
53
  expect(shader).toContain('#pragma variant_axis SKINNING_DISABLED');
54
54
  expect(shader).toContain('#pragma variant_axis ALPHA_MASK');
55
+ expect(shader).toContain('#pragma variant_axis VERTEX_COLOR_AVAILABLE');
55
56
  expect(shader).toContain('#pragma material_slot surface');
56
57
  expect(shader).toContain('#import forgeax_material::parameters::{material}');
57
58
  expect(shader).toContain('@group(1) @binding(46)');
@@ -59,6 +60,8 @@ describe('skinned shadow caster', () => {
59
60
  expect(shader).toContain('discard;');
60
61
  expect(shader).toContain('@group(2) @binding(1)');
61
62
  expect(shader).toContain('let skinMatrix = palette[');
63
+ expect(shader).toContain('@location(13) color : vec4<f32>');
64
+ expect(shader).toContain('out.color = in.color;');
62
65
  expect(record).toContain('entry.source.skin.byteOffset');
63
66
  expect(record).toContain("'shadow-pbr-skin-mesh'");
64
67
  expect(builder).toContain('buffers: [...ctx.vertexBuffers]');
@@ -76,7 +79,8 @@ describe('skinned shadow caster', () => {
76
79
  expect(singleton).toContain('if (runtime.device.caps.storageBuffer)');
77
80
  expect(singleton).toContain('binding: 46');
78
81
  const sceneMaterialEntry = singleton.slice(singleton.lastIndexOf('binding: 46'));
79
- expect(sceneMaterialEntry).toContain('pipelineState.meshStorageBuffer.buffer');
82
+ expect(singleton).toContain('c.gpuDrivenStandardPbrFrameResources?.sceneMaterialBuffer');
83
+ expect(singleton).toContain('pipelineState.meshStorageBuffer.buffer');
80
84
  expect(sceneMaterialEntry).not.toContain('pipelineState.materialUniformBuffer.buffer');
81
85
  expect(singleton.indexOf('if (runtime.device.caps.storageBuffer)')).toBeLessThan(
82
86
  singleton.indexOf('binding: 46'),
@@ -165,6 +169,9 @@ describe('skinned shadow caster', () => {
165
169
  expect(shadowCasterVariantSet(true, false, true)).toBe(
166
170
  'ALPHA_MASK=false+GPU_DRIVEN_SCENE_INDEX_AVAILABLE=true+GPU_DRIVEN_SCENE_INDEX_EXPLICIT=false+SKINNING_DISABLED=true+STORAGE_BUFFER_AVAILABLE=true',
167
171
  );
172
+ expect(shadowCasterVariantSet(true, false, true, false, true)).toContain(
173
+ 'VERTEX_COLOR_AVAILABLE=true',
174
+ );
168
175
  expect(shader).toContain('@group(3) @binding(1) var<storage, read> visibleItems');
169
176
  expect(shader).toContain('#pragma variant_axis GPU_DRIVEN_SCENE_INDEX_EXPLICIT');
170
177
  expect(shader).toContain('GPU_DRIVEN_SCENE_INDEX_EXPLICIT == false');
@@ -27,6 +27,8 @@ it('decodes regular and probe-sentinel specular environment scales without tint
27
27
  @group(0) @binding(2) var<storage, read_write> outputs: array<vec4<f32>>;
28
28
  @compute @workgroup_size(4) fn probe_scale(@builtin(global_invocation_id) id: vec3<u32>) {
29
29
  let scale = decodeSpecularEnvironmentScale(cases[id.x].xyz, cases[id.x].w);
30
+ // Probe intensity is applied while blending local and global radiance.
31
+ // The outer scale stays one for either sentinel, including zero intensity.
30
32
  // RGB is the decoded scale. Alpha is the same lobe after the material AO
31
33
  // factor, matching the detached fallback's post-environment multiplication.
32
34
  outputs[id.x] = vec4<f32>(scale, scale.x * aoValues[id.x]);
@@ -139,8 +141,8 @@ it('decodes regular and probe-sentinel specular environment scales without tint
139
141
  const expected = [
140
142
  [0.375, 0.75, 1.125, 0.13125],
141
143
  [0.05, 0.1, 0.2, 0.04],
142
- [0.4, 0.4, 0.4, 0.24],
143
- [0.0, 0.0, 0.0, 0.0],
144
+ [1, 1, 1, 0.6],
145
+ [1, 1, 1, 0.25],
144
146
  ].flat();
145
147
  values.forEach((value, index) => {
146
148
  const target = expected[index];
@@ -13,6 +13,7 @@ describe('SSAO parameter authority', () => {
13
13
  radius: SSAO_DEFAULT_RADIUS,
14
14
  bias: SSAO_DEFAULT_BIAS,
15
15
  intensity: SSAO_DEFAULT_INTENSITY,
16
+ quality: 'high',
16
17
  });
17
18
  });
18
19
 
@@ -21,9 +22,15 @@ describe('SSAO parameter authority', () => {
21
22
  radius: 0.65,
22
23
  bias: 0.025,
23
24
  intensity: 1.4,
25
+ quality: 'high',
24
26
  });
25
27
  });
26
28
 
29
+ it.each([NaN, Infinity, -Infinity])('rejects non-finite parameters (%s)', (value) => {
30
+ for (const field of ['radius', 'bias', 'intensity'])
31
+ expect(resolveSsaoParameters({ [field]: value }).ok).toBe(false);
32
+ });
33
+
27
34
  it('returns structured failures for invalid radius and bias', () => {
28
35
  const invalidRadius = resolveSsaoParameters({ radius: 0 });
29
36
  const invalidBias = resolveSsaoParameters({ bias: -0.1 });
@@ -0,0 +1,252 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { mat4 } from '@forgeax/engine-math';
4
+ import { expect, it } from 'vitest';
5
+ import {
6
+ GPU_TEXTURE_USAGE_COPY_DST,
7
+ GPU_TEXTURE_USAGE_COPY_SRC,
8
+ GPU_TEXTURE_USAGE_RENDER_ATTACHMENT,
9
+ GPU_TEXTURE_USAGE_TEXTURE_BINDING,
10
+ } from '../gpu-texture-usage';
11
+ import {
12
+ GPU_BUFFER_USAGE_COPY_DST,
13
+ GPU_BUFFER_USAGE_MAP_READ,
14
+ GPU_BUFFER_USAGE_UNIFORM,
15
+ } from '../gpu-usage';
16
+ import { generateSsaoKernel, generateSsaoNoise } from '../ssao-buffers';
17
+
18
+ // Analytic tilted plane: y + z + 4 = 0. Every hemisphere sample lies in
19
+ // empty space. Exercise the production fragment program, not a CPU AO copy.
20
+ it.each([
21
+ ['plane', 16],
22
+ ['plane', 32],
23
+ ['plane', 64],
24
+ ['orthographic', 64],
25
+ ['contact', 64],
26
+ ['distant-edge', 64],
27
+ ['background', 64],
28
+ ] as const)('production SSAO %s with %i samples', async (scene, sampleCount) => {
29
+ const compiler = await import(
30
+ /* @vite-ignore */ new URL('../../../shader-compiler/dist/index.mjs', import.meta.url).href
31
+ );
32
+ const compiled = await compiler.compileShader(
33
+ readFileSync(resolve('packages/shader/src/hdrp-ssao.wgsl'), 'utf8'),
34
+ {
35
+ id: 'forgeax_hdrp::ssao',
36
+ imports: {
37
+ 'forgeax_view::common': readFileSync(resolve('packages/shader/src/common.wgsl'), 'utf8'),
38
+ },
39
+ },
40
+ );
41
+ if (!compiled.ok) throw compiled.error;
42
+ const adapter = await navigator.gpu.requestAdapter();
43
+ if (!adapter) throw new Error('Dawn adapter unavailable');
44
+ const device = await adapter.requestDevice();
45
+ const errors: string[] = [];
46
+ device.addEventListener('uncapturederror', (event) => errors.push(event.error.message));
47
+ const size = 128;
48
+ const texture = (format: GPUTextureFormat, width = size, height = size) =>
49
+ device.createTexture({
50
+ size: [width, height],
51
+ format,
52
+ usage:
53
+ GPU_TEXTURE_USAGE_RENDER_ATTACHMENT |
54
+ GPU_TEXTURE_USAGE_TEXTURE_BINDING |
55
+ GPU_TEXTURE_USAGE_COPY_SRC |
56
+ GPU_TEXTURE_USAGE_COPY_DST,
57
+ });
58
+ const depth = texture('depth32float');
59
+ const normal = texture('rgba16float');
60
+ const aoSize = size / 2;
61
+ const raw = texture('r8unorm', aoSize, aoSize);
62
+ const blurred = texture('r8unorm', aoSize, aoSize);
63
+ const noise = texture('rgba32float', 4, 4);
64
+ const uniform = device.createBuffer({
65
+ size: 256,
66
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
67
+ });
68
+ const kernel = device.createBuffer({
69
+ size: 1024,
70
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
71
+ });
72
+ const readback = device.createBuffer({
73
+ size: 256 * aoSize,
74
+ usage: GPU_BUFFER_USAGE_COPY_DST | GPU_BUFFER_USAGE_MAP_READ,
75
+ });
76
+ try {
77
+ const projection = mat4.create();
78
+ if (scene === 'orthographic') mat4.orthographic(projection, -3, 3, 3, -3, 0.1, 100);
79
+ else mat4.perspective(projection, Math.PI / 3, 1, 0.1, 100);
80
+ const inverse = mat4.create();
81
+ mat4.invert(inverse, projection);
82
+ const payload = new Float32Array(64);
83
+ payload.set(mat4.identity(mat4.create()));
84
+ payload.set(projection, 16);
85
+ payload.set(inverse, 32);
86
+ payload.set([1, 0.5, 0.025, sampleCount], 48);
87
+ device.queue.writeBuffer(uniform, 0, payload);
88
+ device.queue.writeBuffer(
89
+ kernel,
90
+ 0,
91
+ new Float32Array(generateSsaoKernel().flatMap((v) => [...v, 0])),
92
+ );
93
+ const noiseRgb = generateSsaoNoise();
94
+ const noiseRgba = new Float32Array(64);
95
+ for (let i = 0; i < 16; i++) noiseRgba.set(noiseRgb.subarray(i * 3, i * 3 + 3), i * 4);
96
+ device.queue.writeTexture({ texture: noise }, noiseRgba, { bytesPerRow: 64 }, [4, 4]);
97
+ const seedModule = device.createShaderModule({
98
+ code: `
99
+ struct Out { @builtin(position) pos: vec4f, @location(0) uv: vec2f };
100
+ @vertex fn vs(@builtin(vertex_index) i:u32)->Out {
101
+ let p = array<vec2f,3>(vec2f(-1,-1),vec2f(3,-1),vec2f(-1,3));
102
+ return Out(vec4f(p[i],0,1),p[i]*vec2f(0.5,-0.5)+0.5);
103
+ }
104
+ struct Fragment { @builtin(frag_depth) depth:f32, @location(0) normal:vec4f };
105
+ @fragment fn fs(in:Out)->Fragment {
106
+ let rayY = (1.0-2.0*in.uv.y)*0.577350269;
107
+ let distance = ${scene === 'orthographic' ? '4.0+(1.0-2.0*in.uv.y)*3.0' : scene === 'plane' ? '4.0/(1.0-rayY)' : scene === 'contact' ? 'select(4.0,3.75,in.uv.x>0.5)' : scene === 'distant-edge' ? 'select(4.0,1.0,in.uv.x>0.5)' : '100.0'};
108
+ return Fragment(${scene === 'orthographic' ? '(distance-0.1)/99.9' : scene === 'background' ? '1.0' : '100.0/99.9-10.0/(99.9*distance)'},${scene === 'plane' || scene === 'orthographic' ? 'vec4f(0.5,0.853553391,0.853553391,1)' : 'vec4f(0.5,0.5,1,1)'});
109
+ }`,
110
+ });
111
+ const seed = device.createRenderPipeline({
112
+ layout: 'auto',
113
+ vertex: { module: seedModule, entryPoint: 'vs' },
114
+ fragment: { module: seedModule, entryPoint: 'fs', targets: [{ format: 'rgba16float' }] },
115
+ depthStencil: { format: 'depth32float', depthWriteEnabled: true, depthCompare: 'always' },
116
+ });
117
+ const module = device.createShaderModule({ code: compiled.value.wgsl });
118
+ const layout = device.createBindGroupLayout({
119
+ entries: [
120
+ { binding: 0, visibility: 2, buffer: { type: 'uniform' } },
121
+ { binding: 1, visibility: 2, buffer: { type: 'uniform' } },
122
+ { binding: 2, visibility: 2, texture: { sampleType: 'unfilterable-float' } },
123
+ { binding: 3, visibility: 2, sampler: { type: 'non-filtering' } },
124
+ { binding: 4, visibility: 2, texture: { sampleType: 'unfilterable-float' } },
125
+ { binding: 5, visibility: 2, texture: { sampleType: 'depth' } },
126
+ { binding: 6, visibility: 2, sampler: { type: 'non-filtering' } },
127
+ ],
128
+ });
129
+ const calc = device.createRenderPipeline({
130
+ layout: device.createPipelineLayout({ bindGroupLayouts: [layout] }),
131
+ vertex: { module, entryPoint: 'vs_ssao' },
132
+ fragment: { module, entryPoint: 'fs_ssao_calc', targets: [{ format: 'r8unorm' }] },
133
+ });
134
+ const sampler = device.createSampler({ addressModeU: 'repeat', addressModeV: 'repeat' });
135
+ const bindings = device.createBindGroup({
136
+ layout,
137
+ entries: [
138
+ { binding: 0, resource: { buffer: uniform } },
139
+ { binding: 1, resource: { buffer: kernel } },
140
+ { binding: 2, resource: noise.createView() },
141
+ { binding: 3, resource: sampler },
142
+ { binding: 4, resource: normal.createView() },
143
+ { binding: 5, resource: depth.createView() },
144
+ { binding: 6, resource: device.createSampler() },
145
+ ],
146
+ });
147
+ const encoder = device.createCommandEncoder();
148
+ const seedPass = encoder.beginRenderPass({
149
+ colorAttachments: [{ view: normal.createView(), loadOp: 'clear', storeOp: 'store' }],
150
+ depthStencilAttachment: {
151
+ view: depth.createView(),
152
+ depthLoadOp: 'clear',
153
+ depthStoreOp: 'store',
154
+ depthClearValue: 1,
155
+ },
156
+ });
157
+ seedPass.setPipeline(seed);
158
+ seedPass.draw(3);
159
+ seedPass.end();
160
+ const pass = encoder.beginRenderPass({
161
+ colorAttachments: [{ view: raw.createView(), loadOp: 'clear', storeOp: 'store' }],
162
+ });
163
+ pass.setPipeline(calc);
164
+ pass.setBindGroup(0, bindings);
165
+ pass.draw(3);
166
+ pass.end();
167
+ encoder.copyTextureToBuffer({ texture: raw }, { buffer: readback, bytesPerRow: 256 }, [
168
+ aoSize,
169
+ aoSize,
170
+ ]);
171
+ device.queue.submit([encoder.finish()]);
172
+ await readback.mapAsync(GPU_BUFFER_USAGE_MAP_READ);
173
+ const values = new Uint8Array(readback.getMappedRange());
174
+ const mean = (x0: number, x1: number) => {
175
+ let sum = 0;
176
+ for (let y = 8; y < aoSize - 8; y++)
177
+ for (let x = x0; x < x1; x++) sum += 1 - (values[y * 256 + x] ?? NaN) / 255;
178
+ return sum / ((aoSize - 16) * (x1 - x0));
179
+ };
180
+ expect(errors).toEqual([]);
181
+ if (scene === 'contact') {
182
+ expect(
183
+ mean(aoSize / 2 - 4, aoSize / 2),
184
+ 'nearby foreground occludes the contact band',
185
+ ).toBeGreaterThan(0.02);
186
+ expect(mean(8, 16), 'open surface away from contact stays clear').toBeLessThan(0.01);
187
+ } else {
188
+ if (scene === 'distant-edge') expect(mean(aoSize / 2 - 4, aoSize / 2)).toBeLessThan(0.005);
189
+ expect(
190
+ mean(8, aoSize - 8),
191
+ 'unoccluded surface and distant silhouettes stay clear',
192
+ ).toBeLessThan(0.01);
193
+ }
194
+ readback.unmap();
195
+ const blurLayout = device.createBindGroupLayout({
196
+ entries: [
197
+ { binding: 0, visibility: 2, buffer: { type: 'uniform' } },
198
+ { binding: 4, visibility: 2, texture: { sampleType: 'unfilterable-float' } },
199
+ { binding: 5, visibility: 2, texture: { sampleType: 'depth' } },
200
+ { binding: 6, visibility: 2, sampler: { type: 'non-filtering' } },
201
+ { binding: 7, visibility: 2, texture: { sampleType: 'unfilterable-float' } },
202
+ ],
203
+ });
204
+ const blurPipeline = device.createRenderPipeline({
205
+ layout: device.createPipelineLayout({ bindGroupLayouts: [blurLayout] }),
206
+ vertex: { module, entryPoint: 'vs_ssao' },
207
+ fragment: { module, entryPoint: 'fs_ssao_blur', targets: [{ format: 'r8unorm' }] },
208
+ });
209
+ const blurGroup = device.createBindGroup({
210
+ layout: blurLayout,
211
+ entries: [
212
+ { binding: 0, resource: { buffer: uniform } },
213
+ { binding: 4, resource: normal.createView() },
214
+ { binding: 5, resource: depth.createView() },
215
+ { binding: 6, resource: device.createSampler() },
216
+ { binding: 7, resource: raw.createView() },
217
+ ],
218
+ });
219
+ const filterEncoder = device.createCommandEncoder();
220
+ const filter = filterEncoder.beginRenderPass({
221
+ colorAttachments: [{ view: blurred.createView(), loadOp: 'clear', storeOp: 'store' }],
222
+ });
223
+ filter.setPipeline(blurPipeline);
224
+ filter.setBindGroup(0, blurGroup);
225
+ filter.draw(3);
226
+ filter.end();
227
+ filterEncoder.copyTextureToBuffer(
228
+ { texture: blurred },
229
+ { buffer: readback, bytesPerRow: 256 },
230
+ [aoSize, aoSize],
231
+ );
232
+ device.queue.submit([filterEncoder.finish()]);
233
+ await readback.mapAsync(GPU_BUFFER_USAGE_MAP_READ);
234
+ const filtered = new Uint8Array(readback.getMappedRange());
235
+ if (scene === 'contact') {
236
+ let foreground = 0;
237
+ for (let y = 8; y < aoSize - 8; y++)
238
+ for (let x = aoSize / 2; x < aoSize / 2 + 3; x++)
239
+ foreground += 1 - (filtered[y * 256 + x] ?? NaN) / 255;
240
+ expect(
241
+ foreground / ((aoSize - 16) * 3),
242
+ 'bilateral blur must not bleed contact AO onto the foreground',
243
+ ).toBeLessThan(0.005);
244
+ }
245
+ expect(errors).toEqual([]);
246
+ readback.unmap();
247
+ } finally {
248
+ for (const resource of [depth, normal, raw, blurred, noise, uniform, kernel, readback])
249
+ resource.destroy();
250
+ device.destroy();
251
+ }
252
+ });