@forgeax/engine-render 0.1.29 → 0.1.31

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/README.md +248 -21
  2. package/dist/assembly/atmosphere-shader-prewarm.d.ts +1 -1
  3. package/dist/assembly/atmosphere-shader-prewarm.d.ts.map +1 -1
  4. package/dist/assembly/device-feature-admission.d.ts +4 -3
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -1
  6. package/dist/assembly/dynamic-geometry-host.d.ts +6 -0
  7. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  8. package/dist/assembly/host-contract.d.ts +6 -0
  9. package/dist/assembly/host-contract.d.ts.map +1 -1
  10. package/dist/assembly/material/assembly.d.ts +26 -3
  11. package/dist/assembly/material/assembly.d.ts.map +1 -1
  12. package/dist/assembly/material/pipeline-helpers.d.ts +24 -0
  13. package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -0
  14. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  15. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
  16. package/dist/assembly/recovery/recovery-attempt.d.ts +1 -6
  17. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -1
  18. package/dist/assembly/recovery/renderer-recover.d.ts +1 -0
  19. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  20. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  21. package/dist/assembly/shader-prewarm-policy.d.ts +1 -1
  22. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -1
  23. package/dist/assembly/webgpu-ready.d.ts +1 -1
  24. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  25. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  26. package/dist/capture/scene-projection.d.ts +41 -0
  27. package/dist/capture/scene-projection.d.ts.map +1 -0
  28. package/dist/{chunk-BQVQOQJH.mjs → chunk-7MQ3XBSP.mjs} +20 -14
  29. package/dist/chunk-7MQ3XBSP.mjs.map +1 -0
  30. package/dist/{chunk-S4DNKQYQ.mjs → chunk-C3DDSXYK.mjs} +86 -33
  31. package/dist/chunk-C3DDSXYK.mjs.map +1 -0
  32. package/dist/{chunk-BTWH7BP4.mjs → chunk-FUUO4UND.mjs} +73 -29
  33. package/dist/chunk-FUUO4UND.mjs.map +1 -0
  34. package/dist/{chunk-NRNXBLIJ.mjs → chunk-JRBDO2KB.mjs} +46 -6
  35. package/dist/chunk-JRBDO2KB.mjs.map +1 -0
  36. package/dist/{chunk-NICXSHAT.mjs → chunk-MDQJPY3T.mjs} +8 -6
  37. package/dist/chunk-MDQJPY3T.mjs.map +1 -0
  38. package/dist/{chunk-JQTSEI3Z.mjs → chunk-NO4WZCAH.mjs} +3 -3
  39. package/dist/chunk-NO4WZCAH.mjs.map +1 -0
  40. package/dist/{chunk-FUODTTIA.mjs → chunk-PSEQ6QPH.mjs} +621 -108
  41. package/dist/chunk-PSEQ6QPH.mjs.map +1 -0
  42. package/dist/{chunk-33XZFTQU.mjs → chunk-S5UE2T5P.mjs} +48 -6
  43. package/dist/chunk-S5UE2T5P.mjs.map +1 -0
  44. package/dist/components/directional-light.d.ts +2 -2
  45. package/dist/components/dynamic-resolution.d.ts +15 -0
  46. package/dist/components/dynamic-resolution.d.ts.map +1 -0
  47. package/dist/components/index.d.ts +1 -0
  48. package/dist/components/index.d.ts.map +1 -1
  49. package/dist/components/light-helpers.d.ts.map +1 -1
  50. package/dist/components/reflection-probe.d.ts +1 -0
  51. package/dist/components/reflection-probe.d.ts.map +1 -1
  52. package/dist/components/skylight.d.ts +4 -1
  53. package/dist/components/skylight.d.ts.map +1 -1
  54. package/dist/construct-renderer.mjs +2172 -796
  55. package/dist/construct-renderer.mjs.map +1 -1
  56. package/dist/device/device-scope.d.ts +2 -0
  57. package/dist/device/device-scope.d.ts.map +1 -1
  58. package/dist/device/gpu-residency.d.ts +2 -2
  59. package/dist/device/gpu-residency.d.ts.map +1 -1
  60. package/dist/dynamic-geometry.d.ts.map +1 -1
  61. package/dist/environment/background.d.ts +3 -2
  62. package/dist/environment/background.d.ts.map +1 -1
  63. package/dist/environment/ibl.d.ts +11 -0
  64. package/dist/environment/ibl.d.ts.map +1 -0
  65. package/dist/environment/storage.d.ts +14 -0
  66. package/dist/environment/storage.d.ts.map +1 -0
  67. package/dist/errors/gpu-driven.d.ts +2 -2
  68. package/dist/errors/gpu-driven.d.ts.map +1 -1
  69. package/dist/errors/render.d.ts +35 -1
  70. package/dist/errors/render.d.ts.map +1 -1
  71. package/dist/extract/camera.d.ts.map +1 -1
  72. package/dist/extract/contracts.d.ts +7 -0
  73. package/dist/extract/contracts.d.ts.map +1 -1
  74. package/dist/extract/gpu-driven.d.ts +18 -1
  75. package/dist/extract/gpu-driven.d.ts.map +1 -1
  76. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  77. package/dist/gpu-driven/material-artifacts.d.ts +24 -0
  78. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -0
  79. package/dist/gpu-driven/material-bindings.d.ts +7 -0
  80. package/dist/gpu-driven/material-bindings.d.ts.map +1 -0
  81. package/dist/gpu-driven/prepared-draw.d.ts +2 -0
  82. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  83. package/dist/gpu-driven/production-raster.d.ts +27 -6
  84. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  85. package/dist/gpu-resource.d.ts +5 -2
  86. package/dist/gpu-resource.d.ts.map +1 -1
  87. package/dist/gpu-scene-schema.d.ts.map +1 -1
  88. package/dist/gpu-scene.d.ts.map +1 -1
  89. package/dist/hdrp-buffers.d.ts +3 -0
  90. package/dist/hdrp-buffers.d.ts.map +1 -1
  91. package/dist/ibl/IblPipelineCache.d.ts +2 -0
  92. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  93. package/dist/ibl/skylight-bind-group.d.ts +3 -1
  94. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  95. package/dist/index.d.ts +83 -81
  96. package/dist/index.d.ts.map +1 -1
  97. package/dist/index.mjs +7 -5
  98. package/dist/index.mjs.map +1 -1
  99. package/dist/inspection-types.d.ts +16 -1
  100. package/dist/inspection-types.d.ts.map +1 -1
  101. package/dist/internal.mjs +6 -6
  102. package/dist/material-row.d.ts +10 -0
  103. package/dist/material-row.d.ts.map +1 -1
  104. package/dist/pbr-pipeline.d.ts +3 -1
  105. package/dist/pbr-pipeline.d.ts.map +1 -1
  106. package/dist/pipeline/render-extent.d.ts +51 -0
  107. package/dist/pipeline/render-extent.d.ts.map +1 -0
  108. package/dist/pipeline/standard-build-context.d.ts +7 -0
  109. package/dist/pipeline/standard-build-context.d.ts.map +1 -0
  110. package/dist/pipeline/standard-forward-lane.d.ts +3 -2
  111. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  112. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  113. package/dist/pipeline/standard-post.d.ts +1 -1
  114. package/dist/pipeline/standard-post.d.ts.map +1 -1
  115. package/dist/pipeline/standard-profile.d.ts +2 -1
  116. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  117. package/dist/pipeline-builder.d.ts +3 -1
  118. package/dist/pipeline-builder.d.ts.map +1 -1
  119. package/dist/plugin.d.ts.map +1 -1
  120. package/dist/post-process-errors.d.ts +9 -2
  121. package/dist/post-process-errors.d.ts.map +1 -1
  122. package/dist/prepare/extended-lighting/resources.d.ts +3 -3
  123. package/dist/record/frame-lighting.d.ts.map +1 -1
  124. package/dist/record/frame-snapshot.d.ts +3 -8
  125. package/dist/record/frame-snapshot.d.ts.map +1 -1
  126. package/dist/record/frame.d.ts +3 -2
  127. package/dist/record/frame.d.ts.map +1 -1
  128. package/dist/record/main-pass-geometry.d.ts +7 -2
  129. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  130. package/dist/record/main-pass-material.d.ts +6 -3
  131. package/dist/record/main-pass-material.d.ts.map +1 -1
  132. package/dist/record/main-pass.d.ts +2 -0
  133. package/dist/record/main-pass.d.ts.map +1 -1
  134. package/dist/record/prepared-material-bindings.d.ts.map +1 -1
  135. package/dist/record/recovery-pipeline.d.ts +9 -1
  136. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  137. package/dist/record/render-context.d.ts +34 -3
  138. package/dist/record/render-context.d.ts.map +1 -1
  139. package/dist/record/shadow-pass.d.ts +2 -2
  140. package/dist/record/shadow-pass.d.ts.map +1 -1
  141. package/dist/record/typed-frame-graph.d.ts +5 -2
  142. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  143. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  144. package/dist/reflection/background.d.ts +14 -0
  145. package/dist/reflection/background.d.ts.map +1 -0
  146. package/dist/reflection/filter.d.ts.map +1 -1
  147. package/dist/reflection/projection.d.ts +1 -0
  148. package/dist/reflection/projection.d.ts.map +1 -1
  149. package/dist/reflection/record-owner.d.ts +4 -2
  150. package/dist/reflection/record-owner.d.ts.map +1 -1
  151. package/dist/render-contract.d.ts +47 -32
  152. package/dist/render-contract.d.ts.map +1 -1
  153. package/dist/render-graph-primitives.d.ts.map +1 -1
  154. package/dist/render-pipeline.d.ts +17 -1
  155. package/dist/render-pipeline.d.ts.map +1 -1
  156. package/dist/render-system-extract-tail.d.ts +9 -2
  157. package/dist/render-system-extract-tail.d.ts.map +1 -1
  158. package/dist/render-system-extract.d.ts +29 -2
  159. package/dist/render-system-extract.d.ts.map +1 -1
  160. package/dist/render-system.d.ts.map +1 -1
  161. package/dist/scene/render-scene.d.ts +2 -0
  162. package/dist/scene/render-scene.d.ts.map +1 -1
  163. package/dist/ssao-config.d.ts +8 -6
  164. package/dist/ssao-config.d.ts.map +1 -1
  165. package/dist/ssr/graph.d.ts.map +1 -1
  166. package/dist/temporal/gpu.d.ts.map +1 -1
  167. package/dist/temporal/index.mjs +5 -5
  168. package/dist/temporal/standard-scene-data.d.ts +2 -1
  169. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  170. package/dist/temporal/target-coverage-attachment.d.ts +15 -0
  171. package/dist/temporal/target-coverage-attachment.d.ts.map +1 -0
  172. package/dist/temporal/temporal-view.d.ts +6 -2
  173. package/dist/temporal/temporal-view.d.ts.map +1 -1
  174. package/dist/temporal/view.d.ts +6 -0
  175. package/dist/temporal/view.d.ts.map +1 -1
  176. package/dist/typed-render-graph-primitives.d.ts +6 -0
  177. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  178. package/dist/volume/component.d.ts +1 -0
  179. package/dist/volume/component.d.ts.map +1 -1
  180. package/package.json +21 -21
  181. package/src/__tests__/capture-scene-projection.unit.test.ts +24 -0
  182. package/src/__tests__/device-feature-admission.unit.test.ts +15 -24
  183. package/src/__tests__/device-loss-fanout.unit.test.ts +43 -0
  184. package/src/__tests__/dynamic-geometry-batch.unit.test.ts +157 -0
  185. package/src/__tests__/dynamic-geometry.unit.test.ts +36 -0
  186. package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +81 -0
  187. package/src/__tests__/factory-contract.integration.test.ts +164 -1
  188. package/src/__tests__/frame-observation.contract.test.ts +21 -0
  189. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +1 -1
  190. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +20 -1
  191. package/src/__tests__/gpu-driven-lod-cache.unit.test.ts +0 -3
  192. package/src/__tests__/gpu-driven-mesh-world-identity.unit.test.ts +0 -3
  193. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +11 -6
  194. package/src/__tests__/gpu-driven-production.integration.test.ts +13 -25
  195. package/src/__tests__/gpu-driven-view.unit.test.ts +1 -0
  196. package/src/__tests__/gpu-pass-timing-browser-runner.ts +15 -0
  197. package/src/__tests__/gpu-pass-timing.browser.test.ts +14 -1
  198. package/src/__tests__/gpu-resource-scope.unit.test.ts +71 -0
  199. package/src/__tests__/gpu-scene.unit.test.ts +55 -1
  200. package/src/__tests__/materials-standard-contract.unit.test.ts +5 -1
  201. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +8 -0
  202. package/src/__tests__/morph-neutral-admission.unit.test.ts +19 -0
  203. package/src/__tests__/physical-clearcoat.integration.test.ts +6 -1
  204. package/src/__tests__/post-process-error-policy-owner.unit.test.ts +6 -5
  205. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +216 -5
  206. package/src/__tests__/reflection-fallback-async-publication.unit.test.ts +68 -1
  207. package/src/__tests__/reflection-probe-authoring.unit.test.ts +12 -10
  208. package/src/__tests__/reflection-probe-box.dawn.test.ts +71 -0
  209. package/src/__tests__/reflection-probe-filter.unit.test.ts +9 -0
  210. package/src/__tests__/reflection-probe-orientation.dawn.test.ts +294 -0
  211. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +5 -15
  212. package/src/__tests__/reflection-probe-updates.unit.test.ts +129 -0
  213. package/src/__tests__/render-error-code-owner.test-d.ts +3 -0
  214. package/src/__tests__/render-error-detail-ssot.test-d.ts +5 -1
  215. package/src/__tests__/render-extent.unit.test.ts +63 -0
  216. package/src/__tests__/render-scene-projection.unit.test.ts +3 -2
  217. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +173 -9
  218. package/src/__tests__/shader-manifest-fixture.ts +6 -0
  219. package/src/__tests__/skinned-shadow-caster.test.ts +8 -1
  220. package/src/__tests__/specular-environment-scale.dawn.test.ts +4 -2
  221. package/src/__tests__/ssao-config.unit.test.ts +7 -0
  222. package/src/__tests__/ssao-surface.dawn.test.ts +252 -0
  223. package/src/__tests__/ssr-cube-silhouette.dawn.test.ts +270 -0
  224. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +51 -43
  225. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +4 -0
  226. package/src/__tests__/ssr-gpu-dispatch.ts +10 -5
  227. package/src/__tests__/ssr-graph-topology.unit.test.ts +2 -2
  228. package/src/__tests__/ssr-hiz.browser.test.ts +1 -1
  229. package/src/__tests__/ssr-hiz.dawn.test.ts +1 -1
  230. package/src/__tests__/ssr-spatial.integration.test.ts +1 -1
  231. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +40 -0
  232. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +238 -1
  233. package/src/__tests__/standard-profile-oracle.unit.test.ts +27 -0
  234. package/src/__tests__/taa-coverage-edge.dawn.test.ts +6 -0
  235. package/src/__tests__/taa-neighborhood-equivalence.dawn.test.ts +5 -1
  236. package/src/__tests__/taa-stability-feedback.ts +1 -0
  237. package/src/__tests__/temporal-reset-matrix.unit.test.ts +2 -2
  238. package/src/__tests__/temporal-view.unit.test.ts +17 -0
  239. package/src/__tests__/transmission-prewarm.unit.test.ts +57 -0
  240. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +62 -13
  241. package/src/__tests__/volumetric-fog-topology.unit.test.ts +7 -7
  242. package/src/assembly/atmosphere-shader-prewarm.ts +3 -2
  243. package/src/assembly/device-feature-admission.ts +9 -4
  244. package/src/assembly/dynamic-geometry-host.ts +236 -163
  245. package/src/assembly/host-contract.ts +9 -0
  246. package/src/assembly/material/assembly.ts +350 -6
  247. package/src/assembly/material/pipeline-helpers.ts +100 -0
  248. package/src/assembly/material-shader-policy.ts +39 -18
  249. package/src/assembly/recovery/device-loss-fanout.ts +5 -0
  250. package/src/assembly/recovery/recovery-attempt.ts +2 -29
  251. package/src/assembly/recovery/renderer-recover.ts +6 -9
  252. package/src/assembly/renderer-facade.ts +22 -1
  253. package/src/assembly/shader-prewarm-policy.ts +14 -1
  254. package/src/assembly/webgpu-ready.ts +28 -19
  255. package/src/assembly/webgpu-renderer.ts +95 -95
  256. package/src/capture/scene-projection.ts +37 -0
  257. package/src/components/__tests__/dynamic-resolution-schema.unit.test.ts +65 -0
  258. package/src/components/__tests__/light-radius.integration.test.ts +59 -0
  259. package/src/components/directional-light.ts +3 -3
  260. package/src/components/dynamic-resolution.ts +67 -0
  261. package/src/components/index.ts +1 -0
  262. package/src/components/light-helpers.ts +6 -1
  263. package/src/components/reflection-probe.ts +1 -0
  264. package/src/components/skylight.ts +4 -1
  265. package/src/device/device-scope.ts +13 -7
  266. package/src/device/gpu-residency.ts +11 -11
  267. package/src/dynamic-geometry.ts +13 -8
  268. package/src/environment/background.ts +30 -12
  269. package/src/environment/ibl.ts +211 -0
  270. package/src/environment/storage.ts +63 -0
  271. package/src/errors/gpu-driven.ts +10 -3
  272. package/src/errors/render.ts +64 -0
  273. package/src/extract/camera.ts +12 -1
  274. package/src/extract/contracts.ts +10 -0
  275. package/src/extract/gpu-driven.ts +72 -17
  276. package/src/gpu-driven/batch-topology.ts +33 -11
  277. package/src/gpu-driven/material-artifacts.ts +179 -0
  278. package/src/gpu-driven/material-bindings.ts +41 -0
  279. package/src/gpu-driven/prepared-draw.ts +13 -2
  280. package/src/gpu-driven/production-raster.ts +681 -164
  281. package/src/gpu-resource.ts +21 -2
  282. package/src/gpu-scene-schema.ts +11 -2
  283. package/src/gpu-scene.ts +14 -3
  284. package/src/hdrp-buffers.ts +69 -0
  285. package/src/ibl/IblPipelineCache.ts +5 -0
  286. package/src/ibl/skylight-bind-group.ts +27 -17
  287. package/src/index.ts +92 -81
  288. package/src/inspection-types.ts +16 -1
  289. package/src/material-row.ts +60 -11
  290. package/src/pbr-pipeline.ts +13 -2
  291. package/src/pipeline/render-extent.ts +111 -0
  292. package/src/pipeline/standard-build-context.ts +7 -0
  293. package/src/pipeline/standard-forward-lane.ts +41 -24
  294. package/src/pipeline/standard-pipeline.ts +47 -18
  295. package/src/pipeline/standard-post.ts +4 -0
  296. package/src/pipeline/standard-profile.ts +2 -1
  297. package/src/pipeline-builder.ts +12 -3
  298. package/src/plugin.ts +4 -0
  299. package/src/post-process-errors.ts +26 -18
  300. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +2 -2
  301. package/src/prepare/extended-lighting/resources.ts +3 -3
  302. package/src/record/__tests__/main-pass-geometry.unit.test.ts +68 -0
  303. package/src/record/__tests__/material-bg-cache.unit.test.ts +45 -0
  304. package/src/record/__tests__/material-slot-plan.unit.test.ts +9 -0
  305. package/src/record/__tests__/recovery-pipeline.unit.test.ts +47 -1
  306. package/src/record/__tests__/sprite-material-abi.unit.test.ts +7 -3
  307. package/src/record/__tests__/temporal-payload.unit.test.ts +2 -0
  308. package/src/record/frame-lighting.ts +3 -1
  309. package/src/record/frame-snapshot.ts +13 -11
  310. package/src/record/frame.ts +150 -74
  311. package/src/record/main-pass-geometry.ts +60 -9
  312. package/src/record/main-pass-material.ts +47 -19
  313. package/src/record/main-pass.ts +63 -4
  314. package/src/record/prepared-material-bindings.ts +4 -0
  315. package/src/record/recovery-pipeline.ts +56 -0
  316. package/src/record/render-context.ts +34 -3
  317. package/src/record/shadow-pass.ts +187 -50
  318. package/src/record/typed-frame-graph.ts +60 -3
  319. package/src/record/view-ubo.ts +1 -1
  320. package/src/recovery/render-system-candidate.ts +54 -11
  321. package/src/reflection/background.ts +151 -0
  322. package/src/reflection/filter.ts +3 -0
  323. package/src/reflection/projection.ts +16 -6
  324. package/src/reflection/record-owner.ts +282 -86
  325. package/src/render-contract.ts +50 -32
  326. package/src/render-graph-primitives.ts +28 -3
  327. package/src/render-pipeline.ts +20 -1
  328. package/src/render-system-extract-tail.ts +23 -3
  329. package/src/render-system-extract.ts +127 -42
  330. package/src/render-system.ts +35 -7
  331. package/src/scene/render-scene.ts +8 -0
  332. package/src/ssao-config.ts +25 -8
  333. package/src/ssr/graph.ts +78 -58
  334. package/src/temporal/gpu.ts +5 -0
  335. package/src/temporal/standard-scene-data.ts +7 -1
  336. package/src/temporal/target-coverage-attachment.ts +72 -0
  337. package/src/temporal/temporal-view.ts +10 -4
  338. package/src/temporal/view.ts +12 -2
  339. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +24 -9
  340. package/src/typed-render-graph-primitives.ts +38 -0
  341. package/src/volume/component.ts +1 -0
  342. package/src/volume/passes.ts +3 -3
  343. package/dist/chunk-33XZFTQU.mjs.map +0 -1
  344. package/dist/chunk-BQVQOQJH.mjs.map +0 -1
  345. package/dist/chunk-BTWH7BP4.mjs.map +0 -1
  346. package/dist/chunk-FUODTTIA.mjs.map +0 -1
  347. package/dist/chunk-JQTSEI3Z.mjs.map +0 -1
  348. package/dist/chunk-NICXSHAT.mjs.map +0 -1
  349. package/dist/chunk-NRNXBLIJ.mjs.map +0 -1
  350. package/dist/chunk-S4DNKQYQ.mjs.map +0 -1
@@ -10,6 +10,7 @@ const material = {
10
10
  baseColor: new Float32Array([1, 1, 1]),
11
11
  metallic: 0,
12
12
  roughness: 1,
13
+ materialShaderId: 'forgeax::default-standard-pbr',
13
14
  } as MaterialSnapshot;
14
15
 
15
16
  function snapshot(entityKey: number, x: number, resourceClass = 'plain'): RenderableSnapshot {
@@ -104,6 +104,21 @@ function drawTimingFrame(fixture: BrowserTimingFixture): {
104
104
  };
105
105
  }
106
106
 
107
+ export async function observeBrowserGpuPassTimingDisabled() {
108
+ const fixture = await createBrowserTimingFixture(false);
109
+ try {
110
+ const { receipt } = drawTimingFrame(fixture);
111
+ const requested = await fixture.renderer.observe(receipt, { include: ['timings'] });
112
+ const omitted = await fixture.renderer.observe(receipt, { include: ['draws'] });
113
+ if (!requested.ok) throw requested.error;
114
+ if (!omitted.ok) throw omitted.error;
115
+ return { requested: requested.value, omitted: omitted.value };
116
+ } finally {
117
+ await fixture.renderer.dispose();
118
+ fixture.canvas.remove();
119
+ }
120
+ }
121
+
107
122
  function adapterString(value: unknown): string | undefined {
108
123
  return typeof value === 'string' && value.length > 0 ? value : undefined;
109
124
  }
@@ -1,9 +1,22 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import { runBrowserGpuPassTiming } from './gpu-pass-timing-browser-runner.js';
2
+ import {
3
+ observeBrowserGpuPassTimingDisabled,
4
+ runBrowserGpuPassTiming,
5
+ } from './gpu-pass-timing-browser-runner.js';
3
6
 
4
7
  const browserReady = typeof navigator !== 'undefined' && navigator.gpu !== undefined;
5
8
 
6
9
  describe.skipIf(!browserReady)('GPU pass timing Browser dev-server runner', () => {
10
+ it('returns the standard unavailable observation when GPU timing is disabled', async () => {
11
+ const { requested, omitted } = await observeBrowserGpuPassTimingDisabled();
12
+ expect(requested.timings).toMatchObject({
13
+ status: 'unavailable',
14
+ reason: { code: 'gpu-timing-not-enabled' },
15
+ });
16
+ expect(requested.timings).not.toHaveProperty('frame');
17
+ expect(omitted.timings).toBeUndefined();
18
+ });
19
+
7
20
  // The first live lavapipe renderer pair can exceed Vitest's 15s default while
8
21
  // the shader manifest and timestamp-query path are cold; keep this bound
9
22
  // local to the real WebGPU owner instead of weakening the global timeout.
@@ -0,0 +1,71 @@
1
+ import { err, ok, type RhiDevice, RhiError } from '@forgeax/engine-rhi';
2
+ import { rhi } from '@forgeax/engine-rhi-null';
3
+ import { expect, it, vi } from 'vitest';
4
+ import { DeviceScope } from '../device/device-scope';
5
+ import { GpuBuffer, GpuTexture } from '../gpu-resource';
6
+
7
+ it.each([
8
+ 'buffer',
9
+ 'texture',
10
+ ] as const)('keeps %s ownership on failed destruction and releases it after successful retry', async (kind) => {
11
+ const device = (await (await rhi.requestAdapter()).unwrap().requestDevice()).unwrap();
12
+ const scope = DeviceScope.create(1, 'gpu-resource');
13
+ const method = kind === 'buffer' ? 'destroyBuffer' : 'destroyTexture';
14
+ const original = device[method].bind(device);
15
+ const spy = vi
16
+ .spyOn(device, method)
17
+ .mockReturnValueOnce(
18
+ err(
19
+ new RhiError({ code: 'webgpu-runtime-error', expected: 'injected failure', hint: 'retry' }),
20
+ ),
21
+ );
22
+ const resource =
23
+ kind === 'buffer'
24
+ ? new GpuBuffer(device, device.createBuffer({ size: 64, usage: 8 }).unwrap(), scope)
25
+ : new GpuTexture(
26
+ device,
27
+ device
28
+ .createTexture({
29
+ size: [1, 1, 1],
30
+ format: 'rgba8unorm',
31
+ usage: 4,
32
+ textureBindingViewDimension: '2d',
33
+ })
34
+ .unwrap(),
35
+ scope,
36
+ );
37
+ expect(scope.resourceDelta()).toBe(1);
38
+ expect(resource.destroy().ok).toBe(false);
39
+ expect(resource.isDestroyed).toBe(false);
40
+ expect(scope.resourceDelta()).toBe(1);
41
+ spy.mockImplementation(original as RhiDevice[typeof method]);
42
+ expect(resource.destroy().ok).toBe(true);
43
+ expect(resource.isDestroyed).toBe(true);
44
+ expect(scope.resourceDelta()).toBe(0);
45
+ scope.retire();
46
+ expect(spy).toHaveBeenCalledTimes(2);
47
+ expect(resource.destroy().ok).toBe(false);
48
+ expect(scope.resourceDelta()).toBe(0);
49
+ });
50
+
51
+ it('cleans remaining resources in reverse order after an earlier resource was released', async () => {
52
+ const device = (await (await rhi.requestAdapter()).unwrap().requestDevice()).unwrap();
53
+ const scope = DeviceScope.create(1, 'gpu-resource-order');
54
+ const resources = Array.from(
55
+ { length: 3 },
56
+ () => new GpuBuffer(device, device.createBuffer({ size: 64, usage: 8 }).unwrap(), scope),
57
+ );
58
+ const handles: unknown[] = [];
59
+ vi.spyOn(device, 'destroyBuffer').mockImplementation((handle) => {
60
+ handles.push(handle);
61
+ return ok(undefined);
62
+ });
63
+ const [first, second, third] = resources;
64
+ if (first === undefined || second === undefined || third === undefined)
65
+ throw new Error('missing fixture resource');
66
+ second.destroy().unwrap();
67
+ scope.retire();
68
+ scope.dispose();
69
+ expect(handles).toEqual([second.handle, third.handle, first.handle]);
70
+ expect(scope.resourceDelta()).toBe(0);
71
+ });
@@ -7,7 +7,7 @@ import { err } from '@forgeax/engine-types';
7
7
  import { describe, expect, it } from 'vitest';
8
8
  import { GpuScene } from '../gpu-scene';
9
9
  import { GPU_SCENE_LAYOUTS, gpuSceneFieldOffset } from '../gpu-scene-schema';
10
- import { packStandardPbrMaterialRow } from '../material-row';
10
+ import { packMaterialProgramRow, packStandardPbrMaterialRow } from '../material-row';
11
11
  import { makeZeroCameraFallbackSnapshot } from '../record/frame-snapshot';
12
12
  import type {
13
13
  ExtractedFrame,
@@ -176,6 +176,60 @@ describe('GpuScene', () => {
176
176
  ).toBeCloseTo(0.4);
177
177
  });
178
178
 
179
+ it('prefers extracted Standard PBR values over schema defaults', () => {
180
+ const row = packStandardPbrMaterialRow(createStandardPbrArtifactReceipt(), material);
181
+ const view = new DataView(row.buffer);
182
+ expect(
183
+ view.getFloat32(gpuSceneFieldOffset(GPU_SCENE_LAYOUTS.material, 'baseColor'), true),
184
+ ).toBeCloseTo(0.25);
185
+ expect(
186
+ view.getFloat32(gpuSceneFieldOffset(GPU_SCENE_LAYOUTS.material, 'metallic'), true),
187
+ ).toBeCloseTo(0.2);
188
+ expect(
189
+ view.getFloat32(gpuSceneFieldOffset(GPU_SCENE_LAYOUTS.material, 'roughness'), true),
190
+ ).toBeCloseTo(0.8);
191
+ });
192
+
193
+ it('packs a custom schema into the same scene row page', () => {
194
+ const row = packMaterialProgramRow(
195
+ [
196
+ { name: 'tint', type: 'color' },
197
+ { name: 'roughness', type: 'f32' },
198
+ ],
199
+ {
200
+ ...material,
201
+ materialShaderId: 'game::custom-surface',
202
+ materialParamSchema: [
203
+ { name: 'tint', type: 'color' },
204
+ { name: 'roughness', type: 'f32' },
205
+ ],
206
+ paramSnapshot: { tint: [0.1, 0.2, 0.3, 1], roughness: 0.7 },
207
+ },
208
+ );
209
+ expect(row).toBeDefined();
210
+ if (row === undefined) return;
211
+ expect(row.byteLength).toBe(GPU_SCENE_LAYOUTS.material.stride);
212
+ expect(new DataView(row.buffer).getFloat32(0, true)).toBeCloseTo(0.1);
213
+ expect(new DataView(row.buffer).getFloat32(16, true)).toBeCloseTo(0.7);
214
+ });
215
+
216
+ it('writes the last vector in a 512-byte page and rejects overflow', () => {
217
+ const schema = Array.from({ length: 32 }, (_, index) => ({
218
+ name: `value${index}`,
219
+ type: 'vec4' as const,
220
+ }));
221
+ const row = packMaterialProgramRow(schema, {
222
+ ...material,
223
+ paramSnapshot: { value31: [7, 8, 9, 10] },
224
+ });
225
+ if (row === undefined) throw new Error('512-byte row missing');
226
+ expect(row.byteLength).toBe(512);
227
+ expect(Array.from(new Float32Array(row.buffer, 496, 4))).toEqual([7, 8, 9, 10]);
228
+ expect(
229
+ packMaterialProgramRow([...schema, { name: 'overflow', type: 'f32' }], material),
230
+ ).toBeUndefined();
231
+ });
232
+
179
233
  it('exposes GPU scene facts as exact temporal coverage', () => {
180
234
  const coverage = classifySceneDataCoverage({
181
235
  contributors: [{ id: 'gpu-scene', kind: 'exact' }],
@@ -149,6 +149,10 @@ describe('Materials.standard clearcoat public contract', () => {
149
149
  { slot: 2, sampler, view },
150
150
  ],
151
151
  );
152
- expect(merged.slice(-6).map((entry) => entry.binding)).toEqual([26, 27, 28, 29, 30, 31]);
152
+ expect(
153
+ merged
154
+ .filter((entry) => entry.binding >= 26 && entry.binding < 46)
155
+ .map((entry) => entry.binding),
156
+ ).toEqual([26, 27, 28, 29, 30, 31]);
153
157
  });
154
158
  });
@@ -2,6 +2,7 @@ import { World } from '@forgeax/engine-ecs';
2
2
  import { createBoxGeometry } from '@forgeax/engine-geometry';
3
3
  import { rhi } from '@forgeax/engine-rhi-null';
4
4
  import { expect, it } from 'vitest';
5
+ import { DeviceScope } from '../device/device-scope';
5
6
  import { GpuResidencyCache } from '../device/gpu-residency';
6
7
 
7
8
  it('fences ordinary mesh submissions independently of candidates, including reversed completion', async () => {
@@ -15,6 +16,8 @@ it('fences ordinary mesh submissions independently of candidates, including reve
15
16
  },
16
17
  device.caps,
17
18
  );
19
+ const scope = DeviceScope.create(1, 'mesh-submission');
20
+ store.bindDeviceScope(scope);
18
21
  const world = new World();
19
22
  const mesh = createBoxGeometry(1, 1, 1).unwrap();
20
23
  const handle = world.allocSharedRef('MeshAsset', mesh);
@@ -44,10 +47,15 @@ it('fences ordinary mesh submissions independently of candidates, including reve
44
47
  finishSecond();
45
48
  await second;
46
49
  expect(original.vertexBuffer.isDestroyed).toBe(false);
50
+ expect(scope.resourceDelta()).toBe(4);
47
51
  finishFirst();
48
52
  await first;
49
53
  expect(original.vertexBuffer.isDestroyed).toBe(true);
54
+ expect(scope.resourceDelta()).toBe(2);
50
55
  expect(store.getMeshGpuHandles(handle, world)).toBe(replacement);
51
56
  expect(replacement.vertexBuffer.isDestroyed).toBe(false);
52
57
  store.destroyAll();
58
+ scope.retire();
59
+ expect(replacement.vertexBuffer.isDestroyed).toBe(true);
60
+ expect(scope.resourceDelta()).toBe(0);
53
61
  });
@@ -0,0 +1,19 @@
1
+ import type { MeshAsset } from '@forgeax/engine-types';
2
+ import { expect, it } from 'vitest';
3
+ import { morphSnapshotFor } from '../render-system-extract-tail';
4
+
5
+ it('idle morph authoring preserves the source stream; activating and clearing it changes admission', () => {
6
+ const mesh = {
7
+ vertices: new Float32Array(12),
8
+ morphTargets: [{ position: new Float32Array([1, 2, 3]) }],
9
+ } satisfies Pick<MeshAsset, 'vertices' | 'morphTargets'>;
10
+ const weights = new Float32Array([0]);
11
+ expect(morphSnapshotFor(mesh, weights)).toBeUndefined();
12
+ weights[0] = 0.25;
13
+ expect(morphSnapshotFor(mesh, weights)).toMatchObject({
14
+ weights: new Float32Array([0.25]),
15
+ targetCount: 1,
16
+ });
17
+ weights[0] = 0;
18
+ expect(morphSnapshotFor(mesh, weights)).toBeUndefined();
19
+ });
@@ -78,7 +78,7 @@ describe('Standard clearcoat root contract', () => {
78
78
  // their canonical fixed slots (26, 27, 30, 31), followed by the
79
79
  // scene-index row at binding 46. Compacting the authored root here shifts WGSL IBL bindings and
80
80
  // causes Dawn to reject the pipeline before any pixels are submitted.
81
- expect(descriptor.entries).toHaveLength(29);
81
+ expect(descriptor.entries).toHaveLength(30);
82
82
  expect(descriptor.entries.slice(0, 15).map((entry) => entry.binding)).toEqual(
83
83
  Array.from({ length: 15 }, (_, binding) => binding),
84
84
  );
@@ -89,6 +89,10 @@ describe('Standard clearcoat root contract', () => {
89
89
  expect(descriptor.entries.slice(24, 28).map((entry) => entry.binding)).toEqual([
90
90
  26, 27, 30, 31,
91
91
  ]);
92
+ expect(descriptor.entries[29]).toMatchObject({
93
+ binding: 47,
94
+ texture: { sampleType: 'float', viewDimension: 'cube' },
95
+ });
92
96
  expect(descriptor.entries[28]).toMatchObject({
93
97
  binding: 46,
94
98
  visibility: GPU_SHADER_STAGE_VERTEX | GPU_SHADER_STAGE_FRAGMENT,
@@ -118,6 +122,7 @@ describe('Standard clearcoat root contract', () => {
118
122
  expect(descriptor.entries.map((entry) => entry.binding)).toEqual([
119
123
  ...Array.from({ length: 10 }, (_, binding) => binding),
120
124
  46,
125
+ 47,
121
126
  ]);
122
127
  });
123
128
  });
@@ -13,6 +13,7 @@ const expectedCodes = [
13
13
  'fullscreen-input-not-found',
14
14
  'ssao-radius-non-positive',
15
15
  'ssao-bias-negative',
16
+ 'ssao-parameter-invalid',
16
17
  'params-size-mismatch',
17
18
  'params-update-size-mismatch',
18
19
  ] as const satisfies readonly PostProcessErrorCode[];
@@ -38,9 +39,9 @@ const evidence = [
38
39
  detail: { id: 'pkg::missing' },
39
40
  }),
40
41
  expected: 'typed fullscreen plan references a registered post-process id',
41
- hint: "no post-process is registered for id 'pkg::missing'. Register 'pkg::missing' with the feature host ({source, reads?}), then reference it from a typed fullscreen RenderFeaturePlan.",
42
+ hint: "no post-process is registered for id 'pkg::missing'. For Engine-owned effects, rebuild the shader manifest with that Engine entry enabled. For custom effects, register 'pkg::missing' with the feature host ({source, reads?}), then reference it from a typed fullscreen RenderFeaturePlan.",
42
43
  message:
43
- "post-process: post-process-not-found (no post-process is registered for id 'pkg::missing'. Register 'pkg::missing' with the feature host ({source, reads?}), then reference it from a typed fullscreen RenderFeaturePlan.)",
44
+ "post-process: post-process-not-found (no post-process is registered for id 'pkg::missing'. For Engine-owned effects, rebuild the shader manifest with that Engine entry enabled. For custom effects, register 'pkg::missing' with the feature host ({source, reads?}), then reference it from a typed fullscreen RenderFeaturePlan.)",
44
45
  },
45
46
  {
46
47
  code: 'fullscreen-input-not-found',
@@ -100,9 +101,9 @@ const evidence = [
100
101
  ];
101
102
 
102
103
  describe('PostProcessError policy ownership', () => {
103
- it('preserves the exact seven-code vocabulary owned by this policy', () => {
104
- expect(expectedCodes).toHaveLength(7);
105
- expect(new Set(expectedCodes).size).toBe(7);
104
+ it('preserves the closed parameter-validation vocabulary owned by this policy', () => {
105
+ expect(expectedCodes).toHaveLength(8);
106
+ expect(new Set(expectedCodes).size).toBe(8);
106
107
  expectTypeOf<PostProcessErrorCode>().toEqualTypeOf<ExpectedCodeUnion>();
107
108
  expectTypeOf<ExpectedCodeUnion>().toEqualTypeOf<PostProcessErrorCode>();
108
109
  const acceptsCode = (code: PostProcessErrorCode): PostProcessErrorCode => code;
@@ -1,7 +1,17 @@
1
+ import { createBoxGeometry } from '@forgeax/engine-geometry';
1
2
  import { vec3 } from '@forgeax/engine-math';
2
- import { createStandardPbrArtifactReceipt } from '@forgeax/engine-shader';
3
+ import {
4
+ createMaterialProgramArtifactReceipt,
5
+ createStandardPbrArtifactReceipt,
6
+ type ParamSchemaEntry,
7
+ } from '@forgeax/engine-shader';
8
+ import type { MeshAsset } from '@forgeax/engine-types';
3
9
  import { describe, expect, it } from 'vitest';
4
- import { buildGpuDrivenDraws, resolvePreparedGpuDrivenDraw } from '../extract/gpu-driven';
10
+ import {
11
+ buildGpuDrivenDraws,
12
+ gpuDrivenMaterialArtifactKey,
13
+ resolvePreparedGpuDrivenDraw,
14
+ } from '../extract/gpu-driven';
5
15
  import type { MaterialSnapshot, RenderableSnapshot } from '../render-system-extract';
6
16
 
7
17
  const textureNames = [
@@ -52,7 +62,9 @@ function snapshot(
52
62
  };
53
63
  }
54
64
 
55
- function geometry(source: ReturnType<typeof artifact>) {
65
+ function geometry(source: {
66
+ readonly receipt: ReturnType<typeof createStandardPbrArtifactReceipt>;
67
+ }) {
56
68
  return {
57
69
  identity: 'canonical-pbr-geometry',
58
70
  vertexInputs: source.receipt.vertexInputs,
@@ -61,8 +73,8 @@ function geometry(source: ReturnType<typeof artifact>) {
61
73
  };
62
74
  }
63
75
 
64
- function artifact() {
65
- const receipt = createStandardPbrArtifactReceipt();
76
+ function artifact(vertexColorAvailable = false) {
77
+ const receipt = createStandardPbrArtifactReceipt(false, vertexColorAvailable);
66
78
  return {
67
79
  material: 'forgeax::default-standard-pbr',
68
80
  pass: 'forward',
@@ -75,7 +87,174 @@ function artifact() {
75
87
  };
76
88
  }
77
89
 
90
+ function customArtifact() {
91
+ const schema = [
92
+ { name: 'tint', type: 'color' },
93
+ { name: 'roughnessBias', type: 'f32' },
94
+ ] as const satisfies readonly ParamSchemaEntry[];
95
+ const receipt = createMaterialProgramArtifactReceipt({
96
+ schema,
97
+ directEntry: 'vs_main',
98
+ sceneIndexEntry: 'vs_scene_index',
99
+ vertexInputs: [
100
+ { semantic: 'position', location: 0, format: 'float32x3' },
101
+ { semantic: 'normal', location: 1, format: 'float32x3' },
102
+ { semantic: 'uv', location: 2, format: 'float32x2' },
103
+ { semantic: 'tangent', location: 3, format: 'float32x4' },
104
+ ],
105
+ });
106
+ return {
107
+ material: 'game::custom-surface',
108
+ pass: 'forward',
109
+ wgsl: 'producer-composed-custom-surface',
110
+ layoutIdentity: receipt.reflection.layoutIdentity,
111
+ bindings: [],
112
+ deps: ['game::custom-surface'],
113
+ vertexInputs: receipt.vertexInputs.map((input) => ({ ...input })),
114
+ receipt,
115
+ };
116
+ }
117
+
78
118
  describe('prepared Standard PBR producer-to-record integration', () => {
119
+ it.each([
120
+ 'forgeax::default-standard-pbr',
121
+ 'physical-material::standard-clearcoat-factor-r',
122
+ ])('distinguishes canonical roots from legacy direct-only aliases: %s', (materialShaderId) => {
123
+ const source = { ...material(), materialShaderId };
124
+ const draws = buildGpuDrivenDraws({
125
+ mesh: createBoxGeometry(1, 1, 1).unwrap(),
126
+ materials: [source],
127
+ fallbackMaterial: source,
128
+ baseSnapshot: snapshot(source),
129
+ getMaterialShaderArtifact: () => undefined,
130
+ });
131
+ expect(draws).toHaveLength(1);
132
+ expect(draws[0]?.preparationError?.code).toBe(
133
+ materialShaderId.startsWith('forgeax::') ? 'missing-material-receipt' : undefined,
134
+ );
135
+ });
136
+
137
+ it('requires the published scene-index artifact for a custom surface', () => {
138
+ const source: MaterialSnapshot = {
139
+ ...material(),
140
+ materialShaderId: 'game::custom-surface-direct',
141
+ materialProgramKeys: { forward: 'game::custom-surface-direct' },
142
+ materialSceneIndexProgramKeys: {
143
+ forward: { specializationKey: 'game::custom-surface-scene', pass: 'forward' },
144
+ },
145
+ };
146
+ const requested: string[] = [];
147
+ const draws = buildGpuDrivenDraws({
148
+ mesh: createBoxGeometry(1, 1, 1).unwrap(),
149
+ materials: [source],
150
+ fallbackMaterial: source,
151
+ baseSnapshot: snapshot(source),
152
+ getMaterialShaderArtifact: (key) => {
153
+ requested.push(key);
154
+ return undefined;
155
+ },
156
+ });
157
+ expect(requested).toEqual(['game::custom-surface-scene']);
158
+ expect(draws).toHaveLength(1);
159
+ expect(draws[0]?.preparationError?.code).toBe('missing-material-receipt');
160
+ });
161
+
162
+ it('prepares a COLOR_0 geometry with the matching published receipt', () => {
163
+ const source = artifact(true);
164
+ const result = resolvePreparedGpuDrivenDraw({
165
+ snapshot: snapshot(),
166
+ artifact: source,
167
+ geometry: geometry(source),
168
+ generation: source.receipt.generation,
169
+ draw: snapshot().gpuDrivenDraws?.[0] as NonNullable<
170
+ RenderableSnapshot['gpuDrivenDraws']
171
+ >[number],
172
+ });
173
+
174
+ expect(result.ok).toBe(true);
175
+ if (!result.ok) return;
176
+ expect(result.value.receiptIdentity).toBe(source.receipt.receiptIdentity);
177
+ expect(result.value.vertexInputs).toContainEqual({
178
+ semantic: 'color',
179
+ location: 13,
180
+ format: 'float32x4',
181
+ });
182
+ });
183
+
184
+ it('selects the colored receipt while extracting a real colored mesh draw', () => {
185
+ const coloredSource = artifact(true);
186
+ const coloredMesh: MeshAsset = {
187
+ kind: 'mesh',
188
+ vertices: new Float32Array(36),
189
+ indices: new Uint16Array([0, 1, 2]),
190
+ attributes: {
191
+ position: new Float32Array(9),
192
+ normal: new Float32Array(9),
193
+ uv: new Float32Array(6),
194
+ tangent: new Float32Array(12),
195
+ color: new Float32Array(12),
196
+ },
197
+ submeshes: [
198
+ {
199
+ indexOffset: 0,
200
+ indexCount: 3,
201
+ vertexCount: 3,
202
+ materialSlot: 0,
203
+ topology: 'triangle-list',
204
+ },
205
+ ],
206
+ materialSlots: [{ slotName: 'default' }],
207
+ };
208
+ const selectedColorFacts: boolean[] = [];
209
+ const draws = buildGpuDrivenDraws({
210
+ mesh: coloredMesh,
211
+ materials: [material()],
212
+ fallbackMaterial: material(),
213
+ baseSnapshot: snapshot(),
214
+ getMaterialShaderArtifact: (_materialShaderId, request) => {
215
+ selectedColorFacts.push(request?.vertexColorAvailable === true);
216
+ return coloredSource;
217
+ },
218
+ });
219
+
220
+ expect(selectedColorFacts).toEqual([true]);
221
+ expect(draws).toHaveLength(1);
222
+ expect(draws[0]?.prepared?.vertexInputs).toContainEqual({
223
+ semantic: 'color',
224
+ location: 13,
225
+ format: 'float32x4',
226
+ });
227
+ expect(draws[0]?.preparationError).toBeUndefined();
228
+ });
229
+
230
+ it('keeps colored and colorless receipts on distinct production identities', () => {
231
+ const plain = artifact(false);
232
+ const colored = artifact(true);
233
+ const plainKey = gpuDrivenMaterialArtifactKey({
234
+ material: plain.material,
235
+ deformation: 'rigid',
236
+ receiptIdentity: plain.receipt.receiptIdentity,
237
+ receiptGeneration: plain.receipt.generation,
238
+ });
239
+ const coloredKey = gpuDrivenMaterialArtifactKey({
240
+ material: colored.material,
241
+ deformation: 'rigid',
242
+ receiptIdentity: colored.receipt.receiptIdentity,
243
+ receiptGeneration: colored.receipt.generation,
244
+ });
245
+
246
+ expect(plain.receipt.vertexInputs).not.toContainEqual(
247
+ expect.objectContaining({ semantic: 'color', location: 13 }),
248
+ );
249
+ expect(colored.receipt.vertexInputs).toContainEqual({
250
+ semantic: 'color',
251
+ location: 13,
252
+ format: 'float32x4',
253
+ });
254
+ expect(plain.receipt.receiptIdentity).not.toBe(colored.receipt.receiptIdentity);
255
+ expect(plainKey).not.toBe(coloredKey);
256
+ });
257
+
79
258
  it('carries one producer receipt to direct and scene-index preparation', () => {
80
259
  const source = artifact();
81
260
  const result = resolvePreparedGpuDrivenDraw({
@@ -224,4 +403,36 @@ describe('prepared Standard PBR producer-to-record integration', () => {
224
403
  });
225
404
  expect(result.ok).toBe(true);
226
405
  });
406
+
407
+ it('admits a producer-backed custom material without a Standard shader name', () => {
408
+ const source = customArtifact();
409
+ const customMaterial: MaterialSnapshot = {
410
+ ...material(),
411
+ materialShaderId: source.material,
412
+ materialParamSchema: [
413
+ { name: 'tint', type: 'color' },
414
+ { name: 'roughnessBias', type: 'f32' },
415
+ ],
416
+ paramSnapshot: { tint: [0.8, 0.25, 0.1, 1], roughnessBias: 0.12 },
417
+ textureHandles: undefined,
418
+ samplerHandles: undefined,
419
+ };
420
+ const baseSnapshot = snapshot(customMaterial);
421
+ const result = resolvePreparedGpuDrivenDraw({
422
+ snapshot: baseSnapshot,
423
+ artifact: source,
424
+ geometry: geometry(source),
425
+ generation: source.receipt.generation,
426
+ draw: baseSnapshot.gpuDrivenDraws?.[0] as NonNullable<
427
+ RenderableSnapshot['gpuDrivenDraws']
428
+ >[number],
429
+ });
430
+
431
+ expect(result.ok).toBe(true);
432
+ if (!result.ok) return;
433
+ expect(result.value.identity.material).toBe('game::custom-surface');
434
+ expect(result.value.materialRow.byteLength).toBe(512);
435
+ expect(result.value.materialRow.fields).toEqual(['tint', 'roughnessBias']);
436
+ expect(result.value.sceneIndexEntry).toBe('vs_scene_index');
437
+ });
227
438
  });
@@ -3,10 +3,12 @@ import type { RenderSystemInternals } from '../record/render-context';
3
3
  import { ReflectionProbeRecordOwner } from '../reflection/record-owner';
4
4
  import type { SkylightSnapshot } from '../render-system-extract';
5
5
 
6
- function fixture() {
6
+ function fixture(captureReadback = true) {
7
7
  const internals = {
8
8
  device: { caps: { rgba16floatRenderable: true } },
9
9
  deviceScope: { owner: 'test', generation: 0 },
10
+ assets: { catalogEpoch: 0 },
11
+ captureReflectionFallbackReadback: captureReadback,
10
12
  } as unknown as RenderSystemInternals;
11
13
  const owner = new ReflectionProbeRecordOwner(internals);
12
14
  const stage = (frame: number, source = 1) =>
@@ -101,4 +103,69 @@ describe('fallback asynchronous publication', () => {
101
103
  projectionGeneration: 1,
102
104
  });
103
105
  });
106
+
107
+ it('publishes changing sources after GPU completion without requesting pixel readback', async () => {
108
+ const { owner, stage } = fixture(false);
109
+ for (let frame = 1; frame <= 10; frame++) {
110
+ let finish!: () => void;
111
+ const completed = new Promise<void>((resolve) => {
112
+ finish = resolve;
113
+ });
114
+ // biome-ignore lint/complexity/useLiteralKeys: Exercise the real completed-output boundary.
115
+ const publication = owner['completeSubmission'](true, stage(frame, frame), completed, {
116
+ frameId: frame,
117
+ format: 'rgba16float',
118
+ size: { width: 1, height: 1 },
119
+ graphGeneration: 1,
120
+ textureIdentity: frame,
121
+ });
122
+ expect(owner.inspect().reflectionFallback.frameId).not.toBe(frame);
123
+ finish();
124
+ await publication;
125
+ expect(owner.inspect().reflectionFallback).toMatchObject({ state: 'active', frameId: frame });
126
+ expect(owner.inspect().reflectionFallbackReadback).toBeUndefined();
127
+ expect(owner.inspect().reflectionFallback).not.toHaveProperty('linearHdr');
128
+ expect(owner.inspect().reflectionFallbackInspection.failureCode).toBeUndefined();
129
+ }
130
+ });
131
+
132
+ it('does not publish non-neutral source metadata without an executed output', async () => {
133
+ const { owner, stage } = fixture(false);
134
+ // biome-ignore lint/complexity/useLiteralKeys: Missing graph output must remain fail-closed.
135
+ await owner['completeSubmission'](true, stage(1), Promise.resolve());
136
+ expect(owner.inspect().reflectionFallback.state).not.toBe('active');
137
+ expect(owner.inspect().reflectionFallbackInspection.failureCode).toBe(
138
+ 'reflection-fallback-output-missing',
139
+ );
140
+ });
141
+
142
+ it('requires new mapped proof after an asset catalog replacement', async () => {
143
+ const { owner, stage, complete, internals } = fixture();
144
+ await complete(stage(1), 1);
145
+ Object.assign(internals.assets, { catalogEpoch: 1 });
146
+ const replacement = stage(2);
147
+ // biome-ignore lint/complexity/useLiteralKeys: A changed catalog must not reuse the old source proof.
148
+ await owner['completeSubmission'](true, replacement, Promise.resolve());
149
+ expect(owner.inspect().reflectionFallback.state).not.toBe('active');
150
+ expect(owner.inspect().reflectionFallbackInspection.failureCode).toBe(
151
+ 'reflection-fallback-output-missing',
152
+ );
153
+ await complete(replacement, 2);
154
+ expect(owner.inspect().reflectionFallback).toMatchObject({ state: 'active', frameId: 2 });
155
+ expect(owner.inspect().reflectionFallbackReadback?.frameId).toBe(2);
156
+ });
157
+
158
+ it('requires fresh readback to promote a failed source-identical submission from LKG', async () => {
159
+ const { owner, stage, complete } = fixture();
160
+ await complete(stage(1), 1);
161
+ // biome-ignore lint/complexity/useLiteralKeys: Fail at the real submission owner boundary.
162
+ await owner['completeSubmission'](false, stage(2), Promise.resolve());
163
+ expect(owner.inspect().reflectionFallback.state).toBe('lkg');
164
+ const retry = stage(3);
165
+ // biome-ignore lint/complexity/useLiteralKeys: An old proof must not turn a failed publication active again.
166
+ await owner['completeSubmission'](true, retry, Promise.resolve());
167
+ expect(owner.inspect().reflectionFallback).toMatchObject({ state: 'lkg', frameId: 1 });
168
+ await complete(retry, 3);
169
+ expect(owner.inspect().reflectionFallback).toMatchObject({ state: 'active', frameId: 3 });
170
+ });
104
171
  });