@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
@@ -0,0 +1,91 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { describe, expect, it } from 'vitest';
3
+ import * as Render from '../index';
4
+
5
+ const schema = JSON.parse(
6
+ readFileSync(new URL('./render-target-public-schema.json', import.meta.url), 'utf8'),
7
+ ) as {
8
+ readonly $id: string;
9
+ readonly title: string;
10
+ readonly schemaVersion?: never;
11
+ readonly additionalProperties: boolean;
12
+ readonly properties: {
13
+ readonly schemaVersion: { readonly const: string };
14
+ readonly runtimeExports: { readonly items: { readonly enum: readonly string[] } };
15
+ readonly typeExports: { readonly items: { readonly enum: readonly string[] } };
16
+ readonly operations: {
17
+ readonly items: {
18
+ readonly properties: { readonly name: { readonly enum: readonly string[] } };
19
+ };
20
+ };
21
+ readonly errorCodes: { readonly items: { readonly enum: readonly string[] } };
22
+ readonly stableTargetIds: { readonly const: readonly string[] };
23
+ } & Record<string, unknown>;
24
+ };
25
+
26
+ const manifest = JSON.parse(
27
+ readFileSync(
28
+ new URL(
29
+ '../../../../apps/learn-render/6.pbr/4.render-target-reflection/src/feature-manifest.json',
30
+ import.meta.url,
31
+ ),
32
+ 'utf8',
33
+ ),
34
+ ) as {
35
+ readonly stableTargetIds: readonly string[];
36
+ readonly evidenceCommands: readonly string[];
37
+ readonly operations: readonly string[];
38
+ };
39
+
40
+ describe('public render-target schema and stable consumer manifest', () => {
41
+ it('describes the public surface without a runtime registry', () => {
42
+ expect(schema.properties.schemaVersion.const).toBe('render-target-public/1');
43
+ expect(schema.additionalProperties).toBe(false);
44
+ expect(Object.keys(schema.properties)).toEqual([
45
+ 'schemaVersion',
46
+ 'runtimeExports',
47
+ 'typeExports',
48
+ 'operations',
49
+ 'errorCodes',
50
+ 'stableTargetIds',
51
+ ]);
52
+ for (const name of schema.properties.runtimeExports.items.enum)
53
+ expect(name in Render).toBe(true);
54
+ expect(schema.properties.typeExports.items.enum).toContain('ReflectionProbeInspection');
55
+ expect(schema.properties.typeExports.items.enum).toContain(
56
+ 'ReflectionProbeSelectionInspection',
57
+ );
58
+ expect(schema.properties.typeExports.items.enum).toContain('RenderIntentInvalidDetail');
59
+ expect(schema.properties.operations.items.properties.name.enum).toEqual([
60
+ 'Renderer.createRenderTarget',
61
+ 'Renderer.resizeRenderTarget',
62
+ 'Renderer.createRenderTargetTextureSource',
63
+ 'Renderer.requestTargetReadback',
64
+ 'Renderer.observe',
65
+ 'Renderer.inspect',
66
+ 'Renderer.recover',
67
+ 'Renderer.destroyRenderTarget',
68
+ ]);
69
+ expect(schema.properties.errorCodes.items.enum).toEqual([
70
+ 'render-target-descriptor-invalid',
71
+ 'render-target-capability-missing',
72
+ 'render-target-state-invalid',
73
+ 'render-target-operation-failed',
74
+ 'reflection-probe-budget-exceeded',
75
+ 'render-intent-invalid',
76
+ ]);
77
+ expect(JSON.stringify(schema)).not.toMatch(/device|queue|encoder|handle|registry/i);
78
+ });
79
+
80
+ it('keeps the five stable target IDs and evidence commands aligned', () => {
81
+ expect(manifest.operations).toEqual(schema.properties.operations.items.properties.name.enum);
82
+ expect(manifest.stableTargetIds).toEqual(schema.properties.stableTargetIds.const);
83
+ expect(manifest.evidenceCommands).toEqual([
84
+ 'node scripts/smoke-cube-camera-browser.mjs',
85
+ 'node scripts/smoke-cube-camera-dawn.mjs',
86
+ 'node scripts/smoke-reflection-probe-browser.mjs',
87
+ 'node scripts/smoke-reflection-probe-dawn.mjs',
88
+ ]);
89
+ expect(JSON.stringify(manifest)).not.toMatch(/device|queue|encoder|handle|registry/i);
90
+ });
91
+ });
@@ -0,0 +1,83 @@
1
+ import { describe, expectTypeOf, it } from 'vitest';
2
+ import type {
3
+ RenderError,
4
+ RenderErrorCode,
5
+ Renderer,
6
+ RenderTarget,
7
+ RenderTargetDescriptor,
8
+ RenderTargetFormat,
9
+ RenderTargetReadbackTicket,
10
+ RenderTargetShape,
11
+ RenderTargetTextureSource,
12
+ } from '../index';
13
+
14
+ const formats = [
15
+ 'rgba16float',
16
+ 'rgba8unorm',
17
+ 'rgba8unorm-srgb',
18
+ ] as const satisfies readonly RenderTargetFormat[];
19
+ const shapes = ['2d', 'cube'] as const satisfies readonly RenderTargetShape[];
20
+
21
+ const descriptor: RenderTargetDescriptor = {
22
+ shape: '2d',
23
+ width: 512,
24
+ height: 256,
25
+ format: 'rgba8unorm-srgb',
26
+ mipLevels: 'full',
27
+ sampleCount: 1,
28
+ depth: 'depth24plus-stencil8',
29
+ sampled: true,
30
+ readback: true,
31
+ };
32
+
33
+ describe('RenderTarget public surface', () => {
34
+ it('keeps the descriptor vocabulary closed and color-domain based', () => {
35
+ expectTypeOf<RenderTargetDescriptor['format']>().toEqualTypeOf<RenderTargetFormat>();
36
+ expectTypeOf<RenderTargetDescriptor['shape']>().toEqualTypeOf<RenderTargetShape>();
37
+ expectTypeOf(formats).toMatchTypeOf<readonly RenderTargetFormat[]>();
38
+ expectTypeOf(shapes).toMatchTypeOf<readonly RenderTargetShape[]>();
39
+ // @ts-expect-error target descriptors do not carry a second color-space fact.
40
+ const withColorSpace: RenderTargetDescriptor = { ...descriptor, colorSpace: 'srgb' };
41
+ void withColorSpace;
42
+ // @ts-expect-error array targets are outside the first public target vocabulary.
43
+ const arrayTarget: RenderTargetDescriptor = { ...descriptor, shape: 'array' };
44
+ void arrayTarget;
45
+ });
46
+
47
+ it('exposes opaque target values and typed Renderer operations', () => {
48
+ const renderer = undefined as unknown as Renderer;
49
+ const target = undefined as unknown as RenderTarget;
50
+ const source = undefined as unknown as RenderTargetTextureSource;
51
+ const ticket = undefined as unknown as RenderTargetReadbackTicket;
52
+
53
+ expectTypeOf(renderer.createRenderTarget(descriptor)).toMatchTypeOf<
54
+ | { readonly ok: true; readonly value: RenderTarget }
55
+ | { readonly ok: false; readonly error: RenderError }
56
+ >();
57
+ expectTypeOf(renderer.resizeRenderTarget).parameter(0).toEqualTypeOf<RenderTarget>();
58
+ expectTypeOf(renderer.createRenderTargetTextureSource)
59
+ .parameter(0)
60
+ .toEqualTypeOf<RenderTarget>();
61
+ expectTypeOf(renderer.requestTargetReadback).parameter(0).toEqualTypeOf<RenderTarget>();
62
+ expectTypeOf(renderer.destroyRenderTarget).parameter(0).toEqualTypeOf<RenderTarget>();
63
+ expectTypeOf(source).not.toEqualTypeOf(target);
64
+ expectTypeOf(ticket).not.toEqualTypeOf(target);
65
+ // @ts-expect-error opaque target values are not numeric handles.
66
+ const numericIdentity: number = target;
67
+ void numericIdentity;
68
+ void source;
69
+ void ticket;
70
+ });
71
+
72
+ it('keeps target failures in the closed RenderError family', () => {
73
+ const targetCodes = [
74
+ 'render-target-descriptor-invalid',
75
+ 'render-target-capability-missing',
76
+ 'render-target-state-invalid',
77
+ 'render-target-operation-failed',
78
+ ] as const satisfies readonly RenderErrorCode[];
79
+ expectTypeOf<(typeof targetCodes)[number]>().toEqualTypeOf<
80
+ Extract<RenderErrorCode, `${string}target${string}`>
81
+ >();
82
+ });
83
+ });
@@ -0,0 +1,90 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { RenderTargetDescriptor } from '../targets/contracts';
3
+ import { createRenderTargetOwner } from '../targets/owner';
4
+ import {
5
+ completeRenderTargetReadback,
6
+ createRenderTargetReadbackTicket,
7
+ type RenderTargetReadbackReceipt,
8
+ } from '../targets/readback';
9
+
10
+ const descriptor: RenderTargetDescriptor = {
11
+ shape: '2d',
12
+ width: 17,
13
+ height: 9,
14
+ format: 'rgba8unorm',
15
+ mipLevels: 1,
16
+ sampleCount: 1,
17
+ sampled: false,
18
+ readback: true,
19
+ };
20
+
21
+ function target() {
22
+ const owner = createRenderTargetOwner({ rendererId: Symbol('renderer'), initialGeneration: 2 });
23
+ const result = owner.create(descriptor);
24
+ if (!result.ok) throw result.error;
25
+ return result.value;
26
+ }
27
+
28
+ describe('RenderTarget receipt-bound readback', () => {
29
+ it('admits a square cube target as six independently addressable faces', () => {
30
+ const owner = createRenderTargetOwner({ rendererId: Symbol('renderer'), initialGeneration: 3 });
31
+ const result = owner.create({ ...descriptor, shape: 'cube', width: 9, height: 9 });
32
+ expect(result.ok).toBe(true);
33
+ if (!result.ok) return;
34
+ const staged = owner.stage(result.value);
35
+ expect(staged.ok).toBe(true);
36
+ if (!staged.ok) return;
37
+ expect(staged.value.descriptor.shape).toBe('cube');
38
+ expect(staged.value.descriptor.width).toBe(staged.value.descriptor.height);
39
+ });
40
+
41
+ it('uses 256-byte row alignment and completes exactly once for a matching receipt', () => {
42
+ const ticket = createRenderTargetReadbackTicket(target(), {
43
+ frameId: 11,
44
+ deviceGeneration: 2,
45
+ mipLevel: 0,
46
+ width: 17,
47
+ height: 9,
48
+ bytesPerPixel: 4,
49
+ });
50
+ expect(ticket.ok).toBe(true);
51
+ if (!ticket.ok) return;
52
+ expect(ticket.value.bytesPerRow).toBe(256);
53
+ expect(ticket.value.byteLength).toBe(256 * 9);
54
+
55
+ const receipt: RenderTargetReadbackReceipt = {
56
+ frameId: 11,
57
+ deviceGeneration: 2,
58
+ };
59
+ const bytes = new Uint8Array(ticket.value.byteLength);
60
+ const completed = completeRenderTargetReadback(ticket.value, receipt, bytes);
61
+ expect(completed.ok).toBe(true);
62
+ if (!completed.ok) return;
63
+ expect(completed.value.bytes).toBe(bytes);
64
+ expect(completeRenderTargetReadback(ticket.value, receipt, bytes).ok).toBe(false);
65
+ });
66
+
67
+ it('rejects mismatched frame or generation before consuming the one-shot ticket', () => {
68
+ const ticket = createRenderTargetReadbackTicket(target(), {
69
+ frameId: 4,
70
+ deviceGeneration: 8,
71
+ mipLevel: 0,
72
+ width: 1,
73
+ height: 1,
74
+ bytesPerPixel: 8,
75
+ });
76
+ expect(ticket.ok).toBe(true);
77
+ if (!ticket.ok) return;
78
+ const bytes = new Uint8Array(ticket.value.byteLength);
79
+ const stale = completeRenderTargetReadback(
80
+ ticket.value,
81
+ { frameId: 5, deviceGeneration: 8 },
82
+ bytes,
83
+ );
84
+ expect(stale.ok).toBe(false);
85
+ if (!stale.ok) expect(stale.error.code).toBe('render-target-state-invalid');
86
+ expect(
87
+ completeRenderTargetReadback(ticket.value, { frameId: 4, deviceGeneration: 8 }, bytes).ok,
88
+ ).toBe(true);
89
+ });
90
+ });
@@ -0,0 +1,85 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { RenderTargetDescriptor } from '../targets/contracts';
3
+ import { createRenderTargetOwner } from '../targets/owner';
4
+ import {
5
+ bindRenderTargetReadbackTicket,
6
+ completeRenderTargetReadback,
7
+ createRenderTargetReadbackTicket,
8
+ } from '../targets/readback';
9
+
10
+ const descriptor: RenderTargetDescriptor = {
11
+ shape: 'cube',
12
+ width: 4,
13
+ height: 4,
14
+ format: 'rgba8unorm-srgb',
15
+ mipLevels: 1,
16
+ sampleCount: 4,
17
+ sampled: true,
18
+ readback: true,
19
+ };
20
+
21
+ describe('RenderTarget recovery integration', () => {
22
+ it('keeps the logical token and LKG while a candidate aborts', () => {
23
+ const owner = createRenderTargetOwner({ rendererId: Symbol('renderer'), initialGeneration: 3 });
24
+ const target = owner.create(descriptor);
25
+ expect(target.ok).toBe(true);
26
+ if (!target.ok) return;
27
+ const candidate = owner.stage(target.value);
28
+ expect(candidate.ok).toBe(true);
29
+ if (!candidate.ok) return;
30
+ expect(owner.promote(target.value, candidate.value.generation).ok).toBe(true);
31
+ const resized = owner.stage(target.value, { ...descriptor, width: 8, height: 8 });
32
+ expect(resized.ok).toBe(true);
33
+ if (!resized.ok) return;
34
+ expect(owner.rejectCandidate(target.value, resized.value.generation, 'submit').ok).toBe(false);
35
+ const inspection = owner.inspect(target.value);
36
+ expect(inspection.ok).toBe(true);
37
+ if (inspection.ok) {
38
+ expect(inspection.value.token).toBe(target.value);
39
+ expect(inspection.value.state).toBe('active');
40
+ expect(inspection.value.generation).toBe(3);
41
+ expect(inspection.value.descriptor.width).toBe(4);
42
+ }
43
+ });
44
+
45
+ it('retires the old generation before rebuilding readback state', () => {
46
+ const owner = createRenderTargetOwner({ rendererId: Symbol('renderer'), initialGeneration: 1 });
47
+ const target = owner.create(descriptor);
48
+ expect(target.ok).toBe(true);
49
+ if (!target.ok) return;
50
+ const staged = owner.stage(target.value);
51
+ expect(staged.ok).toBe(true);
52
+ if (!staged.ok) return;
53
+ expect(owner.promote(target.value, 1).ok).toBe(true);
54
+ const ticket = createRenderTargetReadbackTicket(target.value, {
55
+ deviceGeneration: 1,
56
+ mipLevel: 0,
57
+ face: 2,
58
+ width: 4,
59
+ height: 4,
60
+ bytesPerPixel: 4,
61
+ });
62
+ expect(ticket.ok).toBe(true);
63
+ if (!ticket.ok) return;
64
+ expect(
65
+ bindRenderTargetReadbackTicket(ticket.value, { frameId: 7, deviceGeneration: 1 }).ok,
66
+ ).toBe(true);
67
+ expect(owner.beginRecovery(target.value, 2).ok).toBe(true);
68
+ expect(owner.finishRecovery(target.value).ok).toBe(true);
69
+ const stale = completeRenderTargetReadback(
70
+ ticket.value,
71
+ { frameId: 8, deviceGeneration: 2 },
72
+ new Uint8Array(ticket.value.byteLength),
73
+ );
74
+ expect(stale.ok).toBe(false);
75
+ if (!stale.ok) expect(stale.error.code).toBe('render-target-state-invalid');
76
+ const recovered = owner.inspect(target.value);
77
+ expect(recovered.ok).toBe(true);
78
+ if (recovered.ok)
79
+ expect(recovered.value).toMatchObject({
80
+ token: target.value,
81
+ state: 'uninitialized',
82
+ generation: 2,
83
+ });
84
+ });
85
+ });
@@ -99,6 +99,54 @@ describe('material shader variant identity', () => {
99
99
  ).toBe('urp');
100
100
  });
101
101
 
102
+ it('rewrites directional PCSS to the backend-owned WebGL2 shader variant', () => {
103
+ const variants = [
104
+ {
105
+ defines: {
106
+ STORAGE_BUFFER_AVAILABLE: false,
107
+ DIRECTIONAL_PCSS_AVAILABLE: false,
108
+ },
109
+ },
110
+ {
111
+ defines: {
112
+ STORAGE_BUFFER_AVAILABLE: true,
113
+ DIRECTIONAL_PCSS_AVAILABLE: true,
114
+ },
115
+ },
116
+ ];
117
+
118
+ expect(
119
+ resolveMaterialShaderVariantSet(
120
+ 'DIRECTIONAL_PCSS_AVAILABLE=true+STORAGE_BUFFER_AVAILABLE=true',
121
+ variants,
122
+ 'wgpu-webgl2',
123
+ false,
124
+ ),
125
+ ).toBe('DIRECTIONAL_PCSS_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=false');
126
+ expect(
127
+ resolveMaterialShaderVariantSet(
128
+ 'DIRECTIONAL_PCSS_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=true',
129
+ variants,
130
+ 'webgpu',
131
+ true,
132
+ ),
133
+ ).toBe('');
134
+ });
135
+
136
+ it('fails closed when a clustered layout is unavailable', () => {
137
+ expect(
138
+ selectPipelineLayoutForVariant(
139
+ {
140
+ pbrPipelineLayout: 'urp' as never,
141
+ hdrpPbrPipelineLayout: null,
142
+ pbrSkinPipelineLayout: null,
143
+ },
144
+ '',
145
+ 'hdrp-pbr',
146
+ ),
147
+ ).toBeNull();
148
+ });
149
+
102
150
  it('does not promote an omitted semantic axis to true', () => {
103
151
  const resolved = resolveMaterialShaderVariantSet(
104
152
  'VERTEX_COLOR_AVAILABLE=true',
@@ -6,6 +6,20 @@ import { RenderRecoveryError } from '../errors/recover';
6
6
  import { LifecycleConstructionError, type RenderError } from '../errors/render';
7
7
 
8
8
  describe('renderer lifecycle', () => {
9
+ it('keeps cube capture in the existing renderer lifecycle boundary', () => {
10
+ const calls: string[] = [];
11
+ const frame = () => {
12
+ calls.push('extract');
13
+ calls.push('encode');
14
+ calls.push('finish');
15
+ calls.push('submit');
16
+ };
17
+ frame();
18
+ expect(calls).toEqual(['extract', 'encode', 'finish', 'submit']);
19
+ expect(calls.filter((call) => call === 'extract')).toHaveLength(1);
20
+ expect(calls.filter((call) => call === 'submit')).toHaveLength(1);
21
+ });
22
+
9
23
  it('rejects missing construction input', async () => {
10
24
  await expect(constructRenderer(undefined, { rhi })).rejects.toBeDefined();
11
25
  });
@@ -5,14 +5,20 @@ import {
5
5
  GPU_SHADER_STAGE_FRAGMENT,
6
6
  GPU_SHADER_STAGE_VERTEX,
7
7
  } from '../gpu-stage';
8
+ import { createHdrpBindGroupLayoutDescriptor } from '../pbr-pipeline';
8
9
 
9
10
  const ownerSource = readFileSync(new URL('../gpu-stage.ts', import.meta.url), 'utf8');
10
11
  const computeConsumerSources = [
11
12
  readFileSync(new URL('../hdrp-buffers.ts', import.meta.url), 'utf8'),
12
13
  ];
14
+ const pbrPipelineSource = readFileSync(new URL('../pbr-pipeline.ts', import.meta.url), 'utf8');
15
+ const iblPipelineCacheSource = readFileSync(
16
+ new URL('../ibl/IblPipelineCache.ts', import.meta.url),
17
+ 'utf8',
18
+ );
13
19
  const fragmentConsumerSources = [
14
- readFileSync(new URL('../pbr-pipeline.ts', import.meta.url), 'utf8'),
15
- readFileSync(new URL('../ibl/IblPipelineCache.ts', import.meta.url), 'utf8'),
20
+ pbrPipelineSource,
21
+ iblPipelineCacheSource,
16
22
  readFileSync(new URL('../ibl/skylight-bind-group.ts', import.meta.url), 'utf8'),
17
23
  readFileSync(new URL('../assembly/factory.ts', import.meta.url), 'utf8'),
18
24
  ];
@@ -37,7 +43,16 @@ describe('shader stage owner', () => {
37
43
  for (const source of computeConsumerSources) {
38
44
  expect(source).toContain('GPU_SHADER_STAGE_COMPUTE');
39
45
  }
40
- expect(fragmentConsumerSources[0]).toContain('GPU_SHADER_STAGE_VERTEX');
41
- expect(fragmentConsumerSources[1]).toContain('GPU_SHADER_STAGE_VERTEX');
46
+ expect(pbrPipelineSource).toContain('GPU_SHADER_STAGE_VERTEX');
47
+ expect(iblPipelineCacheSource).toContain('GPU_SHADER_STAGE_VERTEX');
48
+ });
49
+
50
+ it('keeps HDRP mesh storage visible to both mesh consumers', () => {
51
+ const group2 = createHdrpBindGroupLayoutDescriptor().entries?.find(
52
+ (entry) => entry.binding === 0,
53
+ );
54
+
55
+ expect(group2?.visibility).toBe(GPU_SHADER_STAGE_VERTEX | GPU_SHADER_STAGE_FRAGMENT);
56
+ expect(group2?.buffer?.type).toBe('read-only-storage');
42
57
  });
43
58
  });
@@ -0,0 +1,132 @@
1
+ import type { Mat4 } from '@forgeax/engine-math';
2
+ import type { Buffer, RhiDevice } from '@forgeax/engine-rhi';
3
+ import { ok } from '@forgeax/engine-rhi';
4
+ import { describe, expect, it } from 'vitest';
5
+ import {
6
+ selectHdrpPbrPrewarmVariants,
7
+ selectPipelineLayoutForVariant,
8
+ selectSkinPrewarmVariants,
9
+ } from '../assembly/factory';
10
+ import { resolvePipelineGroup2Contract } from '../pbr-pipeline';
11
+ import { createSkinPaletteAllocator } from '../systems/skin-palette-allocator';
12
+
13
+ describe('custom skinned mesh motion consumer contract', () => {
14
+ it('keeps two animated palette payloads observable at the skin consumer', () => {
15
+ const writes: Array<{ buffer: Buffer; offset: number; data: Float32Array }> = [];
16
+ const device = {
17
+ limits: { maxStorageBufferBindingSize: 65536 },
18
+ createBuffer: () => ok({} as Buffer),
19
+ queue: {
20
+ writeBuffer(buffer: Buffer, offset: number, data: Float32Array) {
21
+ writes.push({ buffer, offset, data: new Float32Array(data) });
22
+ return ok(undefined);
23
+ },
24
+ },
25
+ } as unknown as RhiDevice;
26
+ const allocator = createSkinPaletteAllocator(device, 65536, true);
27
+ const ibm = new Float32Array(16);
28
+ ibm[0] = 1;
29
+ ibm[5] = 1;
30
+ ibm[10] = 1;
31
+ ibm[15] = 1;
32
+ const poseA = new Float32Array(ibm) as unknown as Mat4;
33
+ const poseB = new Float32Array(ibm) as unknown as Mat4;
34
+ poseB[12] = 0.5;
35
+ const first = allocator.allocateSlice(1);
36
+ allocator.writeJointPalette(first, [ibm], [poseA]);
37
+ allocator.resetForFrame();
38
+ const second = allocator.allocateSlice(1);
39
+ allocator.writeJointPalette(second, [ibm], [poseB]);
40
+
41
+ expect(writes).toHaveLength(2);
42
+ expect(writes[0]?.buffer).toBe(writes[1]?.buffer);
43
+ expect(writes[0]?.offset).toBe(writes[1]?.offset);
44
+ expect(writes[0]?.data).not.toEqual(writes[1]?.data);
45
+ expect(writes[1]?.data[12]).toBe(0.5);
46
+ });
47
+
48
+ it('prepares both URP and HDRP pbr-skin consumers with the skin-cluster contract', () => {
49
+ const variants = selectSkinPrewarmVariants(
50
+ {
51
+ identifier: 'forgeax::pbr-skin',
52
+ sourcePath: 'default-standard-pbr-skin.wgsl',
53
+ composedWgsl: '@group(2) @binding(1) var<storage> palette: array<u32>;',
54
+ paramSchema: '[]',
55
+ variants: [
56
+ {
57
+ definesKey:
58
+ 'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=true+VERTEX_COLOR_AVAILABLE=false',
59
+ defines: {
60
+ CLUSTER_FORWARD_AVAILABLE: false,
61
+ STORAGE_BUFFER_AVAILABLE: true,
62
+ VERTEX_COLOR_AVAILABLE: false,
63
+ },
64
+ composedWgsl: '@group(2) @binding(1) var<storage> palette: array<u32>;',
65
+ },
66
+ {
67
+ definesKey:
68
+ 'CLUSTER_FORWARD_AVAILABLE=true+STORAGE_BUFFER_AVAILABLE=true+VERTEX_COLOR_AVAILABLE=false',
69
+ defines: {
70
+ CLUSTER_FORWARD_AVAILABLE: true,
71
+ STORAGE_BUFFER_AVAILABLE: true,
72
+ VERTEX_COLOR_AVAILABLE: false,
73
+ },
74
+ composedWgsl:
75
+ '@group(2) @binding(1) var<storage> palette: array<u32>; @group(2) @binding(4) var<storage> lights: array<u32>;',
76
+ },
77
+ ],
78
+ },
79
+ true,
80
+ );
81
+ expect(variants.map((variant) => variant.definesKey)).toEqual([
82
+ 'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=true+VERTEX_COLOR_AVAILABLE=false',
83
+ 'CLUSTER_FORWARD_AVAILABLE=true+STORAGE_BUFFER_AVAILABLE=true+VERTEX_COLOR_AVAILABLE=false',
84
+ ]);
85
+ expect(resolvePipelineGroup2Contract(variants[1]?.composedWgsl ?? '')).toBe('skin-cluster');
86
+ expect(
87
+ selectPipelineLayoutForVariant(
88
+ {
89
+ pbrPipelineLayout: 'urp' as never,
90
+ hdrpPbrPipelineLayout: 'hdrp' as never,
91
+ pbrSkinPipelineLayout: 'skin-urp' as never,
92
+ hdrpSkinPipelineLayout: 'skin-hdrp' as never,
93
+ },
94
+ variants[1]?.definesKey,
95
+ 'hdrp-skin',
96
+ ),
97
+ ).toBe('skin-hdrp');
98
+ });
99
+
100
+ it('does not expose clustered skin or HDRP PBR prewarm variants on WebGL2', () => {
101
+ const variants = [
102
+ {
103
+ definesKey: 'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=false',
104
+ defines: {
105
+ CLUSTER_FORWARD_AVAILABLE: false,
106
+ STORAGE_BUFFER_AVAILABLE: false,
107
+ },
108
+ composedWgsl: 'urp-uniform',
109
+ },
110
+ {
111
+ definesKey: 'CLUSTER_FORWARD_AVAILABLE=true+STORAGE_BUFFER_AVAILABLE=false',
112
+ defines: {
113
+ CLUSTER_FORWARD_AVAILABLE: true,
114
+ STORAGE_BUFFER_AVAILABLE: false,
115
+ },
116
+ composedWgsl: 'hdrp-uniform',
117
+ },
118
+ ];
119
+ const entry = {
120
+ identifier: 'forgeax::default-standard-pbr-skin',
121
+ sourcePath: 'default-standard-pbr-skin.wgsl',
122
+ composedWgsl: 'default',
123
+ paramSchema: '[]',
124
+ variants,
125
+ };
126
+
127
+ expect(selectSkinPrewarmVariants(entry, false).map((variant) => variant.definesKey)).toEqual([
128
+ 'CLUSTER_FORWARD_AVAILABLE=false+STORAGE_BUFFER_AVAILABLE=false',
129
+ ]);
130
+ expect(selectHdrpPbrPrewarmVariants(entry, false, true)).toEqual([]);
131
+ });
132
+ });
@@ -3,7 +3,9 @@ import { fileURLToPath } from 'node:url';
3
3
  import { describe, expect, it } from 'vitest';
4
4
  import { resolveMaterialShaderVariantSet } from '../assembly/factory';
5
5
  import {
6
+ createPbrSkinMeshBindGroupEntries,
6
7
  isSkinnedShadowCasterVariant,
8
+ pbrSkinMeshDynamicOffsets,
7
9
  SHADOW_CASTER_SHADER_ID,
8
10
  shadowCasterVariantSet,
9
11
  } from '../pbr-pipeline.js';
@@ -96,8 +98,15 @@ describe('skinned shadow caster', () => {
96
98
  expect(recordSource).toContain(
97
99
  "'shadow-caster',\n undefined,\n undefined,\n undefined,\n undefined,\n undefined,\n undefined,\n undefined,\n undefined,\n 'pbr'",
98
100
  );
99
- expect(recordSource).toContain('shadowPass.setBindGroup(2, skinMeshBindGroup, [');
100
- expect(recordSource).toContain('entry.source.skin.byteOffset');
101
+ expect(recordSource).toContain('createPbrSkinMeshBindGroupEntries(');
102
+ expect(recordSource).toContain('pbrSkinMeshDynamicOffsets(');
103
+
104
+ const meshBuffer = {} as never;
105
+ const paletteBuffer = {} as never;
106
+ const entries = createPbrSkinMeshBindGroupEntries(meshBuffer, 64, paletteBuffer, 128);
107
+ expect(entries.map((entry) => entry.binding)).toEqual([0, 1, 2]);
108
+ expect(entries[1]?.resource).toEqual(entries[2]?.resource);
109
+ expect(pbrSkinMeshDynamicOffsets(256, 512)).toEqual([256, 512, 512]);
101
110
  });
102
111
 
103
112
  it('keeps low-limit shadow view bindings paired with the optional projector layout', () => {