@forgeax/engine-render 0.1.19 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (452) hide show
  1. package/README.md +215 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/assembly/device-feature-admission.d.ts +18 -0
  4. package/dist/assembly/device-feature-admission.d.ts.map +1 -0
  5. package/dist/assembly/directional-shadow-inspection.d.ts +38 -0
  6. package/dist/assembly/directional-shadow-inspection.d.ts.map +1 -0
  7. package/dist/assembly/factory.d.ts +34 -20
  8. package/dist/assembly/factory.d.ts.map +1 -1
  9. package/dist/assembly/host-contract.d.ts +9 -0
  10. package/dist/assembly/host-contract.d.ts.map +1 -1
  11. package/dist/assembly/render-target-host.d.ts +46 -0
  12. package/dist/assembly/render-target-host.d.ts.map +1 -0
  13. package/dist/assets/render-asset-port.d.ts.map +1 -1
  14. package/dist/authoring.mjs +3 -0
  15. package/dist/authoring.mjs.map +1 -1
  16. package/dist/capture/cube-views.d.ts +21 -0
  17. package/dist/capture/cube-views.d.ts.map +1 -0
  18. package/dist/capture/scheduler.d.ts +54 -0
  19. package/dist/capture/scheduler.d.ts.map +1 -0
  20. package/dist/{chunk-RJRUS55L.mjs → chunk-5YADUCA7.mjs} +1896 -1624
  21. package/dist/chunk-5YADUCA7.mjs.map +1 -0
  22. package/dist/{chunk-LOSO4CTV.mjs → chunk-6OPXDQ2E.mjs} +423 -46
  23. package/dist/chunk-6OPXDQ2E.mjs.map +1 -0
  24. package/dist/{chunk-IPXCABEP.mjs → chunk-BXBQQKFQ.mjs} +3 -9
  25. package/dist/chunk-BXBQQKFQ.mjs.map +1 -0
  26. package/dist/chunk-FS2KNUQH.mjs +1294 -0
  27. package/dist/chunk-FS2KNUQH.mjs.map +1 -0
  28. package/dist/components/camera.d.ts +1 -0
  29. package/dist/components/camera.d.ts.map +1 -1
  30. package/dist/components/cube-camera.d.ts +20 -0
  31. package/dist/components/cube-camera.d.ts.map +1 -0
  32. package/dist/components/directional-light.d.ts +11 -2
  33. package/dist/components/directional-light.d.ts.map +1 -1
  34. package/dist/components/directional-shadow-filter.d.ts +25 -0
  35. package/dist/components/directional-shadow-filter.d.ts.map +1 -0
  36. package/dist/components/index.d.ts +2 -0
  37. package/dist/components/index.d.ts.map +1 -1
  38. package/dist/components/light-helpers.d.ts.map +1 -1
  39. package/dist/components/reflection-probe.d.ts +17 -0
  40. package/dist/components/reflection-probe.d.ts.map +1 -0
  41. package/dist/components/spot-light.d.ts +12 -0
  42. package/dist/components/spot-light.d.ts.map +1 -1
  43. package/dist/construct-renderer.mjs +8552 -1612
  44. package/dist/construct-renderer.mjs.map +1 -1
  45. package/dist/device/gpu-residency.d.ts +19 -2
  46. package/dist/device/gpu-residency.d.ts.map +1 -1
  47. package/dist/environment/frame.d.ts.map +1 -1
  48. package/dist/environment/lifecycle.d.ts +5 -0
  49. package/dist/environment/lifecycle.d.ts.map +1 -1
  50. package/dist/errors/render.d.ts +209 -93
  51. package/dist/errors/render.d.ts.map +1 -1
  52. package/dist/extract/directional-shadow-projection.d.ts +23 -0
  53. package/dist/extract/directional-shadow-projection.d.ts.map +1 -0
  54. package/dist/extract/environment.d.ts +14 -0
  55. package/dist/extract/environment.d.ts.map +1 -1
  56. package/dist/extract/visibility.d.ts.map +1 -1
  57. package/dist/features/fullscreen.d.ts.map +1 -1
  58. package/dist/features/targets.d.ts.map +1 -1
  59. package/dist/features/types.d.ts +10 -0
  60. package/dist/features/types.d.ts.map +1 -1
  61. package/dist/fullscreen-post-process-pass.d.ts +13 -2
  62. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  63. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  64. package/dist/gpu-driven/production-raster.d.ts +0 -1
  65. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  66. package/dist/gpu-scene.d.ts.map +1 -1
  67. package/dist/gpu-usage.d.ts +2 -0
  68. package/dist/gpu-usage.d.ts.map +1 -1
  69. package/dist/hdrp-buffers.d.ts +19 -17
  70. package/dist/hdrp-buffers.d.ts.map +1 -1
  71. package/dist/ibl/IblPipelineCache.d.ts +4 -0
  72. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  73. package/dist/ibl/kernel-cache.d.ts +36 -0
  74. package/dist/ibl/kernel-cache.d.ts.map +1 -0
  75. package/dist/index.d.ts +35 -4
  76. package/dist/index.d.ts.map +1 -1
  77. package/dist/index.mjs +4 -4
  78. package/dist/index.mjs.map +1 -1
  79. package/dist/inspection-types.d.ts +51 -0
  80. package/dist/inspection-types.d.ts.map +1 -1
  81. package/dist/instances-derived-bounds.d.ts +50 -0
  82. package/dist/instances-derived-bounds.d.ts.map +1 -0
  83. package/dist/internal.mjs +1 -1
  84. package/dist/light-buffer-layout.d.ts +30 -13
  85. package/dist/light-buffer-layout.d.ts.map +1 -1
  86. package/dist/mesh-material-bindings.d.ts +54 -0
  87. package/dist/mesh-material-bindings.d.ts.map +1 -1
  88. package/dist/pbr-pipeline.d.ts +20 -7
  89. package/dist/pbr-pipeline.d.ts.map +1 -1
  90. package/dist/pipeline/standard-forward-lane.d.ts +2 -1
  91. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  92. package/dist/pipeline/standard-lighting/errors.d.ts +4 -0
  93. package/dist/pipeline/standard-lighting/errors.d.ts.map +1 -0
  94. package/dist/pipeline/standard-lighting/graph.d.ts +24 -0
  95. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -0
  96. package/dist/pipeline/standard-lighting/inspection.d.ts +38 -0
  97. package/dist/pipeline/standard-lighting/inspection.d.ts.map +1 -0
  98. package/dist/pipeline/standard-lighting/layout.d.ts +28 -0
  99. package/dist/pipeline/standard-lighting/layout.d.ts.map +1 -0
  100. package/dist/pipeline/standard-lighting/light-frame.d.ts +33 -0
  101. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -0
  102. package/dist/pipeline/standard-lighting/membership.d.ts +14 -0
  103. package/dist/pipeline/standard-lighting/membership.d.ts.map +1 -0
  104. package/dist/pipeline/standard-lighting/prepare.d.ts +16 -0
  105. package/dist/pipeline/standard-lighting/prepare.d.ts.map +1 -0
  106. package/dist/pipeline/standard-lighting/topology.d.ts +45 -0
  107. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -0
  108. package/dist/pipeline/standard-lighting/transport.d.ts +36 -0
  109. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -0
  110. package/dist/pipeline/standard-pipeline.d.ts +4 -13
  111. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  112. package/dist/pipeline/standard-post.d.ts.map +1 -1
  113. package/dist/pipeline/standard-profile.d.ts +12 -20
  114. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  115. package/dist/pipeline-spec.d.ts +40 -7
  116. package/dist/pipeline-spec.d.ts.map +1 -1
  117. package/dist/point-shadow-inspection.d.ts +21 -0
  118. package/dist/point-shadow-inspection.d.ts.map +1 -0
  119. package/dist/record/frame-lighting.d.ts +29 -32
  120. package/dist/record/frame-lighting.d.ts.map +1 -1
  121. package/dist/record/frame-snapshot.d.ts +65 -15
  122. package/dist/record/frame-snapshot.d.ts.map +1 -1
  123. package/dist/record/frame-targets.d.ts +5 -0
  124. package/dist/record/frame-targets.d.ts.map +1 -1
  125. package/dist/record/frame.d.ts +12 -5
  126. package/dist/record/frame.d.ts.map +1 -1
  127. package/dist/record/gpu-pass-timing/contract.d.ts +96 -0
  128. package/dist/record/gpu-pass-timing/contract.d.ts.map +1 -0
  129. package/dist/record/gpu-pass-timing/errors.d.ts +18 -0
  130. package/dist/record/gpu-pass-timing/errors.d.ts.map +1 -0
  131. package/dist/record/gpu-pass-timing/index.d.ts +8 -0
  132. package/dist/record/gpu-pass-timing/index.d.ts.map +1 -0
  133. package/dist/record/gpu-pass-timing/parser.d.ts +11 -0
  134. package/dist/record/gpu-pass-timing/parser.d.ts.map +1 -0
  135. package/dist/record/gpu-pass-timing/session.d.ts +43 -0
  136. package/dist/record/gpu-pass-timing/session.d.ts.map +1 -0
  137. package/dist/record/gpu-timing.d.ts +37 -0
  138. package/dist/record/gpu-timing.d.ts.map +1 -0
  139. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  140. package/dist/record/main-pass-material.d.ts +23 -2
  141. package/dist/record/main-pass-material.d.ts.map +1 -1
  142. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  143. package/dist/record/main-pass.d.ts +2 -0
  144. package/dist/record/main-pass.d.ts.map +1 -1
  145. package/dist/record/render-context.d.ts +48 -3
  146. package/dist/record/render-context.d.ts.map +1 -1
  147. package/dist/record/shadow-pass.d.ts.map +1 -1
  148. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  149. package/dist/record/typed-frame-graph.d.ts +81 -5
  150. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  151. package/dist/record/view-ubo.d.ts +7 -4
  152. package/dist/record/view-ubo.d.ts.map +1 -1
  153. package/dist/reflection/filter.d.ts +22 -0
  154. package/dist/reflection/filter.d.ts.map +1 -0
  155. package/dist/reflection/gpu-table.d.ts +25 -0
  156. package/dist/reflection/gpu-table.d.ts.map +1 -0
  157. package/dist/reflection/projection.d.ts +91 -0
  158. package/dist/reflection/projection.d.ts.map +1 -0
  159. package/dist/render-contract.d.ts +114 -7
  160. package/dist/render-contract.d.ts.map +1 -1
  161. package/dist/render-data.d.ts +10 -6
  162. package/dist/render-data.d.ts.map +1 -1
  163. package/dist/render-graph-primitives.d.ts +1 -0
  164. package/dist/render-graph-primitives.d.ts.map +1 -1
  165. package/dist/render-pipeline.d.ts +78 -2
  166. package/dist/render-pipeline.d.ts.map +1 -1
  167. package/dist/render-system-extract.d.ts +113 -20
  168. package/dist/render-system-extract.d.ts.map +1 -1
  169. package/dist/render-system.d.ts +24 -3
  170. package/dist/render-system.d.ts.map +1 -1
  171. package/dist/renderer-inspect.d.ts +29 -0
  172. package/dist/renderer-inspect.d.ts.map +1 -0
  173. package/dist/renderer.d.ts +43 -0
  174. package/dist/renderer.d.ts.map +1 -0
  175. package/dist/scene/render-scene.d.ts +14 -0
  176. package/dist/scene/render-scene.d.ts.map +1 -1
  177. package/dist/shadow-atlas.d.ts +3 -3
  178. package/dist/shadow-atlas.d.ts.map +1 -1
  179. package/dist/ssao-buffers.d.ts +3 -0
  180. package/dist/ssao-buffers.d.ts.map +1 -1
  181. package/dist/targets/contracts.d.ts +66 -0
  182. package/dist/targets/contracts.d.ts.map +1 -0
  183. package/dist/targets/graph-projection.d.ts +29 -0
  184. package/dist/targets/graph-projection.d.ts.map +1 -0
  185. package/dist/targets/material-source.d.ts +34 -0
  186. package/dist/targets/material-source.d.ts.map +1 -0
  187. package/dist/targets/owner.d.ts +32 -0
  188. package/dist/targets/owner.d.ts.map +1 -0
  189. package/dist/targets/physical.d.ts +18 -0
  190. package/dist/targets/physical.d.ts.map +1 -0
  191. package/dist/targets/readback.d.ts +39 -0
  192. package/dist/targets/readback.d.ts.map +1 -0
  193. package/dist/temporal/frame.d.ts +44 -0
  194. package/dist/temporal/frame.d.ts.map +1 -0
  195. package/dist/temporal/index.mjs +3 -3
  196. package/dist/typed-render-graph-primitives.d.ts +6 -1
  197. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  198. package/dist/typed-shadow-atlas.d.ts +10 -0
  199. package/dist/typed-shadow-atlas.d.ts.map +1 -0
  200. package/dist/typed-shadow-passes.d.ts +1 -1
  201. package/dist/typed-shadow-passes.d.ts.map +1 -1
  202. package/dist/volume/capability.d.ts +57 -0
  203. package/dist/volume/capability.d.ts.map +1 -0
  204. package/dist/volume/component.d.ts +61 -0
  205. package/dist/volume/component.d.ts.map +1 -0
  206. package/dist/volume/extract.d.ts +11 -0
  207. package/dist/volume/extract.d.ts.map +1 -0
  208. package/dist/volume/inspection.d.ts +38 -0
  209. package/dist/volume/inspection.d.ts.map +1 -0
  210. package/dist/volume/passes.d.ts +38 -0
  211. package/dist/volume/passes.d.ts.map +1 -0
  212. package/dist/volume/recovery.d.ts +38 -0
  213. package/dist/volume/recovery.d.ts.map +1 -0
  214. package/dist/volume/resources.d.ts +57 -0
  215. package/dist/volume/resources.d.ts.map +1 -0
  216. package/dist/volume/temporal.d.ts +35 -0
  217. package/dist/volume/temporal.d.ts.map +1 -0
  218. package/package.json +20 -20
  219. package/src/__tests__/bloom-exact-zero.unit.test.ts +48 -19
  220. package/src/__tests__/bloom-lifecycle.integration.test.ts +41 -13
  221. package/src/__tests__/camera-target-consumption.unit.test.ts +53 -0
  222. package/src/__tests__/cluster-binner.unit.test.ts +35 -0
  223. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +7 -2
  224. package/src/__tests__/cube-camera-scheduler.unit.test.ts +70 -0
  225. package/src/__tests__/cube-camera-single-submit.perf.test.ts +75 -0
  226. package/src/__tests__/cube-camera-view-matrices.unit.test.ts +41 -0
  227. package/src/__tests__/cube-camera-vocabulary.unit.test.ts +47 -0
  228. package/src/__tests__/cube-camera.integration.test.ts +42 -0
  229. package/src/__tests__/device-feature-admission.unit.test.ts +73 -0
  230. package/src/__tests__/direct-light-snapshot.unit.test.ts +3 -3
  231. package/src/__tests__/docs-gate-green.unit.test.ts +38 -0
  232. package/src/__tests__/factory-contract.integration.test.ts +255 -29
  233. package/src/__tests__/fog-component.unit.test.ts +67 -0
  234. package/src/__tests__/fog.integration.test.ts +86 -0
  235. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +37 -0
  236. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +10 -9
  237. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +17 -1
  238. package/src/__tests__/gpu-driven-production.integration.test.ts +16 -17
  239. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +12 -4
  240. package/src/__tests__/gpu-pass-timing-browser-runner.ts +281 -0
  241. package/src/__tests__/gpu-pass-timing-coexistence.integration.test.ts +80 -0
  242. package/src/__tests__/gpu-pass-timing-contract.unit.test.ts +243 -0
  243. package/src/__tests__/gpu-pass-timing-exact-zero.unit.test.ts +131 -0
  244. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +230 -0
  245. package/src/__tests__/gpu-pass-timing-observation.unit.test.ts +233 -0
  246. package/src/__tests__/gpu-pass-timing-session.unit.test.ts +438 -0
  247. package/src/__tests__/gpu-pass-timing.browser.test.ts +26 -0
  248. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +125 -0
  249. package/src/__tests__/hdrp-group2-visibility.unit.test.ts +24 -0
  250. package/src/__tests__/instances-culling.integration.test.ts +94 -0
  251. package/src/__tests__/instances-derived-bounds.unit.test.ts +118 -0
  252. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +14 -14
  253. package/src/__tests__/material-observation.integration.test.ts +101 -0
  254. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +1 -1
  255. package/src/__tests__/pbr-view-layout.unit.test.ts +16 -0
  256. package/src/__tests__/point-shadow-inspection.unit.test.ts +60 -0
  257. package/src/__tests__/public-surface.test-d.ts +10 -0
  258. package/src/__tests__/reflection-probe-authoring.unit.test.ts +71 -0
  259. package/src/__tests__/reflection-probe-filter.unit.test.ts +54 -0
  260. package/src/__tests__/reflection-probe-ibl-topology.unit.test.ts +45 -0
  261. package/src/__tests__/reflection-probe-pass-encode.integration.test.ts +101 -0
  262. package/src/__tests__/reflection-probe-performance.perf.test.ts +60 -0
  263. package/src/__tests__/reflection-probe-recovery.integration.test.ts +55 -0
  264. package/src/__tests__/reflection-probe-selection.unit.test.ts +46 -0
  265. package/src/__tests__/render-error-code-owner.test-d.ts +23 -3
  266. package/src/__tests__/render-feature-docs.unit.test.ts +1 -1
  267. package/src/__tests__/render-target-admission.unit.test.ts +80 -0
  268. package/src/__tests__/render-target-graph-projection.unit.test.ts +102 -0
  269. package/src/__tests__/render-target-inspection.unit.test.ts +52 -0
  270. package/src/__tests__/render-target-material-source.test-d.ts +30 -0
  271. package/src/__tests__/render-target-material-source.unit.test.ts +97 -0
  272. package/src/__tests__/render-target-owner-placement.unit.test.ts +203 -0
  273. package/src/__tests__/render-target-owner.unit.test.ts +122 -0
  274. package/src/__tests__/render-target-probe-soak.perf.test.ts +94 -0
  275. package/src/__tests__/render-target-public-schema.json +107 -0
  276. package/src/__tests__/render-target-public-schema.unit.test.ts +91 -0
  277. package/src/__tests__/render-target-public-surface.test-d.ts +83 -0
  278. package/src/__tests__/render-target-readback.integration.test.ts +90 -0
  279. package/src/__tests__/render-target-recovery.integration.test.ts +85 -0
  280. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +76 -0
  281. package/src/__tests__/renderer-frame-transaction.integration.test.ts +1 -1
  282. package/src/__tests__/renderer-lifecycle.integration.test.ts +14 -0
  283. package/src/__tests__/shader-stage-surface.unit.test.ts +19 -4
  284. package/src/__tests__/skin-motion-regression.unit.test.ts +132 -0
  285. package/src/__tests__/skinned-shadow-caster.test.ts +32 -2
  286. package/src/__tests__/standard-cluster-capability.integration.test.ts +63 -0
  287. package/src/__tests__/standard-cluster-transport.unit.test.ts +121 -0
  288. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +232 -0
  289. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +8 -5
  290. package/src/__tests__/standard-lighting-inspection.integration.test.ts +138 -0
  291. package/src/__tests__/standard-lighting-ssot.unit.test.ts +18 -0
  292. package/src/__tests__/standard-lighting-write.unit.test.ts +377 -0
  293. package/src/__tests__/standard-lighting.integration.test.ts +67 -0
  294. package/src/__tests__/standard-pipeline.dawn.test.ts +206 -5
  295. package/src/__tests__/standard-pipeline.integration.test.ts +181 -150
  296. package/src/__tests__/standard-probe-binding.unit.test.ts +25 -0
  297. package/src/__tests__/standard-profile-oracle.unit.test.ts +23 -36
  298. package/src/__tests__/typed-pipeline-topology.unit.test.ts +395 -14
  299. package/src/__tests__/urp-pipeline-targets.unit.test.ts +2 -2
  300. package/src/__tests__/visibility-resolver.unit.test.ts +15 -0
  301. package/src/__tests__/volume-optics-oracle.unit.test.ts +85 -0
  302. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +655 -0
  303. package/src/__tests__/volumetric-fog-component.unit.test.ts +71 -0
  304. package/src/__tests__/volumetric-fog-composite-stage-readback.dawn.test.ts +376 -0
  305. package/src/__tests__/volumetric-fog-errors.unit.test.ts +51 -0
  306. package/src/__tests__/volumetric-fog-inspection.unit.test.ts +77 -0
  307. package/src/__tests__/volumetric-fog-light-capability.unit.test.ts +19 -0
  308. package/src/__tests__/volumetric-fog-light-consumers.integration.test.ts +18 -0
  309. package/src/__tests__/volumetric-fog-light-errors.unit.test.ts +17 -0
  310. package/src/__tests__/volumetric-fog-light-topology.unit.test.ts +21 -0
  311. package/src/__tests__/volumetric-fog-light-transaction.integration.test.ts +53 -0
  312. package/src/__tests__/volumetric-fog-m9-watcher.integration.test.ts +27 -0
  313. package/src/__tests__/volumetric-fog-point-light-owner.integration.test.ts +44 -0
  314. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +49 -0
  315. package/src/__tests__/volumetric-fog-point-spot-contract.unit.test.ts +28 -0
  316. package/src/__tests__/volumetric-fog-point-spot-lkg.integration.test.ts +65 -0
  317. package/src/__tests__/volumetric-fog-point-spot-selection.integration.test.ts +49 -0
  318. package/src/__tests__/volumetric-fog-point-spot-temporal.unit.test.ts +55 -0
  319. package/src/__tests__/volumetric-fog-projector-contract.unit.test.ts +22 -0
  320. package/src/__tests__/volumetric-fog-projector-inspection.unit.test.ts +12 -0
  321. package/src/__tests__/volumetric-fog-projector-surface-volume.integration.test.ts +11 -0
  322. package/src/__tests__/volumetric-fog-projector-transaction.integration.test.ts +26 -0
  323. package/src/__tests__/volumetric-fog-recovery.integration.test.ts +47 -0
  324. package/src/__tests__/volumetric-fog-selected-light-resolution.integration.test.ts +63 -0
  325. package/src/__tests__/volumetric-fog-spot-closure.integration.test.ts +26 -0
  326. package/src/__tests__/volumetric-fog-spot-contract.test-d.ts +21 -0
  327. package/src/__tests__/volumetric-fog-spot-contract.unit.test.ts +27 -0
  328. package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +14 -0
  329. package/src/__tests__/volumetric-fog-spot-optics.integration.test.ts +29 -0
  330. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +550 -0
  331. package/src/__tests__/volumetric-fog-temporal-selected-light.unit.test.ts +33 -0
  332. package/src/__tests__/volumetric-fog-temporal.unit.test.ts +45 -0
  333. package/src/__tests__/volumetric-fog-topology.unit.test.ts +237 -0
  334. package/src/__tests__/volumetric-fog-transparent.integration.test.ts +27 -0
  335. package/src/assembly/device-feature-admission.ts +73 -0
  336. package/src/assembly/directional-shadow-inspection.ts +122 -0
  337. package/src/assembly/factory.ts +1093 -246
  338. package/src/assembly/host-contract.ts +25 -0
  339. package/src/assembly/render-target-host.ts +847 -0
  340. package/src/assets/asset-decoders.ts +1 -1
  341. package/src/assets/render-asset-port.ts +7 -1
  342. package/src/capture/cube-views.ts +69 -0
  343. package/src/capture/scheduler.ts +265 -0
  344. package/src/components/__tests__/schema-contract.unit.test.ts +12 -0
  345. package/src/components/__tests__/schema-parity.unit.test.ts +9 -0
  346. package/src/components/camera.ts +4 -0
  347. package/src/components/cube-camera.ts +52 -0
  348. package/src/components/directional-light.ts +22 -2
  349. package/src/components/directional-shadow-filter.ts +57 -0
  350. package/src/components/index.ts +2 -0
  351. package/src/components/light-helpers.ts +85 -7
  352. package/src/components/reflection-probe.ts +49 -0
  353. package/src/components/spot-light.ts +22 -3
  354. package/src/device/gpu-residency.ts +168 -58
  355. package/src/environment/frame.ts +6 -24
  356. package/src/environment/lifecycle.ts +12 -0
  357. package/src/errors/render.ts +405 -143
  358. package/src/extract/directional-shadow-projection.ts +61 -0
  359. package/src/extract/environment.ts +57 -0
  360. package/src/extract/visibility.ts +25 -1
  361. package/src/features/fullscreen.ts +3 -0
  362. package/src/features/targets.ts +4 -1
  363. package/src/features/types.ts +10 -0
  364. package/src/fullscreen-post-process-pass.ts +43 -2
  365. package/src/gpu-driven/batch-topology.ts +1 -0
  366. package/src/gpu-driven/production-raster.ts +1 -3
  367. package/src/gpu-scene.ts +8 -1
  368. package/src/gpu-usage.ts +2 -0
  369. package/src/hdrp-buffers.ts +165 -59
  370. package/src/ibl/IblPipelineCache.ts +11 -0
  371. package/src/ibl/kernel-cache.ts +88 -0
  372. package/src/index.ts +186 -1
  373. package/src/inspection-types.ts +56 -0
  374. package/src/instances-derived-bounds.ts +171 -0
  375. package/src/light-buffer-layout.ts +74 -26
  376. package/src/mesh-material-bindings.ts +132 -0
  377. package/src/pbr-pipeline.ts +139 -27
  378. package/src/pipeline/standard-forward-lane.ts +65 -15
  379. package/src/pipeline/standard-lighting/errors.ts +6 -0
  380. package/src/pipeline/standard-lighting/graph.ts +157 -0
  381. package/src/pipeline/standard-lighting/inspection.ts +88 -0
  382. package/src/pipeline/standard-lighting/layout.ts +31 -0
  383. package/src/pipeline/standard-lighting/light-frame.ts +34 -0
  384. package/src/pipeline/standard-lighting/membership.ts +43 -0
  385. package/src/pipeline/standard-lighting/prepare.ts +37 -0
  386. package/src/pipeline/standard-lighting/topology.ts +120 -0
  387. package/src/pipeline/standard-lighting/transport.ts +77 -0
  388. package/src/pipeline/standard-pipeline.ts +141 -192
  389. package/src/pipeline/standard-post.ts +26 -7
  390. package/src/pipeline/standard-profile.ts +23 -30
  391. package/src/pipeline-spec.ts +84 -21
  392. package/src/point-shadow-inspection.ts +55 -0
  393. package/src/record/__tests__/temporal-payload.unit.test.ts +42 -1
  394. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +73 -0
  395. package/src/record/__tests__/view-ubo-layout.unit.test.ts +40 -0
  396. package/src/record/frame-lighting.ts +470 -516
  397. package/src/record/frame-snapshot.ts +76 -28
  398. package/src/record/frame-targets.ts +23 -2
  399. package/src/record/frame.ts +1169 -79
  400. package/src/record/gpu-pass-timing/contract.ts +180 -0
  401. package/src/record/gpu-pass-timing/errors.ts +34 -0
  402. package/src/record/gpu-pass-timing/index.ts +37 -0
  403. package/src/record/gpu-pass-timing/parser.ts +97 -0
  404. package/src/record/gpu-pass-timing/session.ts +938 -0
  405. package/src/record/gpu-timing.ts +233 -0
  406. package/src/record/main-pass-geometry.ts +120 -80
  407. package/src/record/main-pass-material.ts +101 -11
  408. package/src/record/main-pass-sprite-draws.ts +54 -25
  409. package/src/record/main-pass.ts +71 -15
  410. package/src/record/render-context.ts +77 -0
  411. package/src/record/shadow-pass.ts +104 -66
  412. package/src/record/skybox-post-pass.ts +20 -4
  413. package/src/record/typed-frame-graph.ts +1016 -32
  414. package/src/record/view-ubo.ts +63 -28
  415. package/src/reflection/filter.ts +81 -0
  416. package/src/reflection/gpu-table.ts +44 -0
  417. package/src/reflection/projection.ts +281 -0
  418. package/src/render-contract.ts +147 -4
  419. package/src/render-data.ts +50 -22
  420. package/src/render-graph-primitives.ts +9 -4
  421. package/src/render-pipeline.ts +161 -2
  422. package/src/render-system-extract.ts +886 -103
  423. package/src/render-system.ts +1174 -66
  424. package/src/renderer-inspect.ts +66 -0
  425. package/src/renderer.ts +222 -0
  426. package/src/scene/render-scene.ts +117 -9
  427. package/src/shadow-atlas.ts +5 -5
  428. package/src/ssao-buffers.ts +9 -2
  429. package/src/targets/contracts.ts +182 -0
  430. package/src/targets/graph-projection.ts +169 -0
  431. package/src/targets/material-source.ts +143 -0
  432. package/src/targets/owner.ts +214 -0
  433. package/src/targets/physical.ts +18 -0
  434. package/src/targets/readback.ts +153 -0
  435. package/src/temporal/frame.ts +128 -0
  436. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +61 -23
  437. package/src/typed-render-graph-primitives.ts +34 -2
  438. package/src/typed-shadow-atlas.ts +17 -0
  439. package/src/typed-shadow-passes.ts +47 -40
  440. package/src/volume/capability.ts +173 -0
  441. package/src/volume/component.ts +110 -0
  442. package/src/volume/extract.ts +21 -0
  443. package/src/volume/inspection.ts +78 -0
  444. package/src/volume/passes.ts +686 -0
  445. package/src/volume/recovery.ts +86 -0
  446. package/src/volume/resources.ts +236 -0
  447. package/src/volume/temporal.ts +89 -0
  448. package/dist/chunk-IPXCABEP.mjs.map +0 -1
  449. package/dist/chunk-LOSO4CTV.mjs.map +0 -1
  450. package/dist/chunk-RJRUS55L.mjs.map +0 -1
  451. package/dist/chunk-YTQ5C7YQ.mjs +0 -584
  452. package/dist/chunk-YTQ5C7YQ.mjs.map +0 -1
@@ -1,7 +1,10 @@
1
- import { readFileSync } from 'node:fs';
2
- import { resolve } from 'node:path';
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { dirname, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { HANDLE_CUBE } from '@forgeax/engine-assets-runtime';
3
5
  import { World } from '@forgeax/engine-ecs';
4
6
  import { createProfiler } from '@forgeax/engine-profiler';
7
+ import { DEFAULT_STANDARD_PROFILE } from '@forgeax/engine-render';
5
8
  import { RhiError, type ShaderModule } from '@forgeax/engine-rhi';
6
9
  import {
7
10
  RhiNullAdapter,
@@ -15,6 +18,7 @@ import { err, ok } from '@forgeax/engine-types';
15
18
  import { describe, expect, it, vi } from 'vitest';
16
19
  import type { RhiBackendPack } from '../assembly/backend-contract';
17
20
  import { createRenderer as constructRenderer } from '../assembly/factory';
21
+ import { DirectionalLight, MeshFilter, MeshRenderer, PointLight, SpotLight } from '../components';
18
22
  import {
19
23
  ANTIALIAS_NONE,
20
24
  ANTIALIAS_TAA,
@@ -26,6 +30,10 @@ import {
26
30
  import type { RenderFeature } from '../features/types';
27
31
  import { renderLifecycleManifestUrl } from './shader-manifest-fixture';
28
32
 
33
+ const assemblyDirectory = dirname(fileURLToPath(import.meta.url));
34
+ const factorySourcePath = resolve(assemblyDirectory, '../assembly/factory.ts');
35
+ const renderTargetHostSourcePath = resolve(assemblyDirectory, '../assembly/render-target-host.ts');
36
+
29
37
  function manifestUrl(): string {
30
38
  return `data:application/json,${encodeURIComponent(JSON.stringify({ schemaVersion: '1.0.0', entries: [] }))}`;
31
39
  }
@@ -92,6 +100,17 @@ async function createBloomScenario() {
92
100
  }
93
101
 
94
102
  describe('factory contract', () => {
103
+ it('requires one Renderer-owned target lifecycle seam', () => {
104
+ expect(existsSync(renderTargetHostSourcePath)).toBe(true);
105
+ const factorySource = readFileSync(factorySourcePath, 'utf8');
106
+ expect(factorySource.match(/createRenderTargetHost\(/g)).toHaveLength(2);
107
+ expect(factorySource).toContain("from './render-target-host'");
108
+ expect(factorySource).toContain('renderTargetHost.beginFrame()');
109
+ expect(factorySource).toContain('renderTargetHost.onFrameSubmitted(completed)');
110
+ expect(factorySource).toContain('renderTargetHost.recover()');
111
+ expect(factorySource).toContain('renderTargetHost.dispose()');
112
+ });
113
+
95
114
  it('keeps the public output-transform identity and closes removed tone symbols', () => {
96
115
  const source = [
97
116
  'render-contract.ts',
@@ -148,6 +167,22 @@ describe('factory contract', () => {
148
167
  renderer.dispose();
149
168
  });
150
169
 
170
+ it('rejects the removed Standard reflection-probe profile field at the Renderer boundary', async () => {
171
+ const renderer = await constructRenderer(
172
+ { getContext: () => null },
173
+ { rhi },
174
+ { shaderManifestUrl: manifestUrl() },
175
+ );
176
+ const staleProfile = {
177
+ ...DEFAULT_STANDARD_PROFILE,
178
+ reflectionProbes: false,
179
+ } as unknown as Parameters<typeof renderer.setProfile>[0];
180
+ const result = renderer.setProfile(staleProfile);
181
+ expect(result.ok).toBe(false);
182
+ if (!result.ok) expect(result.error.code).toBe('frame-input-invalid');
183
+ renderer.dispose();
184
+ });
185
+
151
186
  it('projects stable output and observation identity without pixel payloads', async () => {
152
187
  const renderer = await constructRenderer(
153
188
  { getContext: () => null },
@@ -175,6 +210,59 @@ describe('factory contract', () => {
175
210
  renderer.dispose();
176
211
  });
177
212
 
213
+ it('delivers a target readback only through its matching receipt', async () => {
214
+ const renderer = await constructRenderer(
215
+ { getContext: () => null },
216
+ { rhi },
217
+ { shaderManifestUrl: manifestUrl() },
218
+ );
219
+ const target = renderer.createRenderTarget({
220
+ shape: '2d',
221
+ width: 17,
222
+ height: 9,
223
+ format: 'rgba8unorm',
224
+ mipLevels: 1,
225
+ sampleCount: 1,
226
+ sampled: false,
227
+ readback: true,
228
+ });
229
+ expect(target.ok).toBe(true);
230
+ if (!target.ok) return;
231
+ const ticket = renderer.requestTargetReadback(target.value, { mipLevel: 0 });
232
+ expect(ticket.ok).toBe(true);
233
+ if (!ticket.ok) return;
234
+ const world = new World();
235
+ const attached = renderer.attach(world);
236
+ expect(attached.ok).toBe(true);
237
+ if (!attached.ok) return;
238
+ const frame = renderer.draw({
239
+ leases: [attached.value],
240
+ camera: { lease: attached.value },
241
+ environment: { lease: attached.value },
242
+ });
243
+ expect(frame.ok).toBe(true);
244
+ if (!frame.ok || frame.value === undefined) return;
245
+ const observed = await renderer.observe(frame.value, {
246
+ include: ['target-readbacks'],
247
+ targetReadbacks: [ticket.value],
248
+ });
249
+ expect(observed.ok).toBe(true);
250
+ if (!observed.ok) return;
251
+ expect(observed.value.targetReadbacks?.[0]).toMatchObject({
252
+ frameId: frame.value.frameId,
253
+ deviceGeneration: frame.value.deviceGeneration,
254
+ bytesPerRow: 256,
255
+ byteLength: 256 * 9,
256
+ });
257
+ const repeated = await renderer.observe(frame.value, {
258
+ include: ['target-readbacks'],
259
+ targetReadbacks: [ticket.value],
260
+ });
261
+ expect(repeated.ok).toBe(false);
262
+ if (!repeated.ok) expect(repeated.error.code).toBe('render-target-state-invalid');
263
+ renderer.dispose();
264
+ });
265
+
178
266
  it('does not promote a graph feature without a fullscreen effect into post-effects', async () => {
179
267
  let planCalls = 0;
180
268
  const feature = {
@@ -205,13 +293,16 @@ describe('factory contract', () => {
205
293
  renderer.dispose();
206
294
  });
207
295
 
208
- it('keeps the active graph as LKG when a feature candidate cannot be projected', async () => {
296
+ it('keeps the active graph as LKG while a feature candidate is pending', async () => {
209
297
  let invalid = false;
298
+ let featurePlanCalls = 0;
210
299
  const featureErrors: string[] = [];
211
300
  const feature: RenderFeature<undefined> = {
212
301
  identity: 'synthetic.candidate-lkg',
302
+ shaderModuleMode: 'immediate',
213
303
  extract: () => ok(undefined),
214
304
  plan: () => {
305
+ featurePlanCalls += 1;
215
306
  if (!invalid) return ok({ resources: [], passes: [] });
216
307
  return ok({
217
308
  resources: [
@@ -245,7 +336,7 @@ describe('factory contract', () => {
245
336
  passes: [
246
337
  {
247
338
  kind: 'compute',
248
- name: 'main',
339
+ name: 'forward',
249
340
  program: 'candidate.program',
250
341
  bindings: 'candidate.bindings',
251
342
  dispatches: [{ kind: 'direct', entryPoint: 'main', workgroups: [1] }],
@@ -257,7 +348,7 @@ describe('factory contract', () => {
257
348
  const renderer = await constructRenderer(
258
349
  { width: 64, height: 64, getContext: () => null },
259
350
  { rhi, features: [feature] },
260
- { shaderManifestUrl: manifestUrl() },
351
+ { shaderManifestUrl: renderLifecycleManifestUrl() },
261
352
  );
262
353
  expect((await renderer.initialization).ok).toBe(true);
263
354
  const unsubscribe = renderer.onError((error) => featureErrors.push(error.code));
@@ -269,29 +360,43 @@ describe('factory contract', () => {
269
360
  world
270
361
  .spawn(
271
362
  { component: Transform, data: { pos: [0, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] } },
272
- { component: LocalCamera, data: { fov: 1, aspect: 1, near: 0.1, far: 100 } },
363
+ {
364
+ component: LocalCamera,
365
+ data: {
366
+ fov: 1,
367
+ aspect: 1,
368
+ near: 0.1,
369
+ far: 100,
370
+ antialias: ANTIALIAS_NONE,
371
+ tonemap: TONEMAP_ACES_FILMIC,
372
+ bloom: BLOOM_DISABLED,
373
+ },
374
+ },
273
375
  )
274
376
  .unwrap();
275
377
  expect(world.update().ok).toBe(true);
276
- expect(
277
- renderer.draw({
278
- leases: [attached.value],
279
- camera: { lease: attached.value },
280
- environment: { lease: attached.value },
281
- }).ok,
282
- ).toBe(true);
283
- const lastKnownGoodPasses = [...renderer.perFramePassNames];
284
- expect(lastKnownGoodPasses.length).toBeGreaterThan(0);
285
-
378
+ const initialDraw = renderer.draw({
379
+ leases: [attached.value],
380
+ camera: { lease: attached.value },
381
+ environment: { lease: attached.value },
382
+ });
383
+ expect(initialDraw.ok).toBe(true);
286
384
  invalid = true;
287
- expect(
288
- renderer.draw({
289
- leases: [attached.value],
290
- camera: { lease: attached.value },
291
- environment: { lease: attached.value },
292
- }).ok,
293
- ).toBe(true);
385
+ const candidateDraw = renderer.draw({
386
+ leases: [attached.value],
387
+ camera: { lease: attached.value },
388
+ environment: { lease: attached.value },
389
+ });
390
+ expect(candidateDraw.ok).toBe(true);
294
391
  await Promise.resolve();
392
+ const pendingDraw = renderer.draw({
393
+ leases: [attached.value],
394
+ camera: { lease: attached.value },
395
+ environment: { lease: attached.value },
396
+ });
397
+ expect(pendingDraw.ok).toBe(true);
398
+ const lastKnownGoodPasses = [...renderer.perFramePassNames];
399
+ expect(lastKnownGoodPasses.length).toBeGreaterThan(0);
295
400
  expect(
296
401
  renderer.draw({
297
402
  leases: [attached.value],
@@ -299,12 +404,124 @@ describe('factory contract', () => {
299
404
  environment: { lease: attached.value },
300
405
  }).ok,
301
406
  ).toBe(true);
302
- expect(featureErrors).toContain('duplicate-pass-name');
407
+ expect(featurePlanCalls).toBeGreaterThan(1);
408
+ expect(featureErrors).toEqual([]);
409
+ expect(renderer.inspect().featureDiagnostics[0]).toMatchObject({ status: 'active' });
303
410
  expect(renderer.perFramePassNames).toEqual(lastKnownGoodPasses);
304
411
  unsubscribe();
305
412
  renderer.dispose();
306
413
  });
307
414
 
415
+ it('records a no-storage directional-shadow frame without Cluster resources', async () => {
416
+ const nullAdapter = new RhiNullAdapter();
417
+ const noStorageRhi = {
418
+ ...rhi,
419
+ requestAdapter: async () => {
420
+ const created = await nullAdapter.requestDevice();
421
+ if (!created.ok) return created;
422
+ const baseDevice = created.value;
423
+ const device = new Proxy(baseDevice, {
424
+ get(target, property, receiver) {
425
+ if (property === 'caps') return { ...target.caps, storageBuffer: false };
426
+ return Reflect.get(target, property, receiver);
427
+ },
428
+ });
429
+ return ok({
430
+ features: nullAdapter.features,
431
+ limits: nullAdapter.limits,
432
+ requestDevice: async () => ok(device),
433
+ });
434
+ },
435
+ };
436
+ const renderer = await constructRenderer(
437
+ { width: 64, height: 64, getContext: () => null },
438
+ { rhi: noStorageRhi as never },
439
+ { shaderManifestUrl: renderLifecycleManifestUrl() },
440
+ );
441
+ expect((await renderer.initialization).ok).toBe(true);
442
+ const errors: string[] = [];
443
+ const unsubscribe = renderer.onError((error) => errors.push(error.code));
444
+ const world = new World();
445
+ const attached = renderer.attach(world);
446
+ expect(attached.ok).toBe(true);
447
+ if (!attached.ok) return;
448
+ registerPropagateTransforms(world);
449
+ world.spawn(
450
+ { component: Transform, data: { pos: [0, 0, 4], quat: [0, 0, 0, 1], scale: [1, 1, 1] } },
451
+ {
452
+ component: LocalCamera,
453
+ data: {
454
+ fov: 1,
455
+ aspect: 1,
456
+ near: 0.1,
457
+ far: 100,
458
+ antialias: ANTIALIAS_NONE,
459
+ tonemap: TONEMAP_ACES_FILMIC,
460
+ bloom: BLOOM_DISABLED,
461
+ },
462
+ },
463
+ );
464
+ world.spawn({
465
+ component: DirectionalLight,
466
+ data: {
467
+ direction: [0, -1, 0],
468
+ color: [1, 1, 1],
469
+ intensity: 1,
470
+ cascadeCount: 1,
471
+ mapSize: 64,
472
+ },
473
+ });
474
+ world.spawn(
475
+ { component: Transform, data: { pos: [0, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] } },
476
+ { component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
477
+ { component: MeshRenderer, data: {} },
478
+ );
479
+ expect(world.update().ok).toBe(true);
480
+ const frame = renderer.draw({
481
+ leases: [attached.value],
482
+ camera: { lease: attached.value },
483
+ environment: { lease: attached.value },
484
+ });
485
+ expect(frame.ok).toBe(true);
486
+ expect(renderer.inspect().standardLighting).toMatchObject({
487
+ producer: 'none',
488
+ requested: 0,
489
+ admitted: 0,
490
+ });
491
+ expect(renderer.perFramePassNames).not.toContain('cluster-membership-producer');
492
+ expect(errors).toEqual([]);
493
+
494
+ // The no-storage topology is valid only while the scene has no local
495
+ // lights. A Point/Spot admission must fail before graph/encoder work and
496
+ // retain the last successful pass list rather than silently submitting a
497
+ // direct-light fallback with an incompatible ABI.
498
+ const lastSuccessfulPasses = [...renderer.perFramePassNames];
499
+ const localLight = world.spawn(
500
+ { component: Transform, data: { pos: [0, 0, 1] } },
501
+ { component: PointLight, data: { intensity: 8, range: 10 } },
502
+ );
503
+ expect(localLight.ok).toBe(true);
504
+ const spotLight = world.spawn(
505
+ { component: Transform, data: { pos: [1, 0, 1] } },
506
+ {
507
+ component: SpotLight,
508
+ data: { direction: [0, -1, 0], intensity: 8, range: 10 },
509
+ },
510
+ );
511
+ expect(spotLight.ok).toBe(true);
512
+ expect(world.update().ok).toBe(true);
513
+ const rejected = renderer.draw({
514
+ leases: [attached.value],
515
+ camera: { lease: attached.value },
516
+ environment: { lease: attached.value },
517
+ });
518
+ expect(rejected.ok).toBe(false);
519
+ expect(errors).toContain('standard-cluster-transport-unavailable');
520
+ expect(renderer.perFramePassNames).toEqual(lastSuccessfulPasses);
521
+ unsubscribe();
522
+ await renderer.dispose();
523
+ });
524
+
308
525
  it('attaches derived-state systems once without putting writes in draw', async () => {
309
526
  const renderer = await constructRenderer(
310
527
  { getContext: () => null },
@@ -412,7 +629,7 @@ describe('factory contract', () => {
412
629
  createShaderModule: (_device, desc) => {
413
630
  const label = desc.label ?? '<unlabeled>';
414
631
  moduleCalls.push(label);
415
- if (label !== 'post-process-forgeax.taa-resolve-module') {
632
+ if (!label.startsWith('post-process-forgeax.taa-resolve-pso-')) {
416
633
  return Promise.resolve(ok({} as ShaderModule));
417
634
  }
418
635
  let resolve!: (value: ReturnType<typeof ok<ShaderModule>>) => void;
@@ -473,7 +690,7 @@ describe('factory contract', () => {
473
690
  expect(off.ok).toBe(true);
474
691
 
475
692
  expect(
476
- moduleCalls.filter((label) => label === 'post-process-forgeax.taa-resolve-module'),
693
+ moduleCalls.filter((label) => label.startsWith('post-process-forgeax.taa-resolve-pso-')),
477
694
  ).toHaveLength(1);
478
695
 
479
696
  world.set(camera, LocalCamera, { antialias: ANTIALIAS_TAA }).unwrap();
@@ -501,7 +718,7 @@ describe('factory contract', () => {
501
718
  pending[2]?.resolve(ok({} as ShaderModule));
502
719
  await pending[2]?.promise;
503
720
  expect(
504
- moduleCalls.filter((label) => label === 'post-process-forgeax.taa-resolve-module'),
721
+ moduleCalls.filter((label) => label.startsWith('post-process-forgeax.taa-resolve-pso-')),
505
722
  ).toHaveLength(3);
506
723
  });
507
724
 
@@ -594,7 +811,7 @@ describe('factory contract', () => {
594
811
  .slice(-4);
595
812
  expect(firstGeneration).toHaveLength(4);
596
813
  expect(
597
- moduleCalls.filter((label) => label === 'post-process-forgeax.taa-resolve-module'),
814
+ moduleCalls.filter((label) => label.startsWith('post-process-forgeax.taa-resolve-pso-')),
598
815
  ).toHaveLength(1);
599
816
  const firstFrame = renderer.inspect().frame.frameId;
600
817
 
@@ -618,7 +835,7 @@ describe('factory contract', () => {
618
835
  expect(renderer.inspect().frame.frameId).toBe(firstFrame + 1);
619
836
  expect(renderer.inspect().state).toBe('alive');
620
837
  expect(
621
- moduleCalls.filter((label) => label === 'post-process-forgeax.taa-resolve-module'),
838
+ moduleCalls.filter((label) => label.startsWith('post-process-forgeax.taa-resolve-pso-')),
622
839
  ).toHaveLength(1);
623
840
  world.set(camera, LocalCamera, { antialias: ANTIALIAS_NONE }).unwrap();
624
841
  expect(world.update().ok).toBe(true);
@@ -1460,10 +1677,19 @@ describe('factory contract', () => {
1460
1677
  const failed = await renderer.recover();
1461
1678
  expect(failed.ok).toBe(false);
1462
1679
  expect(renderer.health().reason).toBe('device-lost');
1680
+ expect(renderer.inspect().perFramePassNames).toEqual([]);
1681
+ expect(renderer.inspect().standardLighting).toBeUndefined();
1463
1682
  const recovered = await renderer.recover();
1464
1683
  expect(recovered.ok).toBe(true);
1684
+ // Recovery publishes a device but not a graph. The first post-recovery
1685
+ // draw must rebuild both the graph accessors and Standard inspection
1686
+ // against the replacement device.
1687
+ expect(renderer.inspect().perFramePassNames).toEqual([]);
1688
+ expect(renderer.inspect().standardLighting).toBeUndefined();
1465
1689
  expect(world.update().ok).toBe(true);
1466
1690
  expect(renderer.draw(input()).ok).toBe(true);
1691
+ expect(renderer.inspect().perFramePassNames.length).toBeGreaterThan(0);
1692
+ expect(renderer.inspect().standardLighting).toBeDefined();
1467
1693
  expect(renderer.inspect().bloom.state).toBe('active');
1468
1694
  expect(renderer.inspect().bloom.generation).toBeGreaterThan(0);
1469
1695
  expect(renderer.inspect().temporal.deviceGeneration).toBeGreaterThan(beforeDeviceGeneration);
@@ -0,0 +1,67 @@
1
+ import { World } from '@forgeax/engine-ecs';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { Fog } from '../components/fog';
4
+ import { selectEnvironment } from '../environment/frame';
5
+
6
+ describe('Fog component contract', () => {
7
+ it('exposes one bounded authoring component with stable defaults', () => {
8
+ expect(Fog.name).toBe('Fog');
9
+ expect(Fog.fields.color.type).toBe('array<f32, 3>');
10
+ expect(Fog.fields.density.default).toBe(0.01);
11
+ expect(Fog.fields.heightFalloff.default).toBe(0);
12
+ expect(Fog.fields.maxOpacity.default).toBe(1);
13
+
14
+ const world = new World();
15
+ const entity = world.spawn({ component: Fog, data: {} }).unwrap();
16
+ const value = world.get(entity, Fog).unwrap();
17
+ expect(Array.from(value.color)).toEqual([0.5, 0.5, 0.5]);
18
+ expect(value.density).toBeCloseTo(0.01);
19
+ expect(value.heightFalloff).toBe(0);
20
+ expect(value.maxOpacity).toBe(1);
21
+ });
22
+
23
+ it.each([
24
+ ['color', { color: [1.1, 0.2, 0.3] }],
25
+ ['density', { density: -0.1 }],
26
+ ['heightFalloff', { heightFalloff: Number.NaN }],
27
+ ['maxOpacity', { maxOpacity: 1.1 }],
28
+ ] as const)('rejects invalid %s at the shared selection owner', (field, override) => {
29
+ const result = selectEnvironment({
30
+ environments: [],
31
+ fogs: [
32
+ {
33
+ entityKey: 1,
34
+ color: [0.2, 0.3, 0.4],
35
+ density: 0.01,
36
+ heightFalloff: 0,
37
+ maxOpacity: 1,
38
+ ...override,
39
+ },
40
+ ],
41
+ suns: [],
42
+ lane: 'direct',
43
+ });
44
+ expect(result.ok).toBe(false);
45
+ if (result.ok) return;
46
+ expect(result.error.code).toBe('environment-selection-invalid');
47
+ expect(result.error.detail.field).toContain(field);
48
+ expect(result.error.expected).toContain('finite');
49
+ expect(result.error.hint).toContain(field);
50
+ });
51
+
52
+ it('rejects a second Fog owner without choosing a first-hit winner', () => {
53
+ const result = selectEnvironment({
54
+ environments: [],
55
+ fogs: [
56
+ { entityKey: 2, color: [0.2, 0.3, 0.4], density: 0.01, heightFalloff: 0, maxOpacity: 1 },
57
+ { entityKey: 3, color: [0.4, 0.3, 0.2], density: 0.02, heightFalloff: 0, maxOpacity: 1 },
58
+ ],
59
+ suns: [],
60
+ lane: 'direct',
61
+ });
62
+ expect(result).toMatchObject({
63
+ ok: false,
64
+ error: { code: 'fog-cardinality', detail: { count: 2 } },
65
+ });
66
+ });
67
+ });
@@ -0,0 +1,86 @@
1
+ import { World } from '@forgeax/engine-ecs';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { Atmosphere } from '../components/atmosphere';
4
+ import { DirectionalLight } from '../components/directional-light';
5
+ import { Fog } from '../components/fog';
6
+ import { extractFrames } from '../render-system-extract';
7
+
8
+ function worldWithFog() {
9
+ const world = new World();
10
+ world
11
+ .spawn({
12
+ component: Atmosphere,
13
+ data: {
14
+ turbidity: 2,
15
+ rayleigh: 1,
16
+ mieCoefficient: 0.005,
17
+ mieDirectionalG: 0.8,
18
+ sunAngularRadius: 0.004675,
19
+ },
20
+ })
21
+ .unwrap();
22
+ world
23
+ .spawn({ component: DirectionalLight, data: { direction: [0, -1, 0], intensity: 1 } })
24
+ .unwrap();
25
+ const entity = world
26
+ .spawn({
27
+ component: Fog,
28
+ data: { color: [0.2, 0.3, 0.4], density: 0.03, heightFalloff: 0, maxOpacity: 0.8 },
29
+ })
30
+ .unwrap();
31
+ return { world, entity };
32
+ }
33
+
34
+ describe('Fog frame owner integration', () => {
35
+ it('retains the last valid Fog frame after an invalid update and exposes failure', () => {
36
+ const { world, entity } = worldWithFog();
37
+ const initial = extractFrames([world], 0);
38
+ expect(initial.fog?.density).toBeCloseTo(0.03);
39
+ expect(initial.fog?.heightFalloff).toBe(0);
40
+
41
+ world
42
+ .set(entity, Fog, {
43
+ color: [0.2, 0.3, 0.4],
44
+ density: -1,
45
+ heightFalloff: 0,
46
+ maxOpacity: 0.8,
47
+ })
48
+ .unwrap();
49
+ const invalid = extractFrames([world], 0);
50
+ expect(invalid.fog).toEqual(initial.fog);
51
+ expect(invalid.fogFailure).toMatchObject({
52
+ code: 'environment-selection-invalid',
53
+ detail: { field: 'density', value: -1 },
54
+ });
55
+
56
+ world
57
+ .set(entity, Fog, {
58
+ color: [1.2, 0.3, 0.4],
59
+ density: 0.03,
60
+ heightFalloff: 0,
61
+ maxOpacity: 0.8,
62
+ })
63
+ .unwrap();
64
+ const invalidColor = extractFrames([world], 0);
65
+ expect(invalidColor.fog).toEqual(initial.fog);
66
+ expect(invalidColor.fogFailure).toMatchObject({ detail: { field: 'color[0]' } });
67
+ });
68
+
69
+ it('clears the retained Fog only after a valid removal, never after an invalid frame', () => {
70
+ const { world, entity } = worldWithFog();
71
+ expect(extractFrames([world], 0).fog).toBeDefined();
72
+ world
73
+ .set(entity, Fog, {
74
+ color: [0.2, 0.3, 0.4],
75
+ density: -1,
76
+ heightFalloff: 0,
77
+ maxOpacity: 0.8,
78
+ })
79
+ .unwrap();
80
+ expect(extractFrames([world], 0).fog).toBeDefined();
81
+ world.removeComponent(entity, Fog).unwrap();
82
+ const removed = extractFrames([world], 0);
83
+ expect(removed.fog).toBeUndefined();
84
+ expect(removed.fogFailure).toBeUndefined();
85
+ });
86
+ });
@@ -1,6 +1,11 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { createFullscreenRenderFeature } from '../features/fullscreen';
3
3
  import { freezeRenderFeaturePlan } from '../features/plan';
4
+ import {
5
+ postProcessShaderEntrySignature,
6
+ postProcessShaderModuleLabel,
7
+ postProcessShaderPipelineLabel,
8
+ } from '../fullscreen-post-process-pass';
4
9
  import { createSceneDataCatalog } from '../temporal/scene-data-catalog';
5
10
 
6
11
  describe('fullscreen RenderFeature plan', () => {
@@ -23,6 +28,12 @@ describe('fullscreen RenderFeature plan', () => {
23
28
  }),
24
29
  });
25
30
 
31
+ expect(feature.requiredFullscreenPostProcesses).toEqual([
32
+ {
33
+ identity: 'test::fullscreen',
34
+ source: '@fragment fn fs_main() -> @location(0) vec4f { return vec4f(1); }',
35
+ },
36
+ ]);
26
37
  expect(planned.ok).toBe(true);
27
38
  if (!planned.ok) return;
28
39
  expect(
@@ -36,4 +47,30 @@ describe('fullscreen RenderFeature plan', () => {
36
47
  });
37
48
  expect(planned.value.passes).toEqual([]);
38
49
  });
50
+
51
+ it('treats source, params, and reads as one cached declaration identity', () => {
52
+ const base = {
53
+ source: 'fn fs_main() -> vec4f { return vec4f(1); }',
54
+ params: { byteSize: 16, defaultValue: new Uint8Array(16) },
55
+ reads: ['scene-color'],
56
+ } as const;
57
+ expect(postProcessShaderEntrySignature(base)).not.toBe(
58
+ postProcessShaderEntrySignature({ ...base, source: `${base.source}\n// next` }),
59
+ );
60
+ expect(postProcessShaderEntrySignature(base)).not.toBe(
61
+ postProcessShaderEntrySignature({
62
+ ...base,
63
+ params: { byteSize: 16, defaultValue: new Uint8Array([1, ...new Uint8Array(15)]) },
64
+ }),
65
+ );
66
+ expect(postProcessShaderEntrySignature(base)).not.toBe(
67
+ postProcessShaderEntrySignature({ ...base, reads: ['other-color'] }),
68
+ );
69
+ expect(postProcessShaderModuleLabel('test::fullscreen', base.source)).not.toBe(
70
+ postProcessShaderModuleLabel('test::fullscreen', `${base.source}\n// next`),
71
+ );
72
+ expect(postProcessShaderPipelineLabel('test::fullscreen', base.source)).not.toBe(
73
+ postProcessShaderPipelineLabel('test::fullscreen', `${base.source}\n// next`),
74
+ );
75
+ });
39
76
  });
@@ -87,6 +87,15 @@ export interface GpuDrivenIndirectRasterEvidence {
87
87
  readonly passNames: readonly string[];
88
88
  }
89
89
 
90
+ export const GPU_DRIVEN_PRODUCTION_VIEW_BGL_ENTRIES = [
91
+ { binding: 0, visibility: 0x1, buffer: { type: 'uniform', hasDynamicOffset: true } },
92
+ {
93
+ binding: 10,
94
+ visibility: 0x1,
95
+ buffer: { type: 'uniform', hasDynamicOffset: true },
96
+ },
97
+ ] as const satisfies readonly GPUBindGroupLayoutEntry[];
98
+
90
99
  export async function runGpuDrivenIndirectRasterEvidence(): Promise<GpuDrivenIndirectRasterEvidence> {
91
100
  const adapter = (await rhi.requestAdapter()).unwrap();
92
101
  const device = (await adapter.requestDevice()).unwrap();
@@ -102,14 +111,7 @@ export async function runGpuDrivenIndirectRasterEvidence(): Promise<GpuDrivenInd
102
111
  const viewLayout = device
103
112
  .createBindGroupLayout({
104
113
  label: 'gpu-driven-production-view-bgl',
105
- entries: [
106
- { binding: 0, visibility: 0x1, buffer: { type: 'uniform' } },
107
- {
108
- binding: 10,
109
- visibility: 0x1,
110
- buffer: { type: 'uniform', hasDynamicOffset: true },
111
- },
112
- ],
114
+ entries: [...GPU_DRIVEN_PRODUCTION_VIEW_BGL_ENTRIES],
113
115
  })
114
116
  .unwrap();
115
117
  const viewUniform = device
@@ -249,7 +251,6 @@ export async function runGpuDrivenIndirectRasterEvidence(): Promise<GpuDrivenInd
249
251
  ]),
250
252
  viewBindGroupLayout: viewLayout,
251
253
  meshResidencyEpoch: 1,
252
- hdrp: false,
253
254
  })
254
255
  .unwrap();
255
256
  if (prepared === undefined) throw new Error('GPU-driven production lane inactive');
@@ -1,7 +1,23 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { runGpuDrivenIndirectRasterEvidence } from './gpu-driven-indirect-raster-evidence';
2
+ import { buildPbrViewBglEntries } from '../pbr-pipeline';
3
+ import {
4
+ GPU_DRIVEN_PRODUCTION_VIEW_BGL_ENTRIES,
5
+ runGpuDrivenIndirectRasterEvidence,
6
+ } from './gpu-driven-indirect-raster-evidence';
3
7
 
4
8
  describe('GPU-driven indirect raster Dawn', () => {
9
+ it('matches the shared PBR View BGL dynamic-offset slots', () => {
10
+ const fixtureDynamicBindings = GPU_DRIVEN_PRODUCTION_VIEW_BGL_ENTRIES.filter(
11
+ (entry) => entry.buffer?.hasDynamicOffset === true,
12
+ ).map((entry) => entry.binding);
13
+ const sharedDynamicBindings = buildPbrViewBglEntries({ storageBuffer: true })
14
+ .filter((entry) => entry.buffer?.hasDynamicOffset === true)
15
+ .map((entry) => entry.binding);
16
+
17
+ expect(fixtureDynamicBindings).toEqual([0, 10]);
18
+ expect(fixtureDynamicBindings).toEqual(sharedDynamicBindings);
19
+ });
20
+
5
21
  it('consumes indexed, non-indexed, and multi-submesh indirect args without CPU readback', async () => {
6
22
  const evidence = await runGpuDrivenIndirectRasterEvidence();
7
23
  expect(evidence.pixel).toEqual([64, 128, 191, 255]);