@forgeax/engine-render 0.1.20 → 0.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (372) hide show
  1. package/README.md +215 -0
  2. package/dist/.tsbuildinfo +1 -1
  3. package/dist/assembly/device-feature-admission.d.ts +18 -0
  4. package/dist/assembly/device-feature-admission.d.ts.map +1 -0
  5. package/dist/assembly/directional-shadow-inspection.d.ts +38 -0
  6. package/dist/assembly/directional-shadow-inspection.d.ts.map +1 -0
  7. package/dist/assembly/factory.d.ts +21 -18
  8. package/dist/assembly/factory.d.ts.map +1 -1
  9. package/dist/assembly/host-contract.d.ts +9 -0
  10. package/dist/assembly/host-contract.d.ts.map +1 -1
  11. package/dist/assembly/render-target-host.d.ts +46 -0
  12. package/dist/assembly/render-target-host.d.ts.map +1 -0
  13. package/dist/assets/render-asset-port.d.ts.map +1 -1
  14. package/dist/authoring.mjs +3 -0
  15. package/dist/authoring.mjs.map +1 -1
  16. package/dist/capture/cube-views.d.ts +21 -0
  17. package/dist/capture/cube-views.d.ts.map +1 -0
  18. package/dist/capture/scheduler.d.ts +54 -0
  19. package/dist/capture/scheduler.d.ts.map +1 -0
  20. package/dist/{chunk-4XINDLAY.mjs → chunk-5YADUCA7.mjs} +1552 -1361
  21. package/dist/chunk-5YADUCA7.mjs.map +1 -0
  22. package/dist/{chunk-TBQJOK2L.mjs → chunk-6OPXDQ2E.mjs} +364 -51
  23. package/dist/chunk-6OPXDQ2E.mjs.map +1 -0
  24. package/dist/{chunk-M2MP4ZOU.mjs → chunk-BXBQQKFQ.mjs} +3 -9
  25. package/dist/chunk-BXBQQKFQ.mjs.map +1 -0
  26. package/dist/{chunk-CDTWTHZR.mjs → chunk-FS2KNUQH.mjs} +548 -6
  27. package/dist/chunk-FS2KNUQH.mjs.map +1 -0
  28. package/dist/components/camera.d.ts +1 -0
  29. package/dist/components/camera.d.ts.map +1 -1
  30. package/dist/components/cube-camera.d.ts +20 -0
  31. package/dist/components/cube-camera.d.ts.map +1 -0
  32. package/dist/components/directional-light.d.ts +11 -2
  33. package/dist/components/directional-light.d.ts.map +1 -1
  34. package/dist/components/directional-shadow-filter.d.ts +25 -0
  35. package/dist/components/directional-shadow-filter.d.ts.map +1 -0
  36. package/dist/components/index.d.ts +2 -0
  37. package/dist/components/index.d.ts.map +1 -1
  38. package/dist/components/light-helpers.d.ts.map +1 -1
  39. package/dist/components/reflection-probe.d.ts +17 -0
  40. package/dist/components/reflection-probe.d.ts.map +1 -0
  41. package/dist/construct-renderer.mjs +5826 -1003
  42. package/dist/construct-renderer.mjs.map +1 -1
  43. package/dist/environment/frame.d.ts.map +1 -1
  44. package/dist/environment/lifecycle.d.ts +5 -0
  45. package/dist/environment/lifecycle.d.ts.map +1 -1
  46. package/dist/errors/render.d.ts +141 -93
  47. package/dist/errors/render.d.ts.map +1 -1
  48. package/dist/extract/directional-shadow-projection.d.ts +23 -0
  49. package/dist/extract/directional-shadow-projection.d.ts.map +1 -0
  50. package/dist/extract/environment.d.ts +14 -0
  51. package/dist/extract/environment.d.ts.map +1 -1
  52. package/dist/extract/visibility.d.ts.map +1 -1
  53. package/dist/features/fullscreen.d.ts.map +1 -1
  54. package/dist/features/targets.d.ts.map +1 -1
  55. package/dist/features/types.d.ts +10 -0
  56. package/dist/features/types.d.ts.map +1 -1
  57. package/dist/fullscreen-post-process-pass.d.ts +13 -2
  58. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  59. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  60. package/dist/gpu-driven/production-raster.d.ts +0 -1
  61. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  62. package/dist/gpu-scene.d.ts.map +1 -1
  63. package/dist/hdrp-buffers.d.ts +19 -17
  64. package/dist/hdrp-buffers.d.ts.map +1 -1
  65. package/dist/ibl/IblPipelineCache.d.ts +4 -0
  66. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  67. package/dist/ibl/kernel-cache.d.ts +36 -0
  68. package/dist/ibl/kernel-cache.d.ts.map +1 -0
  69. package/dist/index.d.ts +28 -3
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.mjs +4 -4
  72. package/dist/index.mjs.map +1 -1
  73. package/dist/inspection-types.d.ts +51 -0
  74. package/dist/inspection-types.d.ts.map +1 -1
  75. package/dist/instances-derived-bounds.d.ts +50 -0
  76. package/dist/instances-derived-bounds.d.ts.map +1 -0
  77. package/dist/internal.mjs +1 -1
  78. package/dist/light-buffer-layout.d.ts +10 -1
  79. package/dist/light-buffer-layout.d.ts.map +1 -1
  80. package/dist/mesh-material-bindings.d.ts +54 -0
  81. package/dist/mesh-material-bindings.d.ts.map +1 -1
  82. package/dist/pbr-pipeline.d.ts +14 -4
  83. package/dist/pbr-pipeline.d.ts.map +1 -1
  84. package/dist/pipeline/standard-forward-lane.d.ts +2 -1
  85. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  86. package/dist/pipeline/standard-lighting/errors.d.ts +4 -0
  87. package/dist/pipeline/standard-lighting/errors.d.ts.map +1 -0
  88. package/dist/pipeline/standard-lighting/graph.d.ts +24 -0
  89. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -0
  90. package/dist/pipeline/standard-lighting/inspection.d.ts +38 -0
  91. package/dist/pipeline/standard-lighting/inspection.d.ts.map +1 -0
  92. package/dist/pipeline/standard-lighting/layout.d.ts +28 -0
  93. package/dist/pipeline/standard-lighting/layout.d.ts.map +1 -0
  94. package/dist/pipeline/standard-lighting/light-frame.d.ts +33 -0
  95. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -0
  96. package/dist/pipeline/standard-lighting/membership.d.ts +14 -0
  97. package/dist/pipeline/standard-lighting/membership.d.ts.map +1 -0
  98. package/dist/pipeline/standard-lighting/prepare.d.ts +16 -0
  99. package/dist/pipeline/standard-lighting/prepare.d.ts.map +1 -0
  100. package/dist/pipeline/standard-lighting/topology.d.ts +45 -0
  101. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -0
  102. package/dist/pipeline/standard-lighting/transport.d.ts +36 -0
  103. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -0
  104. package/dist/pipeline/standard-pipeline.d.ts +4 -13
  105. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  106. package/dist/pipeline/standard-post.d.ts.map +1 -1
  107. package/dist/pipeline/standard-profile.d.ts +5 -26
  108. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  109. package/dist/pipeline-spec.d.ts +40 -7
  110. package/dist/pipeline-spec.d.ts.map +1 -1
  111. package/dist/point-shadow-inspection.d.ts +21 -0
  112. package/dist/point-shadow-inspection.d.ts.map +1 -0
  113. package/dist/record/frame-lighting.d.ts +27 -32
  114. package/dist/record/frame-lighting.d.ts.map +1 -1
  115. package/dist/record/frame-snapshot.d.ts +28 -14
  116. package/dist/record/frame-snapshot.d.ts.map +1 -1
  117. package/dist/record/frame-targets.d.ts +5 -0
  118. package/dist/record/frame-targets.d.ts.map +1 -1
  119. package/dist/record/frame.d.ts +10 -4
  120. package/dist/record/frame.d.ts.map +1 -1
  121. package/dist/record/gpu-pass-timing/contract.d.ts +96 -0
  122. package/dist/record/gpu-pass-timing/contract.d.ts.map +1 -0
  123. package/dist/record/gpu-pass-timing/errors.d.ts +18 -0
  124. package/dist/record/gpu-pass-timing/errors.d.ts.map +1 -0
  125. package/dist/record/gpu-pass-timing/index.d.ts +8 -0
  126. package/dist/record/gpu-pass-timing/index.d.ts.map +1 -0
  127. package/dist/record/gpu-pass-timing/parser.d.ts +11 -0
  128. package/dist/record/gpu-pass-timing/parser.d.ts.map +1 -0
  129. package/dist/record/gpu-pass-timing/session.d.ts +43 -0
  130. package/dist/record/gpu-pass-timing/session.d.ts.map +1 -0
  131. package/dist/record/gpu-timing.d.ts +8 -3
  132. package/dist/record/gpu-timing.d.ts.map +1 -1
  133. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  134. package/dist/record/main-pass-material.d.ts +22 -1
  135. package/dist/record/main-pass-material.d.ts.map +1 -1
  136. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  137. package/dist/record/main-pass.d.ts +2 -0
  138. package/dist/record/main-pass.d.ts.map +1 -1
  139. package/dist/record/render-context.d.ts +43 -2
  140. package/dist/record/render-context.d.ts.map +1 -1
  141. package/dist/record/shadow-pass.d.ts.map +1 -1
  142. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  143. package/dist/record/typed-frame-graph.d.ts +72 -4
  144. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  145. package/dist/record/view-ubo.d.ts +7 -4
  146. package/dist/record/view-ubo.d.ts.map +1 -1
  147. package/dist/reflection/filter.d.ts +22 -0
  148. package/dist/reflection/filter.d.ts.map +1 -0
  149. package/dist/reflection/gpu-table.d.ts +25 -0
  150. package/dist/reflection/gpu-table.d.ts.map +1 -0
  151. package/dist/reflection/projection.d.ts +91 -0
  152. package/dist/reflection/projection.d.ts.map +1 -0
  153. package/dist/render-contract.d.ts +49 -7
  154. package/dist/render-contract.d.ts.map +1 -1
  155. package/dist/render-graph-primitives.d.ts +1 -0
  156. package/dist/render-graph-primitives.d.ts.map +1 -1
  157. package/dist/render-pipeline.d.ts +42 -2
  158. package/dist/render-pipeline.d.ts.map +1 -1
  159. package/dist/render-system-extract.d.ts +58 -17
  160. package/dist/render-system-extract.d.ts.map +1 -1
  161. package/dist/render-system.d.ts +17 -2
  162. package/dist/render-system.d.ts.map +1 -1
  163. package/dist/renderer.d.ts +43 -0
  164. package/dist/renderer.d.ts.map +1 -0
  165. package/dist/scene/render-scene.d.ts +14 -0
  166. package/dist/scene/render-scene.d.ts.map +1 -1
  167. package/dist/shadow-atlas.d.ts +3 -3
  168. package/dist/shadow-atlas.d.ts.map +1 -1
  169. package/dist/ssao-buffers.d.ts +3 -0
  170. package/dist/ssao-buffers.d.ts.map +1 -1
  171. package/dist/targets/contracts.d.ts +66 -0
  172. package/dist/targets/contracts.d.ts.map +1 -0
  173. package/dist/targets/graph-projection.d.ts +29 -0
  174. package/dist/targets/graph-projection.d.ts.map +1 -0
  175. package/dist/targets/material-source.d.ts +34 -0
  176. package/dist/targets/material-source.d.ts.map +1 -0
  177. package/dist/targets/owner.d.ts +32 -0
  178. package/dist/targets/owner.d.ts.map +1 -0
  179. package/dist/targets/physical.d.ts +18 -0
  180. package/dist/targets/physical.d.ts.map +1 -0
  181. package/dist/targets/readback.d.ts +39 -0
  182. package/dist/targets/readback.d.ts.map +1 -0
  183. package/dist/temporal/index.mjs +3 -3
  184. package/dist/typed-render-graph-primitives.d.ts +1 -1
  185. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  186. package/dist/typed-shadow-passes.d.ts +1 -1
  187. package/dist/typed-shadow-passes.d.ts.map +1 -1
  188. package/dist/volume/passes.d.ts +2 -2
  189. package/dist/volume/passes.d.ts.map +1 -1
  190. package/package.json +20 -20
  191. package/src/__tests__/bloom-exact-zero.unit.test.ts +48 -19
  192. package/src/__tests__/bloom-lifecycle.integration.test.ts +41 -13
  193. package/src/__tests__/camera-target-consumption.unit.test.ts +53 -0
  194. package/src/__tests__/cluster-binner.unit.test.ts +35 -0
  195. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +7 -2
  196. package/src/__tests__/cube-camera-scheduler.unit.test.ts +70 -0
  197. package/src/__tests__/cube-camera-single-submit.perf.test.ts +75 -0
  198. package/src/__tests__/cube-camera-view-matrices.unit.test.ts +41 -0
  199. package/src/__tests__/cube-camera-vocabulary.unit.test.ts +47 -0
  200. package/src/__tests__/cube-camera.integration.test.ts +42 -0
  201. package/src/__tests__/device-feature-admission.unit.test.ts +73 -0
  202. package/src/__tests__/direct-light-snapshot.unit.test.ts +3 -3
  203. package/src/__tests__/docs-gate-green.unit.test.ts +38 -0
  204. package/src/__tests__/factory-contract.integration.test.ts +255 -29
  205. package/src/__tests__/fog-component.unit.test.ts +67 -0
  206. package/src/__tests__/fog.integration.test.ts +86 -0
  207. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +37 -0
  208. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +10 -9
  209. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +17 -1
  210. package/src/__tests__/gpu-driven-production.integration.test.ts +16 -17
  211. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +12 -4
  212. package/src/__tests__/gpu-pass-timing-browser-runner.ts +281 -0
  213. package/src/__tests__/gpu-pass-timing-coexistence.integration.test.ts +80 -0
  214. package/src/__tests__/gpu-pass-timing-contract.unit.test.ts +243 -0
  215. package/src/__tests__/gpu-pass-timing-exact-zero.unit.test.ts +131 -0
  216. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +230 -0
  217. package/src/__tests__/gpu-pass-timing-observation.unit.test.ts +233 -0
  218. package/src/__tests__/gpu-pass-timing-session.unit.test.ts +438 -0
  219. package/src/__tests__/gpu-pass-timing.browser.test.ts +26 -0
  220. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +125 -0
  221. package/src/__tests__/hdrp-group2-visibility.unit.test.ts +24 -0
  222. package/src/__tests__/instances-culling.integration.test.ts +94 -0
  223. package/src/__tests__/instances-derived-bounds.unit.test.ts +118 -0
  224. package/src/__tests__/material-observation.integration.test.ts +101 -0
  225. package/src/__tests__/pbr-view-layout.unit.test.ts +16 -0
  226. package/src/__tests__/point-shadow-inspection.unit.test.ts +60 -0
  227. package/src/__tests__/public-surface.test-d.ts +10 -0
  228. package/src/__tests__/reflection-probe-authoring.unit.test.ts +71 -0
  229. package/src/__tests__/reflection-probe-filter.unit.test.ts +54 -0
  230. package/src/__tests__/reflection-probe-ibl-topology.unit.test.ts +45 -0
  231. package/src/__tests__/reflection-probe-pass-encode.integration.test.ts +101 -0
  232. package/src/__tests__/reflection-probe-performance.perf.test.ts +60 -0
  233. package/src/__tests__/reflection-probe-recovery.integration.test.ts +55 -0
  234. package/src/__tests__/reflection-probe-selection.unit.test.ts +46 -0
  235. package/src/__tests__/render-error-code-owner.test-d.ts +18 -3
  236. package/src/__tests__/render-feature-docs.unit.test.ts +1 -1
  237. package/src/__tests__/render-target-admission.unit.test.ts +80 -0
  238. package/src/__tests__/render-target-graph-projection.unit.test.ts +102 -0
  239. package/src/__tests__/render-target-inspection.unit.test.ts +52 -0
  240. package/src/__tests__/render-target-material-source.test-d.ts +30 -0
  241. package/src/__tests__/render-target-material-source.unit.test.ts +97 -0
  242. package/src/__tests__/render-target-owner-placement.unit.test.ts +203 -0
  243. package/src/__tests__/render-target-owner.unit.test.ts +122 -0
  244. package/src/__tests__/render-target-probe-soak.perf.test.ts +94 -0
  245. package/src/__tests__/render-target-public-schema.json +107 -0
  246. package/src/__tests__/render-target-public-schema.unit.test.ts +91 -0
  247. package/src/__tests__/render-target-public-surface.test-d.ts +83 -0
  248. package/src/__tests__/render-target-readback.integration.test.ts +90 -0
  249. package/src/__tests__/render-target-recovery.integration.test.ts +85 -0
  250. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +48 -0
  251. package/src/__tests__/renderer-lifecycle.integration.test.ts +14 -0
  252. package/src/__tests__/shader-stage-surface.unit.test.ts +19 -4
  253. package/src/__tests__/skin-motion-regression.unit.test.ts +132 -0
  254. package/src/__tests__/skinned-shadow-caster.test.ts +11 -2
  255. package/src/__tests__/standard-cluster-capability.integration.test.ts +63 -0
  256. package/src/__tests__/standard-cluster-transport.unit.test.ts +121 -0
  257. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +232 -0
  258. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +8 -5
  259. package/src/__tests__/standard-lighting-inspection.integration.test.ts +138 -0
  260. package/src/__tests__/standard-lighting-ssot.unit.test.ts +18 -0
  261. package/src/__tests__/standard-lighting-write.unit.test.ts +377 -0
  262. package/src/__tests__/standard-lighting.integration.test.ts +67 -0
  263. package/src/__tests__/standard-pipeline.dawn.test.ts +206 -5
  264. package/src/__tests__/standard-pipeline.integration.test.ts +181 -150
  265. package/src/__tests__/standard-probe-binding.unit.test.ts +25 -0
  266. package/src/__tests__/standard-profile-oracle.unit.test.ts +23 -38
  267. package/src/__tests__/typed-pipeline-topology.unit.test.ts +395 -14
  268. package/src/__tests__/urp-pipeline-targets.unit.test.ts +2 -2
  269. package/src/__tests__/visibility-resolver.unit.test.ts +15 -0
  270. package/src/assembly/device-feature-admission.ts +73 -0
  271. package/src/assembly/directional-shadow-inspection.ts +122 -0
  272. package/src/assembly/factory.ts +877 -286
  273. package/src/assembly/host-contract.ts +25 -0
  274. package/src/assembly/render-target-host.ts +847 -0
  275. package/src/assets/asset-decoders.ts +1 -1
  276. package/src/assets/render-asset-port.ts +7 -1
  277. package/src/capture/cube-views.ts +69 -0
  278. package/src/capture/scheduler.ts +265 -0
  279. package/src/components/__tests__/schema-contract.unit.test.ts +12 -0
  280. package/src/components/__tests__/schema-parity.unit.test.ts +9 -0
  281. package/src/components/camera.ts +4 -0
  282. package/src/components/cube-camera.ts +52 -0
  283. package/src/components/directional-light.ts +22 -2
  284. package/src/components/directional-shadow-filter.ts +57 -0
  285. package/src/components/index.ts +2 -0
  286. package/src/components/light-helpers.ts +81 -7
  287. package/src/components/reflection-probe.ts +49 -0
  288. package/src/environment/frame.ts +6 -24
  289. package/src/environment/lifecycle.ts +12 -0
  290. package/src/errors/render.ts +286 -142
  291. package/src/extract/directional-shadow-projection.ts +61 -0
  292. package/src/extract/environment.ts +57 -0
  293. package/src/extract/visibility.ts +25 -1
  294. package/src/features/fullscreen.ts +3 -0
  295. package/src/features/targets.ts +4 -1
  296. package/src/features/types.ts +10 -0
  297. package/src/fullscreen-post-process-pass.ts +43 -2
  298. package/src/gpu-driven/batch-topology.ts +1 -0
  299. package/src/gpu-driven/production-raster.ts +1 -3
  300. package/src/gpu-scene.ts +8 -1
  301. package/src/hdrp-buffers.ts +165 -59
  302. package/src/ibl/IblPipelineCache.ts +11 -0
  303. package/src/ibl/kernel-cache.ts +88 -0
  304. package/src/index.ts +148 -0
  305. package/src/inspection-types.ts +56 -0
  306. package/src/instances-derived-bounds.ts +171 -0
  307. package/src/light-buffer-layout.ts +20 -5
  308. package/src/mesh-material-bindings.ts +132 -0
  309. package/src/pbr-pipeline.ts +106 -17
  310. package/src/pipeline/standard-forward-lane.ts +46 -16
  311. package/src/pipeline/standard-lighting/errors.ts +6 -0
  312. package/src/pipeline/standard-lighting/graph.ts +157 -0
  313. package/src/pipeline/standard-lighting/inspection.ts +88 -0
  314. package/src/pipeline/standard-lighting/layout.ts +31 -0
  315. package/src/pipeline/standard-lighting/light-frame.ts +34 -0
  316. package/src/pipeline/standard-lighting/membership.ts +43 -0
  317. package/src/pipeline/standard-lighting/prepare.ts +37 -0
  318. package/src/pipeline/standard-lighting/topology.ts +120 -0
  319. package/src/pipeline/standard-lighting/transport.ts +77 -0
  320. package/src/pipeline/standard-pipeline.ts +122 -193
  321. package/src/pipeline/standard-post.ts +26 -7
  322. package/src/pipeline/standard-profile.ts +17 -47
  323. package/src/pipeline-spec.ts +84 -21
  324. package/src/point-shadow-inspection.ts +55 -0
  325. package/src/record/__tests__/temporal-payload.unit.test.ts +42 -1
  326. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +73 -0
  327. package/src/record/__tests__/view-ubo-layout.unit.test.ts +40 -0
  328. package/src/record/frame-lighting.ts +367 -427
  329. package/src/record/frame-snapshot.ts +35 -27
  330. package/src/record/frame-targets.ts +17 -0
  331. package/src/record/frame.ts +261 -74
  332. package/src/record/gpu-pass-timing/contract.ts +180 -0
  333. package/src/record/gpu-pass-timing/errors.ts +34 -0
  334. package/src/record/gpu-pass-timing/index.ts +37 -0
  335. package/src/record/gpu-pass-timing/parser.ts +97 -0
  336. package/src/record/gpu-pass-timing/session.ts +938 -0
  337. package/src/record/gpu-timing.ts +21 -12
  338. package/src/record/main-pass-geometry.ts +120 -80
  339. package/src/record/main-pass-material.ts +95 -9
  340. package/src/record/main-pass-sprite-draws.ts +54 -25
  341. package/src/record/main-pass.ts +71 -15
  342. package/src/record/render-context.ts +72 -0
  343. package/src/record/shadow-pass.ts +17 -28
  344. package/src/record/skybox-post-pass.ts +20 -4
  345. package/src/record/typed-frame-graph.ts +893 -44
  346. package/src/record/view-ubo.ts +63 -28
  347. package/src/reflection/filter.ts +81 -0
  348. package/src/reflection/gpu-table.ts +44 -0
  349. package/src/reflection/projection.ts +281 -0
  350. package/src/render-contract.ts +76 -5
  351. package/src/render-graph-primitives.ts +9 -4
  352. package/src/render-pipeline.ts +117 -2
  353. package/src/render-system-extract.ts +557 -100
  354. package/src/render-system.ts +1095 -66
  355. package/src/renderer.ts +222 -0
  356. package/src/scene/render-scene.ts +115 -9
  357. package/src/shadow-atlas.ts +5 -5
  358. package/src/ssao-buffers.ts +9 -2
  359. package/src/targets/contracts.ts +182 -0
  360. package/src/targets/graph-projection.ts +169 -0
  361. package/src/targets/material-source.ts +143 -0
  362. package/src/targets/owner.ts +214 -0
  363. package/src/targets/physical.ts +18 -0
  364. package/src/targets/readback.ts +153 -0
  365. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +59 -20
  366. package/src/typed-render-graph-primitives.ts +5 -3
  367. package/src/typed-shadow-passes.ts +46 -40
  368. package/src/volume/passes.ts +15 -9
  369. package/dist/chunk-4XINDLAY.mjs.map +0 -1
  370. package/dist/chunk-CDTWTHZR.mjs.map +0 -1
  371. package/dist/chunk-M2MP4ZOU.mjs.map +0 -1
  372. package/dist/chunk-TBQJOK2L.mjs.map +0 -1
@@ -1,9 +1,13 @@
1
+ import { mat4, vec3 } from '@forgeax/engine-math';
1
2
  import { RenderGraphBuilder } from '@forgeax/engine-render-graph';
2
3
  import type { RhiDevice } from '@forgeax/engine-rhi';
3
4
  import { rhi } from '@forgeax/engine-rhi-null';
4
5
  import { ok } from '@forgeax/engine-types';
5
6
  import { beforeAll, describe, expect, it } from 'vitest';
6
7
  import { DeviceScope, LifecycleTransaction } from '../device/device-scope';
8
+ import { prepareStandardLighting } from '../pipeline/standard-lighting/prepare';
9
+ import { deriveStandardTopologyInput } from '../pipeline/standard-lighting/topology';
10
+ import { selectStandardClusterTransport } from '../pipeline/standard-lighting/transport';
7
11
  import { standardPipeline } from '../pipeline/standard-pipeline';
8
12
  import { DEFAULT_STANDARD_PROFILE, STANDARD_PIPELINE_ID } from '../pipeline/standard-profile';
9
13
  import type { RenderPipelineFrame, RenderPipelineTopology } from '../render-pipeline';
@@ -18,11 +22,11 @@ beforeAll(async () => {
18
22
  device = result.value;
19
23
  });
20
24
 
21
- function build(bloom: 'off' | 'on', lighting: 'direct' | 'clustered') {
25
+ function build(bloom: 'off' | 'on') {
22
26
  const topology: RenderPipelineTopology = {
23
27
  pipelineId: STANDARD_PIPELINE_ID,
24
- standardProfile: { ...DEFAULT_STANDARD_PROFILE, lighting },
25
- config: lighting === 'clustered' ? { ssao: { enabled: false } } : undefined,
28
+ standardProfile: DEFAULT_STANDARD_PROFILE,
29
+ config: { ssao: { enabled: false } },
26
30
  surface: {
27
31
  width: 320,
28
32
  height: 180,
@@ -31,15 +35,15 @@ function build(bloom: 'off' | 'on', lighting: 'direct' | 'clustered') {
31
35
  },
32
36
  camera: { tonemap: 'aces-filmic', antialias: 'fxaa', bloom },
33
37
  shadow: {
34
- mapSize: 64,
35
- cascadeCount: 1,
38
+ directional: { mapSize: 64, cascadeCount: 1 },
39
+ spotMapSize: 64,
36
40
  pointCount: 0,
37
41
  pointFaceSize: 64,
38
42
  spotCount: 0,
39
43
  },
40
44
  lane: {
41
- compute: lighting === 'clustered',
42
- storageBuffer: lighting === 'clustered',
45
+ compute: true,
46
+ storageBuffer: true,
43
47
  multisample: false,
44
48
  maxColorAttachments: 8,
45
49
  },
@@ -47,9 +51,33 @@ function build(bloom: 'off' | 'on', lighting: 'direct' | 'clustered') {
47
51
  gpuDrivenTopologySignature: '',
48
52
  };
49
53
  const graph = new RenderGraphBuilder<RenderPipelineFrame>();
54
+ const prepared = prepareStandardLighting({
55
+ directional: undefined,
56
+ local: [{ kind: 'point' as const, shadowed: false, position: vec3.create(0, 0, -4), range: 2 }],
57
+ view: mat4.lookAt(mat4.create(), [0, 0, 0], [0, 0, -1], [0, 1, 0]),
58
+ projection: mat4.perspective(mat4.create(), Math.PI / 3, 1, 0.1, 100),
59
+ near: 0.1,
60
+ far: 100,
61
+ grid: { x: 4, y: 3, z: 4 },
62
+ lightCount: DEFAULT_STANDARD_PROFILE.lightCount,
63
+ renderPath: 'forward',
64
+ });
65
+ if (!prepared.ok) throw prepared.error;
66
+ const transport = selectStandardClusterTransport(
67
+ { compute: true, storageBuffer: true, membershipPipelineReady: true },
68
+ prepared.value,
69
+ );
70
+ if (!transport.ok) throw transport.error;
71
+ const standardLighting = deriveStandardTopologyInput({
72
+ kind: 'clustered',
73
+ prepared: prepared.value,
74
+ transport: transport.value,
75
+ });
76
+ if (!standardLighting.ok) throw standardLighting.error;
50
77
  const built = standardPipeline.build(
51
78
  {
52
79
  graph,
80
+ standardLighting: standardLighting.value,
53
81
  projectGpuDriven: () => ok(undefined),
54
82
  contributeFeatures: () => ok(undefined),
55
83
  },
@@ -71,10 +99,10 @@ function bloomSummary(info: ReturnType<typeof build>) {
71
99
  }
72
100
 
73
101
  describe('Standard Bloom lifecycle contract', () => {
74
- it.each(['direct', 'clustered'] as const)('supports on -> off -> on for %s', (lighting) => {
75
- const on = bloomSummary(build('on', lighting));
76
- const off = bloomSummary(build('off', lighting));
77
- const recovered = bloomSummary(build('on', lighting));
102
+ it('supports on -> off -> on for the Standard clustered graph', () => {
103
+ const on = bloomSummary(build('on'));
104
+ const off = bloomSummary(build('off'));
105
+ const recovered = bloomSummary(build('on'));
78
106
  expect(on.passes).toHaveLength(4);
79
107
  expect(on.resources.length).toBeGreaterThan(0);
80
108
  expect(off).toEqual({ passes: [], resources: [] });
@@ -82,7 +110,7 @@ describe('Standard Bloom lifecycle contract', () => {
82
110
  });
83
111
 
84
112
  it('keeps post order scene -> Bloom -> output transform -> FXAA -> debug', () => {
85
- const names = build('on', 'clustered').passes.map((pass) => pass.name);
113
+ const names = build('on').passes.map((pass) => pass.name);
86
114
  const order = [
87
115
  'forward',
88
116
  'bloom-bright',
@@ -117,7 +145,7 @@ describe('Standard Bloom lifecycle contract', () => {
117
145
  });
118
146
 
119
147
  it('FALSIFY rejects an injected Bloom contribution on the off path', () => {
120
- const off = bloomSummary(build('off', 'direct'));
148
+ const off = bloomSummary(build('off'));
121
149
  const injected = { ...off, passes: ['bloom-falsified'] };
122
150
  expect(injected).not.toEqual({ passes: [], resources: [] });
123
151
  expect(off).toEqual({ passes: [], resources: [] });
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { type CameraTargetCandidate, selectCameraTargetViews } from '../render-system-extract';
3
+ import type { RenderTargetDescriptor } from '../targets/contracts';
4
+ import { createRenderTargetOwner } from '../targets/owner';
5
+
6
+ const descriptor: RenderTargetDescriptor = {
7
+ shape: '2d',
8
+ width: 16,
9
+ height: 16,
10
+ format: 'rgba8unorm',
11
+ mipLevels: 1,
12
+ sampleCount: 1,
13
+ sampled: true,
14
+ readback: false,
15
+ };
16
+
17
+ function targets() {
18
+ const owner = createRenderTargetOwner({ rendererId: Symbol('renderer'), initialGeneration: 1 });
19
+ const first = owner.create(descriptor);
20
+ const second = owner.create(descriptor);
21
+ if (!first.ok) throw first.error;
22
+ if (!second.ok) throw second.error;
23
+ return [first.value, second.value] as const;
24
+ }
25
+
26
+ describe('Camera target consumption', () => {
27
+ it('keeps display authority separate and applies stable auxiliary ordering and budget', () => {
28
+ const [first, second] = targets();
29
+ const cameras: CameraTargetCandidate[] = [
30
+ { worldId: 2, entityKey: 20, target: first, requestVersion: 1, update: 'once' },
31
+ { worldId: 1, entityKey: 30, target: second, requestVersion: 1, update: 'continuous' },
32
+ { worldId: 1, entityKey: 10, requestVersion: 0, update: 'continuous' },
33
+ ];
34
+ const selected = selectCameraTargetViews(cameras, { displayEntityKey: 10, budget: 1 });
35
+ expect(selected.display?.entityKey).toBe(10);
36
+ expect(selected.auxiliary.map((camera) => camera.entityKey)).toEqual([30]);
37
+ expect(selected.rejected).toHaveLength(1);
38
+ expect(selected.rejected[0]?.reason).toBe('budget');
39
+ });
40
+
41
+ it('rejects a target camera selected as display and duplicate target producers', () => {
42
+ const [first] = targets();
43
+ const cameras: CameraTargetCandidate[] = [
44
+ { worldId: 1, entityKey: 3, target: first, requestVersion: 1, update: 'once' },
45
+ { worldId: 1, entityKey: 4, target: first, requestVersion: 2, update: 'once' },
46
+ ];
47
+ const selected = selectCameraTargetViews(cameras, { displayEntityKey: 3, budget: 2 });
48
+ expect(selected.display).toBeUndefined();
49
+ expect(selected.rejected.map((entry) => entry.reason)).toEqual(
50
+ expect.arrayContaining(['display-target', 'duplicate-target']),
51
+ );
52
+ });
53
+ });
@@ -0,0 +1,35 @@
1
+ import { mat4, vec3 } from '@forgeax/engine-math';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { bin, createClusterBinScratch } from '../cluster-binner';
4
+
5
+ describe('cluster binner structured boundaries', () => {
6
+ it('returns a complete structured overflow without truncating output', () => {
7
+ const grid = { x: 4, y: 3, z: 4 };
8
+ const view = mat4.lookAt(mat4.create(), [0, 0, 0], [0, 0, -1], [0, 1, 0]);
9
+ const projection = mat4.perspective(mat4.create(), Math.PI / 3, 1, 0.1, 100);
10
+ const clusterGrid = new Uint32Array(grid.x * grid.y * grid.z * 2).fill(7);
11
+ const lightIndexList = new Uint32Array(1).fill(9);
12
+ const result = bin(
13
+ [{ position: vec3.create(0, 0, -4), range: 100 }],
14
+ view,
15
+ projection,
16
+ grid,
17
+ 0.1,
18
+ 100,
19
+ clusterGrid,
20
+ lightIndexList,
21
+ 0,
22
+ createClusterBinScratch(),
23
+ );
24
+
25
+ expect(result.ok).toBe(false);
26
+ if (result.ok) return;
27
+ expect(result.error.code).toBe('index-overflow');
28
+ expect(result.error.expected).toContain('writeCount <= 0');
29
+ expect(result.error.hint).toContain('reduce lights');
30
+ expect(result.error.detail.actual).toBeGreaterThan(0);
31
+ expect(result.error.detail.capacity).toBe(0);
32
+ expect(Array.from(clusterGrid)).toEqual(new Array(clusterGrid.length).fill(0));
33
+ expect(Array.from(lightIndexList)).toEqual([9]);
34
+ });
35
+ });
@@ -6,15 +6,20 @@ const ownerSource = readFileSync(
6
6
  new URL('../pipeline/standard-profile.ts', import.meta.url),
7
7
  'utf8',
8
8
  );
9
+ const layoutSource = readFileSync(
10
+ new URL('../pipeline/standard-lighting/layout.ts', import.meta.url),
11
+ 'utf8',
12
+ );
9
13
  const recordSource = readFileSync(new URL('../record/frame-lighting.ts', import.meta.url), 'utf8');
10
14
 
11
15
  describe('cluster grid default owner', () => {
12
16
  it('keeps all record-stage fallbacks on the pipeline owner', () => {
13
17
  expect(DEFAULT_CLUSTER_GRID).toEqual({ x: 16, y: 9, z: 24 });
14
18
  expect(
15
- ownerSource.match(/export const DEFAULT_CLUSTER_GRID\s*=\s*\{ x: 16, y: 9, z: 24 \}/g),
19
+ layoutSource.match(/export const DEFAULT_CLUSTER_GRID\s*=\s*\{ x: 16, y: 9, z: 24 \}/g),
16
20
  ).toHaveLength(1);
17
- expect(recordSource.match(/clusterGrid \?\? DEFAULT_CLUSTER_GRID/g)).toHaveLength(3);
21
+ expect(ownerSource).toMatch(/DEFAULT_CLUSTER_GRID/);
22
+ expect(recordSource.match(/clusterGrid \?\? DEFAULT_CLUSTER_GRID/g)).toHaveLength(1);
18
23
  expect(recordSource).not.toMatch(/clusterGrid \?\? \{\s*x: 16,\s*y: 9,\s*z: 24,\s*\}/);
19
24
  });
20
25
  });
@@ -0,0 +1,70 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { type CubeCaptureRequest, createCubeCaptureScheduler } from '../capture/scheduler';
3
+ import type { RenderTarget } from '../targets/contracts';
4
+
5
+ function request(target: RenderTarget, requestVersion = 1): CubeCaptureRequest {
6
+ return {
7
+ target,
8
+ position: [0, 0, 0],
9
+ near: 0.1,
10
+ far: 100,
11
+ updateIntent: 'on-demand',
12
+ requestVersion,
13
+ faceBudget: 2,
14
+ };
15
+ }
16
+
17
+ describe('CubeCamera capture scheduler', () => {
18
+ it('spends a bounded face budget and promotes only after all faces submit', () => {
19
+ const target = {} as RenderTarget;
20
+ const scheduler = createCubeCaptureScheduler({ maxFacesPerFrame: 2 });
21
+ expect(scheduler.request(request(target)).ok).toBe(true);
22
+
23
+ scheduler.beginFrame();
24
+ expect(scheduler.nextWork()).toHaveLength(2);
25
+ expect(scheduler.inspect(target).pendingFaces).toHaveLength(4);
26
+ expect(scheduler.completeSubmission(true).ok).toBe(true);
27
+ expect(scheduler.inspect(target).activeGeneration).toBe(0);
28
+
29
+ scheduler.beginFrame();
30
+ expect(scheduler.nextWork()).toHaveLength(2);
31
+ expect(scheduler.completeSubmission(true).ok).toBe(true);
32
+ expect(scheduler.inspect(target).pendingFaces).toHaveLength(2);
33
+ scheduler.beginFrame();
34
+ expect(scheduler.nextWork()).toHaveLength(2);
35
+ expect(scheduler.completeSubmission(true).ok).toBe(true);
36
+ expect(scheduler.inspect(target).pendingFaces).toEqual([]);
37
+ expect(scheduler.inspect(target).activeGeneration).toBe(1);
38
+ });
39
+
40
+ it('rejects nested capture and does not publish a partial or failed candidate', () => {
41
+ const target = {} as RenderTarget;
42
+ const scheduler = createCubeCaptureScheduler({ maxFacesPerFrame: 6 });
43
+ expect(scheduler.request(request(target)).ok).toBe(true);
44
+ scheduler.beginFrame();
45
+ expect(scheduler.request(request(target, 2)).ok).toBe(false);
46
+ expect(scheduler.nextWork()).toHaveLength(2);
47
+ expect(scheduler.completeSubmission(false).ok).toBe(false);
48
+ expect(scheduler.inspect(target).activeGeneration).toBe(0);
49
+ expect(scheduler.inspect(target).pendingFaces).toEqual([]);
50
+ });
51
+
52
+ it('waits for the FrameReceipt completion before publishing the six-face candidate', async () => {
53
+ const target = {} as RenderTarget;
54
+ const scheduler = createCubeCaptureScheduler({ maxFacesPerFrame: 6 });
55
+ expect(scheduler.request({ ...request(target), faceBudget: 6 }).ok).toBe(true);
56
+ scheduler.beginFrame();
57
+ expect(scheduler.nextWork()).toHaveLength(6);
58
+
59
+ let resolveCompletion: (() => void) | undefined;
60
+ const completion = new Promise<void>((resolve) => {
61
+ resolveCompletion = resolve;
62
+ });
63
+ expect(scheduler.completeSubmission(true, completion).ok).toBe(true);
64
+ expect(scheduler.inspect(target).activeGeneration).toBe(0);
65
+ resolveCompletion?.();
66
+ await completion;
67
+ await Promise.resolve();
68
+ expect(scheduler.inspect(target).activeGeneration).toBe(1);
69
+ });
70
+ });
@@ -0,0 +1,75 @@
1
+ import { World } from '@forgeax/engine-ecs';
2
+ import { rhi } from '@forgeax/engine-rhi-null';
3
+ import { Transform } from '@forgeax/engine-scene';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { createRenderer as constructRenderer } from '../assembly/factory';
6
+ import { createCubeCaptureScheduler } from '../capture/scheduler';
7
+ import { Camera } from '../components/camera';
8
+ import type { RenderTarget } from '../targets/contracts';
9
+
10
+ function manifestUrl(): string {
11
+ return `data:application/json,${encodeURIComponent(JSON.stringify({ schemaVersion: '1.0.0', entries: [] }))}`;
12
+ }
13
+
14
+ describe('CubeCamera single-submit benchmark contract', () => {
15
+ it('measures actual Renderer receipts for the steady frame path', async () => {
16
+ const renderer = await constructRenderer(
17
+ { width: 1, height: 1, getContext: () => null },
18
+ { rhi },
19
+ { shaderManifestUrl: manifestUrl() },
20
+ );
21
+ expect((await renderer.initialization).ok).toBe(true);
22
+ const world = new World();
23
+ const spawned = world.spawn(
24
+ { component: Transform, data: { pos: [0, 0, 0], quat: [0, 0, 0, 1], scale: [1, 1, 1] } },
25
+ { component: Camera, data: { fov: Math.PI / 4, aspect: 1, near: 0.1, far: 100 } },
26
+ );
27
+ expect(spawned.ok).toBe(true);
28
+ expect(world.update().ok).toBe(true);
29
+ const attached = renderer.attach(world);
30
+ expect(attached.ok).toBe(true);
31
+ if (!attached.ok) return;
32
+
33
+ const initial = renderer.inspect().frame;
34
+ const receipts = [];
35
+ for (let index = 0; index < 6; index += 1) {
36
+ const frame = renderer.draw({
37
+ leases: [attached.value],
38
+ camera: { lease: attached.value },
39
+ environment: { lease: attached.value },
40
+ });
41
+ expect(frame.ok).toBe(true);
42
+ if (!frame.ok || frame.value === undefined) return;
43
+ receipts.push(frame.value);
44
+ await frame.value.completed;
45
+ }
46
+
47
+ const inspected = renderer.inspect();
48
+ expect(receipts).toHaveLength(6);
49
+ expect(inspected.frame.frameId - initial.frameId).toBe(6);
50
+ expect(inspected.perFramePassNames.length).toBeGreaterThan(0);
51
+ expect(inspected.state).toBe('alive');
52
+ await renderer.dispose();
53
+ });
54
+
55
+ it('retains neutral or previous active output after a failed submit', () => {
56
+ const target = {} as RenderTarget;
57
+ const scheduler = createCubeCaptureScheduler({ maxFacesPerFrame: 6 });
58
+ expect(
59
+ scheduler.request({
60
+ target,
61
+ position: [0, 0, 0],
62
+ near: 0.1,
63
+ far: 10,
64
+ updateIntent: 'continuous',
65
+ requestVersion: 1,
66
+ faceBudget: 6,
67
+ }).ok,
68
+ ).toBe(true);
69
+ scheduler.beginFrame();
70
+ expect(scheduler.nextWork()).toHaveLength(6);
71
+ expect(scheduler.completeSubmission(false).ok).toBe(false);
72
+ expect(scheduler.inspect(target).activeGeneration).toBe(0);
73
+ expect(scheduler.inspect(target).fallback).toBe('neutral');
74
+ });
75
+ });
@@ -0,0 +1,41 @@
1
+ import { mat4, vec3 } from '@forgeax/engine-math';
2
+ import { describe, expect, it } from 'vitest';
3
+ import { buildCubeCameraFaceViews } from '../capture/cube-views';
4
+ import { CUBE_CAMERA_FACE_ORDER } from '../components/cube-camera';
5
+
6
+ describe('CubeCamera canonical view matrices', () => {
7
+ it('builds six ordered views from one origin with shared near/far', () => {
8
+ const views = buildCubeCameraFaceViews({
9
+ position: [2, 3, 4],
10
+ near: 0.25,
11
+ far: 80,
12
+ });
13
+
14
+ expect(views.map((view) => view.face)).toEqual(CUBE_CAMERA_FACE_ORDER);
15
+ expect(views).toHaveLength(6);
16
+ expect(views.every((view) => view.near === 0.25 && view.far === 80)).toBe(true);
17
+ expect(
18
+ views.every((view) => view.view.length === 16 && view.viewProjection.length === 16),
19
+ ).toBe(true);
20
+ expect(new Set(views.map((view) => Array.from(view.view))).size).toBe(6);
21
+ });
22
+
23
+ it('keeps asymmetric world directions in the canonical cube face orientation', () => {
24
+ const views = buildCubeCameraFaceViews({ position: [0, 0, 0], near: 0.1, far: 10 });
25
+ const samples = [
26
+ { direction: [1, 0.25, -0.4], ndc: [-0.4, 0.25] },
27
+ { direction: [-1, 0.25, 0.4], ndc: [-0.4, 0.25] },
28
+ { direction: [0.35, 1, 0.25], ndc: [-0.35, -0.25] },
29
+ { direction: [0.35, -1, -0.25], ndc: [-0.35, -0.25] },
30
+ { direction: [0.35, 0.25, 1], ndc: [-0.35, 0.25] },
31
+ { direction: [-0.35, 0.25, -1], ndc: [-0.35, 0.25] },
32
+ ] as const;
33
+
34
+ for (const [index, sample] of samples.entries()) {
35
+ const ndc = vec3.create();
36
+ mat4.transformVec3(ndc, views[index]?.viewProjection ?? mat4.create(), sample.direction);
37
+ expect(ndc[0]).toBeCloseTo(sample.ndc[0], 5);
38
+ expect(ndc[1]).toBeCloseTo(sample.ndc[1], 5);
39
+ }
40
+ });
41
+ });
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ CUBE_CAMERA_FACE_ORDER,
4
+ CUBE_CAMERA_UPDATE_CONTINUOUS,
5
+ CUBE_CAMERA_UPDATE_ON_DEMAND,
6
+ CUBE_CAMERA_UPDATE_ONCE,
7
+ CubeCamera,
8
+ cubeCameraUpdateIntentFromF32,
9
+ } from '../components/cube-camera';
10
+ import { RenderIntentInvalidError } from '../errors/render';
11
+
12
+ describe('CubeCamera vocabulary', () => {
13
+ it('exposes the canonical faces and bounded capture facts', () => {
14
+ expect(CUBE_CAMERA_FACE_ORDER).toEqual(['+X', '-X', '+Y', '-Y', '+Z', '-Z']);
15
+ expect(CubeCamera.fields.target.type).toBe('shared<RenderTarget>');
16
+ expect(CubeCamera.fields.near.type).toBe('f32');
17
+ expect(CubeCamera.fields.far.type).toBe('f32');
18
+ expect(CubeCamera.fields.requestVersion.type).toBe('u32');
19
+ expect(CubeCamera.fields.faceBudget.type).toBe('u32');
20
+ expect(CubeCamera.fields.updateIntent.type).toBe('f32');
21
+ });
22
+
23
+ it('keeps update intent closed and decodable', () => {
24
+ expect(cubeCameraUpdateIntentFromF32(CUBE_CAMERA_UPDATE_ONCE)).toBe('once');
25
+ expect(cubeCameraUpdateIntentFromF32(CUBE_CAMERA_UPDATE_ON_DEMAND)).toBe('on-demand');
26
+ expect(cubeCameraUpdateIntentFromF32(CUBE_CAMERA_UPDATE_CONTINUOUS)).toBe('continuous');
27
+ });
28
+
29
+ it('reports invalid update intent through the structured render error contract', () => {
30
+ expect(() => cubeCameraUpdateIntentFromF32(99)).toThrowError(RenderIntentInvalidError);
31
+ try {
32
+ cubeCameraUpdateIntentFromF32(99);
33
+ } catch (error) {
34
+ expect(error).toBeInstanceOf(RenderIntentInvalidError);
35
+ if (!(error instanceof RenderIntentInvalidError)) return;
36
+ expect(error.code).toBe('render-intent-invalid');
37
+ expect(error.expected).toBe('CubeCamera.updateIntent is one of 0, 1, or 2');
38
+ expect(error.hint).toBe('set CubeCamera.updateIntent to 0, 1, or 2');
39
+ expect(error.detail).toEqual({
40
+ component: 'CubeCamera',
41
+ field: 'updateIntent',
42
+ value: 99,
43
+ allowed: [0, 1, 2],
44
+ });
45
+ }
46
+ });
47
+ });
@@ -0,0 +1,42 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { createCubeCaptureScheduler, selectCubeCaptureRenderables } from '../capture/scheduler';
3
+ import type { RenderTarget } from '../targets/contracts';
4
+
5
+ describe('CubeCamera graph contribution contract', () => {
6
+ it('keeps Standard geometry and environment while excluding debug, UI, and self', () => {
7
+ const target = {} as RenderTarget;
8
+ const included = selectCubeCaptureRenderables(
9
+ [
10
+ { kind: 'opaque' },
11
+ { kind: 'transparent' },
12
+ { kind: 'environment' },
13
+ { kind: 'debug' },
14
+ { kind: 'ui' },
15
+ { kind: 'opaque', target },
16
+ ],
17
+ target,
18
+ );
19
+ expect(included.map((entry) => entry.kind)).toEqual(['opaque', 'transparent', 'environment']);
20
+ });
21
+
22
+ it('does not promote a candidate until the graph submission completes all six faces', () => {
23
+ const target = {} as RenderTarget;
24
+ const scheduler = createCubeCaptureScheduler({ maxFacesPerFrame: 6 });
25
+ expect(
26
+ scheduler.request({
27
+ target,
28
+ position: [0, 0, 0],
29
+ near: 0.1,
30
+ far: 10,
31
+ updateIntent: 'once',
32
+ requestVersion: 1,
33
+ faceBudget: 6,
34
+ }).ok,
35
+ ).toBe(true);
36
+ scheduler.beginFrame();
37
+ expect(scheduler.nextWork()).toHaveLength(6);
38
+ expect(scheduler.completeSubmission(false).ok).toBe(false);
39
+ expect(scheduler.inspect(target).activeGeneration).toBe(0);
40
+ expect(scheduler.inspect(target).fallback).toBe('neutral');
41
+ });
42
+ });
@@ -0,0 +1,73 @@
1
+ import type { RhiAdapter, RhiDevice } from '@forgeax/engine-rhi';
2
+ import { describe, expect, it } from 'vitest';
3
+ import {
4
+ deriveDeviceFeatureAdmission,
5
+ isTimestampQueryAdmitted,
6
+ } from '../assembly/device-feature-admission.js';
7
+
8
+ function adapter(
9
+ features: string[],
10
+ limits?: Readonly<Record<string, number>>,
11
+ ): Pick<RhiAdapter, 'features'> & Partial<Pick<RhiAdapter, 'limits'>> {
12
+ return {
13
+ features: new Set(features) as RhiAdapter['features'],
14
+ ...(limits === undefined ? {} : { limits }),
15
+ };
16
+ }
17
+
18
+ function deviceCaps(
19
+ timestampQuery: boolean,
20
+ timestampPeriodNanoseconds: number | null,
21
+ ): Pick<RhiDevice['caps'], 'timestampQuery' | 'timestampPeriodNanoseconds'> {
22
+ return { timestampQuery, timestampPeriodNanoseconds };
23
+ }
24
+
25
+ describe('shared device feature admission', () => {
26
+ it('keeps timestamp-query out of the disabled and unsupported descriptors', () => {
27
+ const supported = adapter(['timestamp-query', 'texture-compression-bc']);
28
+ const disabled = deriveDeviceFeatureAdmission(supported);
29
+ const unsupported = deriveDeviceFeatureAdmission(adapter(['texture-compression-bc']), {
30
+ gpuPassTiming: {},
31
+ });
32
+
33
+ expect(disabled.requiredFeatures).toEqual(['texture-compression-bc']);
34
+ expect(unsupported.requiredFeatures).toEqual(['texture-compression-bc']);
35
+ });
36
+
37
+ it('adds only adapter-supported timestamp-query once for either timing producer', () => {
38
+ const result = deriveDeviceFeatureAdmission(adapter(['timestamp-query']), {
39
+ gpuPassTiming: {},
40
+ });
41
+
42
+ expect(result.requiredFeatures).toEqual(['timestamp-query']);
43
+ });
44
+
45
+ it('requests the transmission sampled-texture limit only when the adapter supports it', () => {
46
+ expect(
47
+ deriveDeviceFeatureAdmission(adapter([], { maxSampledTexturesPerShaderStage: 17 })),
48
+ ).toEqual({
49
+ requiredFeatures: [],
50
+ requiredLimits: { maxSampledTexturesPerShaderStage: 17 },
51
+ });
52
+ expect(
53
+ deriveDeviceFeatureAdmission(adapter([], { maxSampledTexturesPerShaderStage: 16 })),
54
+ ).toEqual({ requiredFeatures: [] });
55
+ });
56
+
57
+ it('requires both device capability and a finite positive period before admission', () => {
58
+ expect(isTimestampQueryAdmitted(deviceCaps(true, 1))).toBe(true);
59
+ expect(isTimestampQueryAdmitted(deviceCaps(false, 1))).toBe(false);
60
+ expect(isTimestampQueryAdmitted(deviceCaps(true, null))).toBe(false);
61
+ expect(isTimestampQueryAdmitted(deviceCaps(true, Number.NaN))).toBe(false);
62
+ expect(isTimestampQueryAdmitted(deviceCaps(true, 0))).toBe(false);
63
+ });
64
+
65
+ it('derives byte-identical initial and recovery request descriptors', () => {
66
+ const supported = adapter(['texture-compression-etc2', 'timestamp-query']);
67
+ const options = { gpuPassTiming: {} };
68
+
69
+ expect(deriveDeviceFeatureAdmission(supported, options)).toEqual(
70
+ deriveDeviceFeatureAdmission(supported, options),
71
+ );
72
+ });
73
+ });
@@ -48,14 +48,14 @@ describe('one direct-light snapshot proof', () => {
48
48
  'utf8',
49
49
  );
50
50
  const shader = await readFile(
51
- new URL('../../../shader/src/hdrp-cluster-forward.wgsl', import.meta.url),
51
+ new URL('../../../shader/src/standard-cluster.wgsl', import.meta.url),
52
52
  'utf8',
53
53
  );
54
54
 
55
55
  expect(extract.match(/const spotLightQuery =/g)).toHaveLength(1);
56
56
  expect(extract).toContain('direction: dirN');
57
- expect(lighting).toContain('packLightSlot(pl');
58
- expect(lighting).toContain('packLightSlot(sl)');
57
+ expect(lighting).toContain('prepareStandardLighting');
58
+ expect(lighting).toContain('source');
59
59
  expect(shader).not.toContain('normalize(light.direction.xyz)');
60
60
  });
61
61
  });
@@ -0,0 +1,38 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { describe, expect, it } from 'vitest';
3
+
4
+ function read(relativePath: string): string {
5
+ return readFileSync(new URL(relativePath, import.meta.url), 'utf8');
6
+ }
7
+
8
+ const renderReadme = read('../../README.md');
9
+ const assetsReadme = read('../../../assets-runtime/README.md');
10
+ const instancesSource = read('../components/instances.ts');
11
+
12
+ describe('M4 material and Instances documentation contract', () => {
13
+ it('keeps resident observation and derived-bounds recovery discoverable', () => {
14
+ for (const token of [
15
+ 'renderer.inspect().meshMaterialBindings[]',
16
+ '`ready`, `pending`, `failed`, or `last-known-good`',
17
+ 'nearest structured preparation',
18
+ 'does not keep a second readiness ledger',
19
+ '`Instances` remains author data',
20
+ 'renderer derives a CPU union bound',
21
+ 'conservative no-cull result',
22
+ 'GPU path keeps the mesh-local AABB',
23
+ ]) {
24
+ expect(renderReadme).toContain(token);
25
+ }
26
+ for (const token of [
27
+ 'producer lifecycle observations',
28
+ '`inspect -> rebuild/recook or refresh LKG -> loadByGuid`',
29
+ 'parallel readiness ledger',
30
+ ]) {
31
+ expect(assetsReadme).toContain(token);
32
+ }
33
+ });
34
+
35
+ it('keeps bounds out of the public Instances author schema', () => {
36
+ expect(instancesSource).not.toMatch(/\bbounds\s*:/);
37
+ });
38
+ });