@forgeax/engine-render 0.1.32 → 0.1.34

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 (624) hide show
  1. package/README.md +1374 -142
  2. package/dist/assembly/analytic-fog-registration.d.ts +3 -0
  3. package/dist/assembly/analytic-fog-registration.d.ts.map +1 -0
  4. package/dist/assembly/backend-contract.d.ts +2 -0
  5. package/dist/assembly/backend-contract.d.ts.map +1 -1
  6. package/dist/assembly/barrel-distortion-inspection.d.ts +3 -0
  7. package/dist/assembly/barrel-distortion-inspection.d.ts.map +1 -0
  8. package/dist/assembly/canvas-draw-guard.d.ts +3 -0
  9. package/dist/assembly/canvas-draw-guard.d.ts.map +1 -0
  10. package/dist/assembly/cloud-shadow-entry.d.ts +4 -0
  11. package/dist/assembly/cloud-shadow-entry.d.ts.map +1 -0
  12. package/dist/assembly/dynamic-geometry-host.d.ts +4 -1
  13. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  14. package/dist/assembly/dynamic-geometry-runtime.d.ts +1 -1
  15. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -1
  16. package/dist/assembly/factory.d.ts +1 -1
  17. package/dist/assembly/factory.d.ts.map +1 -1
  18. package/dist/assembly/fxaa-registration.d.ts +4 -0
  19. package/dist/assembly/fxaa-registration.d.ts.map +1 -0
  20. package/dist/assembly/host-contract.d.ts +3 -3
  21. package/dist/assembly/host-contract.d.ts.map +1 -1
  22. package/dist/assembly/lod-observation.d.ts +4 -0
  23. package/dist/assembly/lod-observation.d.ts.map +1 -0
  24. package/dist/assembly/material/assembly.d.ts.map +1 -1
  25. package/dist/assembly/material/pipeline-helpers.d.ts +5 -0
  26. package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -1
  27. package/dist/assembly/material-shader-policy.d.ts +16 -2
  28. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  29. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -1
  30. package/dist/assembly/motion-blur-registration.d.ts +3 -0
  31. package/dist/assembly/motion-blur-registration.d.ts.map +1 -0
  32. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
  33. package/dist/assembly/recovery/renderer-recover.d.ts +2 -0
  34. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  35. package/dist/assembly/render-feature-post-processes.d.ts +6 -0
  36. package/dist/assembly/render-feature-post-processes.d.ts.map +1 -0
  37. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  38. package/dist/assembly/renderer-feature-inspection.d.ts +1 -1
  39. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -1
  40. package/dist/assembly/renderer-frame-transaction.d.ts +12 -0
  41. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  42. package/dist/assembly/renderer-inspection.d.ts +11 -0
  43. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  44. package/dist/assembly/temporal-post-process-prewarm.d.ts +13 -0
  45. package/dist/assembly/temporal-post-process-prewarm.d.ts.map +1 -0
  46. package/dist/assembly/webgpu-pbr-ready.d.ts +1 -0
  47. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -1
  48. package/dist/assembly/webgpu-ready-contract.d.ts +4 -3
  49. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -1
  50. package/dist/assembly/webgpu-ready-per-pass.d.ts +39 -0
  51. package/dist/assembly/webgpu-ready-per-pass.d.ts.map +1 -0
  52. package/dist/assembly/webgpu-ready.d.ts +2 -2
  53. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  54. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -1
  55. package/dist/assembly/webgpu-renderer-observation-owner.d.ts +35 -0
  56. package/dist/assembly/webgpu-renderer-observation-owner.d.ts.map +1 -0
  57. package/dist/assembly/webgpu-renderer.d.ts +1 -1
  58. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  59. package/dist/authoring.mjs +1 -1
  60. package/dist/barrel-distortion.d.ts +47 -0
  61. package/dist/barrel-distortion.d.ts.map +1 -0
  62. package/dist/bloom-admission.d.ts +13 -0
  63. package/dist/bloom-admission.d.ts.map +1 -0
  64. package/dist/{chunk-PA5DRJNI.mjs → chunk-ELJFJ2BU.mjs} +2 -2
  65. package/dist/chunk-ELJFJ2BU.mjs.map +1 -0
  66. package/dist/{chunk-JCIRYQ4V.mjs → chunk-HEB564WF.mjs} +10 -4
  67. package/dist/chunk-HEB564WF.mjs.map +1 -0
  68. package/dist/{chunk-JRIPCMYC.mjs → chunk-MKDIT2EM.mjs} +1584 -375
  69. package/dist/chunk-MKDIT2EM.mjs.map +1 -0
  70. package/dist/{chunk-S665NJNF.mjs → chunk-OCDLEI6I.mjs} +3 -3
  71. package/dist/{chunk-S665NJNF.mjs.map → chunk-OCDLEI6I.mjs.map} +1 -1
  72. package/dist/chunk-ODZWR6JQ.mjs +4927 -0
  73. package/dist/chunk-ODZWR6JQ.mjs.map +1 -0
  74. package/dist/{chunk-DRR7D3KX.mjs → chunk-SHFUFJ5N.mjs} +192 -18
  75. package/dist/chunk-SHFUFJ5N.mjs.map +1 -0
  76. package/dist/{chunk-4NNZQCL3.mjs → chunk-UI5SFODX.mjs} +223 -277
  77. package/dist/chunk-UI5SFODX.mjs.map +1 -0
  78. package/dist/{chunk-GWCEMDVR.mjs → chunk-UINHLCZA.mjs} +787 -80
  79. package/dist/chunk-UINHLCZA.mjs.map +1 -0
  80. package/dist/{chunk-6TFBXLRJ.mjs → chunk-WEX45HKH.mjs} +222 -337
  81. package/dist/chunk-WEX45HKH.mjs.map +1 -0
  82. package/dist/cloud/density.d.ts +55 -0
  83. package/dist/cloud/density.d.ts.map +1 -0
  84. package/dist/cloud/extract.d.ts +25 -0
  85. package/dist/cloud/extract.d.ts.map +1 -0
  86. package/dist/cloud/feature.d.ts +57 -0
  87. package/dist/cloud/feature.d.ts.map +1 -0
  88. package/dist/cloud/frame-context.d.ts +8 -0
  89. package/dist/cloud/frame-context.d.ts.map +1 -0
  90. package/dist/cloud/inspection.d.ts +59 -0
  91. package/dist/cloud/inspection.d.ts.map +1 -0
  92. package/dist/cloud/optics.d.ts +56 -0
  93. package/dist/cloud/optics.d.ts.map +1 -0
  94. package/dist/cloud/parameters.d.ts +73 -0
  95. package/dist/cloud/parameters.d.ts.map +1 -0
  96. package/dist/cloud/resources.d.ts +60 -0
  97. package/dist/cloud/resources.d.ts.map +1 -0
  98. package/dist/cloud/shadow.d.ts +42 -0
  99. package/dist/cloud/shadow.d.ts.map +1 -0
  100. package/dist/cloud/temporal.d.ts +74 -0
  101. package/dist/cloud/temporal.d.ts.map +1 -0
  102. package/dist/components/atmosphere.d.ts +2 -0
  103. package/dist/components/atmosphere.d.ts.map +1 -1
  104. package/dist/components/barrel-distortion.d.ts +13 -0
  105. package/dist/components/barrel-distortion.d.ts.map +1 -0
  106. package/dist/components/camera.d.ts +20 -4
  107. package/dist/components/camera.d.ts.map +1 -1
  108. package/dist/components/cloud-layer.d.ts +29 -0
  109. package/dist/components/cloud-layer.d.ts.map +1 -0
  110. package/dist/components/index.d.ts +2 -0
  111. package/dist/components/index.d.ts.map +1 -1
  112. package/dist/components/instances.d.ts +20 -6
  113. package/dist/components/instances.d.ts.map +1 -1
  114. package/dist/components/mesh-filter.d.ts +6 -5
  115. package/dist/components/mesh-filter.d.ts.map +1 -1
  116. package/dist/components/motion-blur.d.ts +1 -0
  117. package/dist/components/motion-blur.d.ts.map +1 -1
  118. package/dist/construct-renderer.d.ts +1 -1
  119. package/dist/construct-renderer.d.ts.map +1 -1
  120. package/dist/construct-renderer.mjs +9751 -3528
  121. package/dist/construct-renderer.mjs.map +1 -1
  122. package/dist/device/gpu-residency.d.ts +6 -0
  123. package/dist/device/gpu-residency.d.ts.map +1 -1
  124. package/dist/dynamic-geometry.d.ts +5 -1
  125. package/dist/dynamic-geometry.d.ts.map +1 -1
  126. package/dist/environment/analytic-fog.d.ts +6 -0
  127. package/dist/environment/analytic-fog.d.ts.map +1 -0
  128. package/dist/errors/cloud.d.ts +59 -0
  129. package/dist/errors/cloud.d.ts.map +1 -0
  130. package/dist/errors/render.d.ts +23 -5
  131. package/dist/errors/render.d.ts.map +1 -1
  132. package/dist/examples/single-layer-medium-surface.d.ts +47 -0
  133. package/dist/examples/single-layer-medium-surface.d.ts.map +1 -0
  134. package/dist/extract/camera.d.ts.map +1 -1
  135. package/dist/extract/contracts.d.ts +12 -1
  136. package/dist/extract/contracts.d.ts.map +1 -1
  137. package/dist/extract/environment.d.ts +2 -0
  138. package/dist/extract/environment.d.ts.map +1 -1
  139. package/dist/features/barrel-distortion.d.ts +29 -0
  140. package/dist/features/barrel-distortion.d.ts.map +1 -0
  141. package/dist/features/error-reporting.d.ts +17 -0
  142. package/dist/features/error-reporting.d.ts.map +1 -0
  143. package/dist/features/host.d.ts +15 -0
  144. package/dist/features/host.d.ts.map +1 -1
  145. package/dist/features/motion-blur/motion-blur-feature.d.ts +20 -2
  146. package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -1
  147. package/dist/features/motion-blur/motion-blur-params.d.ts +21 -0
  148. package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -1
  149. package/dist/features/motion-blur/motion-blur-runtime.d.ts +54 -0
  150. package/dist/features/motion-blur/motion-blur-runtime.d.ts.map +1 -0
  151. package/dist/features/plan.d.ts +15 -4
  152. package/dist/features/plan.d.ts.map +1 -1
  153. package/dist/features/prepared-gpu-work.d.ts +9 -5
  154. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  155. package/dist/features/render-graph-compute.d.ts.map +1 -1
  156. package/dist/features/render-graph-contribution.d.ts +5 -0
  157. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  158. package/dist/features/render-graph-raster.d.ts +1 -0
  159. package/dist/features/render-graph-raster.d.ts.map +1 -1
  160. package/dist/features/targets.d.ts +2 -0
  161. package/dist/features/targets.d.ts.map +1 -1
  162. package/dist/features/types.d.ts +44 -0
  163. package/dist/features/types.d.ts.map +1 -1
  164. package/dist/fullscreen-post-process-pass.d.ts +15 -7
  165. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  166. package/dist/gpu-dirty-ranges.d.ts +11 -0
  167. package/dist/gpu-dirty-ranges.d.ts.map +1 -0
  168. package/dist/gpu-driven/batch-topology.d.ts +17 -0
  169. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  170. package/dist/gpu-driven/material-artifacts.d.ts +6 -0
  171. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
  172. package/dist/gpu-driven/prepared-draw.d.ts +7 -0
  173. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  174. package/dist/gpu-driven/production-raster-material.d.ts +178 -0
  175. package/dist/gpu-driven/production-raster-material.d.ts.map +1 -0
  176. package/dist/gpu-driven/production-raster.d.ts +50 -4
  177. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  178. package/dist/gpu-driven/resource-allocation.d.ts +31 -0
  179. package/dist/gpu-driven/resource-allocation.d.ts.map +1 -0
  180. package/dist/gpu-driven/shadow-ownership.d.ts +14 -0
  181. package/dist/gpu-driven/shadow-ownership.d.ts.map +1 -0
  182. package/dist/gpu-driven/view-gpu.d.ts +35 -2
  183. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  184. package/dist/gpu-scene.d.ts +11 -1
  185. package/dist/gpu-scene.d.ts.map +1 -1
  186. package/dist/hdrp-buffers.d.ts +2 -2
  187. package/dist/hdrp-buffers.d.ts.map +1 -1
  188. package/dist/ibl/skylight-bind-group.d.ts +10 -1
  189. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  190. package/dist/index.d.ts +31 -8
  191. package/dist/index.d.ts.map +1 -1
  192. package/dist/index.mjs +258 -8
  193. package/dist/index.mjs.map +1 -1
  194. package/dist/inspection-types.d.ts +178 -3
  195. package/dist/inspection-types.d.ts.map +1 -1
  196. package/dist/instance-buffer-cache.d.ts +4 -4
  197. package/dist/instance-buffer-cache.d.ts.map +1 -1
  198. package/dist/instances.d.ts +49 -94
  199. package/dist/instances.d.ts.map +1 -1
  200. package/dist/internal.mjs +7 -7
  201. package/dist/pbr-pipeline.d.ts +19 -1
  202. package/dist/pbr-pipeline.d.ts.map +1 -1
  203. package/dist/pipeline/single-layer-medium-passes.d.ts +73 -0
  204. package/dist/pipeline/single-layer-medium-passes.d.ts.map +1 -0
  205. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  206. package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
  207. package/dist/pipeline/standard-output/types.d.ts +4 -2
  208. package/dist/pipeline/standard-output/types.d.ts.map +1 -1
  209. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  210. package/dist/pipeline/standard-post.d.ts +28 -19
  211. package/dist/pipeline/standard-post.d.ts.map +1 -1
  212. package/dist/pipeline-builder.d.ts +1 -1
  213. package/dist/pipeline-builder.d.ts.map +1 -1
  214. package/dist/pipeline-spec.d.ts +2 -2
  215. package/dist/plugin.d.ts.map +1 -1
  216. package/dist/prepare/prepared-graphics-resolver.d.ts +2 -0
  217. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  218. package/dist/record/barrel-distortion-frame.d.ts +15 -0
  219. package/dist/record/barrel-distortion-frame.d.ts.map +1 -0
  220. package/dist/record/dynamic-geometry-consumption.d.ts +10 -2
  221. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -1
  222. package/dist/record/fold-buckets.d.ts +95 -0
  223. package/dist/record/fold-buckets.d.ts.map +1 -0
  224. package/dist/record/fold-instance-buffer.d.ts +6 -0
  225. package/dist/record/fold-instance-buffer.d.ts.map +1 -0
  226. package/dist/record/frame-lighting.d.ts +1 -0
  227. package/dist/record/frame-lighting.d.ts.map +1 -1
  228. package/dist/record/frame-snapshot.d.ts +40 -3
  229. package/dist/record/frame-snapshot.d.ts.map +1 -1
  230. package/dist/record/frame-targets.d.ts.map +1 -1
  231. package/dist/record/frame.d.ts +14 -62
  232. package/dist/record/frame.d.ts.map +1 -1
  233. package/dist/record/gpu-draw-receipts.d.ts +11 -0
  234. package/dist/record/gpu-draw-receipts.d.ts.map +1 -0
  235. package/dist/record/gpu-timing.d.ts +8 -0
  236. package/dist/record/gpu-timing.d.ts.map +1 -1
  237. package/dist/record/main-pass-geometry.d.ts +21 -5
  238. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  239. package/dist/record/main-pass-material.d.ts +7 -1
  240. package/dist/record/main-pass-material.d.ts.map +1 -1
  241. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  242. package/dist/record/main-pass.d.ts +13 -2
  243. package/dist/record/main-pass.d.ts.map +1 -1
  244. package/dist/record/mesh-ssbo.d.ts +6 -5
  245. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  246. package/dist/record/motion-blur-receipt.d.ts +11 -0
  247. package/dist/record/motion-blur-receipt.d.ts.map +1 -0
  248. package/dist/record/probe-blend-buffer.d.ts +16 -0
  249. package/dist/record/probe-blend-buffer.d.ts.map +1 -0
  250. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  251. package/dist/record/render-context.d.ts +34 -31
  252. package/dist/record/render-context.d.ts.map +1 -1
  253. package/dist/record/shadow-pass.d.ts +2 -0
  254. package/dist/record/shadow-pass.d.ts.map +1 -1
  255. package/dist/record/skybox-post-pass.d.ts +6 -4
  256. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  257. package/dist/record/typed-frame-graph.d.ts +22 -2
  258. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  259. package/dist/record/view-ubo.d.ts +11 -3
  260. package/dist/record/view-ubo.d.ts.map +1 -1
  261. package/dist/record/volume-params.d.ts +9 -0
  262. package/dist/record/volume-params.d.ts.map +1 -0
  263. package/dist/recovery/render-system-candidate.d.ts +10 -0
  264. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  265. package/dist/reflection/record-owner.d.ts.map +1 -1
  266. package/dist/render-contract.d.ts +66 -8
  267. package/dist/render-contract.d.ts.map +1 -1
  268. package/dist/render-graph-primitives.d.ts.map +1 -1
  269. package/dist/render-pipeline.d.ts +106 -3
  270. package/dist/render-pipeline.d.ts.map +1 -1
  271. package/dist/render-system-extract-tail.d.ts +3 -2
  272. package/dist/render-system-extract-tail.d.ts.map +1 -1
  273. package/dist/render-system-extract.d.ts +24 -10
  274. package/dist/render-system-extract.d.ts.map +1 -1
  275. package/dist/render-system-presentation.d.ts +23 -0
  276. package/dist/render-system-presentation.d.ts.map +1 -0
  277. package/dist/render-system-producer-roots.d.ts +10 -0
  278. package/dist/render-system-producer-roots.d.ts.map +1 -0
  279. package/dist/render-system-residency.d.ts +12 -0
  280. package/dist/render-system-residency.d.ts.map +1 -0
  281. package/dist/render-system.d.ts +26 -15
  282. package/dist/render-system.d.ts.map +1 -1
  283. package/dist/scene/probe-blend.d.ts +27 -0
  284. package/dist/scene/probe-blend.d.ts.map +1 -1
  285. package/dist/scene/render-scene.d.ts +14 -5
  286. package/dist/scene/render-scene.d.ts.map +1 -1
  287. package/dist/ssao-buffers.d.ts +9 -0
  288. package/dist/ssao-buffers.d.ts.map +1 -1
  289. package/dist/surface/dynamic-input.d.ts +133 -0
  290. package/dist/surface/dynamic-input.d.ts.map +1 -0
  291. package/dist/surface/gpu-driven.d.ts +41 -0
  292. package/dist/surface/gpu-driven.d.ts.map +1 -0
  293. package/dist/surface/submission-observation.d.ts +136 -0
  294. package/dist/surface/submission-observation.d.ts.map +1 -0
  295. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  296. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  297. package/dist/temporal/frame.d.ts +7 -0
  298. package/dist/temporal/frame.d.ts.map +1 -1
  299. package/dist/temporal/gpu.d.ts +13 -1
  300. package/dist/temporal/gpu.d.ts.map +1 -1
  301. package/dist/temporal/index.mjs +5 -5
  302. package/dist/temporal/inspection.d.ts.map +1 -1
  303. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  304. package/dist/temporal/target-coverage-attachment.d.ts.map +1 -1
  305. package/dist/temporal/view.d.ts +6 -1
  306. package/dist/temporal/view.d.ts.map +1 -1
  307. package/dist/transmission/single-layer-medium.d.ts +89 -0
  308. package/dist/transmission/single-layer-medium.d.ts.map +1 -0
  309. package/dist/typed-render-graph-primitives.d.ts +24 -9
  310. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  311. package/dist/volume/component.d.ts +9 -5
  312. package/dist/volume/component.d.ts.map +1 -1
  313. package/dist/volume/extract.d.ts +1 -1
  314. package/dist/volume/extract.d.ts.map +1 -1
  315. package/dist/volume/inspection.d.ts +2 -0
  316. package/dist/volume/inspection.d.ts.map +1 -1
  317. package/dist/volume/passes.d.ts +4 -1
  318. package/dist/volume/passes.d.ts.map +1 -1
  319. package/dist/volume/resources.d.ts.map +1 -1
  320. package/dist/volume/temporal.d.ts +3 -1
  321. package/dist/volume/temporal.d.ts.map +1 -1
  322. package/package.json +22 -21
  323. package/src/__tests__/analytic-fog-graph.unit.test.ts +199 -0
  324. package/src/__tests__/analytic-fog-optics.unit.test.ts +38 -0
  325. package/src/__tests__/atmosphere-daylight.dawn.test.ts +72 -0
  326. package/src/__tests__/barrel-distortion-lkg.integration.test.ts +353 -0
  327. package/src/__tests__/barrel-distortion.dawn.test.ts +41 -0
  328. package/src/__tests__/barrel-distortion.unit.test.ts +364 -0
  329. package/src/__tests__/bloom-exact-zero.unit.test.ts +112 -18
  330. package/src/__tests__/bloom-lifecycle.integration.test.ts +17 -4
  331. package/src/__tests__/construct-renderer-identity.unit.test.ts +49 -0
  332. package/src/__tests__/detached-recovery-graph.unit.test.ts +277 -0
  333. package/src/__tests__/device-loss-fanout.unit.test.ts +36 -1
  334. package/src/__tests__/docs-gate-green.unit.test.ts +112 -1
  335. package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +20 -0
  336. package/src/__tests__/environment-fog-lanes.integration.test.ts +14 -0
  337. package/src/__tests__/environment-selection.unit.test.ts +12 -0
  338. package/src/__tests__/factory-contract.integration.test.ts +626 -113
  339. package/src/__tests__/feature-depth-input.dawn.test.ts +129 -2
  340. package/src/__tests__/frame-camera-selection.unit.test.ts +39 -1
  341. package/src/__tests__/frame-plan-contract.unit.test.ts +2 -0
  342. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +14 -3
  343. package/src/__tests__/gpu-dirty-ranges.unit.test.ts +22 -0
  344. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +16 -3
  345. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +12 -0
  346. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +2 -1
  347. package/src/__tests__/gpu-driven-material-artifacts.unit.test.ts +143 -0
  348. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +2 -2
  349. package/src/__tests__/gpu-driven-production.integration.test.ts +482 -3
  350. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +7 -6
  351. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +1 -1
  352. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +103 -4
  353. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +18 -2
  354. package/src/__tests__/gpu-driven-view.browser.test.ts +5 -8
  355. package/src/__tests__/gpu-driven-view.dawn.test.ts +6 -2
  356. package/src/__tests__/gpu-driven-view.unit.test.ts +1 -1
  357. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +1 -0
  358. package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -4
  359. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +1 -0
  360. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +12 -0
  361. package/src/__tests__/gpu-scene.unit.test.ts +210 -9
  362. package/src/__tests__/instance-allocation-failure.unit.test.ts +57 -0
  363. package/src/__tests__/instances-culling.integration.test.ts +10 -36
  364. package/src/__tests__/instances-store.unit.test.ts +105 -238
  365. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  366. package/src/__tests__/main-pass-material-medium-selection.unit.test.ts +146 -0
  367. package/src/__tests__/material-shader-prewarm.unit.test.ts +59 -0
  368. package/src/__tests__/material-snapshot-mutation.integration.test.ts +39 -1
  369. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +31 -0
  370. package/src/__tests__/morph-culling-reentry.dawn.test.ts +5 -5
  371. package/src/__tests__/morph-visual-evidence.integration.test.ts +8 -7
  372. package/src/__tests__/motion-blur-compute.dawn.test.ts +589 -0
  373. package/src/__tests__/motion-blur.pixel.integration.test.ts +2 -2
  374. package/src/__tests__/nineslice-handle-surface.unit.test.ts +4 -4
  375. package/src/__tests__/pbr-view-layout.unit.test.ts +9 -0
  376. package/src/__tests__/physical-clearcoat.integration.test.ts +58 -0
  377. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +21 -0
  378. package/src/__tests__/prepared-graphics-capability.unit.test.ts +66 -2
  379. package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +86 -0
  380. package/src/__tests__/probe-bindings.unit.test.ts +582 -1
  381. package/src/__tests__/probe-scene-dirty.integration.test.ts +28 -1
  382. package/src/__tests__/public-surface.test-d.ts +8 -0
  383. package/src/__tests__/receipt-observation-retirement.unit.test.ts +65 -0
  384. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +69 -0
  385. package/src/__tests__/render-error-code-owner.test-d.ts +9 -0
  386. package/src/__tests__/render-feature-diagnostics.unit.test.ts +15 -2
  387. package/src/__tests__/render-feature-error-reporting.unit.test.ts +44 -0
  388. package/src/__tests__/render-feature-prepared-diagnostics.unit.test.ts +10 -1
  389. package/src/__tests__/render-feature-retry.unit.test.ts +12 -1
  390. package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +23 -0
  391. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +145 -19
  392. package/src/__tests__/render-scene-projection.unit.test.ts +36 -11
  393. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +37 -6
  394. package/src/__tests__/render-target-probe-soak.perf.test.ts +2 -2
  395. package/src/__tests__/render-temporal-environment-consumer.browser.test.ts +3 -3
  396. package/src/__tests__/render-temporal-environment-consumer.dawn.test.ts +5 -2
  397. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +41 -7
  398. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +16 -0
  399. package/src/__tests__/renderer-frame-transaction.integration.test.ts +33 -11
  400. package/src/__tests__/resource-allocation.unit.test.ts +123 -0
  401. package/src/__tests__/shader-manifest-fixture.ts +4 -4
  402. package/src/__tests__/single-layer-medium-passes.unit.test.ts +275 -0
  403. package/src/__tests__/skin-motion-regression.unit.test.ts +70 -1
  404. package/src/__tests__/skin-palette-recycling.dawn.test.ts +97 -0
  405. package/src/__tests__/skinned-shadow-caster.test.ts +17 -0
  406. package/src/__tests__/ssr-carrier-public-contract.unit.test.ts +21 -0
  407. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +4 -4
  408. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +2 -2
  409. package/src/__tests__/ssr-gpu-dispatch.ts +1 -1
  410. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +6 -2
  411. package/src/__tests__/standard-lighting-write.unit.test.ts +2 -1
  412. package/src/__tests__/standard-output-chain-contract.unit.test.ts +28 -0
  413. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +46 -1
  414. package/src/__tests__/standard-pipeline.dawn.test.ts +1 -1
  415. package/src/__tests__/standard-pipeline.integration.test.ts +200 -9
  416. package/src/__tests__/surface-dynamic-buffer-lifecycle.unit.test.ts +506 -0
  417. package/src/__tests__/surface-submission-observation.unit.test.ts +812 -0
  418. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +32 -0
  419. package/src/__tests__/temporal-performance.integration.test.ts +3 -3
  420. package/src/__tests__/temporal-reset-matrix.unit.test.ts +10 -0
  421. package/src/__tests__/temporal-view.unit.test.ts +2 -1
  422. package/src/__tests__/typed-pipeline-topology.unit.test.ts +20 -4
  423. package/src/__tests__/visibility-instances.integration.test.ts +7 -19
  424. package/src/__tests__/visibility-producer-matrix.integration.test.ts +9 -20
  425. package/src/__tests__/volume-optics-oracle.unit.test.ts +7 -3
  426. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +189 -15
  427. package/src/__tests__/volumetric-fog-errors.unit.test.ts +4 -3
  428. package/src/__tests__/volumetric-fog-multiple.unit.test.ts +55 -0
  429. package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +1 -1
  430. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +558 -68
  431. package/src/__tests__/volumetric-fog-temporal.unit.test.ts +27 -0
  432. package/src/__tests__/volumetric-fog-topology.unit.test.ts +23 -5
  433. package/src/assembly/analytic-fog-registration.ts +14 -0
  434. package/src/assembly/backend-contract.ts +30 -0
  435. package/src/assembly/barrel-distortion-inspection.ts +22 -0
  436. package/src/assembly/canvas-draw-guard.ts +11 -0
  437. package/src/assembly/cloud-shadow-entry.ts +22 -0
  438. package/src/assembly/dynamic-geometry-host.ts +65 -37
  439. package/src/assembly/dynamic-geometry-runtime.ts +7 -1
  440. package/src/assembly/factory.ts +2 -30
  441. package/src/assembly/fxaa-registration.ts +17 -0
  442. package/src/assembly/host-contract.ts +3 -3
  443. package/src/assembly/lod-observation.ts +12 -0
  444. package/src/assembly/material/assembly.ts +25 -13
  445. package/src/assembly/material/pipeline-helpers.ts +53 -12
  446. package/src/assembly/material-shader-policy.ts +55 -0
  447. package/src/assembly/material-shader-prewarm.ts +21 -10
  448. package/src/assembly/motion-blur-registration.ts +17 -0
  449. package/src/assembly/recovery/device-loss-fanout.ts +13 -0
  450. package/src/assembly/recovery/renderer-recover.ts +41 -18
  451. package/src/assembly/render-feature-post-processes.ts +54 -0
  452. package/src/assembly/renderer-facade.ts +6 -1
  453. package/src/assembly/renderer-feature-inspection.ts +8 -2
  454. package/src/assembly/renderer-frame-transaction.ts +23 -0
  455. package/src/assembly/renderer-inspection.ts +28 -0
  456. package/src/assembly/skin-palette-owner.ts +1 -1
  457. package/src/assembly/temporal-post-process-prewarm.ts +40 -0
  458. package/src/assembly/webgpu-pbr-ready.ts +9 -1
  459. package/src/assembly/webgpu-ready-contract.ts +8 -3
  460. package/src/assembly/webgpu-ready-per-pass.ts +104 -0
  461. package/src/assembly/webgpu-ready.ts +296 -306
  462. package/src/assembly/webgpu-renderer-bootstrap.ts +0 -12
  463. package/src/assembly/webgpu-renderer-observation-owner.ts +222 -0
  464. package/src/assembly/webgpu-renderer.ts +228 -281
  465. package/src/assembly/webgpu-vertex-layouts.ts +1 -1
  466. package/src/barrel-distortion.ts +247 -0
  467. package/src/bloom-admission.ts +21 -0
  468. package/src/cloud/__tests__/cloud-density-parity.dawn.test.ts +173 -0
  469. package/src/cloud/__tests__/cloud-layer-framebuffer-y.dawn.test.ts +265 -0
  470. package/src/cloud/__tests__/cloud-layer.browser.test.ts +432 -0
  471. package/src/cloud/__tests__/cloud-layer.unit.test.ts +634 -0
  472. package/src/cloud/density.ts +600 -0
  473. package/src/cloud/extract.ts +110 -0
  474. package/src/cloud/feature.ts +1810 -0
  475. package/src/cloud/frame-context.ts +42 -0
  476. package/src/cloud/inspection.ts +130 -0
  477. package/src/cloud/optics.ts +285 -0
  478. package/src/cloud/parameters.ts +255 -0
  479. package/src/cloud/resources.ts +122 -0
  480. package/src/cloud/shadow.ts +167 -0
  481. package/src/cloud/temporal.ts +226 -0
  482. package/src/components/__tests__/motion-blur.unit.test.ts +1 -0
  483. package/src/components/atmosphere.ts +4 -0
  484. package/src/components/barrel-distortion.ts +46 -0
  485. package/src/components/camera.ts +66 -10
  486. package/src/components/cloud-layer.ts +47 -0
  487. package/src/components/index.ts +2 -0
  488. package/src/components/instances.ts +18 -45
  489. package/src/components/mesh-filter.ts +11 -9
  490. package/src/components/motion-blur.ts +1 -0
  491. package/src/components/sprite-instances.ts +1 -1
  492. package/src/construct-renderer.ts +1 -1
  493. package/src/device/gpu-residency.ts +56 -12
  494. package/src/dynamic-geometry.ts +12 -1
  495. package/src/environment/analytic-fog.ts +44 -0
  496. package/src/environment/background.ts +2 -2
  497. package/src/errors/cloud.ts +103 -0
  498. package/src/errors/render.ts +58 -8
  499. package/src/examples/single-layer-medium-surface.ts +59 -0
  500. package/src/extract/camera.ts +35 -8
  501. package/src/extract/contracts.ts +12 -0
  502. package/src/extract/environment.ts +4 -0
  503. package/src/features/barrel-distortion.ts +172 -0
  504. package/src/features/error-reporting.ts +98 -0
  505. package/src/features/host.ts +138 -24
  506. package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +111 -1
  507. package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +58 -3
  508. package/src/features/motion-blur/motion-blur-feature.ts +1519 -15
  509. package/src/features/motion-blur/motion-blur-params.ts +70 -2
  510. package/src/features/motion-blur/motion-blur-runtime.ts +176 -0
  511. package/src/features/plan.ts +33 -8
  512. package/src/features/prepared-gpu-work.ts +86 -35
  513. package/src/features/render-graph-compute.ts +13 -5
  514. package/src/features/render-graph-contribution.ts +11 -0
  515. package/src/features/render-graph-raster.ts +45 -3
  516. package/src/features/targets.ts +66 -0
  517. package/src/features/types.ts +47 -0
  518. package/src/fullscreen-post-process-pass.ts +41 -9
  519. package/src/gpu-dirty-ranges.ts +48 -0
  520. package/src/gpu-driven/batch-topology.ts +53 -0
  521. package/src/gpu-driven/material-artifacts.ts +35 -32
  522. package/src/gpu-driven/prepared-draw.ts +27 -14
  523. package/src/gpu-driven/production-raster-material.ts +1894 -0
  524. package/src/gpu-driven/production-raster.ts +904 -787
  525. package/src/gpu-driven/resource-allocation.ts +142 -0
  526. package/src/gpu-driven/shadow-ownership.ts +71 -0
  527. package/src/gpu-driven/view-gpu.ts +181 -10
  528. package/src/gpu-scene.ts +259 -101
  529. package/src/hdrp-buffers.ts +4 -3
  530. package/src/ibl/skylight-bind-group.ts +49 -7
  531. package/src/index.ts +228 -9
  532. package/src/inspection-types.ts +222 -2
  533. package/src/instance-buffer-cache.ts +4 -4
  534. package/src/instances.ts +151 -492
  535. package/src/pbr-pipeline.ts +219 -13
  536. package/src/pipeline/single-layer-medium-passes.ts +267 -0
  537. package/src/pipeline/standard-forward-lane.ts +313 -57
  538. package/src/pipeline/standard-output/graph.ts +8 -2
  539. package/src/pipeline/standard-output/types.ts +12 -1
  540. package/src/pipeline/standard-pipeline.ts +291 -18
  541. package/src/pipeline/standard-post.ts +397 -102
  542. package/src/pipeline-builder.ts +1 -1
  543. package/src/pipeline-spec.ts +5 -5
  544. package/src/plugin.ts +4 -0
  545. package/src/points-lines/__tests__/points-lines.bench.ts +17 -8
  546. package/src/points-lines/__tests__/view-ubo.unit.test.ts +2 -1
  547. package/src/prepare/prepared-graphics-resolver.ts +2 -0
  548. package/src/record/__tests__/main-pass-geometry.unit.test.ts +30 -0
  549. package/src/record/__tests__/sprite-material-abi.unit.test.ts +1 -1
  550. package/src/record/__tests__/temporal-payload.unit.test.ts +3 -2
  551. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +11 -0
  552. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -0
  553. package/src/record/__tests__/view-ubo-layout.unit.test.ts +4 -3
  554. package/src/record/barrel-distortion-frame.ts +110 -0
  555. package/src/record/dynamic-geometry-consumption.ts +39 -9
  556. package/src/record/fold-buckets.ts +852 -0
  557. package/src/record/fold-instance-buffer.ts +51 -0
  558. package/src/record/frame-lighting.ts +36 -2
  559. package/src/record/frame-snapshot.ts +44 -4
  560. package/src/record/frame-targets.ts +10 -6
  561. package/src/record/frame.ts +373 -1099
  562. package/src/record/gpu-draw-receipts.ts +38 -0
  563. package/src/record/gpu-timing.ts +48 -5
  564. package/src/record/main-pass-geometry.ts +281 -83
  565. package/src/record/main-pass-material.ts +66 -4
  566. package/src/record/main-pass-sprite-draws.ts +13 -66
  567. package/src/record/main-pass.ts +156 -20
  568. package/src/record/mesh-ssbo.ts +43 -6
  569. package/src/record/motion-blur-receipt.ts +91 -0
  570. package/src/record/probe-blend-buffer.ts +180 -0
  571. package/src/record/recovery-pipeline.ts +35 -3
  572. package/src/record/render-context.ts +51 -31
  573. package/src/record/shadow-pass.ts +36 -2
  574. package/src/record/skybox-post-pass.ts +150 -200
  575. package/src/record/typed-frame-graph.ts +824 -44
  576. package/src/record/view-ubo.ts +26 -3
  577. package/src/record/volume-params.ts +225 -0
  578. package/src/recovery/render-system-candidate.ts +257 -52
  579. package/src/reflection/record-owner.ts +7 -3
  580. package/src/render-contract.ts +71 -8
  581. package/src/render-graph-primitives.ts +7 -1
  582. package/src/render-pipeline.ts +136 -1
  583. package/src/render-system-extract-tail.ts +72 -60
  584. package/src/render-system-extract.ts +117 -87
  585. package/src/render-system-presentation.ts +56 -0
  586. package/src/render-system-producer-roots.ts +96 -0
  587. package/src/render-system-residency.ts +159 -0
  588. package/src/render-system.ts +601 -482
  589. package/src/scene/probe-blend.ts +96 -4
  590. package/src/scene/render-scene.ts +431 -281
  591. package/src/ssao-buffers.ts +95 -2
  592. package/src/surface/__tests__/dynamic-input.unit.test.ts +374 -0
  593. package/src/surface/__tests__/gpu-driven.unit.test.ts +87 -0
  594. package/src/surface/dynamic-input.ts +658 -0
  595. package/src/surface/gpu-driven.ts +147 -0
  596. package/src/surface/submission-observation.ts +517 -0
  597. package/src/systems/skin-palette-allocator.ts +4 -1
  598. package/src/temporal/__tests__/temporal-frame-transaction.unit.test.ts +36 -0
  599. package/src/temporal/frame-coordinator.ts +8 -1
  600. package/src/temporal/frame.ts +20 -0
  601. package/src/temporal/gpu.ts +101 -17
  602. package/src/temporal/inspection.ts +7 -1
  603. package/src/temporal/standard-scene-data.ts +5 -0
  604. package/src/temporal/target-coverage-attachment.ts +4 -0
  605. package/src/temporal/view.ts +13 -2
  606. package/src/transmission/__tests__/single-layer-medium.unit.test.ts +149 -0
  607. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +12 -11
  608. package/src/transmission/single-layer-medium.ts +322 -0
  609. package/src/typed-render-graph-primitives.ts +191 -50
  610. package/src/volume/component.ts +10 -10
  611. package/src/volume/extract.ts +11 -5
  612. package/src/volume/inspection.ts +3 -0
  613. package/src/volume/passes.ts +95 -8
  614. package/src/volume/resources.ts +2 -1
  615. package/src/volume/temporal.ts +11 -0
  616. package/dist/chunk-4NNZQCL3.mjs.map +0 -1
  617. package/dist/chunk-6TFBXLRJ.mjs.map +0 -1
  618. package/dist/chunk-DRR7D3KX.mjs.map +0 -1
  619. package/dist/chunk-GWCEMDVR.mjs.map +0 -1
  620. package/dist/chunk-JCIRYQ4V.mjs.map +0 -1
  621. package/dist/chunk-JRIPCMYC.mjs.map +0 -1
  622. package/dist/chunk-LULCA3OF.mjs +0 -1713
  623. package/dist/chunk-LULCA3OF.mjs.map +0 -1
  624. package/dist/chunk-PA5DRJNI.mjs.map +0 -1
@@ -19,6 +19,7 @@ import { describe, expect, it, vi } from 'vitest';
19
19
  import type { RhiBackendPack } from '../assembly/backend-contract';
20
20
  import { createRenderer as constructRenderer } from '../assembly/factory';
21
21
  import {
22
+ BarrelDistortion,
22
23
  DirectionalLight,
23
24
  DynamicResolution,
24
25
  MeshFilter,
@@ -83,6 +84,10 @@ async function createBloomScenario() {
83
84
  antialias: ANTIALIAS_NONE,
84
85
  tonemap: TONEMAP_ACES_FILMIC,
85
86
  bloom: BLOOM_ENABLED,
87
+ bloomThreshold: 1,
88
+ bloomIntensity: 1,
89
+ bloomSoftKnee: 0.5,
90
+ bloomScatter: 0.7,
86
91
  },
87
92
  },
88
93
  )
@@ -103,13 +108,132 @@ async function createBloomScenario() {
103
108
  const updated = world.update();
104
109
  if (!updated.ok) throw updated.error;
105
110
  };
111
+ const setBloomIntensity = (bloomIntensity: number) => {
112
+ world.set(camera, LocalCamera, { bloomIntensity }).unwrap();
113
+ const updated = world.update();
114
+ if (!updated.ok) throw updated.error;
115
+ };
106
116
  const dispose = async () => {
107
117
  await renderer.dispose();
108
118
  };
109
- return { renderer, draw, setBloom, dispose, canvas };
119
+ return { renderer, draw, setBloom, setBloomIntensity, dispose, canvas };
120
+ }
121
+
122
+ async function createMotionBlurScenario() {
123
+ const renderer = await constructRenderer(
124
+ { width: 64, height: 64, getContext: () => null },
125
+ { rhi },
126
+ { shaderManifestUrl: renderLifecycleManifestUrl() },
127
+ );
128
+ if (!(await renderer.initialization).ok)
129
+ throw new Error('Motion Blur renderer initialization failed');
130
+ const world = new World();
131
+ const attached = renderer.attach(world);
132
+ if (!attached.ok) throw attached.error;
133
+ registerPropagateTransforms(world);
134
+ const camera = world
135
+ .spawn(
136
+ { component: Transform, data: { pos: [0, 0, 4] } },
137
+ {
138
+ component: LocalCamera,
139
+ data: {
140
+ fov: 1,
141
+ aspect: 1,
142
+ near: 0.1,
143
+ far: 100,
144
+ antialias: ANTIALIAS_NONE,
145
+ tonemap: TONEMAP_ACES_FILMIC,
146
+ },
147
+ },
148
+ )
149
+ .unwrap();
150
+ world
151
+ .addComponent(camera, {
152
+ component: MotionBlur,
153
+ data: { shutterAngle: 180, maxRadiusPixels: 8, sampleCount: 4, targetFps: 60 },
154
+ })
155
+ .unwrap();
156
+ const frameInput = () => ({
157
+ leases: [attached.value],
158
+ camera: { lease: attached.value },
159
+ environment: { lease: attached.value },
160
+ });
161
+ return {
162
+ renderer,
163
+ draw: () => renderer.draw(frameInput()),
164
+ dispose: () => renderer.dispose(),
165
+ };
166
+ }
167
+
168
+ function createGenericPostProbeFeature(): RenderFeature<undefined> {
169
+ return {
170
+ identity: 'synthetic.generic-post-probe',
171
+ placement: 'post',
172
+ shaderModuleMode: 'immediate',
173
+ extract: () => ok(undefined),
174
+ plan: () =>
175
+ ok({
176
+ resources: [
177
+ {
178
+ kind: 'compute-program' as const,
179
+ name: 'generic-post-probe-program',
180
+ program: {
181
+ wgsl: '@compute @workgroup_size(1) fn main() {}',
182
+ entryPoints: ['main'],
183
+ },
184
+ },
185
+ {
186
+ kind: 'compute-bindings' as const,
187
+ name: 'generic-post-probe-bindings',
188
+ program: 'generic-post-probe-program',
189
+ entries: [],
190
+ },
191
+ ],
192
+ passes: [
193
+ {
194
+ kind: 'compute' as const,
195
+ name: 'generic-post-probe',
196
+ program: 'generic-post-probe-program',
197
+ bindings: 'generic-post-probe-bindings',
198
+ dispatches: [{ kind: 'direct' as const, entryPoint: 'main', workgroups: [1] }],
199
+ },
200
+ ],
201
+ }),
202
+ };
110
203
  }
111
204
 
112
205
  describe('factory contract', () => {
206
+ it('does not publish renderer initialization before boot queue work settles', async () => {
207
+ const fence = deferred<undefined>();
208
+ const onSubmittedWorkDone = vi
209
+ .spyOn(RhiNullQueue.prototype, 'onSubmittedWorkDone')
210
+ .mockReturnValue(fence.promise);
211
+ const canvas = { width: 64, height: 64, getContext: () => null };
212
+ let renderer: Awaited<ReturnType<typeof constructRenderer>> | undefined;
213
+ try {
214
+ renderer = await constructRenderer(
215
+ canvas,
216
+ { rhi },
217
+ { shaderManifestUrl: renderLifecycleManifestUrl() },
218
+ );
219
+ let settled = false;
220
+ const initialization = renderer.initialization.then(() => {
221
+ settled = true;
222
+ });
223
+ await vi.waitFor(() => expect(onSubmittedWorkDone).toHaveBeenCalled(), {
224
+ timeout: 2_000,
225
+ });
226
+ expect(settled).toBe(false);
227
+ fence.resolve(undefined);
228
+ await initialization;
229
+ expect(settled).toBe(true);
230
+ } finally {
231
+ fence.resolve(undefined);
232
+ onSubmittedWorkDone.mockRestore();
233
+ await renderer?.dispose();
234
+ }
235
+ });
236
+
113
237
  it('requires one Renderer-owned target lifecycle seam', () => {
114
238
  expect(existsSync(renderTargetHostSourcePath)).toBe(true);
115
239
  const factorySource = readFileSync(factorySourcePath, 'utf8');
@@ -144,7 +268,7 @@ describe('factory contract', () => {
144
268
  it('releases its profiler catalog contribution on dispose', async () => {
145
269
  const profiler = createProfiler();
146
270
  const renderer = await constructRenderer(
147
- { getContext: () => null },
271
+ { width: 64, height: 64, getContext: () => null },
148
272
  { profiler, rhi },
149
273
  { shaderManifestUrl: manifestUrl() },
150
274
  );
@@ -172,7 +296,7 @@ describe('factory contract', () => {
172
296
 
173
297
  try {
174
298
  const renderer = await constructRenderer(
175
- { getContext: () => null },
299
+ { width: 64, height: 64, getContext: () => null },
176
300
  { rhi },
177
301
  { shaderManifestUrl: renderLifecycleManifestUrl() },
178
302
  );
@@ -218,7 +342,7 @@ describe('factory contract', () => {
218
342
  }),
219
343
  } as never;
220
344
  const renderer = await constructRenderer(
221
- { getContext: () => null },
345
+ { width: 64, height: 64, getContext: () => null },
222
346
  {
223
347
  rhi: timingRhi,
224
348
  gpuPassTiming: { maxPassesPerFrame: 4, maxFramesInFlight: 1, retentionFrames: 2 },
@@ -231,7 +355,7 @@ describe('factory contract', () => {
231
355
 
232
356
  it('completes the receipt-bound create, attach, draw, observe, recover chain', async () => {
233
357
  const renderer = await constructRenderer(
234
- { getContext: () => null },
358
+ { width: 64, height: 64, getContext: () => null },
235
359
  { rhi },
236
360
  { shaderManifestUrl: manifestUrl() },
237
361
  );
@@ -248,7 +372,7 @@ describe('factory contract', () => {
248
372
  camera: { lease },
249
373
  environment: { lease },
250
374
  });
251
- expect(frame.ok).toBe(true);
375
+ expect(frame.ok, frame.ok ? '' : JSON.stringify(frame.error)).toBe(true);
252
376
  if (!frame.ok || frame.value === undefined) return;
253
377
 
254
378
  const observed = await renderer.observe(frame.value, { include: ['timings'] });
@@ -261,7 +385,7 @@ describe('factory contract', () => {
261
385
 
262
386
  it('rejects the removed Standard reflection-probe profile field at the Renderer boundary', async () => {
263
387
  const renderer = await constructRenderer(
264
- { getContext: () => null },
388
+ { width: 64, height: 64, getContext: () => null },
265
389
  { rhi },
266
390
  { shaderManifestUrl: manifestUrl() },
267
391
  );
@@ -277,7 +401,7 @@ describe('factory contract', () => {
277
401
 
278
402
  it('projects stable output and observation identity without pixel payloads', async () => {
279
403
  const renderer = await constructRenderer(
280
- { getContext: () => null },
404
+ { width: 64, height: 64, getContext: () => null },
281
405
  { rhi },
282
406
  { shaderManifestUrl: manifestUrl() },
283
407
  );
@@ -306,7 +430,7 @@ describe('factory contract', () => {
306
430
 
307
431
  it('delivers a target readback only through its matching receipt', async () => {
308
432
  const renderer = await constructRenderer(
309
- { getContext: () => null },
433
+ { width: 64, height: 64, getContext: () => null },
310
434
  { rhi },
311
435
  { shaderManifestUrl: manifestUrl() },
312
436
  );
@@ -335,7 +459,7 @@ describe('factory contract', () => {
335
459
  camera: { lease: attached.value },
336
460
  environment: { lease: attached.value },
337
461
  });
338
- expect(frame.ok).toBe(true);
462
+ expect(frame.ok, frame.ok ? '' : JSON.stringify(frame.error)).toBe(true);
339
463
  if (!frame.ok || frame.value === undefined) return;
340
464
  const observed = await renderer.observe(frame.value, {
341
465
  include: ['target-readbacks'],
@@ -384,11 +508,65 @@ describe('factory contract', () => {
384
508
  camera: { lease: attached.value },
385
509
  environment: { lease: attached.value },
386
510
  });
387
- expect(frame.ok).toBe(true);
511
+ expect(frame.ok, frame.ok ? '' : JSON.stringify(frame.error)).toBe(true);
388
512
  expect(planCalls).toBe(1);
389
513
  renderer.dispose();
390
514
  });
391
515
 
516
+ it('projects the active barrel stage through the ordinary feature host once', async () => {
517
+ const renderer = await constructRenderer(
518
+ { width: 64, height: 64, getContext: () => null },
519
+ { rhi },
520
+ { shaderManifestUrl: renderLifecycleManifestUrl() },
521
+ );
522
+ expect((await renderer.initialization).ok).toBe(true);
523
+ const world = new World();
524
+ const attached = renderer.attach(world);
525
+ expect(attached.ok).toBe(true);
526
+ if (!attached.ok) return;
527
+ registerPropagateTransforms(world);
528
+ world
529
+ .spawn(
530
+ { component: Transform, data: { pos: [0, 0, 4] } },
531
+ {
532
+ component: LocalCamera,
533
+ data: {
534
+ fov: 1,
535
+ aspect: 1,
536
+ near: 0.1,
537
+ far: 100,
538
+ antialias: ANTIALIAS_NONE,
539
+ tonemap: TONEMAP_ACES_FILMIC,
540
+ bloom: BLOOM_DISABLED,
541
+ },
542
+ },
543
+ { component: BarrelDistortion, data: { strength: 0.2, centerX: 0.5, centerY: 0.5 } },
544
+ )
545
+ .unwrap();
546
+ expect(world.update().ok).toBe(true);
547
+ const frame = renderer.draw({
548
+ leases: [attached.value],
549
+ camera: { lease: attached.value },
550
+ environment: { lease: attached.value },
551
+ });
552
+ expect(frame.ok, frame.ok ? '' : JSON.stringify(frame.error)).toBe(true);
553
+ if (frame.ok && frame.value !== undefined) {
554
+ expect(frame.value.barrelDistortion).toMatchObject({
555
+ width: 64,
556
+ height: 64,
557
+ camera: { projection: 'perspective' },
558
+ });
559
+ expect(frame.value.barrelDistortion?.strength).toBeCloseTo(0.2, 6);
560
+ expect(
561
+ renderer.perFramePassNames.filter((name) => name === 'barrel-distortion'),
562
+ ).toHaveLength(1);
563
+ expect(
564
+ renderer.perFramePassNames.filter((name) => name.startsWith('post-effect-')),
565
+ ).toHaveLength(0);
566
+ }
567
+ await renderer.dispose();
568
+ });
569
+
392
570
  it('keeps the active graph as LKG while a feature candidate is pending', async () => {
393
571
  let invalid = false;
394
572
  let featurePlanCalls = 0;
@@ -624,7 +802,7 @@ describe('factory contract', () => {
624
802
 
625
803
  it('attaches derived-state systems once without putting writes in draw', async () => {
626
804
  const renderer = await constructRenderer(
627
- { getContext: () => null },
805
+ { width: 64, height: 64, getContext: () => null },
628
806
  { rhi },
629
807
  { shaderManifestUrl: manifestUrl() },
630
808
  );
@@ -640,12 +818,12 @@ describe('factory contract', () => {
640
818
 
641
819
  it('releases one World without disposing the shared Renderer', async () => {
642
820
  const first = await constructRenderer(
643
- { getContext: () => null },
821
+ { width: 64, height: 64, getContext: () => null },
644
822
  { rhi },
645
823
  { shaderManifestUrl: manifestUrl() },
646
824
  );
647
825
  const second = await constructRenderer(
648
- { getContext: () => null },
826
+ { width: 64, height: 64, getContext: () => null },
649
827
  { rhi },
650
828
  { shaderManifestUrl: manifestUrl() },
651
829
  );
@@ -662,7 +840,11 @@ describe('factory contract', () => {
662
840
  it('resolves a renderer for a host canvas and rejects missing input', async () => {
663
841
  const manifest = `data:application/json,${encodeURIComponent(JSON.stringify({ schemaVersion: '1.0.0', entries: [] }))}`;
664
842
  await expect(
665
- constructRenderer({ getContext: () => null }, { rhi }, { shaderManifestUrl: manifest }),
843
+ constructRenderer(
844
+ { width: 64, height: 64, getContext: () => null },
845
+ { rhi },
846
+ { shaderManifestUrl: manifest },
847
+ ),
666
848
  ).resolves.toMatchObject({
667
849
  attach: expect.any(Function),
668
850
  draw: expect.any(Function),
@@ -673,7 +855,7 @@ describe('factory contract', () => {
673
855
  it('temporarily releases presentation while preserving the Renderer identity', async () => {
674
856
  const manifest = `data:application/json,${encodeURIComponent(JSON.stringify({ schemaVersion: '1.0.0', entries: [] }))}`;
675
857
  const renderer = await constructRenderer(
676
- { getContext: () => null },
858
+ { width: 64, height: 64, getContext: () => null },
677
859
  { rhi },
678
860
  { shaderManifestUrl: manifest },
679
861
  );
@@ -689,7 +871,7 @@ describe('factory contract', () => {
689
871
  it('does not unconfigure a surface after its ownership was released', async () => {
690
872
  let unconfigureCalls = 0;
691
873
  const renderer = await constructRenderer(
692
- { getContext: () => null },
874
+ { width: 64, height: 64, getContext: () => null },
693
875
  {
694
876
  rhi: {
695
877
  ...rhi,
@@ -718,19 +900,70 @@ describe('factory contract', () => {
718
900
  expect(unconfigureCalls).toBe(1);
719
901
  });
720
902
 
903
+ it('keeps direct RHI instrumentation explicit-only at the factory boundary', async () => {
904
+ const drawOneFrame = (renderer: Awaited<ReturnType<typeof constructRenderer>>) => {
905
+ const world = new World();
906
+ const attached = renderer.attach(world);
907
+ expect(attached.ok).toBe(true);
908
+ if (!attached.ok) return;
909
+ expect(world.update().ok).toBe(true);
910
+ const frame = renderer.draw({
911
+ leases: [attached.value],
912
+ camera: { lease: attached.value },
913
+ environment: { lease: attached.value },
914
+ });
915
+ expect(frame.ok, frame.ok ? '' : JSON.stringify(frame.error)).toBe(true);
916
+ };
917
+ const moduleBoundary = vi.fn();
918
+ const implicit = await constructRenderer(
919
+ { width: 64, height: 64, getContext: () => null },
920
+ {
921
+ rhi: {
922
+ ...rhi,
923
+ instrumentation: { onFrameBoundary: moduleBoundary },
924
+ } as never,
925
+ },
926
+ { shaderManifestUrl: manifestUrl() },
927
+ );
928
+ expect((await implicit.initialization).ok).toBe(true);
929
+ drawOneFrame(implicit);
930
+ expect(moduleBoundary).not.toHaveBeenCalled();
931
+ expect((await implicit.dispose()).ok).toBe(true);
932
+
933
+ const explicitBoundary = vi.fn();
934
+ const explicit = await constructRenderer(
935
+ { width: 64, height: 64, getContext: () => null },
936
+ {
937
+ rhi: {
938
+ ...rhi,
939
+ instrumentation: { onFrameBoundary: moduleBoundary },
940
+ } as never,
941
+ rhiInstrumentation: { onFrameBoundary: explicitBoundary },
942
+ },
943
+ { shaderManifestUrl: manifestUrl() },
944
+ );
945
+ expect((await explicit.initialization).ok).toBe(true);
946
+ drawOneFrame(explicit);
947
+ expect(explicitBoundary).toHaveBeenCalled();
948
+ expect(moduleBoundary).not.toHaveBeenCalled();
949
+ expect((await explicit.dispose()).ok).toBe(true);
950
+ });
951
+
721
952
  it('retains the device-owned prewarmed TAA shader across off and on', async () => {
722
953
  const pending: Array<{
723
954
  readonly label: string;
724
955
  readonly promise: Promise<ReturnType<typeof ok<ShaderModule>>>;
725
956
  readonly resolve: (value: ReturnType<typeof ok<ShaderModule>>) => void;
726
957
  }> = [];
727
- const moduleCalls: string[] = [];
958
+ const moduleSources: string[] = [];
959
+ const frameBoundary = vi.fn();
728
960
  const backend: RhiBackendPack = {
729
961
  rhi,
962
+ instrumentation: { onFrameBoundary: frameBoundary },
730
963
  createShaderModule: (_device, desc) => {
731
964
  const label = desc.label ?? '<unlabeled>';
732
- moduleCalls.push(label);
733
- if (!label.startsWith('post-process-forgeax.taa-resolve-pso-')) {
965
+ moduleSources.push(desc.code);
966
+ if (!desc.code.includes('fs_taa_resolve')) {
734
967
  return Promise.resolve(ok({} as ShaderModule));
735
968
  }
736
969
  let resolve!: (value: ReturnType<typeof ok<ShaderModule>>) => void;
@@ -781,7 +1014,8 @@ describe('factory contract', () => {
781
1014
  });
782
1015
 
783
1016
  const first = renderer.draw(frameInput());
784
- expect(first.ok).toBe(true);
1017
+ expect(first.ok, first.ok ? '' : JSON.stringify(first.error)).toBe(true);
1018
+ expect(frameBoundary).toHaveBeenCalled();
785
1019
  expect(pending).toHaveLength(1);
786
1020
 
787
1021
  world.set(camera, LocalCamera, { antialias: ANTIALIAS_NONE }).unwrap();
@@ -789,14 +1023,12 @@ describe('factory contract', () => {
789
1023
  const off = renderer.draw(frameInput());
790
1024
  expect(off.ok).toBe(true);
791
1025
 
792
- expect(
793
- moduleCalls.filter((label) => label.startsWith('post-process-forgeax.taa-resolve-pso-')),
794
- ).toHaveLength(1);
1026
+ expect(moduleSources.filter((source) => source.includes('fs_taa_resolve'))).toHaveLength(1);
795
1027
 
796
1028
  world.set(camera, LocalCamera, { antialias: ANTIALIAS_TAA }).unwrap();
797
1029
  expect(world.update().ok).toBe(true);
798
1030
  const second = renderer.draw(frameInput());
799
- expect(second.ok).toBe(true);
1031
+ expect(second.ok, second.ok ? '' : JSON.stringify(second.error)).toBe(true);
800
1032
  expect(pending).toHaveLength(1);
801
1033
  expect(renderer.draw(frameInput()).ok).toBe(true);
802
1034
  world.set(camera, LocalCamera, { antialias: ANTIALIAS_NONE }).unwrap();
@@ -810,13 +1042,11 @@ describe('factory contract', () => {
810
1042
 
811
1043
  expect((await renderer.dispose()).ok).toBe(true);
812
1044
  expect((await renderer.dispose()).ok).toBe(true);
813
- expect(
814
- moduleCalls.filter((label) => label.startsWith('post-process-forgeax.taa-resolve-pso-')),
815
- ).toHaveLength(1);
1045
+ expect(moduleSources.filter((source) => source.includes('fs_taa_resolve'))).toHaveLength(1);
816
1046
  });
817
1047
 
818
1048
  it('rebuilds a TAA graph for a same-mode target resize', async () => {
819
- const moduleCalls: string[] = [];
1049
+ const moduleSources: string[] = [];
820
1050
  const created = new Map<object, string>();
821
1051
  const destroyed = new Set<object>();
822
1052
  let wrappedDevice: object | undefined;
@@ -859,7 +1089,7 @@ describe('factory contract', () => {
859
1089
  const backend: RhiBackendPack = {
860
1090
  rhi: instrumentedRhi,
861
1091
  createShaderModule: async (_device, desc) => {
862
- moduleCalls.push(desc.label ?? '<unlabeled>');
1092
+ moduleSources.push(desc.code);
863
1093
  return ok({} as ShaderModule);
864
1094
  },
865
1095
  };
@@ -898,14 +1128,12 @@ describe('factory contract', () => {
898
1128
  await Promise.resolve();
899
1129
  first = renderer.draw(frameInput());
900
1130
  }
901
- expect(first.ok).toBe(true);
1131
+ expect(first.ok, first.ok ? '' : JSON.stringify(first.error)).toBe(true);
902
1132
  const firstGeneration = [...created.entries()]
903
1133
  .filter(([_, label]) => label.startsWith('taa-history-'))
904
1134
  .slice(-4);
905
1135
  expect(firstGeneration).toHaveLength(4);
906
- expect(
907
- moduleCalls.filter((label) => label.startsWith('post-process-forgeax.taa-resolve-pso-')),
908
- ).toHaveLength(1);
1136
+ expect(moduleSources.filter((source) => source.includes('fs_taa_resolve'))).toHaveLength(1);
909
1137
  const firstFrame = renderer.inspect().frame.frameId;
910
1138
 
911
1139
  canvas.width = 96;
@@ -927,9 +1155,7 @@ describe('factory contract', () => {
927
1155
  expect(replacementGeneration.every(([handle]) => !destroyed.has(handle))).toBe(true);
928
1156
  expect(renderer.inspect().frame.frameId).toBe(firstFrame + 1);
929
1157
  expect(renderer.inspect().state).toBe('alive');
930
- expect(
931
- moduleCalls.filter((label) => label.startsWith('post-process-forgeax.taa-resolve-pso-')),
932
- ).toHaveLength(1);
1158
+ expect(moduleSources.filter((source) => source.includes('fs_taa_resolve'))).toHaveLength(1);
933
1159
  world.set(camera, LocalCamera, { antialias: ANTIALIAS_NONE }).unwrap();
934
1160
  expect(world.update().ok).toBe(true);
935
1161
  expect(renderer.draw(frameInput()).ok).toBe(true);
@@ -1086,12 +1312,189 @@ describe('factory contract', () => {
1086
1312
  }
1087
1313
  const inspection = renderer.inspect();
1088
1314
  expect(inspection.perFramePassNames).toContain('motion-blur');
1315
+ // The same Rhi device/frame admitted the compute plan, graph, and both
1316
+ // production dispatches. This is the application-side admission proof;
1317
+ // Dawn/physical timing remains a separate backend gate.
1318
+ expect(inspection.capabilities).toMatchObject({
1319
+ compute: true,
1320
+ storageBuffer: true,
1321
+ storageTexture: true,
1322
+ rgba16floatRenderable: true,
1323
+ });
1324
+ expect(inspection.motionBlur).toMatchObject({ lane: 'compute', passCount: 2 });
1325
+ expect(inspection.perFramePassNames).toContain('motion-blur');
1326
+ expect(inspection.recoveryEvidence.graph).toMatchObject({ ready: true });
1327
+ expect(inspection.featureGraph?.compileSuccesses ?? 0).toBeGreaterThan(0);
1328
+ // The graph is present, but this is the first accepted temporal pair
1329
+ // after the effect was added. Inspection must expose the reset boundary
1330
+ // until a subsequent successful pair can provide a valid baseline.
1089
1331
  expect(inspection.motionBlur).toMatchObject({
1090
1332
  enabled: true,
1091
- status: 'active',
1333
+ status: 'reset',
1334
+ temporalDemand: 'scene-data-temporal-v1',
1335
+ });
1336
+ await renderer.dispose();
1337
+ });
1338
+
1339
+ it('projects generic post work once and keeps Motion Blur independent across Standard lanes', async () => {
1340
+ for (const renderPath of ['forward', 'deferred'] as const) {
1341
+ for (const taa of [false, true]) {
1342
+ const renderer = await constructRenderer(
1343
+ { width: 64, height: 64, getContext: () => null },
1344
+ { rhi, features: [createGenericPostProbeFeature()] },
1345
+ { shaderManifestUrl: renderLifecycleManifestUrl() },
1346
+ );
1347
+ try {
1348
+ expect((await renderer.initialization).ok).toBe(true);
1349
+ expect(renderer.setProfile({ ...DEFAULT_STANDARD_PROFILE, renderPath }).ok).toBe(true);
1350
+ const world = new World();
1351
+ const attached = renderer.attach(world);
1352
+ expect(attached.ok).toBe(true);
1353
+ if (!attached.ok) continue;
1354
+ registerPropagateTransforms(world);
1355
+ const camera = world
1356
+ .spawn(
1357
+ { component: Transform, data: { pos: [0, 0, 4] } },
1358
+ {
1359
+ component: LocalCamera,
1360
+ data: {
1361
+ fov: 1,
1362
+ aspect: 1,
1363
+ near: 0.1,
1364
+ far: 100,
1365
+ antialias: taa ? ANTIALIAS_TAA : ANTIALIAS_NONE,
1366
+ tonemap: TONEMAP_ACES_FILMIC,
1367
+ },
1368
+ },
1369
+ )
1370
+ .unwrap();
1371
+ world
1372
+ .addComponent(camera, {
1373
+ component: MotionBlur,
1374
+ data: { shutterAngle: 180, maxRadiusPixels: 8, sampleCount: 4, targetFps: 60 },
1375
+ })
1376
+ .unwrap();
1377
+ expect(world.update().ok).toBe(true);
1378
+ const draw = renderer.draw({
1379
+ leases: [attached.value],
1380
+ camera: { lease: attached.value },
1381
+ environment: { lease: attached.value },
1382
+ });
1383
+ expect(draw.ok).toBe(true);
1384
+ if (!draw.ok || draw.value === undefined) continue;
1385
+ const completed = await draw.value.completed;
1386
+ expect(completed.ok).toBe(true);
1387
+ if (!completed.ok) continue;
1388
+ const inspection = renderer.inspect();
1389
+ const names = inspection.perFramePassNames;
1390
+ const genericIndex = names.indexOf('generic-post-probe');
1391
+ const motionSummaryIndex = names.indexOf('motion-blur-tile-summary');
1392
+ const motionIndex = names.indexOf('motion-blur');
1393
+ const outputIndex = names.indexOf('output-transform');
1394
+ expect(names.filter((name) => name === 'generic-post-probe')).toHaveLength(1);
1395
+ expect(names.filter((name) => name === 'motion-blur-tile-summary')).toHaveLength(1);
1396
+ expect(names.filter((name) => name === 'motion-blur')).toHaveLength(1);
1397
+ expect(genericIndex).toBeGreaterThanOrEqual(0);
1398
+ expect(motionSummaryIndex).toBeGreaterThanOrEqual(0);
1399
+ expect(motionIndex).toBeGreaterThan(motionSummaryIndex);
1400
+ expect(outputIndex).toBeGreaterThan(motionIndex);
1401
+ if (taa) expect(names.indexOf('taa-resolve')).toBeLessThan(motionSummaryIndex);
1402
+ else expect(names).not.toContain('taa-resolve');
1403
+ expect(inspection.motionBlur).toMatchObject({
1404
+ enabled: true,
1405
+ passCount: 2,
1406
+ execution: { passes: [{ name: 'motion-blur-tile-summary' }, { name: 'motion-blur' }] },
1407
+ });
1408
+ // A completed two-pass execution is the public Renderer proof that
1409
+ // the named motion-blurred-color target had a writer and was then
1410
+ // consumed by the shared output chain.
1411
+ expect(inspection.featureGraph?.compileSuccesses ?? 0).toBeGreaterThan(0);
1412
+ } finally {
1413
+ await renderer.dispose();
1414
+ }
1415
+ }
1416
+ }
1417
+ });
1418
+
1419
+ it('publishes invalid MotionBlur parameters through renderer inspection', async () => {
1420
+ const renderer = await constructRenderer(
1421
+ { width: 64, height: 64, getContext: () => null },
1422
+ { rhi },
1423
+ { shaderManifestUrl: renderLifecycleManifestUrl() },
1424
+ );
1425
+ expect((await renderer.initialization).ok).toBe(true);
1426
+ const errors: unknown[] = [];
1427
+ renderer.onError((error) => errors.push(error));
1428
+ const world = new World();
1429
+ const attached = renderer.attach(world);
1430
+ expect(attached.ok).toBe(true);
1431
+ if (!attached.ok) return;
1432
+ registerPropagateTransforms(world);
1433
+ const camera = world
1434
+ .spawn(
1435
+ { component: Transform, data: { pos: [0, 0, 4] } },
1436
+ {
1437
+ component: LocalCamera,
1438
+ data: { fov: 1, aspect: 1, near: 0.1, far: 100, antialias: ANTIALIAS_TAA },
1439
+ },
1440
+ { component: MotionBlur, data: { targetFps: 59.5 } },
1441
+ )
1442
+ .unwrap();
1443
+ const frameInput = () => ({
1444
+ leases: [attached.value],
1445
+ camera: { lease: attached.value },
1446
+ environment: { lease: attached.value },
1447
+ });
1448
+ expect(world.update().ok).toBe(true);
1449
+
1450
+ const draw = renderer.draw(frameInput());
1451
+ expect(draw.ok).toBe(false);
1452
+ if (!draw.ok) {
1453
+ expect(draw.error).toMatchObject({
1454
+ code: 'motion-blur-invalid-params',
1455
+ detail: { field: 'targetFps', value: 59.5, integer: true },
1456
+ });
1457
+ }
1458
+ expect(errors).toEqual(
1459
+ expect.arrayContaining([expect.objectContaining({ code: 'motion-blur-invalid-params' })]),
1460
+ );
1461
+ expect(renderer.inspect().motionBlur).toMatchObject({
1462
+ enabled: true,
1463
+ status: 'invalid',
1464
+ lastFailure: 'invalid-params',
1092
1465
  temporalDemand: 'scene-data-temporal-v1',
1093
1466
  });
1094
1467
  await renderer.dispose();
1468
+ void camera;
1469
+ });
1470
+
1471
+ it('projects a rejected Motion Blur submit while retaining its accepted baseline', async () => {
1472
+ const scenario = await createMotionBlurScenario();
1473
+ const submit = vi.spyOn(RhiNullQueue.prototype, 'submit');
1474
+ const failure = new RhiError({
1475
+ code: 'webgpu-runtime-error',
1476
+ expected: 'Motion Blur queue submission succeeds',
1477
+ hint: 'repair the queue and retry the same frame',
1478
+ });
1479
+ try {
1480
+ expect(scenario.draw().ok).toBe(true);
1481
+ expect(scenario.draw().ok).toBe(true);
1482
+ const accepted = scenario.renderer.inspect().motionBlur;
1483
+ expect(accepted).toMatchObject({ enabled: true });
1484
+ expect(accepted).not.toHaveProperty('lastFailure');
1485
+ submit.mockReturnValueOnce(err(failure));
1486
+ expect(scenario.draw().ok).toBe(false);
1487
+ expect(scenario.renderer.inspect().motionBlur).toMatchObject({
1488
+ enabled: true,
1489
+ lastFailure: 'submit-failure',
1490
+ });
1491
+ submit.mockRestore();
1492
+ expect(scenario.draw().ok).toBe(true);
1493
+ expect(scenario.renderer.inspect().motionBlur?.lastFailure).toBeUndefined();
1494
+ } finally {
1495
+ submit.mockRestore();
1496
+ await scenario.dispose();
1497
+ }
1095
1498
  });
1096
1499
 
1097
1500
  it('retires factory-owned TAA textures and params on an off submit', async () => {
@@ -1168,7 +1571,7 @@ describe('factory contract', () => {
1168
1571
  await Promise.resolve();
1169
1572
  first = renderer.draw(frameInput());
1170
1573
  }
1171
- expect(first.ok).toBe(true);
1574
+ expect(first.ok, first.ok ? '' : JSON.stringify(first.error)).toBe(true);
1172
1575
  const historyHandles = [...created.entries()].filter(([_, label]) =>
1173
1576
  label.startsWith('taa-history-'),
1174
1577
  );
@@ -1230,12 +1633,12 @@ describe('factory contract', () => {
1230
1633
  const onInspection = renderer.inspect().bloom;
1231
1634
  expect(onInspection.state).toBe('active');
1232
1635
  expect(onInspection.graphStatus).toBe('valid');
1233
- expect(onInspection.targetCount).toBe(4);
1234
- expect(onInspection.targetBytes).toBe((64 * 64 + 3 * 32 * 32) * 8);
1235
- expect(onInspection.passCount).toBe(4);
1236
- expect(onInspection.uploadCount).toBe(4);
1237
- expect(onInspection.bindGroupCount).toBe(4);
1238
- expect(onInspection.encodeCount).toBe(4);
1636
+ expect(onInspection.targetCount).toBe(10);
1637
+ expect(onInspection.targetBytes).toBe(54_560);
1638
+ expect(onInspection.passCount).toBe(10);
1639
+ expect(onInspection.uploadCount).toBe(10);
1640
+ expect(onInspection.bindGroupCount).toBe(10);
1641
+ expect(onInspection.encodeCount).toBe(10);
1239
1642
  expect(JSON.parse(JSON.stringify(onInspection))).toEqual(onInspection);
1240
1643
  const firstGeneration = onInspection.generation;
1241
1644
  expect(firstGeneration).toBeGreaterThan(0);
@@ -1266,10 +1669,10 @@ describe('factory contract', () => {
1266
1669
  const recoveredInspection = renderer.inspect().bloom;
1267
1670
  expect(recoveredInspection.state).toBe('active');
1268
1671
  expect(recoveredInspection.resourceCount).toBeGreaterThan(0);
1269
- expect(recoveredInspection.passCount).toBe(4);
1270
- expect(recoveredInspection.uploadCount).toBe(4);
1271
- expect(recoveredInspection.bindGroupCount).toBe(4);
1272
- expect(recoveredInspection.encodeCount).toBe(4);
1672
+ expect(recoveredInspection.passCount).toBe(10);
1673
+ expect(recoveredInspection.uploadCount).toBe(10);
1674
+ expect(recoveredInspection.bindGroupCount).toBe(10);
1675
+ expect(recoveredInspection.encodeCount).toBe(10);
1273
1676
  expect(recoveredInspection.generation).toBeGreaterThan(firstGeneration);
1274
1677
  await Promise.resolve();
1275
1678
  await Promise.resolve();
@@ -1290,25 +1693,25 @@ describe('factory contract', () => {
1290
1693
  try {
1291
1694
  expect(scenario.draw().ok).toBe(true);
1292
1695
  expect(scenario.renderer.inspect().bloom).toMatchObject({
1293
- uploadCount: 4,
1294
- bindGroupCount: 4,
1295
- encodeCount: 4,
1696
+ uploadCount: 10,
1697
+ bindGroupCount: 10,
1698
+ encodeCount: 10,
1296
1699
  });
1297
1700
 
1298
1701
  submit.mockReturnValueOnce(err(failure));
1299
1702
  expect(scenario.draw().ok).toBe(false);
1300
1703
  expect(scenario.renderer.inspect().bloom).toMatchObject({
1301
1704
  state: 'active',
1302
- uploadCount: 4,
1303
- bindGroupCount: 4,
1304
- encodeCount: 4,
1705
+ uploadCount: 10,
1706
+ bindGroupCount: 10,
1707
+ encodeCount: 10,
1305
1708
  });
1306
1709
 
1307
1710
  expect(scenario.draw().ok).toBe(true);
1308
1711
  expect(scenario.renderer.inspect().bloom).toMatchObject({
1309
- uploadCount: 4,
1310
- bindGroupCount: 4,
1311
- encodeCount: 4,
1712
+ uploadCount: 10,
1713
+ bindGroupCount: 10,
1714
+ encodeCount: 10,
1312
1715
  });
1313
1716
  } finally {
1314
1717
  submit.mockRestore();
@@ -1316,6 +1719,26 @@ describe('factory contract', () => {
1316
1719
  }
1317
1720
  });
1318
1721
 
1722
+ it('uploads positive Bloom parameter changes without rebuilding the accepted bundle', async () => {
1723
+ const scenario = await createBloomScenario();
1724
+ try {
1725
+ expect(scenario.draw().ok).toBe(true);
1726
+ const first = scenario.renderer.inspect().bloom;
1727
+ scenario.setBloomIntensity(0.35);
1728
+ expect(scenario.draw().ok).toBe(true);
1729
+ const updated = scenario.renderer.inspect().bloom;
1730
+ expect(updated.state).toBe('active');
1731
+ expect(updated.generation).toBe(first.generation);
1732
+ expect(updated.resourceCount).toBe(first.resourceCount);
1733
+ expect(updated.targetCount).toBe(first.targetCount);
1734
+ expect(updated.targetBytes).toBe(first.targetBytes);
1735
+ expect(updated.uploadCount).toBe(10);
1736
+ expect(updated.passCount).toBe(10);
1737
+ } finally {
1738
+ await scenario.dispose();
1739
+ }
1740
+ });
1741
+
1319
1742
  it('aborts a Bloom pass write failure without publishing partial receipts', async () => {
1320
1743
  const originalWriteBuffer = RhiNullQueue.prototype.writeBuffer;
1321
1744
  const createBuffer = vi.spyOn(RhiNullDevice.prototype, 'createBuffer');
@@ -1324,30 +1747,30 @@ describe('factory contract', () => {
1324
1747
  const scenario = await createBloomScenario();
1325
1748
  const failure = new RhiError({
1326
1749
  code: 'webgpu-runtime-error',
1327
- expected: 'Bloom blur-H params upload succeeds',
1750
+ expected: 'Bloom downsample params upload succeeds',
1328
1751
  hint: 'repair the Bloom params buffer and retry the frame',
1329
1752
  });
1330
1753
  const errors: unknown[] = [];
1331
1754
  const unsubscribe = scenario.renderer.onError((error) => errors.push(error));
1332
1755
  try {
1333
1756
  expect(scenario.draw().ok).toBe(true);
1334
- const blurHParams = createBuffer.mock.calls.reduce<object | undefined>(
1757
+ const downsampleParams = createBuffer.mock.calls.reduce<object | undefined>(
1335
1758
  (found, args, index) => {
1336
- if (found !== undefined || args[0].label !== 'bloom-blur-h-params-ubo') return found;
1759
+ if (found !== undefined || args[0].label !== 'bloom-downsample-params-ubo') return found;
1337
1760
  const result = createBuffer.mock.results[index];
1338
1761
  if (result?.type !== 'return' || !result.value.ok) return undefined;
1339
1762
  return result.value.value as object;
1340
1763
  },
1341
1764
  undefined,
1342
1765
  );
1343
- expect(blurHParams).toBeDefined();
1766
+ expect(downsampleParams).toBeDefined();
1344
1767
  const lkg = scenario.renderer.inspect().bloom;
1345
- expect(lkg).toMatchObject({ uploadCount: 4, bindGroupCount: 4, encodeCount: 4 });
1768
+ expect(lkg).toMatchObject({ uploadCount: 10, bindGroupCount: 10, encodeCount: 10 });
1346
1769
  const submitCount = submit.mock.calls.length;
1347
1770
 
1348
1771
  let injected = false;
1349
1772
  writeBuffer.mockImplementation((buffer, ...args) => {
1350
- if (buffer === blurHParams && !injected) {
1773
+ if (buffer === downsampleParams && !injected) {
1351
1774
  injected = true;
1352
1775
  return err(failure);
1353
1776
  }
@@ -1359,23 +1782,23 @@ describe('factory contract', () => {
1359
1782
  expect(errors).toHaveLength(1);
1360
1783
  expect(errors[0]).toMatchObject({
1361
1784
  code: 'pass-encode-failed',
1362
- detail: { passName: 'bloom-blur-h', cause: failure },
1785
+ detail: { passName: 'bloom-downsample-0', cause: failure },
1363
1786
  });
1364
1787
  expect(scenario.renderer.inspect().bloom).toMatchObject({
1365
1788
  state: 'active',
1366
1789
  generation: lkg.generation,
1367
- uploadCount: 4,
1368
- bindGroupCount: 4,
1369
- encodeCount: 4,
1790
+ uploadCount: 10,
1791
+ bindGroupCount: 10,
1792
+ encodeCount: 10,
1370
1793
  });
1371
1794
 
1372
1795
  writeBuffer.mockRestore();
1373
1796
  expect(scenario.draw().ok).toBe(true);
1374
1797
  expect(submit).toHaveBeenCalledTimes(submitCount + 1);
1375
1798
  expect(scenario.renderer.inspect().bloom).toMatchObject({
1376
- uploadCount: 4,
1377
- bindGroupCount: 4,
1378
- encodeCount: 4,
1799
+ uploadCount: 10,
1800
+ bindGroupCount: 10,
1801
+ encodeCount: 10,
1379
1802
  });
1380
1803
  } finally {
1381
1804
  writeBuffer.mockRestore();
@@ -1388,18 +1811,17 @@ describe('factory contract', () => {
1388
1811
 
1389
1812
  const bloomConstructionFailures = [
1390
1813
  ['createSampler', 'bloom-sampler'],
1391
- ['createBindGroupLayout', 'bloom-bright-bgl'],
1392
- ['createBindGroupLayout', 'bloom-blur-bgl'],
1814
+ ['createBindGroupLayout', 'bloom-downsample-bgl'],
1815
+ ['createBindGroupLayout', 'bloom-upsample-bgl'],
1393
1816
  ['createBindGroupLayout', 'bloom-composite-bgl'],
1394
- ['createPipelineLayout', 'bloom-bright-pl'],
1395
- ['createPipelineLayout', 'bloom-blur-pl'],
1817
+ ['createPipelineLayout', 'bloom-downsample-pl'],
1818
+ ['createPipelineLayout', 'bloom-upsample-pl'],
1396
1819
  ['createPipelineLayout', 'bloom-composite-pl'],
1397
- ['createRenderPipeline', 'bloom-bright-pipeline'],
1398
- ['createRenderPipeline', 'bloom-blur-h-pipeline'],
1820
+ ['createRenderPipeline', 'bloom-downsample-pipeline'],
1821
+ ['createRenderPipeline', 'bloom-upsample-pipeline'],
1399
1822
  ['createRenderPipeline', 'bloom-composite-pipeline'],
1400
- ['createBuffer', 'bloom-bright-params-ubo'],
1401
- ['createBuffer', 'bloom-blur-h-params-ubo'],
1402
- ['createBuffer', 'bloom-blur-v-params-ubo'],
1823
+ ['createBuffer', 'bloom-downsample-params-ubo'],
1824
+ ['createBuffer', 'bloom-upsample-params-ubo'],
1403
1825
  ['createBuffer', 'bloom-composite-params-ubo'],
1404
1826
  ] as const;
1405
1827
 
@@ -1464,11 +1886,11 @@ describe('factory contract', () => {
1464
1886
  expect(scenario.renderer.inspect().bloom).toMatchObject({
1465
1887
  state: 'active',
1466
1888
  graphStatus: 'valid',
1467
- targetCount: 4,
1468
- passCount: 4,
1469
- uploadCount: 4,
1470
- bindGroupCount: 4,
1471
- encodeCount: 4,
1889
+ targetCount: 10,
1890
+ passCount: 10,
1891
+ uploadCount: 10,
1892
+ bindGroupCount: 10,
1893
+ encodeCount: 10,
1472
1894
  });
1473
1895
  } finally {
1474
1896
  operation.mockRestore();
@@ -1485,7 +1907,7 @@ describe('factory contract', () => {
1485
1907
  const submit = vi.spyOn(RhiNullQueue.prototype, 'submit');
1486
1908
  const failure = new RhiError({
1487
1909
  code: 'webgpu-runtime-error',
1488
- expected: 'Bloom blur-V bind-group creation succeeds',
1910
+ expected: 'Bloom upsample bind-group creation succeeds',
1489
1911
  hint: 'repair the Bloom bind group and retry the frame',
1490
1912
  });
1491
1913
  let injected = false;
@@ -1496,7 +1918,7 @@ describe('factory contract', () => {
1496
1918
  scenario.canvas.width = 96;
1497
1919
  scenario.canvas.height = 64;
1498
1920
  createBindGroup.mockImplementation(function (this: RhiNullDevice, descriptor) {
1499
- if (!injected && descriptor.label === 'bloom-blur-v-bg') {
1921
+ if (!injected && descriptor.label === 'bloom-upsample-3-bg') {
1500
1922
  injected = true;
1501
1923
  return err(failure);
1502
1924
  }
@@ -1508,9 +1930,9 @@ describe('factory contract', () => {
1508
1930
  expect(scenario.renderer.inspect().bloom).toMatchObject({
1509
1931
  state: 'active',
1510
1932
  generation: lkg.generation,
1511
- uploadCount: 4,
1512
- bindGroupCount: 4,
1513
- encodeCount: 4,
1933
+ uploadCount: 10,
1934
+ bindGroupCount: 10,
1935
+ encodeCount: 10,
1514
1936
  });
1515
1937
 
1516
1938
  createBindGroup.mockRestore();
@@ -1518,9 +1940,9 @@ describe('factory contract', () => {
1518
1940
  expect(submit).toHaveBeenCalledTimes(submitCount + 1);
1519
1941
  expect(scenario.renderer.inspect().bloom).toMatchObject({
1520
1942
  state: 'active',
1521
- uploadCount: 4,
1522
- bindGroupCount: 4,
1523
- encodeCount: 4,
1943
+ uploadCount: 10,
1944
+ bindGroupCount: 10,
1945
+ encodeCount: 10,
1524
1946
  });
1525
1947
  } finally {
1526
1948
  createBindGroup.mockRestore();
@@ -1530,11 +1952,11 @@ describe('factory contract', () => {
1530
1952
  });
1531
1953
 
1532
1954
  it('aggregates active and fence-retiring Bloom bundles through Renderer.inspect', async () => {
1955
+ const scenario = await createBloomScenario();
1533
1956
  const fence = deferred<undefined>();
1534
1957
  const onSubmittedWorkDone = vi
1535
1958
  .spyOn(RhiNullQueue.prototype, 'onSubmittedWorkDone')
1536
1959
  .mockReturnValue(fence.promise);
1537
- const scenario = await createBloomScenario();
1538
1960
  try {
1539
1961
  expect(scenario.draw().ok).toBe(true);
1540
1962
  scenario.setBloom('off');
@@ -1557,10 +1979,10 @@ describe('factory contract', () => {
1557
1979
  expect(overlap.state).toBe('active');
1558
1980
  expect(overlap.graphStatus).toBe('valid');
1559
1981
  expect(overlap.enabled).toBe(true);
1560
- expect(overlap.passCount).toBe(4);
1561
- expect(overlap.uploadCount).toBe(4);
1562
- expect(overlap.bindGroupCount).toBe(4);
1563
- expect(overlap.encodeCount).toBe(4);
1982
+ expect(overlap.passCount).toBe(10);
1983
+ expect(overlap.uploadCount).toBe(10);
1984
+ expect(overlap.bindGroupCount).toBe(10);
1985
+ expect(overlap.encodeCount).toBe(10);
1564
1986
  expect(overlap.resourceCount).toBeGreaterThan(retiring.resourceCount);
1565
1987
 
1566
1988
  fence.resolve(undefined);
@@ -1569,9 +1991,9 @@ describe('factory contract', () => {
1569
1991
  const settled = scenario.renderer.inspect().bloom;
1570
1992
  expect(settled.state).toBe('active');
1571
1993
  expect(settled.graphStatus).toBe('valid');
1572
- expect(settled.passCount).toBe(4);
1573
- expect(settled.targetCount).toBe(4);
1574
- expect(settled.targetBytes).toBe((64 * 64 + 3 * 32 * 32) * 8);
1994
+ expect(settled.passCount).toBe(10);
1995
+ expect(settled.targetCount).toBe(10);
1996
+ expect(settled.targetBytes).toBe(54_560);
1575
1997
  expect(settled.resourceCount).toBeLessThan(overlap.resourceCount);
1576
1998
  } finally {
1577
1999
  onSubmittedWorkDone.mockRestore();
@@ -1580,11 +2002,11 @@ describe('factory contract', () => {
1580
2002
  });
1581
2003
 
1582
2004
  it('settles Bloom off to exact zero without re-enabling it', async () => {
2005
+ const scenario = await createBloomScenario();
1583
2006
  const fence = deferred<undefined>();
1584
2007
  const onSubmittedWorkDone = vi
1585
2008
  .spyOn(RhiNullQueue.prototype, 'onSubmittedWorkDone')
1586
2009
  .mockReturnValue(fence.promise);
1587
- const scenario = await createBloomScenario();
1588
2010
  try {
1589
2011
  expect(scenario.draw().ok).toBe(true);
1590
2012
  scenario.setBloom('off');
@@ -1617,11 +2039,11 @@ describe('factory contract', () => {
1617
2039
  });
1618
2040
 
1619
2041
  it('reports one structured Bloom fence rejection and drains late-retiring resources', async () => {
2042
+ const scenario = await createBloomScenario();
1620
2043
  const fence = deferred<undefined>();
1621
2044
  const onSubmittedWorkDone = vi
1622
2045
  .spyOn(RhiNullQueue.prototype, 'onSubmittedWorkDone')
1623
2046
  .mockReturnValue(fence.promise);
1624
- const scenario = await createBloomScenario();
1625
2047
  const errors: unknown[] = [];
1626
2048
  let errorSnapshot: ReturnType<typeof scenario.renderer.inspect> | undefined;
1627
2049
  const unsubscribe = scenario.renderer.onError((error) => {
@@ -1667,6 +2089,7 @@ describe('factory contract', () => {
1667
2089
  });
1668
2090
 
1669
2091
  it('keeps drained Bloom fence rejection callbacks inert', async () => {
2092
+ const scenario = await createBloomScenario();
1670
2093
  const callbacks: Array<{
1671
2094
  readonly resolve: (value: undefined) => unknown;
1672
2095
  readonly reject: ((reason: unknown) => unknown) | undefined;
@@ -1681,7 +2104,6 @@ describe('factory contract', () => {
1681
2104
  const onSubmittedWorkDone = vi
1682
2105
  .spyOn(RhiNullQueue.prototype, 'onSubmittedWorkDone')
1683
2106
  .mockReturnValue(lateFence);
1684
- const scenario = await createBloomScenario();
1685
2107
  const errors: unknown[] = [];
1686
2108
  const unsubscribe = scenario.renderer.onError((error) => errors.push(error));
1687
2109
  try {
@@ -1797,7 +2219,7 @@ describe('factory contract', () => {
1797
2219
  const destroyCounts = new Map<object, number>();
1798
2220
  const failure = new RhiError({
1799
2221
  code: 'webgpu-runtime-error',
1800
- expected: 'Bloom blur-v params buffer construction succeeds',
2222
+ expected: 'Bloom upsample params buffer construction succeeds',
1801
2223
  hint: 'repair the injected construction failure and retry the same frame',
1802
2224
  });
1803
2225
  try {
@@ -1807,7 +2229,7 @@ describe('factory contract', () => {
1807
2229
  const lkg = scenario.renderer.inspect().bloom;
1808
2230
  scenario.setBloom('on');
1809
2231
  createBuffer.mockImplementation(function (this: RhiNullDevice, descriptor) {
1810
- if (descriptor.label === 'bloom-blur-v-params-ubo') {
2232
+ if (descriptor.label === 'bloom-upsample-params-ubo') {
1811
2233
  return err(failure);
1812
2234
  }
1813
2235
  const result = originalCreateBuffer.call(this, descriptor);
@@ -1832,12 +2254,12 @@ describe('factory contract', () => {
1832
2254
  expect(scenario.renderer.inspect().bloom.resourceCount).toBe(lkg.resourceCount);
1833
2255
  expect(scenario.renderer.inspect().bloom.passCount).toBe(0);
1834
2256
  const adoptedBeforeFailure = [...created.entries()].filter(
1835
- ([, label]) => label === 'bloom-bright-params-ubo' || label === 'bloom-blur-h-params-ubo',
2257
+ ([, label]) => label === 'bloom-downsample-params-ubo',
1836
2258
  );
1837
- expect(adoptedBeforeFailure).toHaveLength(2);
2259
+ expect(adoptedBeforeFailure).toHaveLength(1);
1838
2260
  expect(adoptedBeforeFailure.every(([handle]) => destroyCounts.get(handle) === 1)).toBe(true);
1839
2261
  expect(
1840
- [...created.values()].filter((label) => label === 'bloom-blur-v-params-ubo'),
2262
+ [...created.values()].filter((label) => label === 'bloom-upsample-params-ubo'),
1841
2263
  ).toHaveLength(0);
1842
2264
  createBuffer.mockRestore();
1843
2265
  destroyBuffer.mockRestore();
@@ -1902,6 +2324,7 @@ describe('factory contract', () => {
1902
2324
  near: 0.1,
1903
2325
  far: 100,
1904
2326
  antialias: ANTIALIAS_NONE,
2327
+ tonemap: TONEMAP_ACES_FILMIC,
1905
2328
  bloom: BLOOM_ENABLED,
1906
2329
  },
1907
2330
  },
@@ -1957,10 +2380,9 @@ describe('factory contract', () => {
1957
2380
  staleLossEvents: 0,
1958
2381
  });
1959
2382
  expect(renderer.inspect().recovery.rehydratedRoots).toBeGreaterThan(0);
1960
- // Recovery publishes only after the retained CPU/LKG frame has prepared
1961
- // the replacement graph and visible residency. The first post-recovery
1962
- // draw therefore starts with graph accessors and Standard inspection.
1963
- expect(renderer.inspect().perFramePassNames.length).toBeGreaterThan(0);
2383
+ // Publication retires the graph bound to the previous canvas context;
2384
+ // the first recovered draw acquires the replacement presentation target.
2385
+ expect(renderer.inspect().perFramePassNames).toHaveLength(0);
1964
2386
  expect(renderer.inspect().standardLighting).toBeDefined();
1965
2387
  expect(world.update().ok).toBe(true);
1966
2388
  expect(renderer.draw(input()).ok).toBe(true);
@@ -1977,6 +2399,96 @@ describe('factory contract', () => {
1977
2399
  }
1978
2400
  });
1979
2401
 
2402
+ it('does not publish Bloom resources for an intensity-zero recovery candidate', async () => {
2403
+ const adapter = new RhiNullAdapter();
2404
+ const lost = deferred<{ readonly reason: 'unknown'; readonly message: string }>();
2405
+ const firstDevice = (await adapter.requestDevice()).unwrap();
2406
+ const replacementDevice = (await adapter.requestDevice()).unwrap();
2407
+ const lostProxy = new Proxy(firstDevice, {
2408
+ get(target, property) {
2409
+ if (property === 'lost') return lost.promise;
2410
+ const value = Reflect.get(target, property, target);
2411
+ return typeof value === 'function' ? value.bind(target) : value;
2412
+ },
2413
+ });
2414
+ let requestCount = 0;
2415
+ const recoverableRhi = {
2416
+ ...rhi,
2417
+ requestAdapter: async () =>
2418
+ ok({
2419
+ features: adapter.features,
2420
+ limits: adapter.limits,
2421
+ requestDevice: async () => {
2422
+ requestCount += 1;
2423
+ return requestCount === 1 ? ok(lostProxy) : ok(replacementDevice);
2424
+ },
2425
+ } as never),
2426
+ } as never;
2427
+ const renderer = await constructRenderer(
2428
+ { width: 64, height: 64, getContext: () => null },
2429
+ { rhi: recoverableRhi },
2430
+ { shaderManifestUrl: renderLifecycleManifestUrl() },
2431
+ );
2432
+ const world = new World();
2433
+ const attached = renderer.attach(world);
2434
+ expect(attached.ok).toBe(true);
2435
+ if (!attached.ok) return;
2436
+ registerPropagateTransforms(world);
2437
+ world.spawn(
2438
+ { component: Transform, data: { pos: [0, 0, 4] } },
2439
+ {
2440
+ component: LocalCamera,
2441
+ data: {
2442
+ fov: 1,
2443
+ aspect: 1,
2444
+ near: 0.1,
2445
+ far: 100,
2446
+ antialias: ANTIALIAS_NONE,
2447
+ tonemap: TONEMAP_ACES_FILMIC,
2448
+ bloom: BLOOM_ENABLED,
2449
+ bloomIntensity: 0,
2450
+ },
2451
+ },
2452
+ );
2453
+ const input = () => ({
2454
+ leases: [attached.value],
2455
+ camera: { lease: attached.value },
2456
+ environment: { lease: attached.value },
2457
+ });
2458
+ const assertZero = (): void => {
2459
+ expect(renderer.inspect().bloom).toMatchObject({
2460
+ graphStatus: 'empty',
2461
+ enabled: false,
2462
+ targetCount: 0,
2463
+ targetBytes: 0,
2464
+ resourceCount: 0,
2465
+ passCount: 0,
2466
+ encodeCount: 0,
2467
+ bindGroupCount: 0,
2468
+ uploadCount: 0,
2469
+ residentChildBytes: 0,
2470
+ state: 'off',
2471
+ });
2472
+ };
2473
+ try {
2474
+ expect((await renderer.initialization).ok).toBe(true);
2475
+ expect(world.update().ok).toBe(true);
2476
+ expect(renderer.draw(input()).ok).toBe(true);
2477
+ assertZero();
2478
+ lost.reject({ reason: 'unknown', message: 'forced intensity-zero recovery loss' });
2479
+ await Promise.resolve();
2480
+ await Promise.resolve();
2481
+ expect(renderer.health().reason).toBe('device-lost');
2482
+ expect((await renderer.recover()).ok).toBe(true);
2483
+ assertZero();
2484
+ expect(world.update().ok).toBe(true);
2485
+ expect(renderer.draw(input()).ok).toBe(true);
2486
+ assertZero();
2487
+ } finally {
2488
+ await renderer.dispose();
2489
+ }
2490
+ });
2491
+
1980
2492
  it('abandons a post-process recovery candidate before promotion and retries once', async () => {
1981
2493
  const adapter = new RhiNullAdapter();
1982
2494
  const lost = deferred<{ readonly reason: 'unknown'; readonly message: string }>();
@@ -2092,6 +2604,7 @@ describe('factory contract', () => {
2092
2604
  near: 0.1,
2093
2605
  far: 100,
2094
2606
  antialias: ANTIALIAS_NONE,
2607
+ tonemap: TONEMAP_ACES_FILMIC,
2095
2608
  bloom: BLOOM_DISABLED,
2096
2609
  },
2097
2610
  },