@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
@@ -57,15 +57,17 @@ describe('ReflectionProbe authoring and admission', () => {
57
57
  it('admits the profile boundary and rejects the first over-budget probe', () => {
58
58
  const probe = { halfExtents: [1, 1, 1] as const, priority: 0, intensity: 1, resolution: 256 };
59
59
  const bytes = estimateReflectionProbeBytes(probe.resolution);
60
- expect(bytes * DEFAULT_REFLECTION_PROBE_LIMITS.maxProbes).toBeLessThanOrEqual(
61
- DEFAULT_REFLECTION_PROBE_LIMITS.maxBytes,
62
- );
63
- expect(admitReflectionProbe(probe, { acceptedCount: 15, acceptedBytes: bytes * 15 })).toEqual({
64
- ok: true,
65
- acceptedBytes: bytes * 16,
66
- });
67
- expect(admitReflectionProbe(probe, { acceptedCount: 16, acceptedBytes: bytes * 16 }).ok).toBe(
68
- false,
69
- );
60
+ const count = Math.floor(DEFAULT_REFLECTION_PROBE_LIMITS.maxBytes / bytes);
61
+ expect(count).toBe(5);
62
+ expect(
63
+ admitReflectionProbe(probe, { acceptedCount: count - 1, acceptedBytes: bytes * (count - 1) }),
64
+ ).toEqual({ ok: true, acceptedBytes: bytes * count });
65
+ expect(
66
+ admitReflectionProbe(probe, { acceptedCount: count, acceptedBytes: bytes * count }).ok,
67
+ ).toBe(false);
68
+ expect(
69
+ admitReflectionProbe({ ...probe, resolution: 16 }, { acceptedCount: 16, acceptedBytes: 0 })
70
+ .ok,
71
+ ).toBe(false);
70
72
  });
71
73
  });
@@ -0,0 +1,71 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { expect, it } from 'vitest';
3
+ import {
4
+ GPU_BUFFER_USAGE_COPY_DST,
5
+ GPU_BUFFER_USAGE_COPY_SRC,
6
+ GPU_BUFFER_USAGE_MAP_READ,
7
+ GPU_BUFFER_USAGE_STORAGE,
8
+ } from '../gpu-usage';
9
+
10
+ it('projects only interior probe rays and preserves the sign of nearly parallel axes', async () => {
11
+ const source = readFileSync('packages/shader/src/ibl-sampling.wgsl', 'utf8');
12
+ const helper = source.slice(
13
+ source.indexOf('fn box_project('),
14
+ source.indexOf('// Split-sum probe sample.'),
15
+ );
16
+ const adapter = await navigator.gpu.requestAdapter();
17
+ if (!adapter) throw new Error('Dawn adapter unavailable');
18
+ const device = await adapter.requestDevice();
19
+ const errors: string[] = [];
20
+ device.addEventListener('uncapturederror', (event) => errors.push(event.error.message));
21
+ const output = device.createBuffer({
22
+ size: 48,
23
+ usage: GPU_BUFFER_USAGE_STORAGE | GPU_BUFFER_USAGE_COPY_SRC,
24
+ });
25
+ const readback = device.createBuffer({
26
+ size: 48,
27
+ usage: GPU_BUFFER_USAGE_COPY_DST | GPU_BUFFER_USAGE_MAP_READ,
28
+ });
29
+ try {
30
+ const module = device.createShaderModule({
31
+ code: `${helper}
32
+ @group(0) @binding(0) var<storage, read_write> result: array<vec4f>;
33
+ @compute @workgroup_size(1) fn main() {
34
+ result[0] = vec4f(box_project(vec3f(4,-1,3),vec3f(0,1,0),vec3f(0),vec3f(1)),0);
35
+ result[1] = vec4f(box_project(vec3f(4,-1,3),vec3f(0,-1,0),vec3f(0),vec3f(1)),0);
36
+ result[2] = vec4f(box_project(vec3f(0.5,0.25,-0.2),vec3f(-0.000001,0.6,0.8),vec3f(0),vec3f(1)),0);
37
+ }`,
38
+ });
39
+ const pipeline = await device.createComputePipelineAsync({
40
+ layout: 'auto',
41
+ compute: { module, entryPoint: 'main' },
42
+ });
43
+ const group = device.createBindGroup({
44
+ layout: pipeline.getBindGroupLayout(0),
45
+ entries: [{ binding: 0, resource: { buffer: output } }],
46
+ });
47
+ const encoder = device.createCommandEncoder();
48
+ const pass = encoder.beginComputePass();
49
+ pass.setPipeline(pipeline);
50
+ pass.setBindGroup(0, group);
51
+ pass.dispatchWorkgroups(1);
52
+ pass.end();
53
+ encoder.copyBufferToBuffer(output, 0, readback, 0, 48);
54
+ device.queue.submit([encoder.finish()]);
55
+ await readback.mapAsync(1);
56
+ const values = new Float32Array(readback.getMappedRange()).slice();
57
+ readback.unmap();
58
+ expect(Array.from(values.slice(0, 3))).toEqual([0, 1, 0]);
59
+ expect(Array.from(values.slice(4, 7))).toEqual([0, -1, 0]);
60
+ // The finite ray exits the y=1 face at t=1.25, independently of its tiny x component.
61
+ const intersection = [0.5 - 0.000001 * 1.25, 1, 0.8] as const;
62
+ const length = Math.hypot(...intersection);
63
+ for (const axis of [0, 1, 2] as const)
64
+ expect(values[8 + axis]).toBeCloseTo(intersection[axis] / length, 5);
65
+ expect(errors).toEqual([]);
66
+ } finally {
67
+ readback.destroy();
68
+ output.destroy();
69
+ device.destroy();
70
+ }
71
+ });
@@ -43,6 +43,15 @@ describe('ReflectionProbe bounded filtering and table projection', () => {
43
43
  expect(table.cpuIndexByPrimitive('0:9')).toBe(1);
44
44
  });
45
45
 
46
+ it('does not turn an outside ray into a direction from the box center', () => {
47
+ expect(boxProjectReflectionDirection([0, 1, 0], [0, 0, 0], [1, 1, 1], [4, -1, 3])).toEqual([
48
+ 0, 1, 0,
49
+ ]);
50
+ expect(boxProjectReflectionDirection([0, -1, 0], [0, 0, 0], [1, 1, 1], [4, -1, 3])).toEqual([
51
+ 0, -1, 0,
52
+ ]);
53
+ });
54
+
46
55
  it('projects a reflection direction from inside and outside the box', () => {
47
56
  expect(boxProjectReflectionDirection([1, 0, 0], [0, 0, 0], [2, 2, 2], [0, 0, 0])).toEqual([
48
57
  1, 0, 0,
@@ -0,0 +1,294 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { mat4 } from '@forgeax/engine-math';
3
+ import { expect, it } from 'vitest';
4
+ import { buildCubeCameraFaceViews } from '../capture/cube-views';
5
+ import {
6
+ GPU_TEXTURE_USAGE_COPY_DST,
7
+ GPU_TEXTURE_USAGE_COPY_SRC,
8
+ GPU_TEXTURE_USAGE_RENDER_ATTACHMENT,
9
+ GPU_TEXTURE_USAGE_TEXTURE_BINDING,
10
+ } from '../gpu-texture-usage';
11
+ import {
12
+ GPU_BUFFER_USAGE_COPY_DST,
13
+ GPU_BUFFER_USAGE_MAP_READ,
14
+ GPU_BUFFER_USAGE_UNIFORM,
15
+ GPU_BUFFER_USAGE_VERTEX,
16
+ } from '../gpu-usage';
17
+ import { CAPTURE_VIEW_PROJS, CUBEMAP_FACE_VERTICES } from '../ibl/IblPipelineCache';
18
+
19
+ // Independent WebGPU cube lookup coordinates, sampled away from face seams.
20
+ function direction(face: number, x: number, y: number, size: number) {
21
+ const u = ((x + 0.5) * 2) / size - 1;
22
+ const v = ((y + 0.5) * 2) / size - 1;
23
+ const vector =
24
+ [
25
+ [1, -v, -u],
26
+ [-1, -v, u],
27
+ [u, 1, v],
28
+ [u, -1, -v],
29
+ [u, -v, 1],
30
+ [-u, -v, -1],
31
+ ][face] ?? [];
32
+ const length = Math.hypot(...vector);
33
+ return vector.map((value) => value / length);
34
+ }
35
+
36
+ function shader(file: string) {
37
+ return readFileSync(`packages/shader/src/${file}`, 'utf8').replace(
38
+ /^#(?:define_import_path|import).*$/gm,
39
+ '',
40
+ );
41
+ }
42
+
43
+ function half(bits: number) {
44
+ const exponent = (bits >>> 10) & 31;
45
+ const fraction = bits & 1023;
46
+ return (
47
+ (bits & 32768 ? -1 : 1) *
48
+ (exponent === 0 ? fraction * 2 ** -24 : (1 + fraction / 1024) * 2 ** (exponent - 15))
49
+ );
50
+ }
51
+
52
+ it('preserves asymmetric world directions through cube capture, PMREM and probe lookup', async () => {
53
+ const adapter = await navigator.gpu.requestAdapter();
54
+ if (adapter === null) throw new Error('Dawn adapter unavailable');
55
+ const device = await adapter.requestDevice();
56
+ const errors: string[] = [];
57
+ device.addEventListener('uncapturederror', (event) => errors.push(event.error.message));
58
+ const size = 16;
59
+ const cube = (format: GPUTextureFormat, usage: number) =>
60
+ device.createTexture({ size: [size, size, 6], format, usage });
61
+ const source = cube('rgba8unorm', GPU_TEXTURE_USAGE_COPY_DST | GPU_TEXTURE_USAGE_TEXTURE_BINDING);
62
+ const raw = cube(
63
+ 'rgba16float',
64
+ GPU_TEXTURE_USAGE_RENDER_ATTACHMENT | GPU_TEXTURE_USAGE_TEXTURE_BINDING,
65
+ );
66
+ const filtered = cube(
67
+ 'rgba16float',
68
+ GPU_TEXTURE_USAGE_RENDER_ATTACHMENT | GPU_TEXTURE_USAGE_COPY_SRC,
69
+ );
70
+ const depth = device.createTexture({
71
+ size: [size, size],
72
+ format: 'depth24plus-stencil8',
73
+ usage: GPU_TEXTURE_USAGE_RENDER_ATTACHMENT,
74
+ });
75
+ const readback = device.createBuffer({
76
+ size: 256 * size * 6,
77
+ usage: GPU_BUFFER_USAGE_COPY_DST | GPU_BUFFER_USAGE_MAP_READ,
78
+ });
79
+ const captureUniforms = device.createBuffer({
80
+ size: 256 * 6,
81
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
82
+ });
83
+ const faceUniforms = device.createBuffer({
84
+ size: 256 * 6,
85
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
86
+ });
87
+ const filterUniforms = device.createBuffer({
88
+ size: 16,
89
+ usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
90
+ });
91
+ const vertices = device.createBuffer({
92
+ size: CUBEMAP_FACE_VERTICES.byteLength,
93
+ usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
94
+ });
95
+ try {
96
+ const shared = shader('ibl-shared.wgsl');
97
+ const background = device.createShaderModule({
98
+ code: shared + shader('ibl-probe-background.wgsl'),
99
+ });
100
+ const prefilter = device.createShaderModule({ code: shared + shader('ibl-prefilter.wgsl') });
101
+ const capturePipeline = await device.createRenderPipelineAsync({
102
+ layout: 'auto',
103
+ vertex: { module: background, entryPoint: 'probe_background_vs' },
104
+ fragment: {
105
+ module: background,
106
+ entryPoint: 'probe_background_fs',
107
+ targets: [{ format: 'rgba16float' }],
108
+ },
109
+ depthStencil: {
110
+ format: 'depth24plus-stencil8',
111
+ depthWriteEnabled: false,
112
+ depthCompare: 'less-equal',
113
+ },
114
+ });
115
+ const filterPipeline = await device.createRenderPipelineAsync({
116
+ layout: 'auto',
117
+ vertex: {
118
+ module: prefilter,
119
+ entryPoint: 'cubemap_vs',
120
+ buffers: [
121
+ { arrayStride: 12, attributes: [{ shaderLocation: 0, offset: 0, format: 'float32x3' }] },
122
+ ],
123
+ },
124
+ fragment: {
125
+ module: prefilter,
126
+ entryPoint: 'prefilterEnv_fs',
127
+ targets: [{ format: 'rgba16float' }],
128
+ },
129
+ primitive: { cullMode: 'none' },
130
+ });
131
+ const sampler = device.createSampler({
132
+ magFilter: 'linear',
133
+ minFilter: 'linear',
134
+ addressModeU: 'clamp-to-edge',
135
+ addressModeV: 'clamp-to-edge',
136
+ });
137
+ const sourceView = source.createView({ dimension: 'cube' });
138
+ const rawView = raw.createView({ dimension: 'cube' });
139
+ const filterGroup = device.createBindGroup({
140
+ layout: filterPipeline.getBindGroupLayout(1),
141
+ entries: [
142
+ { binding: 0, resource: rawView },
143
+ { binding: 1, resource: sampler },
144
+ ],
145
+ });
146
+ device.queue.writeBuffer(vertices, 0, CUBEMAP_FACE_VERTICES);
147
+ device.queue.writeBuffer(filterUniforms, 0, new Float32Array([0, size, 1, 1]));
148
+ const views = buildCubeCameraFaceViews({ position: [3, 5, -7], near: 0.1, far: 100 });
149
+ const encoder = device.createCommandEncoder();
150
+ for (let face = 0; face < 6; face++) {
151
+ const pixels = new Uint8Array(size * size * 4);
152
+ for (let y = 0; y < size; y++)
153
+ for (let x = 0; x < size; x++) {
154
+ const d = direction(face, x, y, size);
155
+ pixels.set(
156
+ [
157
+ Math.round((0.5 + 0.4 * (d[0] ?? 0)) * 255),
158
+ Math.round((0.5 - 0.4 * (d[1] ?? 0)) * 255),
159
+ Math.round((0.5 + 0.4 * (d[2] ?? 0)) * 255),
160
+ 255,
161
+ ],
162
+ (y * size + x) * 4,
163
+ );
164
+ }
165
+ device.queue.writeTexture(
166
+ { texture: source, origin: [0, 0, face] },
167
+ pixels,
168
+ { bytesPerRow: size * 4 },
169
+ [size, size],
170
+ );
171
+ const pose = mat4.invert(mat4.create(), views[face]?.view ?? mat4.create());
172
+ device.queue.writeBuffer(
173
+ captureUniforms,
174
+ face * 256,
175
+ new Float32Array([
176
+ ...pose.slice(0, 3),
177
+ 0,
178
+ ...pose.slice(4, 7),
179
+ 0,
180
+ ...pose.slice(8, 11),
181
+ 0,
182
+ 0,
183
+ 0,
184
+ 0,
185
+ 1,
186
+ 1,
187
+ 1,
188
+ 1,
189
+ 0,
190
+ ]),
191
+ );
192
+ device.queue.writeBuffer(faceUniforms, face * 256, CAPTURE_VIEW_PROJS[face] ?? mat4.create());
193
+ const group = device.createBindGroup({
194
+ layout: capturePipeline.getBindGroupLayout(0),
195
+ entries: [
196
+ { binding: 0, resource: sourceView },
197
+ { binding: 1, resource: sampler },
198
+ { binding: 2, resource: { buffer: captureUniforms, offset: face * 256, size: 80 } },
199
+ ],
200
+ });
201
+ const pass = encoder.beginRenderPass({
202
+ colorAttachments: [
203
+ {
204
+ view: raw.createView({ dimension: '2d', baseArrayLayer: face, arrayLayerCount: 1 }),
205
+ loadOp: 'clear',
206
+ storeOp: 'store',
207
+ clearValue: [0, 0, 0, 1],
208
+ },
209
+ ],
210
+ depthStencilAttachment: {
211
+ view: depth.createView(),
212
+ depthClearValue: 1,
213
+ depthLoadOp: 'clear',
214
+ depthStoreOp: 'discard',
215
+ stencilClearValue: 0,
216
+ stencilLoadOp: 'clear',
217
+ stencilStoreOp: 'discard',
218
+ },
219
+ });
220
+ pass.setPipeline(capturePipeline);
221
+ pass.setBindGroup(0, group);
222
+ pass.draw(3);
223
+ pass.end();
224
+ }
225
+ for (let face = 0; face < 6; face++) {
226
+ const group = device.createBindGroup({
227
+ layout: filterPipeline.getBindGroupLayout(0),
228
+ entries: [
229
+ { binding: 0, resource: { buffer: faceUniforms, offset: face * 256, size: 64 } },
230
+ { binding: 1, resource: { buffer: filterUniforms } },
231
+ ],
232
+ });
233
+ const pass = encoder.beginRenderPass({
234
+ colorAttachments: [
235
+ {
236
+ view: filtered.createView({
237
+ dimension: '2d',
238
+ baseArrayLayer: face,
239
+ arrayLayerCount: 1,
240
+ }),
241
+ loadOp: 'clear',
242
+ storeOp: 'store',
243
+ clearValue: [0, 0, 0, 1],
244
+ },
245
+ ],
246
+ });
247
+ pass.setPipeline(filterPipeline);
248
+ pass.setBindGroup(0, group);
249
+ pass.setBindGroup(1, filterGroup);
250
+ pass.setVertexBuffer(0, vertices);
251
+ pass.draw(6, 1, face * 6);
252
+ pass.end();
253
+ }
254
+ encoder.copyTextureToBuffer(
255
+ { texture: filtered },
256
+ { buffer: readback, bytesPerRow: 256, rowsPerImage: size },
257
+ [size, size, 6],
258
+ );
259
+ device.queue.submit([encoder.finish()]);
260
+ await readback.mapAsync(1);
261
+ const data = new DataView(readback.getMappedRange());
262
+ let maximum = 0;
263
+ for (let face = 0; face < 6; face++)
264
+ for (let y = 2; y < size - 2; y++)
265
+ for (let x = 2; x < size - 2; x++) {
266
+ const d = direction(face, x, y, size);
267
+ for (let channel = 0; channel < 3; channel++)
268
+ maximum = Math.max(
269
+ maximum,
270
+ Math.abs(
271
+ half(data.getUint16((face * size + y) * 256 + x * 8 + channel * 2, true)) -
272
+ (0.5 + 0.4 * (d[channel] ?? 0)),
273
+ ),
274
+ );
275
+ }
276
+ expect(errors).toEqual([]);
277
+ expect(maximum).toBeLessThan(0.04);
278
+ readback.unmap();
279
+ } finally {
280
+ for (const resource of [
281
+ source,
282
+ raw,
283
+ filtered,
284
+ depth,
285
+ readback,
286
+ captureUniforms,
287
+ faceUniforms,
288
+ filterUniforms,
289
+ vertices,
290
+ ])
291
+ resource.destroy();
292
+ device.destroy();
293
+ }
294
+ });
@@ -28,7 +28,7 @@ describe('ReflectionProbeRecordOwner characterization', () => {
28
28
  expect(ownerContractsSource).toContain('active: ProbeCubeSet | undefined;');
29
29
  expect(ownerContractsSource).toContain('candidate: ProbeCubeSet;');
30
30
  expect(ownerContractsSource).toContain('spare: ProbeCubeSet | undefined;');
31
- expect(ownerSource).toContain('const active = resource.active;');
31
+ expect(ownerSource).toContain('const active = resource.activeFact?.center.every');
32
32
  expect(ownerSource).toContain('if (active === undefined) return undefined;');
33
33
  expect(ownerSource).toContain('filteredView: active.cubeView');
34
34
  });
@@ -51,7 +51,9 @@ describe('ReflectionProbeRecordOwner characterization', () => {
51
51
 
52
52
  it('discards failed completion and preserves incremental filter state', () => {
53
53
  expect(ownerSource).toContain('fallbackRequested,');
54
- expect(ownerSource).toContain('if (!fallbackRequested)');
54
+ expect(ownerSource).toMatch(
55
+ /if \(fallbackRequested\)\s+await this\.completeFallbackSubmission\(/,
56
+ );
55
57
  expect(ownerSource).toContain('if (pending.step === undefined) continue;');
56
58
  expect(ownerSource).toContain('if (resource.pending !== undefined) continue;');
57
59
 
@@ -70,7 +72,7 @@ describe('ReflectionProbeRecordOwner characterization', () => {
70
72
  });
71
73
 
72
74
  it('resets capture work on revision changes while retaining active generation identity', () => {
73
- expect(ownerSource).toContain('if (existing.fact.revision !== fact.revision)');
75
+ expect(ownerSource).toContain('existing.captureFact.revision !== fact.revision');
74
76
  expect(ownerSource).toContain('activeGeneration: existing.filter.activeGeneration');
75
77
  expect(ownerSource).toContain('existing.rawFaceCursor = 0;');
76
78
  expect(ownerSource).toContain('this.resources.clear();');
@@ -98,18 +100,6 @@ describe('ReflectionProbeRecordOwner characterization', () => {
98
100
  expect(ownerSource).toContain("pending.projection.source === 'neutral'");
99
101
  });
100
102
 
101
- it('does not publish an active receipt from a descriptor without readback', () => {
102
- expect(ownerSource).toContain('fallbackOutput?.readback === undefined');
103
- expect(ownerSource).toContain('this.fallbackReadback = Object.freeze');
104
- expect(ownerSource).toContain("readbackStatus: 'complete'");
105
- expect(ownerSource).not.toContain("readbackStatus: 'scheduled'");
106
- expect(ownerSource).toContain('readbackHash: readback.hash');
107
- expect(ownerSource).not.toContain('linearHdr: readback.linearHdr');
108
- expect(ownerSource).toContain('reflectionFallbackProjectionSignature(latest.projection)');
109
- expect(ownerSource).toContain('pending.frameId <= committedFrame');
110
- expect(ownerSource).toContain('latestFallbackTickets');
111
- });
112
-
113
103
  it('keeps fallback capability and recovery facts on the same owner', () => {
114
104
  expect(ownerSource).toContain('private fallbackFormatAvailable: boolean;');
115
105
  expect(ownerSource).toContain('this.refreshFallbackCapabilities(frameId);');
@@ -0,0 +1,129 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { RenderSystemInternals } from '../record/render-context';
3
+ import { createProbeFilterState } from '../reflection/filter';
4
+ import type { ReflectionProbeFact } from '../reflection/projection';
5
+ import { ReflectionProbeRecordOwner } from '../reflection/record-owner';
6
+
7
+ // Tests exercise the existing owner without widening its production API.
8
+ type ProbeOwnerFixture = {
9
+ resources: ReflectionProbeRecordOwner['resources'];
10
+ resourceFor: ReflectionProbeRecordOwner['resourceFor'];
11
+ completeSubmission: ReflectionProbeRecordOwner['completeSubmission'];
12
+ };
13
+
14
+ function fixture(intent: 'once' | 'on-change' | 'continuous') {
15
+ const owner = new ReflectionProbeRecordOwner({
16
+ device: { caps: { rgba16floatRenderable: true }, queue: { writeBuffer: () => ({ ok: true }) } },
17
+ deviceScope: { generation: 1 },
18
+ } as unknown as RenderSystemInternals) as unknown as ProbeOwnerFixture;
19
+ const fact: ReflectionProbeFact = {
20
+ worldId: 0,
21
+ entityKey: 1,
22
+ center: [0, 0, 0],
23
+ halfExtents: [2, 2, 2],
24
+ intensity: 1,
25
+ priority: 0,
26
+ resolution: 64,
27
+ revision: 1,
28
+ updateIntent: intent,
29
+ };
30
+ const resource = {
31
+ fact,
32
+ captureFact: fact,
33
+ diffusePayload: [0, 0, 0, 0, 0, 0, 0, 1],
34
+ captureSceneRevision: '1',
35
+ index: 0,
36
+ filter: {
37
+ ...createProbeFilterState({ probeIndex: 0, mipCount: 5, activeGeneration: 1 }),
38
+ cursor: 30,
39
+ },
40
+ rawFaceCursor: 6,
41
+ raw: { size: 64, faceViews: new Array(6) },
42
+ active: {},
43
+ candidate: {},
44
+ spare: undefined,
45
+ } as unknown as NonNullable<ReturnType<(typeof owner)['resourceFor']>>;
46
+ owner.resources.set('0:1', resource);
47
+ return { owner, fact, resource };
48
+ }
49
+
50
+ describe('ReflectionProbe real resource update ownership', () => {
51
+ it.each([
52
+ 'continuous',
53
+ 'on-change',
54
+ ] as const)('starts another bounded capture for %s', (intent) => {
55
+ const { owner, fact, resource } = fixture(intent);
56
+ owner.resourceFor('0:1', fact, 0, '2');
57
+ expect(resource.rawFaceCursor).toBe(0);
58
+ expect(resource.filter.cursor).toBe(0);
59
+ expect(resource.filter.activeGeneration).toBe(1);
60
+ });
61
+ it('keeps once probes steady across ordinary scene changes', () => {
62
+ const { owner, fact, resource } = fixture('once');
63
+ owner.resourceFor('0:1', fact, 0, '2');
64
+ expect(resource.rawFaceCursor).toBe(6);
65
+ expect(resource.filter.cursor).toBe(30);
66
+ });
67
+ it('coalesces a changing scene without starving an in-progress capture', () => {
68
+ const { owner, fact, resource } = fixture('on-change');
69
+ resource.rawFaceCursor = 3;
70
+ resource.filter = { ...resource.filter, cursor: 0 };
71
+ owner.resourceFor('0:1', { ...fact, revision: 2 }, 0, '2');
72
+ expect(resource.rawFaceCursor).toBe(3);
73
+ });
74
+ it('commits capture tickets even when no fallback MRT was requested', async () => {
75
+ const { owner, resource } = fixture('once');
76
+ resource.rawFaceCursor = 2;
77
+ const pending = { rawCaptureFace: 2, step: undefined };
78
+ resource.pending = pending;
79
+ await owner.completeSubmission(
80
+ true,
81
+ new Map(),
82
+ Promise.resolve(),
83
+ undefined,
84
+ false,
85
+ [{ resource, pending }],
86
+ 1,
87
+ 37,
88
+ );
89
+ expect(resource.rawFaceCursor).toBe(3);
90
+ expect(resource.pending).toBeUndefined();
91
+ });
92
+ it('failed submissions retry the same face', async () => {
93
+ const { owner, resource } = fixture('once');
94
+ resource.rawFaceCursor = 2;
95
+ const pending = { rawCaptureFace: 2, step: undefined };
96
+ resource.pending = pending;
97
+ await owner.completeSubmission(
98
+ false,
99
+ new Map(),
100
+ undefined,
101
+ undefined,
102
+ false,
103
+ [{ resource, pending }],
104
+ 1,
105
+ 37,
106
+ );
107
+ expect(resource.rawFaceCursor).toBe(2);
108
+ expect(resource.pending).toBeUndefined();
109
+ });
110
+ it('a stale completion cannot advance a replaced pending ticket', async () => {
111
+ const { owner, resource } = fixture('once');
112
+ resource.rawFaceCursor = 2;
113
+ const old = { rawCaptureFace: 2, step: undefined };
114
+ const current = { rawCaptureFace: 2, step: undefined };
115
+ resource.pending = current;
116
+ await owner.completeSubmission(
117
+ true,
118
+ new Map(),
119
+ undefined,
120
+ undefined,
121
+ false,
122
+ [{ resource, pending: old }],
123
+ 1,
124
+ 37,
125
+ );
126
+ expect(resource.rawFaceCursor).toBe(2);
127
+ expect(resource.pending).toBe(current);
128
+ });
129
+ });
@@ -36,6 +36,9 @@ const expectedCodes = [
36
36
  'observation-unavailable',
37
37
  'scene-data-unavailable',
38
38
  'taa-unavailable',
39
+ 'dynamic-resolution-invalid-parameter',
40
+ 'dynamic-resolution-requires-taa',
41
+ 'dynamic-resolution-timing-unavailable',
39
42
  'shadow-invalid-config',
40
43
  'equirect-projection-failed',
41
44
  'standard-light-budget-exceeded',
@@ -36,6 +36,7 @@ type ExpectedPostProcessDetails = {
36
36
  readonly 'fullscreen-input-not-found': FullscreenInputNotFoundDetail;
37
37
  readonly 'ssao-radius-non-positive': SsaoRadiusNonPositiveDetail;
38
38
  readonly 'ssao-bias-negative': SsaoBiasNegativeDetail;
39
+ readonly 'ssao-parameter-invalid': { readonly paramName: string; readonly value: unknown };
39
40
  readonly 'params-size-mismatch': PostProcessParamsSizeMismatchDetail;
40
41
  readonly 'params-update-size-mismatch': PostProcessParamsUpdateSizeMismatchDetail;
41
42
  };
@@ -60,7 +61,7 @@ describe('render error detail aliases derive from their code resolvers', () => {
60
61
  >();
61
62
  });
62
63
 
63
- it('keeps the exact seven-code vocabulary and derives the public code view', () => {
64
+ it('keeps the closed parameter-validation vocabulary and derives the public code view', () => {
64
65
  expectTypeOf<PostProcessErrorCode>().toEqualTypeOf<ExpectedPostProcessCodes>();
65
66
  expectTypeOf<ExpectedPostProcessCodes>().toEqualTypeOf<PostProcessErrorCode>();
66
67
  expectTypeOf<PostProcessErrorType['code']>().toEqualTypeOf<PostProcessErrorCode>();
@@ -140,6 +141,9 @@ describe('render error detail aliases derive from their code resolvers', () => {
140
141
  expectTypeOf(error.detail.paramName).toEqualTypeOf<string>();
141
142
  expectTypeOf(error.detail.value).toEqualTypeOf<number>();
142
143
  return `${error.detail.paramName}:${error.detail.value}`;
144
+ case 'ssao-parameter-invalid':
145
+ expectTypeOf(error.detail.value).toEqualTypeOf<unknown>();
146
+ return error.detail.paramName;
143
147
  case 'ssao-bias-negative':
144
148
  expectTypeOf(error.detail.paramName).toEqualTypeOf<string>();
145
149
  expectTypeOf(error.detail.value).toEqualTypeOf<number>();
@@ -0,0 +1,63 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import {
3
+ deriveRenderExtent,
4
+ renderExtentSize,
5
+ standardExtentDomainPlan,
6
+ } from '../pipeline/render-extent';
7
+
8
+ describe('RenderExtent', () => {
9
+ it('keeps output and internal domains distinct at a fixed TAAU scale', () => {
10
+ const extent = deriveRenderExtent({
11
+ outputWidth: 1920,
12
+ outputHeight: 1080,
13
+ requestedScale: 0.67,
14
+ generation: 4,
15
+ });
16
+
17
+ expect(extent).toMatchObject({
18
+ outputWidth: 1920,
19
+ outputHeight: 1080,
20
+ internalWidth: 1256,
21
+ internalHeight: 704,
22
+ scale: 0.65625,
23
+ generation: 4,
24
+ });
25
+ expect(renderExtentSize(extent, 'internal')).toEqual({ width: 1256, height: 704 });
26
+ expect(renderExtentSize(extent, 'output')).toEqual({ width: 1920, height: 1080 });
27
+ });
28
+
29
+ it('keeps a non-8-aligned output domain exact while fixed TAAU aligns only its internal domain', () => {
30
+ const extent = deriveRenderExtent({
31
+ outputWidth: 200,
32
+ outputHeight: 150,
33
+ requestedScale: 0.67,
34
+ generation: 2,
35
+ });
36
+
37
+ expect(renderExtentSize(extent, 'output')).toEqual({ width: 200, height: 150 });
38
+ expect(renderExtentSize(extent, 'internal')).toEqual({ width: 128, height: 96 });
39
+ expect(extent.outputWidth).not.toBe(extent.internalWidth);
40
+ expect(extent.outputHeight).not.toBe(extent.internalHeight);
41
+ });
42
+
43
+ it('keeps tiny surfaces native and assigns persistent/history domains explicitly', () => {
44
+ const extent = deriveRenderExtent({
45
+ outputWidth: 7,
46
+ outputHeight: 5,
47
+ requestedScale: 0.5,
48
+ generation: 9,
49
+ });
50
+ const plan = standardExtentDomainPlan(extent, 'cpu-webgl2');
51
+
52
+ expect(extent.internalWidth).toBe(7);
53
+ expect(extent.internalHeight).toBe(5);
54
+ expect(extent.scale).toBe(1);
55
+ expect(plan.resources).toEqual(
56
+ expect.arrayContaining([
57
+ { name: 'scene', domain: 'internal' },
58
+ { name: 'taa-history', domain: 'output' },
59
+ { name: 'output', domain: 'output' },
60
+ ]),
61
+ );
62
+ });
63
+ });