@forgeax/engine-render 0.1.33 → 0.1.35

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 +1062 -127
  2. package/dist/assembly/backend-contract.d.ts +2 -0
  3. package/dist/assembly/backend-contract.d.ts.map +1 -1
  4. package/dist/assembly/barrel-distortion-inspection.d.ts +3 -0
  5. package/dist/assembly/barrel-distortion-inspection.d.ts.map +1 -0
  6. package/dist/assembly/canvas-draw-guard.d.ts +3 -0
  7. package/dist/assembly/canvas-draw-guard.d.ts.map +1 -0
  8. package/dist/assembly/cloud-shadow-entry.d.ts +4 -0
  9. package/dist/assembly/cloud-shadow-entry.d.ts.map +1 -0
  10. package/dist/assembly/device-feature-admission.d.ts +2 -2
  11. package/dist/assembly/dynamic-geometry-host.d.ts +4 -1
  12. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  13. package/dist/assembly/dynamic-geometry-runtime.d.ts +1 -1
  14. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -1
  15. package/dist/assembly/factory.d.ts +1 -1
  16. package/dist/assembly/factory.d.ts.map +1 -1
  17. package/dist/assembly/host-contract.d.ts +6 -5
  18. package/dist/assembly/host-contract.d.ts.map +1 -1
  19. package/dist/assembly/material/assembly.d.ts.map +1 -1
  20. package/dist/assembly/material/pipeline-helpers.d.ts +5 -0
  21. package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -1
  22. package/dist/assembly/material-shader-policy.d.ts +16 -2
  23. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  24. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -1
  25. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
  26. package/dist/assembly/recovery/renderer-recover.d.ts +2 -0
  27. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  28. package/dist/assembly/render-feature-post-processes.d.ts +6 -0
  29. package/dist/assembly/render-feature-post-processes.d.ts.map +1 -0
  30. package/dist/assembly/render-target-host.d.ts +5 -0
  31. package/dist/assembly/render-target-host.d.ts.map +1 -1
  32. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  33. package/dist/assembly/renderer-feature-inspection.d.ts +1 -1
  34. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -1
  35. package/dist/assembly/renderer-frame-transaction.d.ts +12 -0
  36. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  37. package/dist/assembly/renderer-inspection.d.ts +11 -0
  38. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  39. package/dist/assembly/webgpu-pbr-ready.d.ts +1 -0
  40. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -1
  41. package/dist/assembly/webgpu-ready-per-pass.d.ts +39 -0
  42. package/dist/assembly/webgpu-ready-per-pass.d.ts.map +1 -0
  43. package/dist/assembly/webgpu-ready.d.ts +1 -1
  44. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  45. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -1
  46. package/dist/assembly/webgpu-renderer-observation-owner.d.ts +35 -0
  47. package/dist/assembly/webgpu-renderer-observation-owner.d.ts.map +1 -0
  48. package/dist/assembly/webgpu-renderer.d.ts +2 -1
  49. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  50. package/dist/authoring.mjs +2 -4
  51. package/dist/authoring.mjs.map +1 -1
  52. package/dist/barrel-distortion.d.ts +47 -0
  53. package/dist/barrel-distortion.d.ts.map +1 -0
  54. package/dist/chunk-3XUBWRY2.mjs +1842 -0
  55. package/dist/chunk-3XUBWRY2.mjs.map +1 -0
  56. package/dist/{chunk-OO3DRYK7.mjs → chunk-G6HDNGY7.mjs} +2400 -1767
  57. package/dist/chunk-G6HDNGY7.mjs.map +1 -0
  58. package/dist/{chunk-ZDQWGQW7.mjs → chunk-LM5IE7GF.mjs} +4 -4
  59. package/dist/{chunk-ZDQWGQW7.mjs.map → chunk-LM5IE7GF.mjs.map} +1 -1
  60. package/dist/{chunk-VOZWDM3L.mjs → chunk-LNIUIKQ7.mjs} +1514 -498
  61. package/dist/chunk-LNIUIKQ7.mjs.map +1 -0
  62. package/dist/chunk-MXTWKZO4.mjs +3268 -0
  63. package/dist/chunk-MXTWKZO4.mjs.map +1 -0
  64. package/dist/chunk-MZ4C7EHF.mjs +6942 -0
  65. package/dist/chunk-MZ4C7EHF.mjs.map +1 -0
  66. package/dist/chunk-RIPAEGI6.mjs +48 -0
  67. package/dist/chunk-RIPAEGI6.mjs.map +1 -0
  68. package/dist/{chunk-LEAXOMYJ.mjs → chunk-SGPUJW3F.mjs} +117 -5
  69. package/dist/chunk-SGPUJW3F.mjs.map +1 -0
  70. package/dist/{chunk-PA5DRJNI.mjs → chunk-X2KA6WHM.mjs} +29 -4
  71. package/dist/chunk-X2KA6WHM.mjs.map +1 -0
  72. package/dist/{chunk-BLXGONR2.mjs → chunk-XSFB7XXQ.mjs} +51 -4
  73. package/dist/chunk-XSFB7XXQ.mjs.map +1 -0
  74. package/dist/cloud/density.d.ts +55 -0
  75. package/dist/cloud/density.d.ts.map +1 -0
  76. package/dist/cloud/extract.d.ts +25 -0
  77. package/dist/cloud/extract.d.ts.map +1 -0
  78. package/dist/cloud/feature.d.ts +57 -0
  79. package/dist/cloud/feature.d.ts.map +1 -0
  80. package/dist/cloud/frame-context.d.ts +8 -0
  81. package/dist/cloud/frame-context.d.ts.map +1 -0
  82. package/dist/cloud/inspection.d.ts +59 -0
  83. package/dist/cloud/inspection.d.ts.map +1 -0
  84. package/dist/cloud/optics.d.ts +56 -0
  85. package/dist/cloud/optics.d.ts.map +1 -0
  86. package/dist/cloud/parameters.d.ts +73 -0
  87. package/dist/cloud/parameters.d.ts.map +1 -0
  88. package/dist/cloud/resources.d.ts +60 -0
  89. package/dist/cloud/resources.d.ts.map +1 -0
  90. package/dist/cloud/shadow.d.ts +42 -0
  91. package/dist/cloud/shadow.d.ts.map +1 -0
  92. package/dist/cloud/temporal.d.ts +74 -0
  93. package/dist/cloud/temporal.d.ts.map +1 -0
  94. package/dist/components/barrel-distortion.d.ts +13 -0
  95. package/dist/components/barrel-distortion.d.ts.map +1 -0
  96. package/dist/components/cloud-layer.d.ts +29 -0
  97. package/dist/components/cloud-layer.d.ts.map +1 -0
  98. package/dist/components/index.d.ts +2 -0
  99. package/dist/components/index.d.ts.map +1 -1
  100. package/dist/components/instances.d.ts +20 -6
  101. package/dist/components/instances.d.ts.map +1 -1
  102. package/dist/construct-renderer.d.ts +1 -1
  103. package/dist/construct-renderer.d.ts.map +1 -1
  104. package/dist/construct-renderer.mjs +8406 -5785
  105. package/dist/construct-renderer.mjs.map +1 -1
  106. package/dist/device/gpu-residency.d.ts +19 -19
  107. package/dist/device/gpu-residency.d.ts.map +1 -1
  108. package/dist/dynamic-geometry.d.ts +5 -1
  109. package/dist/dynamic-geometry.d.ts.map +1 -1
  110. package/dist/errors/cloud.d.ts +59 -0
  111. package/dist/errors/cloud.d.ts.map +1 -0
  112. package/dist/errors/render.d.ts +20 -5
  113. package/dist/errors/render.d.ts.map +1 -1
  114. package/dist/examples/single-layer-medium-surface.d.ts +47 -0
  115. package/dist/examples/single-layer-medium-surface.d.ts.map +1 -0
  116. package/dist/extract/camera.d.ts.map +1 -1
  117. package/dist/extract/contracts.d.ts +5 -1
  118. package/dist/extract/contracts.d.ts.map +1 -1
  119. package/dist/extract/material-context.d.ts +7 -0
  120. package/dist/extract/material-context.d.ts.map +1 -0
  121. package/dist/features/barrel-distortion.d.ts +29 -0
  122. package/dist/features/barrel-distortion.d.ts.map +1 -0
  123. package/dist/features/error-reporting.d.ts +6 -2
  124. package/dist/features/error-reporting.d.ts.map +1 -1
  125. package/dist/features/host.d.ts +15 -0
  126. package/dist/features/host.d.ts.map +1 -1
  127. package/dist/features/plan.d.ts +10 -0
  128. package/dist/features/plan.d.ts.map +1 -1
  129. package/dist/features/render-graph-contribution.d.ts +5 -0
  130. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  131. package/dist/features/render-graph-raster.d.ts +1 -0
  132. package/dist/features/render-graph-raster.d.ts.map +1 -1
  133. package/dist/features/targets.d.ts +2 -0
  134. package/dist/features/targets.d.ts.map +1 -1
  135. package/dist/features/types.d.ts +48 -0
  136. package/dist/features/types.d.ts.map +1 -1
  137. package/dist/fullscreen-post-process-pass.d.ts +15 -7
  138. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  139. package/dist/glyph-text-layout-system.d.ts +1 -1
  140. package/dist/glyph-text-layout-system.d.ts.map +1 -1
  141. package/dist/gpu-dirty-ranges.d.ts +11 -0
  142. package/dist/gpu-dirty-ranges.d.ts.map +1 -0
  143. package/dist/gpu-driven/batch-topology.d.ts +17 -0
  144. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  145. package/dist/gpu-driven/material-artifacts.d.ts +6 -0
  146. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
  147. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  148. package/dist/gpu-driven/production-raster-material.d.ts +178 -0
  149. package/dist/gpu-driven/production-raster-material.d.ts.map +1 -0
  150. package/dist/gpu-driven/production-raster.d.ts +47 -3
  151. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  152. package/dist/gpu-driven/resource-allocation.d.ts +31 -0
  153. package/dist/gpu-driven/resource-allocation.d.ts.map +1 -0
  154. package/dist/gpu-driven/shadow-ownership.d.ts +14 -0
  155. package/dist/gpu-driven/shadow-ownership.d.ts.map +1 -0
  156. package/dist/gpu-driven/view-gpu.d.ts +26 -2
  157. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  158. package/dist/gpu-scene.d.ts +5 -0
  159. package/dist/gpu-scene.d.ts.map +1 -1
  160. package/dist/hdrp-buffers.d.ts +2 -2
  161. package/dist/hdrp-buffers.d.ts.map +1 -1
  162. package/dist/ibl/frame-resources.d.ts.map +1 -1
  163. package/dist/ibl/skylight-bind-group.d.ts +14 -5
  164. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  165. package/dist/index.d.ts +30 -7
  166. package/dist/index.d.ts.map +1 -1
  167. package/dist/index.mjs +780 -9
  168. package/dist/index.mjs.map +1 -1
  169. package/dist/inspection-types.d.ts +135 -1
  170. package/dist/inspection-types.d.ts.map +1 -1
  171. package/dist/instance-buffer-cache.d.ts +4 -4
  172. package/dist/instance-buffer-cache.d.ts.map +1 -1
  173. package/dist/instances.d.ts +47 -100
  174. package/dist/instances.d.ts.map +1 -1
  175. package/dist/internal.mjs +7 -8
  176. package/dist/internal.mjs.map +1 -1
  177. package/dist/pbr-pipeline.d.ts +22 -4
  178. package/dist/pbr-pipeline.d.ts.map +1 -1
  179. package/dist/pipeline/single-layer-medium-passes.d.ts +73 -0
  180. package/dist/pipeline/single-layer-medium-passes.d.ts.map +1 -0
  181. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  182. package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
  183. package/dist/pipeline/standard-output/lut-gpu.d.ts +2 -2
  184. package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -1
  185. package/dist/pipeline/standard-output/resources.d.ts +2 -2
  186. package/dist/pipeline/standard-output/resources.d.ts.map +1 -1
  187. package/dist/pipeline/standard-output/types.d.ts +4 -2
  188. package/dist/pipeline/standard-output/types.d.ts.map +1 -1
  189. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  190. package/dist/pipeline/standard-post.d.ts.map +1 -1
  191. package/dist/pipeline-builder.d.ts +1 -1
  192. package/dist/pipeline-builder.d.ts.map +1 -1
  193. package/dist/plugin.d.ts.map +1 -1
  194. package/dist/prepare/prepared-graphics-resolver.d.ts +2 -0
  195. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  196. package/dist/publication/contract.d.ts +77 -0
  197. package/dist/publication/contract.d.ts.map +1 -0
  198. package/dist/publication/dependencies.d.ts +7 -0
  199. package/dist/publication/dependencies.d.ts.map +1 -0
  200. package/dist/publication/prepare-geometry.d.ts +9 -0
  201. package/dist/publication/prepare-geometry.d.ts.map +1 -0
  202. package/dist/publication/programs.d.ts +5 -0
  203. package/dist/publication/programs.d.ts.map +1 -0
  204. package/dist/publication/publisher.d.ts +33 -0
  205. package/dist/publication/publisher.d.ts.map +1 -0
  206. package/dist/publication/receiver.d.ts +29 -0
  207. package/dist/publication/receiver.d.ts.map +1 -0
  208. package/dist/publication/resource-scope.d.ts +18 -0
  209. package/dist/publication/resource-scope.d.ts.map +1 -0
  210. package/dist/publication/targets.d.ts +33 -0
  211. package/dist/publication/targets.d.ts.map +1 -0
  212. package/dist/publication/video.d.ts +5 -0
  213. package/dist/publication/video.d.ts.map +1 -0
  214. package/dist/record/barrel-distortion-frame.d.ts +15 -0
  215. package/dist/record/barrel-distortion-frame.d.ts.map +1 -0
  216. package/dist/record/dynamic-geometry-consumption.d.ts +10 -2
  217. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -1
  218. package/dist/record/fold-buckets.d.ts +95 -0
  219. package/dist/record/fold-buckets.d.ts.map +1 -0
  220. package/dist/record/fold-instance-buffer.d.ts +6 -0
  221. package/dist/record/fold-instance-buffer.d.ts.map +1 -0
  222. package/dist/record/frame-lighting.d.ts +1 -0
  223. package/dist/record/frame-lighting.d.ts.map +1 -1
  224. package/dist/record/frame-snapshot.d.ts +43 -9
  225. package/dist/record/frame-snapshot.d.ts.map +1 -1
  226. package/dist/record/frame.d.ts +15 -65
  227. package/dist/record/frame.d.ts.map +1 -1
  228. package/dist/record/gpu-draw-receipts.d.ts +11 -0
  229. package/dist/record/gpu-draw-receipts.d.ts.map +1 -0
  230. package/dist/record/gpu-timing.d.ts +8 -0
  231. package/dist/record/gpu-timing.d.ts.map +1 -1
  232. package/dist/record/helpers.d.ts +2 -2
  233. package/dist/record/helpers.d.ts.map +1 -1
  234. package/dist/record/main-pass-geometry.d.ts +22 -6
  235. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  236. package/dist/record/main-pass-material.d.ts +14 -10
  237. package/dist/record/main-pass-material.d.ts.map +1 -1
  238. package/dist/record/main-pass-sprite-draws.d.ts +3 -16
  239. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  240. package/dist/record/main-pass.d.ts +13 -2
  241. package/dist/record/main-pass.d.ts.map +1 -1
  242. package/dist/record/motion-blur-receipt.d.ts +11 -0
  243. package/dist/record/motion-blur-receipt.d.ts.map +1 -0
  244. package/dist/record/prepared-material-bindings.d.ts +3 -3
  245. package/dist/record/prepared-material-bindings.d.ts.map +1 -1
  246. package/dist/record/probe-blend-buffer.d.ts +16 -0
  247. package/dist/record/probe-blend-buffer.d.ts.map +1 -0
  248. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  249. package/dist/record/render-context.d.ts +24 -5
  250. package/dist/record/render-context.d.ts.map +1 -1
  251. package/dist/record/shadow-pass.d.ts +2 -0
  252. package/dist/record/shadow-pass.d.ts.map +1 -1
  253. package/dist/record/skybox-post-pass.d.ts +1 -1
  254. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  255. package/dist/record/sprite-instance-buffer.d.ts +31 -0
  256. package/dist/record/sprite-instance-buffer.d.ts.map +1 -0
  257. package/dist/record/target-capture-graph.d.ts +19 -0
  258. package/dist/record/target-capture-graph.d.ts.map +1 -0
  259. package/dist/record/typed-frame-graph.d.ts +24 -15
  260. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  261. package/dist/record/view-ubo.d.ts +11 -3
  262. package/dist/record/view-ubo.d.ts.map +1 -1
  263. package/dist/record/volume-params.d.ts +9 -0
  264. package/dist/record/volume-params.d.ts.map +1 -0
  265. package/dist/recovery/render-system-candidate.d.ts +14 -4
  266. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  267. package/dist/reflection/record-owner.d.ts.map +1 -1
  268. package/dist/render-contract.d.ts +46 -5
  269. package/dist/render-contract.d.ts.map +1 -1
  270. package/dist/render-graph-primitives.d.ts.map +1 -1
  271. package/dist/render-pipeline.d.ts +103 -2
  272. package/dist/render-pipeline.d.ts.map +1 -1
  273. package/dist/render-system-extract-tail.d.ts +3 -2
  274. package/dist/render-system-extract-tail.d.ts.map +1 -1
  275. package/dist/render-system-extract.d.ts +18 -9
  276. package/dist/render-system-extract.d.ts.map +1 -1
  277. package/dist/render-system-presentation.d.ts +23 -0
  278. package/dist/render-system-presentation.d.ts.map +1 -0
  279. package/dist/render-system-producer-roots.d.ts +10 -0
  280. package/dist/render-system-producer-roots.d.ts.map +1 -0
  281. package/dist/render-system-projections.d.ts +3 -2
  282. package/dist/render-system-projections.d.ts.map +1 -1
  283. package/dist/render-system-residency.d.ts +12 -0
  284. package/dist/render-system-residency.d.ts.map +1 -0
  285. package/dist/render-system.d.ts +22 -21
  286. package/dist/render-system.d.ts.map +1 -1
  287. package/dist/scene/probe-blend.d.ts +27 -0
  288. package/dist/scene/probe-blend.d.ts.map +1 -1
  289. package/dist/scene/render-scene.d.ts +18 -7
  290. package/dist/scene/render-scene.d.ts.map +1 -1
  291. package/dist/scene/render-source.d.ts +42 -0
  292. package/dist/scene/render-source.d.ts.map +1 -0
  293. package/dist/scene/source-systems.d.ts +6 -0
  294. package/dist/scene/source-systems.d.ts.map +1 -0
  295. package/dist/ssao-buffers.d.ts +9 -0
  296. package/dist/ssao-buffers.d.ts.map +1 -1
  297. package/dist/surface/dynamic-input.d.ts +133 -0
  298. package/dist/surface/dynamic-input.d.ts.map +1 -0
  299. package/dist/surface/gpu-driven.d.ts +41 -0
  300. package/dist/surface/gpu-driven.d.ts.map +1 -0
  301. package/dist/surface/submission-observation.d.ts +136 -0
  302. package/dist/surface/submission-observation.d.ts.map +1 -0
  303. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  304. package/dist/systems/skin-palette-types.d.ts +10 -0
  305. package/dist/systems/skin-palette-types.d.ts.map +1 -1
  306. package/dist/systems/transparent-dispatch.d.ts +2 -2
  307. package/dist/systems/transparent-dispatch.d.ts.map +1 -1
  308. package/dist/systems/transparent-sort-config.d.ts +2 -1
  309. package/dist/systems/transparent-sort-config.d.ts.map +1 -1
  310. package/dist/temporal/gpu.d.ts +13 -1
  311. package/dist/temporal/gpu.d.ts.map +1 -1
  312. package/dist/temporal/index.mjs +6 -7
  313. package/dist/temporal/inspection.d.ts.map +1 -1
  314. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  315. package/dist/temporal/target-coverage-attachment.d.ts.map +1 -1
  316. package/dist/transmission/single-layer-medium.d.ts +89 -0
  317. package/dist/transmission/single-layer-medium.d.ts.map +1 -0
  318. package/dist/typed-render-graph-primitives.d.ts +14 -2
  319. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  320. package/dist/volume/component.d.ts +9 -4
  321. package/dist/volume/component.d.ts.map +1 -1
  322. package/dist/volume/extract.d.ts +1 -1
  323. package/dist/volume/extract.d.ts.map +1 -1
  324. package/dist/volume/inspection.d.ts +2 -0
  325. package/dist/volume/inspection.d.ts.map +1 -1
  326. package/dist/volume/passes.d.ts +4 -1
  327. package/dist/volume/passes.d.ts.map +1 -1
  328. package/dist/volume/resources.d.ts.map +1 -1
  329. package/package.json +22 -21
  330. package/src/__tests__/atmosphere-daylight.dawn.test.ts +72 -0
  331. package/src/__tests__/auto-exposure-transaction.integration.test.ts +4 -9
  332. package/src/__tests__/barrel-distortion-lkg.integration.test.ts +353 -0
  333. package/src/__tests__/barrel-distortion.dawn.test.ts +41 -0
  334. package/src/__tests__/barrel-distortion.unit.test.ts +364 -0
  335. package/src/__tests__/construct-renderer-identity.unit.test.ts +49 -0
  336. package/src/__tests__/detached-recovery-graph.unit.test.ts +277 -0
  337. package/src/__tests__/device-feature-admission.unit.test.ts +3 -2
  338. package/src/__tests__/device-loss-fanout.unit.test.ts +36 -1
  339. package/src/__tests__/docs-gate-green.unit.test.ts +112 -1
  340. package/src/__tests__/factory-contract.integration.test.ts +295 -45
  341. package/src/__tests__/feature-depth-input.dawn.test.ts +129 -2
  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-material-artifacts.unit.test.ts +143 -0
  347. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +5 -6
  348. package/src/__tests__/gpu-driven-production.integration.test.ts +482 -3
  349. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +6 -5
  350. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +94 -6
  351. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +18 -2
  352. package/src/__tests__/gpu-driven-view.browser.test.ts +5 -8
  353. package/src/__tests__/gpu-driven-view.dawn.test.ts +6 -2
  354. package/src/__tests__/gpu-driven-view.unit.test.ts +1 -1
  355. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +1 -0
  356. package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -4
  357. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +1 -0
  358. package/src/__tests__/gpu-scene.unit.test.ts +119 -9
  359. package/src/__tests__/instance-allocation-failure.unit.test.ts +57 -0
  360. package/src/__tests__/instances-culling.integration.test.ts +10 -36
  361. package/src/__tests__/instances-store.unit.test.ts +97 -305
  362. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  363. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +4 -1
  364. package/src/__tests__/main-pass-material-medium-selection.unit.test.ts +145 -0
  365. package/src/__tests__/material-shader-prewarm.unit.test.ts +59 -0
  366. package/src/__tests__/material-snapshot-mutation.integration.test.ts +39 -1
  367. package/src/__tests__/materials-standard-contract.unit.test.ts +0 -1
  368. package/src/__tests__/morph-culling-reentry.dawn.test.ts +5 -5
  369. package/src/__tests__/morph-visual-evidence.integration.test.ts +8 -7
  370. package/src/__tests__/motion-blur-compute.dawn.test.ts +13 -7
  371. package/src/__tests__/motion-blur.pixel.integration.test.ts +2 -2
  372. package/src/__tests__/nineslice-handle-surface.unit.test.ts +4 -4
  373. package/src/__tests__/pbr-view-layout.unit.test.ts +9 -0
  374. package/src/__tests__/physical-clearcoat.integration.test.ts +66 -8
  375. package/src/__tests__/physical-material-budget.unit.test.ts +40 -0
  376. package/src/__tests__/prepared-graphics-capability.unit.test.ts +66 -2
  377. package/src/__tests__/probe-bindings.unit.test.ts +582 -1
  378. package/src/__tests__/probe-scene-dirty.integration.test.ts +28 -1
  379. package/src/__tests__/receipt-observation-retirement.unit.test.ts +65 -0
  380. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +69 -0
  381. package/src/__tests__/render-error-code-owner.test-d.ts +8 -0
  382. package/src/__tests__/render-feature-diagnostics.unit.test.ts +15 -2
  383. package/src/__tests__/render-feature-error-reporting.unit.test.ts +21 -1
  384. package/src/__tests__/render-feature-prepared-diagnostics.unit.test.ts +10 -1
  385. package/src/__tests__/render-feature-retry.unit.test.ts +12 -1
  386. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +145 -19
  387. package/src/__tests__/render-scene-projection.unit.test.ts +36 -11
  388. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +20 -20
  389. package/src/__tests__/render-target-probe-soak.perf.test.ts +2 -2
  390. package/src/__tests__/render-temporal-environment-consumer.browser.test.ts +3 -3
  391. package/src/__tests__/render-temporal-environment-consumer.dawn.test.ts +5 -2
  392. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +42 -8
  393. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +16 -0
  394. package/src/__tests__/renderer-frame-transaction.integration.test.ts +26 -10
  395. package/src/__tests__/resource-allocation.unit.test.ts +123 -0
  396. package/src/__tests__/single-layer-medium-passes.unit.test.ts +275 -0
  397. package/src/__tests__/skin-motion-regression.unit.test.ts +70 -1
  398. package/src/__tests__/skin-palette-recycling.dawn.test.ts +97 -0
  399. package/src/__tests__/skinned-shadow-caster.test.ts +15 -0
  400. package/src/__tests__/ssr-carrier-public-contract.unit.test.ts +21 -0
  401. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +4 -4
  402. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +2 -2
  403. package/src/__tests__/ssr-gpu-dispatch.ts +1 -1
  404. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +6 -2
  405. package/src/__tests__/standard-output-chain-contract.unit.test.ts +28 -0
  406. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +46 -1
  407. package/src/__tests__/standard-pipeline.integration.test.ts +182 -1
  408. package/src/__tests__/surface-dynamic-buffer-lifecycle.unit.test.ts +506 -0
  409. package/src/__tests__/surface-submission-observation.unit.test.ts +812 -0
  410. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +32 -0
  411. package/src/__tests__/temporal-performance.integration.test.ts +3 -3
  412. package/src/__tests__/visibility-instances.integration.test.ts +7 -19
  413. package/src/__tests__/visibility-producer-matrix.integration.test.ts +9 -20
  414. package/src/__tests__/volume-optics-oracle.unit.test.ts +4 -2
  415. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +30 -20
  416. package/src/__tests__/volumetric-fog-errors.unit.test.ts +4 -3
  417. package/src/__tests__/volumetric-fog-multiple.unit.test.ts +55 -0
  418. package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +1 -1
  419. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +113 -9
  420. package/src/__tests__/volumetric-fog-topology.unit.test.ts +5 -3
  421. package/src/assembly/backend-contract.ts +30 -0
  422. package/src/assembly/barrel-distortion-inspection.ts +22 -0
  423. package/src/assembly/canvas-draw-guard.ts +11 -0
  424. package/src/assembly/cloud-shadow-entry.ts +22 -0
  425. package/src/assembly/device-feature-admission.ts +2 -2
  426. package/src/assembly/dynamic-geometry-host.ts +65 -37
  427. package/src/assembly/dynamic-geometry-runtime.ts +7 -1
  428. package/src/assembly/factory.ts +2 -30
  429. package/src/assembly/host-contract.ts +8 -5
  430. package/src/assembly/material/assembly.ts +25 -13
  431. package/src/assembly/material/pipeline-helpers.ts +53 -12
  432. package/src/assembly/material-shader-policy.ts +55 -0
  433. package/src/assembly/material-shader-prewarm.ts +21 -10
  434. package/src/assembly/recovery/device-loss-fanout.ts +13 -0
  435. package/src/assembly/recovery/renderer-recover.ts +50 -18
  436. package/src/assembly/render-feature-post-processes.ts +54 -0
  437. package/src/assembly/render-target-host.ts +57 -47
  438. package/src/assembly/renderer-facade.ts +7 -2
  439. package/src/assembly/renderer-feature-inspection.ts +8 -2
  440. package/src/assembly/renderer-frame-transaction.ts +23 -0
  441. package/src/assembly/renderer-inspection.ts +28 -0
  442. package/src/assembly/skin-palette-owner.ts +1 -1
  443. package/src/assembly/temporal-post-process-prewarm.ts +3 -3
  444. package/src/assembly/webgpu-pbr-ready.ts +9 -1
  445. package/src/assembly/webgpu-ready-per-pass.ts +104 -0
  446. package/src/assembly/webgpu-ready.ts +116 -62
  447. package/src/assembly/webgpu-renderer-bootstrap.ts +0 -12
  448. package/src/assembly/webgpu-renderer-observation-owner.ts +222 -0
  449. package/src/assembly/webgpu-renderer.ts +303 -313
  450. package/src/assembly/webgpu-vertex-layouts.ts +1 -1
  451. package/src/barrel-distortion.ts +247 -0
  452. package/src/cloud/__tests__/cloud-density-parity.dawn.test.ts +173 -0
  453. package/src/cloud/__tests__/cloud-layer-framebuffer-y.dawn.test.ts +265 -0
  454. package/src/cloud/__tests__/cloud-layer.browser.test.ts +432 -0
  455. package/src/cloud/__tests__/cloud-layer.unit.test.ts +634 -0
  456. package/src/cloud/density.ts +600 -0
  457. package/src/cloud/extract.ts +110 -0
  458. package/src/cloud/feature.ts +1810 -0
  459. package/src/cloud/frame-context.ts +42 -0
  460. package/src/cloud/inspection.ts +130 -0
  461. package/src/cloud/optics.ts +285 -0
  462. package/src/cloud/parameters.ts +255 -0
  463. package/src/cloud/resources.ts +122 -0
  464. package/src/cloud/shadow.ts +167 -0
  465. package/src/cloud/temporal.ts +226 -0
  466. package/src/components/barrel-distortion.ts +46 -0
  467. package/src/components/cloud-layer.ts +47 -0
  468. package/src/components/index.ts +2 -0
  469. package/src/components/instances.ts +18 -45
  470. package/src/components/sprite-instances.ts +1 -1
  471. package/src/construct-renderer.ts +1 -1
  472. package/src/device/gpu-residency.ts +26 -25
  473. package/src/dynamic-geometry.ts +12 -1
  474. package/src/errors/cloud.ts +103 -0
  475. package/src/errors/render.ts +47 -8
  476. package/src/examples/single-layer-medium-surface.ts +59 -0
  477. package/src/extract/camera.ts +15 -1
  478. package/src/extract/contracts.ts +5 -0
  479. package/src/extract/material-context.ts +21 -0
  480. package/src/features/barrel-distortion.ts +172 -0
  481. package/src/features/error-reporting.ts +26 -6
  482. package/src/features/host.ts +128 -19
  483. package/src/features/plan.ts +10 -0
  484. package/src/features/render-graph-contribution.ts +9 -0
  485. package/src/features/render-graph-raster.ts +45 -3
  486. package/src/features/targets.ts +66 -0
  487. package/src/features/types.ts +51 -0
  488. package/src/fullscreen-post-process-pass.ts +41 -9
  489. package/src/glyph-text-layout-system.ts +2 -2
  490. package/src/gpu-dirty-ranges.ts +48 -0
  491. package/src/gpu-driven/batch-topology.ts +53 -0
  492. package/src/gpu-driven/material-artifacts.ts +33 -25
  493. package/src/gpu-driven/prepared-draw.ts +12 -4
  494. package/src/gpu-driven/production-raster-material.ts +1894 -0
  495. package/src/gpu-driven/production-raster.ts +825 -784
  496. package/src/gpu-driven/resource-allocation.ts +142 -0
  497. package/src/gpu-driven/shadow-ownership.ts +71 -0
  498. package/src/gpu-driven/view-gpu.ts +164 -10
  499. package/src/gpu-scene.ts +75 -84
  500. package/src/hdrp-buffers.ts +4 -3
  501. package/src/ibl/frame-resources.ts +0 -1
  502. package/src/ibl/skylight-bind-group.ts +62 -32
  503. package/src/index.ts +205 -9
  504. package/src/inspection-types.ts +162 -0
  505. package/src/instance-buffer-cache.ts +4 -4
  506. package/src/instances.ts +152 -569
  507. package/src/pbr-pipeline.ts +230 -30
  508. package/src/pipeline/single-layer-medium-passes.ts +267 -0
  509. package/src/pipeline/standard-forward-lane.ts +313 -57
  510. package/src/pipeline/standard-output/graph.ts +8 -2
  511. package/src/pipeline/standard-output/lut-gpu.ts +3 -3
  512. package/src/pipeline/standard-output/resources.ts +2 -2
  513. package/src/pipeline/standard-output/types.ts +12 -1
  514. package/src/pipeline/standard-pipeline.ts +291 -18
  515. package/src/pipeline/standard-post.ts +90 -24
  516. package/src/pipeline-builder.ts +1 -1
  517. package/src/plugin.ts +4 -0
  518. package/src/points-lines/__tests__/points-lines.bench.ts +17 -8
  519. package/src/prepare/prepared-graphics-resolver.ts +2 -0
  520. package/src/publication/__tests__/derived-geometry.integration.test.ts +177 -0
  521. package/src/publication/__tests__/features.integration.test.ts +122 -0
  522. package/src/publication/__tests__/programs.unit.test.ts +25 -0
  523. package/src/publication/__tests__/publication.integration.test.ts +370 -0
  524. package/src/publication/__tests__/targets.integration.test.ts +120 -0
  525. package/src/publication/__tests__/video.browser.test.ts +50 -0
  526. package/src/publication/contract.ts +104 -0
  527. package/src/publication/dependencies.ts +54 -0
  528. package/src/publication/prepare-geometry.ts +94 -0
  529. package/src/publication/programs.ts +45 -0
  530. package/src/publication/publisher.ts +618 -0
  531. package/src/publication/receiver.ts +297 -0
  532. package/src/publication/resource-scope.ts +31 -0
  533. package/src/publication/targets.ts +182 -0
  534. package/src/publication/video.ts +33 -0
  535. package/src/record/__tests__/main-pass-geometry.unit.test.ts +30 -0
  536. package/src/record/__tests__/material-bg-cache.unit.test.ts +15 -1
  537. package/src/record/__tests__/sprite-material-abi.unit.test.ts +9 -11
  538. package/src/record/__tests__/temporal-payload.unit.test.ts +1 -1
  539. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +11 -0
  540. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -0
  541. package/src/record/__tests__/video-texture-view.unit.test.ts +9 -9
  542. package/src/record/__tests__/view-ubo-layout.unit.test.ts +4 -3
  543. package/src/record/barrel-distortion-frame.ts +110 -0
  544. package/src/record/dynamic-geometry-consumption.ts +39 -9
  545. package/src/record/fold-buckets.ts +855 -0
  546. package/src/record/fold-instance-buffer.ts +51 -0
  547. package/src/record/frame-lighting.ts +36 -2
  548. package/src/record/frame-snapshot.ts +45 -9
  549. package/src/record/frame.ts +258 -1105
  550. package/src/record/gpu-draw-receipts.ts +38 -0
  551. package/src/record/gpu-timing.ts +48 -5
  552. package/src/record/helpers.ts +2 -2
  553. package/src/record/main-pass-geometry.ts +272 -84
  554. package/src/record/main-pass-material.ts +102 -36
  555. package/src/record/main-pass-sprite-draws.ts +13 -217
  556. package/src/record/main-pass.ts +164 -27
  557. package/src/record/motion-blur-receipt.ts +91 -0
  558. package/src/record/prepared-material-bindings.ts +11 -6
  559. package/src/record/probe-blend-buffer.ts +180 -0
  560. package/src/record/recovery-pipeline.ts +35 -3
  561. package/src/record/render-context.ts +41 -5
  562. package/src/record/shadow-pass.ts +30 -4
  563. package/src/record/skybox-post-pass.ts +28 -4
  564. package/src/record/sprite-instance-buffer.ts +155 -0
  565. package/src/record/target-capture-graph.ts +258 -0
  566. package/src/record/typed-frame-graph.ts +776 -340
  567. package/src/record/view-ubo.ts +27 -4
  568. package/src/record/volume-params.ts +225 -0
  569. package/src/recovery/render-system-candidate.ts +243 -50
  570. package/src/reflection/record-owner.ts +7 -3
  571. package/src/render-contract.ts +54 -5
  572. package/src/render-graph-primitives.ts +7 -1
  573. package/src/render-pipeline.ts +130 -0
  574. package/src/render-system-extract-tail.ts +90 -78
  575. package/src/render-system-extract.ts +76 -76
  576. package/src/render-system-presentation.ts +56 -0
  577. package/src/render-system-producer-roots.ts +96 -0
  578. package/src/render-system-projections.ts +3 -2
  579. package/src/render-system-residency.ts +159 -0
  580. package/src/render-system.ts +421 -408
  581. package/src/scene/probe-blend.ts +96 -4
  582. package/src/scene/render-scene.ts +416 -337
  583. package/src/scene/render-source.ts +90 -0
  584. package/src/scene/source-systems.ts +33 -0
  585. package/src/ssao-buffers.ts +95 -2
  586. package/src/surface/__tests__/dynamic-input.unit.test.ts +374 -0
  587. package/src/surface/__tests__/gpu-driven.unit.test.ts +87 -0
  588. package/src/surface/dynamic-input.ts +658 -0
  589. package/src/surface/gpu-driven.ts +147 -0
  590. package/src/surface/submission-observation.ts +517 -0
  591. package/src/systems/skin-palette-allocator.ts +5 -1
  592. package/src/systems/skin-palette-types.ts +11 -0
  593. package/src/systems/transparent-dispatch.ts +2 -2
  594. package/src/systems/transparent-sort-config.ts +3 -1
  595. package/src/temporal/gpu.ts +101 -17
  596. package/src/temporal/inspection.ts +7 -1
  597. package/src/temporal/standard-scene-data.ts +5 -0
  598. package/src/temporal/target-coverage-attachment.ts +4 -0
  599. package/src/transmission/__tests__/single-layer-medium.unit.test.ts +149 -0
  600. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +12 -11
  601. package/src/transmission/single-layer-medium.ts +322 -0
  602. package/src/typed-render-graph-primitives.ts +70 -2
  603. package/src/volume/component.ts +10 -9
  604. package/src/volume/extract.ts +11 -5
  605. package/src/volume/inspection.ts +3 -0
  606. package/src/volume/passes.ts +87 -4
  607. package/src/volume/resources.ts +2 -1
  608. package/dist/chunk-3RTCJTH4.mjs +0 -1610
  609. package/dist/chunk-3RTCJTH4.mjs.map +0 -1
  610. package/dist/chunk-BLXGONR2.mjs.map +0 -1
  611. package/dist/chunk-ILQ5MEZF.mjs +0 -32
  612. package/dist/chunk-ILQ5MEZF.mjs.map +0 -1
  613. package/dist/chunk-LEAXOMYJ.mjs.map +0 -1
  614. package/dist/chunk-NB2S7ZRX.mjs +0 -1783
  615. package/dist/chunk-NB2S7ZRX.mjs.map +0 -1
  616. package/dist/chunk-NNQL7CB3.mjs +0 -1713
  617. package/dist/chunk-NNQL7CB3.mjs.map +0 -1
  618. package/dist/chunk-OO3DRYK7.mjs.map +0 -1
  619. package/dist/chunk-PA5DRJNI.mjs.map +0 -1
  620. package/dist/chunk-PLAXOOX5.mjs +0 -1907
  621. package/dist/chunk-PLAXOOX5.mjs.map +0 -1
  622. package/dist/chunk-RXZ3RGOU.mjs +0 -53
  623. package/dist/chunk-RXZ3RGOU.mjs.map +0 -1
  624. package/dist/chunk-VOZWDM3L.mjs.map +0 -1
package/README.md CHANGED
@@ -14,6 +14,32 @@
14
14
 
15
15
  ## Public frame contract
16
16
 
17
+ Every accepted `FrameReceipt` with a submitted camera context carries an
18
+ immutable `barrelDistortion` mapping, including the exact identity mapping when
19
+ the camera has no active distortion. The optional property is a transport and
20
+ pre-first-frame shape: `undefined` means that no accepted submitted display
21
+ context exists. It is fail-closed, never an identity guess; display consumers
22
+ must wait for a new submitted frame.
23
+ The mapping is tied to the submitted output extent, camera matrices, device
24
+ generation, graph generation, and frame identity. Consumers must keep those
25
+ facts together; a mapping from a newer World edit cannot be applied to an older
26
+ picture. The renderer publishes the mapping only after queue submission, and a
27
+ candidate that fails validation keeps the previous accepted picture and
28
+ mapping. The public projection is deeply frozen and contains no device, graph,
29
+ texture, or mutable resource handle.
30
+
31
+ `renderer.inspect().barrelDistortion` projects the same accepted context as
32
+ `effectiveMapping`, `extent`, `frameId`, `deviceGeneration`,
33
+ `graphGeneration`, and `lastKnownGood`. Before the first accepted frame, after
34
+ surface retirement, or during device loss, `effectiveMapping` is unavailable;
35
+ display consumers must wait for a new submitted frame. An invalid authoring
36
+ value returns `barrel-distortion-invalid-parameter` with its field and bound.
37
+ Unsupported float attachment capability is reported only for an active plan;
38
+ an empty or zero-strength plan stays zero-work. A pending or failed disable
39
+ keeps the old nonzero mapping with its picture, while a successful disable
40
+ publishes identity and retires the old feature resources through the normal
41
+ in-flight fence.
42
+
17
43
  `Fog` provides distance fog with exponential height density through one
18
44
  finite-depth fullscreen pass before temporal resolve, Bloom and tone mapping.
19
45
  `density`, `heightFalloff`, linear `color` and `maxOpacity` are selected from
@@ -24,6 +50,41 @@ matching depth resolve is available. Translucent surfaces that do not write dept
24
50
  use the opaque background depth. Use `VolumetricFog` for spatial density and
25
51
  lighting/scattering effects; the meadow-style distance haze needs only `Fog`.
26
52
 
53
+ ### Local volumetric fog
54
+
55
+ Spawn one `VolumetricFog` per local medium in the rendered environment World.
56
+ Each owner selects its own light, 3D density texture, world-space bounds, and
57
+ optical coefficients. Up to `MAX_VOLUMETRIC_FOG_OWNERS` (8) owners share the
58
+ renderer-owned inject, integrate, temporal, and composite passes. Overlapping
59
+ media add coefficients before integration; removing one owner keeps the others.
60
+ Removing the last owner retires the volume graph resources.
61
+
62
+ | Author field | Contract |
63
+ |:--|:--|
64
+ | `density` | Shared linear `TextureAsset`, `viewDimension: '3d'` |
65
+ | `sampling` | ECS enum: `VolumetricFogSamplingValue.noise` (default animated atmospheric grain), or `.density` (local normalized bounds sample) |
66
+ | `boundsMin`, `boundsMax` | Finite world coordinates, strictly increasing on each axis; not transformed by `Transform` |
67
+ | `extinction`, `albedo`, `emission` | Nonnegative RGB; albedo additionally at most 1 |
68
+ | `anisotropy`, `maxDistance` | Finite, respectively `(-1, 1)` and positive world units |
69
+ | `light`, `spotLight` | Same-World selected light and optional Point/Spot pair, as in the existing single-volume contract |
70
+
71
+ Pure `VolumetricFogAuthoring` validation uses the string sampling labels, not
72
+ the ECS numeric values. `extractVolumetricFog()` returns an available `fogs`
73
+ collection and validates every member atomically. Overflow returns
74
+ `volume-owner-conflict` with `detail.ownerCount`; invalid author parameters
75
+ retain the existing structured error contract.
76
+
77
+ Read `renderer.inspect().volumetricFog.ownerCount` after a completed frame for
78
+ the accepted collection, alongside status, pass count, memory, and generation.
79
+ It is not an echo of a rejected candidate. Light projectors retain the shared
80
+ renderer-selected projector contract; only its selected light samples the map.
81
+
82
+ > [!IMPORTANT]
83
+ > This is local-medium integration against scene depth, not a claim of
84
+ > depth-aware fogging of every transparent or VFX feature. Those consumers
85
+ > require their own rendered integration evidence. Software GPU readback is
86
+ > correctness evidence, not hardware performance evidence.
87
+
27
88
  ## 灯光最短入口
28
89
 
29
90
  三条最短入口:
@@ -73,6 +134,10 @@ payload; the first-draw guard still rejects lazy cold uploads or pipeline builds
73
134
  inside rendering. Preparation does not make an old-generation receipt current.
74
135
  Recovery prepares the last successfully submitted workset; the next draw still
75
136
  consumes current World edits and admits new resources through normal residency.
137
+ Fullscreen recovery warms only active declared post-process identities; graph-local
138
+ resource aliases are resolved by the prepared feature and are not separate pipelines.
139
+ Ordinary graph replacement retains the previous graph until submission settles,
140
+ using the same retirement boundary for resize and post-process changes.
76
141
 
77
142
  `inspect().recovery` is always present. Its `phase` is `null` outside an active
78
143
  attempt and otherwise follows `quiesce`, `acquire-adapter`, `acquire-device`,
@@ -92,10 +157,39 @@ uninitialized until a successful receipt; a neutral target or last-known-good
92
157
  fallback is not proof of real recovery. Public consumers never receive graph
93
158
  nodes, devices, history textures, or prepared handles.
94
159
 
95
- The current-source manifest and schema identify `source`, `build`, `backend`,
96
- `runner`, and `frameIdentity`. Structural graph receipts, Browser/Dawn
97
- readback or PNG evidence, and historical oracle data are separate evidence
98
- classes. An unavailable backend is reported as unavailable.
160
+ The current-source manifest and schema identify `source`, `build`, `backend`,
161
+ `runner`, and `frameIdentity`. Structural graph receipts, Browser/Dawn
162
+ readback or PNG evidence, and historical oracle data are separate evidence
163
+ classes. An unavailable backend is reported as unavailable.
164
+
165
+ ### Renderer-wide graph allocation inspection
166
+
167
+ `renderer.inspect().renderGraphGenerationAllocation` is the renderer-owned
168
+ logical allocation receipt across active, volumetric candidate, retiring, and
169
+ detached recovery graph generations:
170
+
171
+ ```ts
172
+ const allocation = renderer.inspect().renderGraphGenerationAllocation;
173
+ if (allocation?.availability === 'complete') {
174
+ console.log(allocation.liveBytes, allocation.pendingRetirementBytes, allocation.peakBytes);
175
+ for (const entry of allocation.entries) {
176
+ console.log(entry.generation, entry.roles, entry.retirement, entry.allocation);
177
+ }
178
+ }
179
+ ```
180
+
181
+ `roles` identifies `active`, `candidate`, and `retiring` ownership; `retirement`
182
+ is `active`, `pending`, or `failed`. `liveBytes` and
183
+ `pendingRetirementBytes` are current logical bytes, while `peakBytes` is the
184
+ simultaneous logical high-water mark captured at compile, replacement, and
185
+ retirement events. `failedRetirementCount` and `failedRetirementBytes` keep a
186
+ destroy refusal, throw, or fence failure visible until its owner is repaired.
187
+ `availability` is `complete`, `partial`, or `unavailable`, with
188
+ `unavailableGenerationCount` explaining omitted graph facts. `unit` is
189
+ `engine-allocation-bytes` and `physicalResidency` is always `unknown`: imported
190
+ resources remain with their importing owner and logical byte size is not a VRAM
191
+ measurement. After a device-loss recovery, inspect the replacement generation
192
+ and retry the frame; a retained failed or pending row is recovery evidence.
99
193
 
100
194
  ## RenderFeature: the producer seam (first-read index)
101
195
 
@@ -108,6 +202,589 @@ fallback material. The projection preserves `layoutIdentity`,
108
202
  `programIdentity`, `cookIdentity`, and `materialPublicationIdentity` so a
109
203
  stale draw can be traced to the first producer divergence.
110
204
 
205
+ ### Single-layer medium Surface
206
+
207
+ `MaterialAsset.surface` selects the Engine template and one imported Surface
208
+ implementation. A Standard Surface imports
209
+ `forgeax_material::surface_v1::{SurfaceInput, SurfaceData}` and exports
210
+ `evaluate_surface(SurfaceInput) -> SurfaceData`. A
211
+ `model: 'single-layer-medium'` Surface instead imports
212
+ `forgeax_material::single_layer_medium_surface_v1::{SingleLayerMediumSurfaceInput,
213
+ SingleLayerMediumSurfaceData}` and exports that separate input/data ABI. The
214
+ Surface returns normal, roughness, coverage, foam, absorption, scattering, IOR,
215
+ and phase facts; Render performs the finite Beer-Lambert/single-scatter
216
+ integral, one Fresnel allocation, and consumes renderer-owned paired
217
+ color/depth frame facts. `maxDistanceMeters` is a finite authored upper bound
218
+ in metres for the sky-miss case. A valid value is carried through the Surface
219
+ ABI and Cook output, while invalid or absent input uses the conservative Engine
220
+ bound; it never turns an unavailable depth producer into a synthetic sample.
221
+ The Surface does not declare a stage, binding, attachment, BRDF, or vertex
222
+ displacement.
223
+
224
+ ```ts
225
+ import { definePack, definePackageId } from '@forgeax/engine/pack/source';
226
+ import { ok, type MaterialAsset } from '@forgeax/engine/types';
227
+
228
+ const packageId = definePackageId('01900000-0000-7000-8000-000000000001');
229
+
230
+ const material = {
231
+ kind: 'material',
232
+ surface: {
233
+ model: 'single-layer-medium',
234
+ module: 'game::water_surface_a',
235
+ dynamicInput: {
236
+ name: 'waterEvents',
237
+ fields: [
238
+ { name: 'position', type: 'vec3<f32>' },
239
+ { name: 'time', type: 'f32' },
240
+ { name: 'eventId', type: 'u32' },
241
+ ],
242
+ maxRecords: 64,
243
+ maxDomains: 8,
244
+ maxPageBytes: 2048,
245
+ maxBindings: 1,
246
+ maxEventsPerSample: 8,
247
+ },
248
+ },
249
+ passes: [{ name: 'color', program: { module: 'forgeax::single-layer-medium' } }],
250
+ parameters: [
251
+ { name: 'coverage', type: 'f32' },
252
+ { name: 'absorption', type: 'vec3' },
253
+ ],
254
+ values: { coverage: 0.85, absorption: [0.22, 0.07, 0.025] },
255
+ } satisfies MaterialAsset;
256
+
257
+ export default definePack({
258
+ schemaVersion: '2.0.0',
259
+ packageId,
260
+ name: 'Water',
261
+ build: () => ok({ 'material/water': material }),
262
+ });
263
+ ```
264
+
265
+ The authored module reads that schema through the compiler-generated accessor;
266
+ parameter names are fields on `material`, not free uniforms or string lookups:
267
+
268
+ ```wgsl
269
+ #define_import_path game::water_surface_a
270
+ #import forgeax_material::parameters::{material}
271
+ #import forgeax_material::single_layer_medium_surface_v1::{SingleLayerMediumSurfaceInput, SingleLayerMediumSurfaceData}
272
+
273
+ fn evaluate_surface(input: SingleLayerMediumSurfaceInput) -> SingleLayerMediumSurfaceData {
274
+ return SingleLayerMediumSurfaceData(
275
+ normalize(input.geometricNormalWS),
276
+ 0.16,
277
+ material.coverage,
278
+ 0.0,
279
+ material.absorption,
280
+ vec3<f32>(0.018, 0.04, 0.085),
281
+ 1.333,
282
+ 0.24,
283
+ 900.0,
284
+ );
285
+ }
286
+ ```
287
+
288
+ Cook validates this accessor against `parameters`, the Pack transport writes
289
+ the cooked material through JSON while preserving its GUID, and runtime loads
290
+ that GUID before allocating the `MaterialAsset` handle used by `MeshRenderer`.
291
+ The executable [LightProbe to ProbeBlend to Surface example](#lightprobe-to-probeblend-to-surface-example)
292
+ shows the `loadByGuid<MaterialAsset>` → World → `MeshRenderer` → `draw` half of
293
+ the same route.
294
+
295
+ The generated dynamic page is a bounded, read-only storage record. Create one
296
+ `ReadonlyDynamicInputPage`, write only changed records, then reserve an explicit
297
+ `{ domain, recordStart, recordCount, instanceIndex, member }` range for each draw.
298
+ `member` is the stable public address
299
+ `{ worldIdentity: world.identity, entityKey, drawItemIndex, instanceOrdinal }`.
300
+ Its
301
+ `contentRevision`, `bufferGeneration`, and `deviceGeneration` are separate
302
+ facts; stale ranges and consumption before an upload return a structured error.
303
+ The page uses the existing instance bind group's free binding and never uses
304
+ skinning's `customDataStart`. Publish the page to the existing Renderer owner
305
+ before drawing:
306
+ `renderer.setSurfaceDynamicInput({ page, ranges, projectionRevision, frameTime })`.
307
+ Advance `projectionRevision` only when range membership or addresses change;
308
+ record values and World time keep the current revision so stable frames reuse
309
+ the retained validation and consumption projection.
310
+ Range order is irrelevant: Render joins each range to the admitted candidate by
311
+ `member` and rejects missing, duplicate, or non-admitted identities.
312
+
313
+ #### Linear HDR optical oracle
314
+
315
+ The physical oracle observes the real Pack → GUID → World → medium nearest/color
316
+ path before exposure, tone mapping, and anti-aliasing. Arm the next successful
317
+ submit with `renderer.requestObservation(['linear-hdr'])`, keep its `FrameReceipt`,
318
+ then call `renderer.observe(receipt, { include: ['linear-hdr'] })`. The returned
319
+ bytes are padded-row `rgba16float`; `frameId`, device and graph generations,
320
+ texture identity, and readback identity bind them to that exact completed frame.
321
+ When no observation is requested, the renderer does not copy, map, or synchronously
322
+ read this attachment.
323
+
324
+ > [!IMPORTANT]
325
+ > Compare optical math in this linear attachment. A canvas screenshot is an
326
+ > exposed, tone-mapped, anti-aliased presentation artifact and cannot prove the
327
+ > Beer-Lambert, scatter, or Fresnel contract.
328
+
329
+ The permanent fixture loads authored water through the Preview Pack, freezes the
330
+ camera, medium parameters, paired background, light, event time, and fixed ROI,
331
+ then compares the observed half-floats with an independent double-precision
332
+ screen-space reference. Its evidence also carries the actual nearest/color
333
+ program and resource generations plus the completed submission receipt.
334
+
335
+ Author 4x coverage through the Camera component. ECS fields retain their numeric
336
+ schema representation; `ANTIALIAS_MSAA` extracts to the public closed
337
+ `antialias: 'msaa'` render fact:
338
+
339
+ ```ts
340
+ import { ANTIALIAS_MSAA, Camera, orthographic } from '@forgeax/engine/render';
341
+
342
+ world.spawn({
343
+ component: Camera,
344
+ data: {
345
+ ...orthographic({ left: -3, right: 3, bottom: -2, top: 2 }),
346
+ antialias: ANTIALIAS_MSAA,
347
+ },
348
+ });
349
+ ```
350
+
351
+ The edge oracle independently projects the authored rod, wall, shoreline, and
352
+ rough-water rectangles onto the canonical four sample positions. Fully covered
353
+ control regions calibrate each authored linear-HDR endpoint. For opaque edges,
354
+ the nearest depth sample selects its matching color before the full-coverage
355
+ water pass. For partial water coverage, the final hardware resolve averages the
356
+ independently predicted covered samples. The oracle rejects the opposite policy,
357
+ checks a small moving-camera interval, repeats after an 800-by-450 backing resize,
358
+ and keeps the physical maximum per-channel error at `0.05`. Its pure four-sample
359
+ counterexample separately proves why averaging color and then choosing the
360
+ nearest depth loses the color/depth pair.
361
+
362
+ ```sh
363
+ FORGEAX_SURFACE_LANE_PARITY=1 pnpm exec vitest run --project=dawn --retry=0 \
364
+ --maxWorkers=1 --no-file-parallelism \
365
+ packages/runtime/src/__tests__/surface-standard-pipeline.dawn.test.ts
366
+
367
+ FORGEAX_SURFACE_LANE_PARITY=1 pnpm exec vitest run \
368
+ --config vitest.browser.config.ts --project=browser --retry=0 \
369
+ --maxWorkers=1 --no-file-parallelism \
370
+ packages/runtime/src/__tests__/surface-standard-pipeline.browser.test.ts
371
+
372
+ FORGEAX_SURFACE_MSAA4X=1 FORGEAX_SURFACE_LANE_PARITY=1 \
373
+ pnpm exec vitest run --project=dawn --retry=0 --maxWorkers=1 \
374
+ --no-file-parallelism \
375
+ packages/runtime/src/__tests__/surface-standard-pipeline.dawn.test.ts
376
+
377
+ VITE_FORGEAX_SURFACE_MSAA4X=1 FORGEAX_SURFACE_LANE_PARITY=1 \
378
+ pnpm exec vitest run --project=browser --retry=0 --maxWorkers=1 \
379
+ --no-file-parallelism \
380
+ packages/runtime/src/__tests__/surface-standard-pipeline.browser.test.ts
381
+
382
+ # Public App lifecycle: World-time water events plus a cooked GPU splash.
383
+ FORGEAX_SURFACE_APP_LIFECYCLE_ONLY=1 pnpm exec vitest run --project=dawn \
384
+ --retry=0 --maxWorkers=1 --no-file-parallelism \
385
+ packages/runtime/src/__tests__/surface-standard-pipeline.dawn.test.ts
386
+
387
+ VITE_FORGEAX_SURFACE_APP_LIFECYCLE_ONLY=1 pnpm exec vitest run --project=browser \
388
+ --retry=0 --maxWorkers=1 --no-file-parallelism \
389
+ packages/runtime/src/__tests__/surface-standard-pipeline.browser.test.ts
390
+ ```
391
+
392
+ The lifecycle gate enqueues two deduplicated impacts into one attached World.
393
+ Its Update system takes one `Time.elapsed` snapshot, stamps both Surface records,
394
+ and starts the matching `ParticleEffectPlayer` edges. The splash is the authored
395
+ `surface-water-splash.pack.json` asset: the Pack transport resolves its GUID,
396
+ `loadVfxGpuEffect` loads its cooked program, and `createVfxRuntimeHost` contributes
397
+ the production renderer feature. The gate rejects an effect without a renderer;
398
+ an intent-only program is not visible evidence.
399
+
400
+ Before each target submit, the gate arms the existing `linear-hdr` observation.
401
+ It reads pre-frozen 16-by-16 water-A, water-B, and above-water splash ROIs from
402
+ that completed receipt, and compares every pixel. The mask never depends on
403
+ brightness, medians, or observed error. Evidence carries the frame, device,
404
+ graph, texture, and readback identities. The active splash ROI must change and
405
+ return to its baseline after the effect expires; the VFX host must also retire
406
+ both players. `rgba16float` samples remain unclamped linear values, including
407
+ values above 1, and use the same maximum per-channel error of `0.05` at every
408
+ lifecycle stage. The oracle does not round them through an 8-bit domain.
409
+
410
+ A real wait while paused preserves World time, committed VFX state, receipt
411
+ count, and the exact active completed image: `paused` reuses the active receipt
412
+ and does not submit a zero-delta frame. `resume()` continues from retained World
413
+ time without adding the paused host interval or replaying either splash. The
414
+ later `stepFrame(0.05)` calls are explicit deterministic simulation steps used
415
+ to reach camera coverage and expiry; they advance World time and therefore are
416
+ not evidence that a paused App remains frozen. In Dawn only, a timer supplies
417
+ the missing host scheduling primitive; the same App remains the sole owner of
418
+ World update and Renderer draw.
419
+
420
+ ### LightProbe to ProbeBlend to Surface example
421
+
422
+ The following scene uses one authored water material for two entities. Each
423
+ entity selects a different local `LightProbe` domain, while both consume the
424
+ same bounded dynamic-input page. The renderer derives `ProbeBlendRecord`, slot,
425
+ generation, and the direct or scene-index consumer lane from the attached
426
+ World; application code does not build a second probe table.
427
+
428
+ `drawProbeLitWater` receives an already-constructed Runtime `Renderer`.
429
+ Construct it at the Runtime boundary with `await createRenderer(canvas)`, check
430
+ the returned `Result` (`if (!result.ok) throw result.error`), and pass
431
+ `result.value` to this helper. Runtime completes renderer initialization before
432
+ returning that value, so the render helper does not own a second initialization
433
+ promise.
434
+
435
+ ```ts
436
+ import { Time, World } from '@forgeax/engine-ecs';
437
+ import { createPlaneGeometry } from '@forgeax/engine-geometry';
438
+ import {
439
+ Camera,
440
+ DirectionalLight,
441
+ LightProbe,
442
+ MeshFilter,
443
+ MeshRenderer,
444
+ ReadonlyDynamicInputPage,
445
+ type Renderer,
446
+ } from '@forgeax/engine-render';
447
+ import { Transform } from '@forgeax/engine-scene';
448
+ import { type AssetRegistry } from '@forgeax/engine-assets-runtime';
449
+ import type { AssetGuid, MaterialAsset } from '@forgeax/engine-types';
450
+
451
+ export async function drawProbeLitWater(
452
+ renderer: Renderer,
453
+ assets: AssetRegistry,
454
+ waterGuid: AssetGuid,
455
+ ) {
456
+ // The Pack producer cooks the authored MaterialAsset and its Surface module;
457
+ // runtime consumers load only the GUID-addressed publication.
458
+ const loaded = await assets.loadByGuid<MaterialAsset>(waterGuid);
459
+ if (!loaded.ok) throw loaded.error;
460
+ const water = loaded.value;
461
+ const dynamicInput = water.surface?.dynamicInput;
462
+ if (water.surface?.model !== 'single-layer-medium' || dynamicInput === undefined) {
463
+ throw new Error('expected an authored single-layer-medium Surface');
464
+ }
465
+
466
+ const world = new World();
467
+ const plane = createPlaneGeometry(1.8, 1.8).unwrap();
468
+ const mesh = world.allocSharedRef('MeshAsset', plane);
469
+ const material = world.allocSharedRef('MaterialAsset', water);
470
+ const waterEntities = [-1.2, 1.2].map((x) =>
471
+ world
472
+ .spawn(
473
+ { component: Transform, data: { pos: [x, 0, 0] } },
474
+ { component: MeshFilter, data: { assetHandle: mesh } },
475
+ { component: MeshRenderer, data: { materials: [material] } },
476
+ )
477
+ .unwrap(),
478
+ );
479
+
480
+ const red = new Float32Array(27);
481
+ red[0] = 1.8;
482
+ const blue = new Float32Array(27);
483
+ blue[2] = 1.8;
484
+ world.spawn(
485
+ { component: Transform, data: { pos: [-1.2, 0, 0] } },
486
+ { component: LightProbe, data: { irradiance: red, radius: 1 } },
487
+ );
488
+ world.spawn(
489
+ { component: Transform, data: { pos: [1.2, 0, 0] } },
490
+ { component: LightProbe, data: { irradiance: blue, radius: 1 } },
491
+ );
492
+ world.spawn(
493
+ { component: Transform, data: { pos: [0, 0, 6] } },
494
+ {
495
+ component: Camera,
496
+ data: { fov: Math.PI / 4, aspect: 16 / 9, near: 0.1, far: 20 },
497
+ },
498
+ );
499
+ world.spawn({
500
+ component: DirectionalLight,
501
+ data: { direction: [0, 0, -1], color: [1, 1, 1], intensity: 1 },
502
+ });
503
+
504
+ const page = ReadonlyDynamicInputPage.create({
505
+ sourceId: 'water-events',
506
+ pageId: 1,
507
+ schema: dynamicInput,
508
+ }).unwrap();
509
+ const eventLifetimeSeconds = 0.45;
510
+ const events = [
511
+ { position: [-1.2, 0, 0] as const, time: 0, eventId: 1 },
512
+ { position: [1.2, 0, 0] as const, time: 0, eventId: 2 },
513
+ ] as const;
514
+ let publishedEventCount = 0;
515
+ let projectionRevision = 1;
516
+ let publishedDeviceGeneration = renderer.inspect().frame.deviceGeneration;
517
+ page.reconfigureDevice(publishedDeviceGeneration).unwrap();
518
+
519
+ const reserveFreshRanges = () =>
520
+ waterEntities.map((entityKey, index) =>
521
+ page
522
+ .reserveRange({
523
+ domain: index === 0 ? 'water-left' : 'water-right',
524
+ recordStart: index,
525
+ recordCount: 1,
526
+ instanceIndex: index,
527
+ member: {
528
+ worldIdentity: world.identity,
529
+ entityKey,
530
+ drawItemIndex: 0,
531
+ instanceOrdinal: 0,
532
+ },
533
+ })
534
+ .unwrap(),
535
+ );
536
+ let ranges = reserveFreshRanges();
537
+ let deviceLost = renderer.state() === 'device-lost';
538
+
539
+ const publishDynamicInput = (now: number): void => {
540
+ const activeEvents = events.filter(
541
+ (event) => now >= event.time && now - event.time < eventLifetimeSeconds,
542
+ );
543
+ const recordsToRewrite = Math.max(publishedEventCount, activeEvents.length);
544
+ for (let index = 0; index < recordsToRewrite; index += 1) {
545
+ page
546
+ .writeRecord(
547
+ index,
548
+ activeEvents[index] ?? { position: [0, 0, 0], time: -1_000, eventId: 0 },
549
+ )
550
+ .unwrap();
551
+ }
552
+ publishedEventCount = activeEvents.length;
553
+ renderer.setSurfaceDynamicInput({ page, ranges, projectionRevision, frameTime: now });
554
+ };
555
+
556
+ const recoverAndRepublish = async (): Promise<number> => {
557
+ if (renderer.state() !== 'device-lost') {
558
+ throw new Error('water recovery requires the Renderer device-lost state');
559
+ }
560
+ const recovered = await renderer.recover();
561
+ if (!recovered.ok) throw recovered.error;
562
+ const nextDeviceGeneration = renderer.inspect().frame.deviceGeneration;
563
+ if (nextDeviceGeneration <= publishedDeviceGeneration) {
564
+ throw new Error('water recovery did not publish a new device generation');
565
+ }
566
+ const previousRanges = ranges;
567
+ page.reconfigureDevice(nextDeviceGeneration).unwrap();
568
+ ranges = reserveFreshRanges();
569
+ if (
570
+ ranges.some(
571
+ (range, index) =>
572
+ range === previousRanges[index] ||
573
+ range.deviceGeneration !== nextDeviceGeneration ||
574
+ range.bufferGeneration === previousRanges[index]?.bufferGeneration,
575
+ )
576
+ ) {
577
+ throw new Error('water recovery reused a pre-recovery dynamic range');
578
+ }
579
+ publishedDeviceGeneration = nextDeviceGeneration;
580
+ projectionRevision += 1;
581
+ publishDynamicInput(world.getResource(Time).elapsed);
582
+ deviceLost = false;
583
+ return nextDeviceGeneration;
584
+ };
585
+
586
+ const unsubscribe = renderer.subscribe((event) => {
587
+ if (event.kind === 'state-changed' && event.current === 'device-lost') {
588
+ deviceLost = true;
589
+ }
590
+ });
591
+ try {
592
+ const attachment = renderer.attach(world);
593
+ if (!attachment.ok) throw attachment.error;
594
+ const attached = attachment.value;
595
+ publishDynamicInput(world.getResource(Time).elapsed);
596
+ world.update(1 / 60).unwrap();
597
+ const submitAndObserve = async () => {
598
+ // The Renderer keeps this logical World lease valid across recovery;
599
+ // retry the same lease after the page and ranges are revalidated.
600
+ const submitted = renderer.draw({
601
+ leases: [attached],
602
+ camera: { lease: attached },
603
+ environment: { lease: attached },
604
+ });
605
+ if (!submitted.ok) throw submitted.error;
606
+ const completed = await submitted.value.completed;
607
+ if (!completed.ok) throw completed.error;
608
+ const observed = await renderer.observe(submitted.value, { include: ['draws'] });
609
+ if (!observed.ok) throw observed.error;
610
+ return submitted.value;
611
+ };
612
+
613
+ let submitted: Awaited<ReturnType<typeof submitAndObserve>>;
614
+ try {
615
+ submitted = await submitAndObserve();
616
+ } catch (cause) {
617
+ // A first frame can fail before it yields a receipt. Recover only when
618
+ // the public lifecycle reports the actual device-lost state.
619
+ if (renderer.state() !== 'device-lost') throw cause;
620
+ await recoverAndRepublish();
621
+ submitted = await submitAndObserve();
622
+ }
623
+ if (deviceLost || renderer.state() === 'device-lost') {
624
+ await recoverAndRepublish();
625
+ submitted = await submitAndObserve();
626
+ }
627
+ const inspection = renderer.inspect();
628
+ if (submitted.deviceGeneration !== publishedDeviceGeneration) {
629
+ throw new Error('water draw receipt does not match the published device generation');
630
+ }
631
+ return {
632
+ world,
633
+ attached,
634
+ page,
635
+ receipt: submitted,
636
+ submission: inspection.renderScene.submission,
637
+ deviceGeneration: inspection.frame.deviceGeneration,
638
+ };
639
+ } finally {
640
+ unsubscribe();
641
+ }
642
+ }
643
+ ```
644
+
645
+ The repository exercises this route through real Pack/GUID publication and a
646
+ physical texture or compositor readback:
647
+
648
+ ```bash
649
+ pnpm exec vitest run --project=dawn \
650
+ packages/runtime/src/__tests__/surface-standard-pipeline.dawn.test.ts --retry=0
651
+ pnpm exec vitest run --config vitest.browser.config.ts --project=browser \
652
+ packages/runtime/src/__tests__/surface-standard-pipeline.browser.test.ts --retry=0
653
+
654
+ # Same material, scene, time, events, ProbeBlend domains, exposure, and ROI;
655
+ # cold direct -> GPU -> direct. The original 0.05 RGB oracle remains active.
656
+ FORGEAX_SURFACE_LANE_PARITY=1 FORGEAX_DAWN_LIGHTWEIGHT=1 \
657
+ pnpm exec vitest run --project=dawn \
658
+ packages/runtime/src/__tests__/surface-standard-pipeline.dawn.test.ts --retry=0
659
+ ```
660
+
661
+ Direct medium draws use a dedicated group(3): ordinary instance transforms,
662
+ the retained ProbeBlend record, dynamic page, per-member frame rows, shared
663
+ frame time, and a producer-owned per-draw frame-base uniform. The vertex shader
664
+ addresses `frameBase + instance_index`; neither GUID order nor a scene-index
665
+ visible slot is used to infer that address. Missing member identity, page,
666
+ generation, or frame-base publication fails the draw through the structured
667
+ Render error path.
668
+
669
+ After a successful submission, `renderer.inspect().renderScene.gpuDriven.surfaceArtifact`
670
+ exposes the selected direct/scene-index entries and artifact/resource
671
+ generation. `renderer.inspect().renderScene.submission` separates
672
+ `requestedLane` from the command-derived `actualLane` and publishes only
673
+ nearest/color passes that encoded a real `draw*` or `draw*Indirect` command.
674
+ When the current Standard frame carries the reflection fallback MRT, the same
675
+ submission projection reports `actualLane: 'direct'` with
676
+ `actualLaneReason: 'reflection-fallback-mrt'`. This is a pass-ownership
677
+ constraint, not a device-capability result; the reason is absent again when a
678
+ later frame resumes the ordinary GPU-driven lane.
679
+ Direct rows contain the actual command range (`firstInstance` remains `0`),
680
+ the producer's separate `surfaceFrameBase`, and the member identities bound by
681
+ that command. Every command reports the exact selected artifact's
682
+ `receiptIdentity`/`receiptGeneration`; `programEvidence: 'missing'` means the
683
+ recorder could not prove that producer identity. Indirect rows contain the
684
+ actual indirect buffer identity and byte offset. `status: 'completed'` is published only after the existing queue
685
+ completion fence resolves for the same device generation.
686
+
687
+ GPU member identity is opt-in because it requires readback. Call
688
+ `await renderer.observe(frameReceipt, { include: ['draws'] })` before reading
689
+ the inspection row. Until that call, an indirect pass reports
690
+ `memberEvidence: 'indirect-readback-required'` and omits members. The observe
691
+ path reuses the selector telemetry copy, reads its actual visible rows, and
692
+ then reports `indirect-visible-readback`. The readback carries its recording
693
+ sequence, frame/device/resource/view identity, and per-pass indirect ranges;
694
+ an older or reordered shared-buffer result is rejected even within one device
695
+ generation. Nearest and color share the same
696
+ visible member set because both consume the same submitted selector/indirect
697
+ projection; each pass still has its own encoded command receipt. CPU candidate
698
+ lists are never substituted for this GPU readback. The lane-parity gates pair
699
+ these command facts with completed fixed-ROI pixels at the unchanged `0.05`
700
+ RGB epsilon.
701
+
702
+ The public consumer sequence is `draw` -> `observe` -> `inspect`:
703
+
704
+ ```ts
705
+ import type { SurfaceGpuIndirectParameters } from '@forgeax/engine/render';
706
+
707
+ const receipt = renderer.draw(frameInput);
708
+ if (!receipt.ok) throw receipt.error;
709
+ const observed = await renderer.observe(receipt.value, { include: ['draws'] });
710
+ if (!observed.ok) throw observed.error;
711
+ const inspection = renderer.inspect();
712
+ const completePasses = (inspection.renderScene.submission?.passes ?? []).filter(
713
+ (pass) => pass.memberEvidence === 'indirect-visible-readback',
714
+ );
715
+ const indirectParameters: readonly SurfaceGpuIndirectParameters[] = completePasses.flatMap(
716
+ (pass) => {
717
+ if (pass.indirectParameters === undefined) throw new Error('complete GPU readback is missing');
718
+ return pass.indirectParameters;
719
+ },
720
+ );
721
+ void indirectParameters;
722
+ ```
723
+
724
+ `pass.indirectParameters` appears only after the requested observation resolves
725
+ and the renderer accepts a complete matching GPU readback. A pass before
726
+ readback, with no complete identity match, reports
727
+ `memberEvidence: 'indirect-readback-required'` and has no parameters. A valid
728
+ readback whose bounded command sample is truncated reports
729
+ `memberEvidence: 'indirect-visible-readback-truncated'` and also has no
730
+ parameters; its saved and dropped counts remain diagnostic facts. GPU-byte
731
+ truncation, a stale/reordered copy, or an identity mismatch is rejected and
732
+ leaves the pass in `indirect-readback-required`. Only a complete valid copy
733
+ reports `memberEvidence: 'indirect-visible-readback'` with parameters. These
734
+ states are diagnostic facts and never proof when the required evidence is
735
+ absent. Each record is bound to the same
736
+ `sequence`, `frameId`, `deviceGeneration`, `resourceGeneration`, `viewIdentity`,
737
+ and Surface `pass` as the published submission, plus the exact indirect
738
+ `indirectBufferIdentity` and `indirectOffset`. For indexed commands, `count`,
739
+ `first`, `instanceCount`, signed `baseVertex`, and `firstInstance` decode the
740
+ five WebGPU indirect words. For non-indexed commands, `count`, `first`,
741
+ `instanceCount`, and `firstInstance` decode the four words and `baseVertex` is
742
+ zero by definition.
743
+
744
+ Each pass exposes `totalCommandCount`, bounded `savedCommandCount`,
745
+ `droppedCommandCount`, and `truncated`. A truncated indirect row is diagnostic
746
+ evidence, never a complete member set; a valid readback labels it
747
+ `indirect-visible-readback-truncated` and omits `indirectParameters`. A direct
748
+ row uses `direct-command-members-truncated` for the same bounded-sample limit.
749
+
750
+ The Render owner validates the cooked layout, source/page identity, generation,
751
+ range bounds, and device storage limit. It begins a detached upload and commits
752
+ the page revision only after every queue write succeeds; graph submission then
753
+ consumes the validated ranges. The same input is carried through GPU
754
+ preparation and the recovery candidate. In the scene-index lane, the renderer
755
+ maps each GPU-written visible member's stable candidate row to a frame row and
756
+ keeps each producer `instanceIndex` in the row; the atomic compact draw slot is
757
+ never used as a dynamic-input address.
758
+
759
+ On a device with compute, storage-buffer, and indirect-drawing capabilities,
760
+ GPU-driven preparation records the published Surface submission and binds the
761
+ Surface page and frame records through the existing GPU-driven instance owner.
762
+ The Standard Forward and Deferred graph owners allocate the two
763
+ `nearest-layer`/`color` targets and share the transmission backdrop producer.
764
+ The backdrop is the paired opaque color input. At 1x, both Standard paths copy
765
+ the completed depth into an `r32float` sampled target. At 4x MSAA, Render does
766
+ not use the hardware-averaged color resolve as the medium backdrop. A typed
767
+ fullscreen producer examines all four depth samples, selects the nearest
768
+ sample, and publishes both that depth and the color at the same sample index.
769
+ The nearest-water pass uses 4x color and depth attachments and the identical
770
+ paired rule before the color pass consumes it. Both resolved depth facts are
771
+ `r32float` with an unfilterable-float/non-filtering binding, so the color pass
772
+ never samples a depth attachment while writing scene depth. Inputs other than
773
+ the admitted 1x or paired 4x forms fail with `resource-descriptor-invalid`;
774
+ `addSingleLayerMediumPasses` never creates a synthetic depth value. This policy
775
+ is a nearest-sample coverage resolve, not an average of already-resolved color.
776
+ The color pass binds the copied depth, the preceding nearest-layer color/depth
777
+ pair, and the shared backdrop through the existing material bind-group owner.
778
+ GPU-driven recording reuses the existing Standard raster
779
+ dispatch with a Surface-family filter; devices without the required
780
+ capability use the existing CPU record lane and its structured admission
781
+ reason. These tests prove the owner boundary and fail-closed behavior;
782
+ Browser, Dawn, and physical GPU frame evidence remain separate gates. A
783
+ missing Surface ABI, pass, resource, or prepared generation is a
784
+ producer/preparation error. The public paired math and two example Surface
785
+ modules live in
786
+ [`src/examples/single-layer-medium-surface.ts`](src/examples/single-layer-medium-surface.ts).
787
+
111
788
  The public route is one `RenderFeature<FrameData>` through the Standard
112
789
  Pipeline and the active RenderGraph pass. In the examples below, `type FrameData`
113
790
  is the producer-owned extracted value. A feature extracts one frame value,
@@ -125,6 +802,14 @@ that was still pending. This is a receipt of admitted commands, not a GPU
125
802
  readback or proof that an indirect draw produced non-zero instances; producers
126
803
  that need those facts must use their existing readback or tape evidence.
127
804
 
805
+ `BarrelDistortion` is supplied by the ordinary ordered RenderFeature host. A
806
+ missing or zero-strength component produces no allocation, upload, or pass;
807
+ the feature still declares its shader for production validation. A positive
808
+ component adds one fullscreen pass after linear-LDR LUT output and before FXAA
809
+ and final encoding. There is no separate built-in registration path, so feature
810
+ ordering and retirement use the same graph and in-flight lifetime rules as
811
+ other post-process features.
812
+
128
813
  ## RenderFeature: the producer seam (first-read index)
129
814
 
130
815
  The public route is one `RenderFeature<FrameData>` through the active RenderGraph
@@ -269,6 +954,16 @@ Its color and intensity remain the lighting controls; no Skylight means no
269
954
  global ambient contribution. An explicit equirect keeps its image source.
270
955
  Use a neutral Skylight tint when comparing local captures against the sky.
271
956
 
957
+ The cube uses one bounded analytic Rayleigh/Mie daylight evaluator. Rayleigh
958
+ and Mie controls affect their spectral scattering and extinction, while solar
959
+ radiance scales linearly with the selected light. A 1.5 exponent shapes the
960
+ Rayleigh color response before solar scaling; the Mie lobe remains additive.
961
+ A smooth effective air-mass
962
+ bound (four zenith columns) avoids a saturated neutral horizon. This is an
963
+ explicit game-oriented clear-sky approximation, not the retired Perez fit or a
964
+ full spherical multiple-scattering solution. The existing cache owns all work;
965
+ background pixels still sample the cube, and the separate sun disc is unchanged.
966
+
272
967
  ### Atmosphere controls and fixed-exposure comparisons
273
968
 
274
969
  `Atmosphere` keeps the visible disc and the analytic sky response as separate
@@ -339,8 +1034,6 @@ import { createRenderer } from '@forgeax/engine-runtime';
339
1034
  const created = await createRenderer(canvas, { gpuPassTiming: {} });
340
1035
  if (!created.ok) throw created.error;
341
1036
  const renderer = created.value;
342
- const ready = await renderer.initialization;
343
- if (!ready.ok) throw ready.error;
344
1037
  const attached = renderer.attach(world);
345
1038
  if (!attached.ok) throw attached.error;
346
1039
  const drawn = renderer.draw({
@@ -536,8 +1229,6 @@ import type { Renderer } from '@forgeax/engine-render';
536
1229
  // After the existing createRenderer(canvas) happy path returns a Renderer,
537
1230
  // call `await recoverAndDraw(renderer, world)` with the World above.
538
1231
  async function recoverAndDraw(renderer: Renderer, world: World): Promise<void> {
539
- const ready = await renderer.initialization;
540
- if (!ready.ok) throw ready.error;
541
1232
  const attached = renderer.attach(world);
542
1233
  if (!attached.ok) throw attached.error;
543
1234
  const lease = attached.value;
@@ -573,8 +1264,157 @@ browser check; homogeneous transmittance alone cannot detect light-band artifact
573
1264
  History is clipped to the current
574
1265
  neighborhood and its weight decays with elapsed World time (at most 0.875 per
575
1266
  frame, about 87 ms half-life at 60 Hz or slower), limiting trails from animated
576
- density. This route covers authored density and time projection only; it does
577
- not add a procedural cloud API or a Voxel integration.
1267
+ density. `CloudLayer` is the renderer-owned procedural cloud route described
1268
+ below; it does not change the volumetric fog component or add a second clock.
1269
+
1270
+ ### CloudLayer authoring and derived transport
1271
+
1272
+ `CloudLayer` is a single World-authored component. Its schema keeps the seed,
1273
+ layer bounds, noise scale, coverage, density, wind and quality as serializable
1274
+ source facts. `renderComponentsPlugin()` registers it with the normal ECS
1275
+ component lease. Cache bytes, light-space shadow projections, per-view history
1276
+ and GPU handles remain Render-derived state and can be reconstructed from the
1277
+ validated source key.
1278
+
1279
+ Coverage is monotonic: zero clears the layer and increasing it admits more of
1280
+ its weather-shaped body. Detail erodes this body after altitude shaping; it
1281
+ cannot create detached density outside the body. Formation cache version 5
1282
+ uses the same integer hash and density composition as both analytic WGSL
1283
+ paths, with center-aligned periodic interpolation. Layer thickness controls
1284
+ the physical height of two vertical body cells; scale controls four horizontal
1285
+ cells per world-space period. Choose comparable body dimensions for cumulus
1286
+ rather than compressing a broad cloud into a thin layer. The broad field owns
1287
+ 85% of body shaping; attenuated high-frequency octaves and bounded subtractive
1288
+ erosion preserve connected masses instead of dense detached fragments.
1289
+
1290
+ Install the producer through the existing RenderFeature seam when a scene uses
1291
+ clouds:
1292
+
1293
+ ```ts
1294
+ import { World } from '@forgeax/engine-ecs';
1295
+ import { createRenderer } from '@forgeax/engine-runtime';
1296
+ import {
1297
+ Camera,
1298
+ CloudLayer,
1299
+ CloudQualityValue,
1300
+ DirectionalLight,
1301
+ createCloudLayerFeature,
1302
+ perspective,
1303
+ } from '@forgeax/engine-render';
1304
+ import { Transform } from '@forgeax/engine-scene';
1305
+
1306
+ const canvas = document.querySelector('canvas');
1307
+ if (!(canvas instanceof HTMLCanvasElement)) throw new Error('canvas is required');
1308
+ const world = new World();
1309
+ const camera = world.spawn(
1310
+ { component: Transform, data: { pos: [0, 140, 240] } },
1311
+ { component: Camera, data: { ...perspective({ fov: Math.PI / 4, aspect: 16 / 9 }) } },
1312
+ ).unwrap();
1313
+ const sun = world.spawn({
1314
+ component: DirectionalLight,
1315
+ data: { direction: [-0.4, -0.8, -0.3], color: [1, 0.95, 0.9], intensity: 2 },
1316
+ }).unwrap();
1317
+ const cloud = world.spawn({
1318
+ component: CloudLayer,
1319
+ data: {
1320
+ seed: 1337,
1321
+ baseHeight: 120,
1322
+ thickness: 80,
1323
+ scale: 0.004,
1324
+ coverage: 0.48,
1325
+ density: 1,
1326
+ wind: [8, 0, 2],
1327
+ quality: CloudQualityValue.medium,
1328
+ shadowRange: 512,
1329
+ },
1330
+ }).unwrap();
1331
+ const created = await createRenderer(canvas, { features: [createCloudLayerFeature()] });
1332
+ if (!created.ok) throw created.error;
1333
+ const renderer = created.value;
1334
+ const attached = renderer.attach(world);
1335
+ if (!attached.ok) throw attached.error;
1336
+ world.update(1 / 60).unwrap();
1337
+ const drawn = renderer.draw({
1338
+ leases: [attached.value],
1339
+ camera: { lease: attached.value },
1340
+ environment: { lease: attached.value },
1341
+ });
1342
+ if (!drawn.ok) throw drawn.error;
1343
+ console.log(camera, sun, cloud, renderer.inspect().cloudLayer);
1344
+
1345
+ world.set(cloud, CloudLayer, { wind: [10, 0, 2] }).unwrap();
1346
+ const changed = renderer.draw({
1347
+ leases: [attached.value],
1348
+ camera: { lease: attached.value },
1349
+ environment: { lease: attached.value },
1350
+ });
1351
+ if (!changed.ok) {
1352
+ console.error(renderer.state(), renderer.inspect().cloudLayer, changed.error);
1353
+ throw changed.error;
1354
+ }
1355
+ ```
1356
+
1357
+ If the capability report does not admit the cloud lane, disable that feature at
1358
+ assembly time and keep the rest of the renderer running:
1359
+
1360
+ ```ts
1361
+ const created = await createRenderer(canvas, {
1362
+ features: [createCloudLayerFeature({ enabled: false })],
1363
+ });
1364
+ ```
1365
+
1366
+ The runtime `createRenderer()` result is returned after the renderer's internal
1367
+ initialization barrier has completed, so the public `Renderer` can attach and
1368
+ draw immediately after the `Result` check above. The `initialization` Promise
1369
+ belongs to the lower-level internal construction seam.
1370
+
1371
+ The feature extracts the authored component, World `Time`, and the selected
1372
+ directional sun through the normal frame owner. It declares a bounded packed
1373
+ `u32` 3D formation cache with three contiguous R8 planes (`weather`, `body`,
1374
+ `erosion`; `ceil(3 * R8 texels / 4)` elements and `elements * 4` storage
1375
+ bytes), then composites scene-linear HDR color with inverse-view-projection
1376
+ rays, scene depth termination, bounded Beer extinction, solar-column
1377
+ transmittance, and cloud-interior phase scattering. The same formation and
1378
+ coverage contract is used by the CPU reference and the production WGSL path;
1379
+ coverage, density and wind are evaluation inputs and do not rebuild the
1380
+ reusable planes. The erosion plane repeats three times across the body period,
1381
+ adding smaller boundary structure without increasing cache bytes or density
1382
+ reads. Direct and approximate multiple scattering use the same light/phase unit
1383
+ convention as the volume path and CPU optical reference; view and solar
1384
+ sample limits are unchanged. The exported CPU helpers (`buildCloudDensityCache`,
1385
+ `integrateCloudCameraPath`,
1386
+ `integrateCloudSolarColumn`, `integrateCloudInterior`, and
1387
+ `sampleCloudShadow`) remain deterministic reconstruction and recovery tools.
1388
+
1389
+ The cloud shadow target is a square, quality-derived `rgba16float` map whose
1390
+ world-space projection is texel-snapped around the selected sun anchor. The
1391
+ cloud view, shadow and interior consumers share that projection and attenuate
1392
+ direct solar transport once. A cloud-enabled view opts into six additional
1393
+ `rgba16float` temporal surfaces: radiance, transmittance and representative
1394
+ cloud depth, each with current/previous ping-pong slots at `ceil(surface / 2)`
1395
+ resolution. One half-resolution transport raster writes the three surfaces as
1396
+ an MRT from the same camera integral. A full-resolution resolve upsamples those
1397
+ fields, rejects history by world depth, clamps radiance and transmittance to a
1398
+ four-neighbour history envelope, and performs the one HDR composite. The
1399
+ transport samples the world-space cloud-shadow cache for remaining solar
1400
+ optical depth; invalid projection or out-of-range samples use the bounded
1401
+ analytic column fallback. There is no second full-resolution raymarch solely
1402
+ for cloud depth.
1403
+
1404
+ Temporal slots are created only when a view demands them; submit advances the
1405
+ transaction from the resolve pass, while abort, resize, recovery and unload
1406
+ retire the old generation through the normal queue-fence owner.
1407
+
1408
+ Transparent geometry remains depth-read-only unless its authored render state
1409
+ explicitly enables `depthWriteEnabled`; this keeps ordinary alpha smoke from
1410
+ truncating later volume/cloud segments while allowing an intentional cutout or
1411
+ water surface to publish its own depth. Use `inspectCloudLayer()` and the
1412
+ renderer's normal feature inspection to read capability, generation,
1413
+ temporal-reset and resource facts. Inspection distinguishes CPU cache bytes,
1414
+ declared GPU target bytes, and measured GPU bytes; physical adapter timing and
1415
+ residency are reported as unavailable until a prepared adapter submits those
1416
+ receipts. Unavailable compute or storage support is data rather than a hidden
1417
+ fallback texture.
578
1418
 
579
1419
  Point shadows use one renderer-owned cube-array `ShadowAtlas`. The public
580
1420
  `SHADOW_ATLAS_DEFAULT_FACE_SIZE` and `SHADOW_ATLAS_DEFAULT_LAYERS` constants
@@ -612,8 +1452,6 @@ rebuild or cold-cook its source, then retry the same request.
612
1452
  const created = await createRenderer(canvas);
613
1453
  if (!created.ok) throw created.error;
614
1454
  const renderer = created.value;
615
- const ready = await renderer.initialization;
616
- if (!ready.ok) throw ready.error;
617
1455
  const attached = renderer.attach(world);
618
1456
  if (!attached.ok) throw attached.error;
619
1457
  const frame = renderer.draw({
@@ -727,7 +1565,7 @@ switch (spatial.status) {
727
1565
  `history.state`, `history.bytes`, `history.resetCount`, `passRoster`, and
728
1566
  `fallbackSource` are bounded POD facts. They are evidence about the current
729
1567
  owner state, not an acceptance claim: M1/M2 remain an implementation checkpoint
730
- until the paired Browser/Dawn 300-frame carrier, readback, falsifiers, and
1568
+ until the paired Browser/Dawn 60-frame carrier, readback, falsifiers, and
731
1569
  performance gates are present.
732
1570
 
733
1571
  SSR temporal feedback and presentation use distinct coordinates without another
@@ -820,9 +1658,10 @@ history, so moved geometry cannot leave a retained AO image.
820
1658
 
821
1659
  The public target route is one Renderer owner: create a typed `RenderTarget`, create a
822
1660
  `RenderTargetTextureSource` for the material slot, request a readback ticket, draw once, await
823
- `FrameReceipt.completed`, then call `observe(receipt, ...)`. `CubeCamera` contributes six real
824
- face views to the existing frame graph; a candidate becomes active only after its completed
825
- receipt. `ReflectionProbe` adds bounded PMREM face/mip work, local box projection, and Skylight
1661
+ `FrameReceipt.completed`, then call `observe(receipt, ...)`. A selected `Camera.target` contributes
1662
+ a 2D capture; `CubeCamera` contributes six real face views to the existing frame graph; a candidate becomes active only after its completed
1663
+ receipt. Target captures omit material draws that sample the same target, preventing a GPU
1664
+ read/write feedback loop; other submeshes remain eligible. `ReflectionProbe` adds bounded PMREM face/mip work, local box projection, and Skylight
826
1665
  irradiance fallback through the same Standard material binding path.
827
1666
 
828
1667
  Use `inspect()` for bounded target/probe counts, pending work, and the last structured failure.
@@ -980,7 +1819,7 @@ that a physical Browser/Dawn workload or timing gate completed.
980
1819
  `renderer.inspect()` and `renderer.observe()` never carry ROI pixels, scanlines,
981
1820
  brightness or level metrics. The hello-fxaa `hello-fxaa/dark-gradient/v1`
982
1821
  fixture report owns those fields and records the fixed camera, low-light scene,
983
- 800x600 resolution, 300-frame schedule, ROI, scanline, backend/lane, surface
1822
+ 800x600 resolution, 60-frame schedule, ROI, scanline, backend/lane, surface
984
1823
  formats, pixel hash, validation errors and mutation falsifiers.
985
1824
 
986
1825
  The report gates are explicit: unique-color ratio $r_U \ge 0.75$, level ratio
@@ -1407,115 +2246,75 @@ infer state from URLs, or repair a producer failure. Aggregate counts are
1407
2246
  derived from the observations, so an AI can inspect first, repair or recook the
1408
2247
  producer, and retry without guessing at hidden renderer state.
1409
2248
 
1410
- ### Renderer-owned Instances and CPU bounds
2249
+ ### World-owned Instances and CPU bounds
1411
2250
 
1412
- `Instances` is a lightweight ECS association containing only a
1413
- renderer-owned `collectionId`. Create and mutate the canonical packed matrices
1414
- through the same `renderer.instances` owner; it validates mat4 stride, keeps
1415
- revision/capacity/dirty ranges, and returns detached inspection snapshots.
2251
+ `Instances.transforms` holds packed column-major mat4 values in World-managed
2252
+ storage. Author the same field in keyed Scene assets, glTF imports, or ECS;
2253
+ neither creation nor saving requires a Renderer.
1416
2254
 
1417
2255
  ```ts
1418
- function identityMatrices(count: number): Float32Array {
1419
- const matrices = new Float32Array(count * 16);
1420
- for (let index = 0; index < count; index += 1) {
1421
- const offset = index * 16;
1422
- // Column-major identity, with a deterministic layout so the instances are visible.
1423
- matrices[offset] = 1;
1424
- matrices[offset + 5] = 1;
1425
- matrices[offset + 10] = 1;
1426
- matrices[offset + 15] = 1;
1427
- matrices[offset + 12] = (index % 100) * 2;
1428
- matrices[offset + 14] = Math.floor(index / 100) * 2;
1429
- }
1430
- return matrices;
2256
+ const transforms = new Float32Array(20_000 * 16);
2257
+ for (let index = 0; index < 20_000; index++) {
2258
+ const offset = index * 16;
2259
+ transforms[offset] = transforms[offset + 5] =
2260
+ transforms[offset + 10] = transforms[offset + 15] = 1;
2261
+ transforms[offset + 12] = (index % 100) * 2;
2262
+ transforms[offset + 14] = Math.floor(index / 100) * 2;
1431
2263
  }
1432
-
1433
- const collection = renderer.instances.create({
1434
- transforms: identityMatrices(instanceCount),
1435
- }).unwrap();
1436
- world.spawn(
2264
+ const entity = world.spawn(
2265
+ { component: Transform, data: {} },
1437
2266
  { component: MeshFilter, data: { assetHandle: cube } },
1438
2267
  { component: MeshRenderer, data: {} },
1439
- { component: Instances, data: { collectionId: collection.collectionId } },
1440
- );
1441
-
1442
- const patch = identityMatrices(1);
1443
- patch[12] = 4;
1444
- renderer.instances.update(collection.collectionId, {
1445
- start: 0,
1446
- transforms: patch,
1447
- });
2268
+ { component: Instances, data: { transforms } },
2269
+ ).unwrap();
1448
2270
 
1449
- // Replacement publishes a new revision. Pass prior generation tokens in their
1450
- // new order when compaction/reorder preserves logical instance identity;
1451
- // omitted tokens intentionally seed replacement rows.
1452
- renderer.instances.replace(collection.collectionId, identityMatrices(instanceCount + 1));
1453
- const current = renderer.instances.inspect(collection.collectionId).unwrap();
1454
- const detached = renderer.instances.snapshot(collection.collectionId).unwrap();
1455
- // A read-only observation does not advance the renderer upload cursor.
1456
- const observed = renderer.instances.peek(collection.collectionId).unwrap();
1457
- renderer.instances.release(collection.collectionId);
2271
+ transforms[12] = 4;
2272
+ world.set(entity, Instances, { transforms }).unwrap();
1458
2273
  ```
1459
2274
 
1460
- The lifecycle is deliberately one owner: `create` allocates canonical CPU
1461
- storage, `replace` publishes a complete new revision (and grows capacity when
1462
- needed), `update` validates a bounded matrix interval and records its dirty
1463
- range, `inspect` returns identity/count/capacity/revision, `snapshot` returns a
1464
- detached matrix copy plus dirty ranges for extraction, and `release` makes the
1465
- collection id terminal. `peek` returns the same detached matrices without
1466
- advancing the renderer upload cursor, so read-only inspection cannot hide a
1467
- later dirty range. A caller never provides a GPU buffer or maintains
1468
- backend-specific chunks. `detached.transforms` is an observation, not mutable
1469
- renderer state.
1470
-
1471
- Render snapshots one collection per frame and derives CPU union bounds from the
1472
- mesh AABB, entity world matrix, and detached matrices. Direct, GPU-driven, and
1473
- backend fallback lanes consume that same projection; internal GPU chunks never
1474
- become ECS or game-side state. Missing, malformed, or empty collections fail
1475
- closed or remain a conservative no-cull result without manufacturing an
1476
- identity instance. The initial payload must contain valid matrices: an
1477
- all-zero mat4 has a zero homogeneous `w` and can produce a blank frame. Do not
1478
- add matrix bytes, bounds, or chunk fields to the public `Instances` component or
1479
- duplicate this renderer fact in Pack/asset state.
2275
+ World copies external input and publishes changes through its normal component
2276
+ write path. Mutating the input array after spawn/set does not edit the World.
2277
+ Use `world.set` or a declared writable query; do not mutate inspection snapshots.
2278
+
2279
+ | Boundary | Owner and invariant |
2280
+ |:--|:--|
2281
+ | Authoring and Scene save | World/Scene retains matrices, not renderer-local IDs. Saving and reopening preserves the layout. |
2282
+ | Renderer projection | Current World change evidence drives the existing persistent RenderScene. Independent Renderers accept independently; stable matrices reuse the detached snapshot and revision. |
2283
+ | CPU bounds and drawing | Mesh bounds, holder world transform and instance-local matrices feed one projection shared by direct, GPU-driven and capability fallback lanes. |
2284
+ | Temporal identity | Ordinals retain identity across pose edits. Count changes seed new identities; previous matrices come from the last successful submission. |
2285
+ | GPU lifetime | Record owns buffer capacity, upload revisions, internal chunks and device generation. No game-side collection manager is required. |
2286
+
2287
+ Render derives CPU union bounds from the mesh AABB and instance-local matrices,
2288
+ then applies the holder's world transform. Bounds are not a second authored
2289
+ field. A missing or invalid mesh AABB yields a conservative no-cull result;
2290
+ repair the mesh producer and reload the same asset instead of inventing a
2291
+ game-side bounds override.
2292
+
2293
+ An empty array means zero instances. Invalid stride reports
2294
+ `instance-transforms-stride-mismatch`; non-finite values report
2295
+ `instance-transforms-invalid` before rendering that entity. Repair the World or
2296
+ source Scene data and retry. All-zero matrices are finite but have zero
2297
+ homogeneous `w`; initialize valid transforms for visible geometry.
1480
2298
 
1481
2299
  #### Instance inspection and recovery
1482
2300
 
1483
- `renderer.inspect().instanceCollections` is detached, bounded evidence for
1484
- every live collection. Each row contains:
2301
+ `renderer.inspect().instanceCollections` reports detached runtime evidence.
2302
+ Its `collectionId` is a private projection identity, not a Scene field or an
2303
+ authoring handle. There is no public `renderer.instances` mutation API.
1485
2304
 
1486
2305
  | Field | Meaning |
1487
2306
  |:--|:--|
1488
- | `collectionId`, `count`, `capacity`, `revision` | Logical identity and canonical authoring revision. |
1489
- | `residentGeneration`, `lane` | Device generation and the selected `direct-storage`, `chunked-storage`, `direct-uniform`, `chunked-uniform`, `unresident`, or `unavailable` lane. |
1490
- | `dirtyRanges`, `uploadRanges` | Canonical pending edits and the ranges uploaded by the current resident. |
1491
- | `uploadedBytes`, `requestedBytes`, `supportedBytes` | Measured upload and admission byte facts; unsupported limits remain explicit. |
1492
- | `backend`, `owner` | Backend identity and the fixed owner `renderer.instances`. |
1493
- | `error` | A structured record-stage failure with `code`, `expected`, `hint`, and typed `detail` facts. |
1494
-
1495
- Authoring failures use the same closed `InstanceCollectionError` contract.
1496
- For example, an invalid stride is rejected before publication and carries
1497
- typed facts instead of requiring message parsing:
1498
-
1499
- ```ts
1500
- const result = renderer.instances.create({ transforms: new Float32Array(17) });
1501
- if (!result.ok) {
1502
- const { code, expected, hint, detail, facts } = result.error;
1503
- // facts expose requestedBytes, supportedBytes, backend, owner, cause, and
1504
- // recovery; detail retains operation metadata. Repair the payload and retry.
1505
- void code;
1506
- void expected;
1507
- void hint;
1508
- void detail;
1509
- void facts;
1510
- }
1511
- ```
1512
-
1513
- After a `device-lost` transition, call `await renderer.recover()` and retry the
1514
- same draw request. The renderer drops generation-owned residents, keeps the
1515
- canonical collection, and uploads every matrix into the new resident before
1516
- recording; it does not reuse an old partial dirty range for uninitialized GPU
1517
- memory. If inspection reports `unavailable`, follow its `error.detail.recovery`
1518
- and repair the named capability/producer before retrying.
2307
+ | `collectionId`, `count`, `revision` | Renderer-local projection identity, matrix count and accepted content revision. |
2308
+ | `residentGeneration`, `lane` | Device generation and direct/chunked storage/uniform residency, or unresident/unavailable state. |
2309
+ | `uploadRanges`, `uploadedBytes` | Upload work observed in the latest rendered frame; stable frames report zero. |
2310
+ | `requestedBytes`, `supportedBytes`, `backend`, `owner` | Admission facts. The diagnostic owner label `renderer.instances` names internal residency, not a public API. |
2311
+ | `error` | Structured record failure with `code`, `expected`, `hint` and `detail`. |
2312
+
2313
+ After `device-lost`, call `await renderer.recover()` and retry the same draw.
2314
+ Recovery prepares new device resources from the retained CPU projection, without
2315
+ rescanning World or reusing old GPU handles. Failed candidates release their
2316
+ instance buffers. Writes made while the device is lost remain World-owned and
2317
+ are observed on the next draw. Disposing the Renderer never deletes author data.
1519
2318
 
1520
2319
  The large-instance smoke exercises the real Dawn path for all admitted sizes:
1521
2320
 
@@ -1533,7 +2332,7 @@ transport through the same production fixture and parameterizes the same three
1533
2332
  populations:
1534
2333
 
1535
2334
  ```bash
1536
- pnpm exec vitest run --config vitest.browser.config.ts --project=browser \
2335
+ pnpm exec vitest run --config config/vitest.browser.config.ts --project=browser \
1537
2336
  apps/parity/instancing-static/src/__tests__/instances.browser.test.ts
1538
2337
  ```
1539
2338
 
@@ -1694,7 +2493,9 @@ same forward-only policy. This section is about the Standard lane, not the
1694
2493
  separate Points/Lines admission rule above. See the
1695
2494
  [`shader Surface contract`](../shader/README.md#standard-surface-contract) and
1696
2495
  the [`game-3d` import example](../../templates/game-3d/README.md#import-first-surface-material-example)
1697
- for the source and Pack route.
2496
+ for the source and Pack route. Water and other participating media use the
2497
+ separate [Single-layer medium Surface](#single-layer-medium-surface) ABI and
2498
+ runtime route.
1698
2499
 
1699
2500
  The compiler and renderer share one pure `StandardLayerPlan`:
1700
2501
  `deriveStandardLayerPlan(effectiveParameters)`. It is the only derivation used
@@ -1779,23 +2580,31 @@ state published by `World.update()`.
1779
2580
  ## Persistent render scene
1780
2581
 
1781
2582
  Every attached World composition bootstraps one renderer-owned CPU projection,
1782
- then consumes each World's component and shared-reference change versions. One identity-based update
2583
+ then consumes each World's current membership and component block revisions. One identity-based update
1783
2584
  publication merges content, root transforms, and instance changes, including
1784
2585
  when all occur in the same frame. Its GPU projection compares affected matrix
1785
- and metadata rows before uploading them. An unchanged frame retains its existing
2586
+ and metadata rows before uploading them. Shared material rows are packed once per
2587
+ GPU synchronization; Primitive, DrawTemplate, and Material rows upload only when
2588
+ their packed bytes change. Failed uploads retain their dirty rows for retry. An unchanged frame retains its existing
1786
2589
  snapshot; unrelated gameplay component writes do not invalidate render state.
1787
2590
  There is no exclusive transform/instance scene admission followed by a separate
1788
2591
  rebuild implementation. Missing producer evidence causes conservative source
1789
2592
  extraction into the same retained projection. World reordering and catalog
1790
2593
  reconciliation preserve surviving slots and their submitted temporal history.
1791
2594
 
1792
- Mutable shared payloads use the same explicit-dirty rule: mutate the resolved
1793
- payload, then call `world.sharedRefs.markChanged(handle)`. The renderer compares
1794
- one monotonic shared-ref epoch on the no-change path, reads changed handles
1795
- when it advances, and refreshes the projection slots indexed by changed
1796
- material or mesh handles. Missing source evidence requests conservative
1797
- source reconciliation. Camera, light, and environment facts refresh independently
1798
- of geometry publication; their edits do not discard retained geometry.
2595
+ Shared runtime material parameters and mesh buffers are authored through
2596
+ `RuntimeMaterialValue` and `RuntimeMeshVertices` from `assets-runtime`, using
2597
+ ordinary managed `world.set` writes. The shared handle remains identity-only.
2598
+ Renderer reverse dependencies wake all users of both the old and new handle
2599
+ when content is rebound or removed. Each Renderer independently accepts its
2600
+ source candidate; one consumer cannot drain another's updates. See the
2601
+ [content contract](../assets-runtime/README.md#runtime-content).
2602
+
2603
+ Camera, light, and environment facts refresh independently of geometry.
2604
+ Initial attachment and updates use the same block candidate discovery and direct
2605
+ entity lookup. GPU dirty row addresses use fixed-width radix ordering before
2606
+ adjacent ranges are coalesced. A failed GPU upload retires that resident; source
2607
+ acceptance and submitted temporal history remain separate boundaries.
1799
2608
 
1800
2609
  Instance collection revisions refresh only their consumers. Visibility and
1801
2610
  parent changes refresh the affected subtree, and joint changes refresh the
@@ -1846,6 +2655,13 @@ The shortest public declaration uses the same `Materials.standard` producer as
1846
2655
  the runtime and imported-skin carriers. `alphaCutoff` is the Alpha Mask
1847
2656
  contract; `castShadow` publishes the matching ShadowCaster pass.
1848
2657
 
2658
+ Frame preparation and device recovery share the material-artifact collector.
2659
+ It indexes ShadowCaster dispatch once by renderable index and material handle;
2660
+ the selected last matching dispatch owns both the program identity and its
2661
+ vertex/fragment entries. Per-draw lookup does not rescan the dispatch roster,
2662
+ and the index is rebuilt for each collection so replacement publications are
2663
+ observed without a persistent cache.
2664
+
1849
2665
  ```ts
1850
2666
  import { Materials } from '@forgeax/engine/render';
1851
2667
 
@@ -2247,6 +3063,10 @@ consume the structured `error.detail` context.
2247
3063
  Use `renderer.inspect()` for a read-only snapshot. Call `renderer.dispose()`
2248
3064
  once or repeatedly; disposal is idempotent. Feature plans are assembled once
2249
3065
  and the Standard host owns graph replacement and last-known-good recovery.
3066
+ Repeated recoverable preparation errors are reported at most once per 60 frames.
3067
+ A changed stage exception name/message is reported immediately, retaining one
3068
+ report record per feature/error owner. Normal asynchronous resource warm-up
3069
+ remains silent.
2250
3070
 
2251
3071
  For a temporary presentation-owner handoff, call `renderer.releaseSurface()`.
2252
3072
  It unconfigures the canvas and makes `draw()` fail closed without disposing the
@@ -2293,6 +3113,12 @@ consumer that composes sprites with an existing 3D game can call
2293
3113
  `TransparentSort.configure(world, { mode: TransparentSort.layerY, yzAlpha: 1 })`;
2294
3114
  it should not reach into `/internal`.
2295
3115
 
3116
+ Eligible sprite buckets share one fold instance buffer across the typed transparent
3117
+ geometry pass and the legacy sprite pass. Both consume the same head/skip plan;
3118
+ 10,000 equal-depth sprites remain one instanced draw rather than 10,000 submissions.
3119
+ The sprite-atlas Dawn smoke checks this for 60 frames, and its browser probe
3120
+ checks the instance count, typed upload payload and WebGPU validation.
3121
+
2296
3122
  The root barrel does not expose frame stores or extract/plan/record owners.
2297
3123
  Applications contribute work through `RenderFeature` plans; the Standard
2298
3124
  renderer owns graph compilation and submission.
@@ -2393,7 +3219,7 @@ before promoting that status.
2393
3219
 
2394
3220
  The canonical paired carrier is `apps/hello/ssr`. It runs the same fixture at
2395
3221
  `http://127.0.0.1:4173/?forgeax-evidence=ssr` and
2396
- `dawn://hello/ssr?forgeax-evidence=ssr`, records 300-frame identity-bound
3222
+ `dawn://hello/ssr?forgeax-evidence=ssr`, records 60-frame identity-bound
2397
3223
  readbacks, and keeps visual rows in the form `observed` / `verdict` /
2398
3224
  `confidence`. Its performance lane derives the 1920x1080 descriptor and checks
2399
3225
  it against `estimateSsrSpatialMemory`; timestamp or paired-lane absence remains
@@ -2653,16 +3479,19 @@ existing observable state; resource disposal is distinct from JavaScript GC.
2653
3479
 
2654
3480
  ### Volumetric density coordinates
2655
3481
 
2656
- `VolumetricFog.density` is a producer-owned linear 3D source tile. The volume
2657
- integrator samples that tile in repeated world-space coordinates, combines the
3482
+ `VolumetricFog.density` is a producer-owned linear 3D source tile. In the default
3483
+ `noise` mode the integrator samples repeated world-space coordinates, combines the
2658
3484
  same three scales used by the approved fog fixture, and uses World time for the
2659
3485
  continuous advection term. The renderer keeps the source texture resident; it
2660
3486
  does not rebuild noise data for each frame. Its source expression is mapped to a
2661
3487
  signed density and clamped only at the optical-depth boundary so Beer-Lambert
2662
3488
  extinction remains non-negative.
2663
3489
 
2664
- Density integration uses fixed midpoints over 96 base ray intervals, including
2665
- the clipped final segment. Segments near a selected PointLight or SpotLight
3490
+ The local `density` mode samples normalized owner bounds directly. Each owner
3491
+ retains its own texture and optics; overlap sums coefficients before integration.
3492
+ Density integration partitions at owner entry/exit boundaries and uses 96
3493
+ fixed midpoint steps per interval, including the clipped final segment.
3494
+ Segments near a selected PointLight or SpotLight
2666
3495
  receive four local midpoints to resolve the inverse-square peak without making
2667
3496
  the whole ray uniformly expensive. Frame identity does not rotate density
2668
3497
  samples. Shadow visibility uses matching froxel centers without per-frame XY
@@ -2716,3 +3545,109 @@ orthographic camera stay on the normal camera snapshot path with structured
2716
3545
  The Engine Preview/game capability lab uses this same component on its ordinary
2717
3546
  mesh camera, and its settings control toggles component presence through the
2718
3547
  normal World path.
3548
+
3549
+ ## Standard camera barrel distortion
3550
+
3551
+ `BarrelDistortion` is the bounded output-space camera companion. Its
3552
+ `strength` is finite in `[0, 0.35]`; `centerX` and `centerY` are finite fractions
3553
+ in `[0, 1]` with a top-left origin. The Standard stage samples linear-LDR color
3554
+ after LUT and before FXAA and the single output encoding. Positive strength uses
3555
+ the automatic crop rule, so the fixed-FOV camera sees a narrower region. Missing
3556
+ or zero strength preserves the exact zero-work path.
3557
+
3558
+ If a DOM or `OffscreenCanvas` drawing buffer is temporarily zero-sized (for
3559
+ example while hidden or detached), `renderer.draw()` stops before configuring
3560
+ or submitting a zero-sized swapchain texture. The public result is a structured
3561
+ `device-operation-failed` whose nested cause is `rhi-not-available`; the last
3562
+ accepted `FrameReceipt` remains the LKG and a later positive-size draw resumes
3563
+ the same camera mapping without a GPU validation cascade.
3564
+
3565
+ ```ts
3566
+ import { BarrelDistortion } from '@forgeax/engine-render';
3567
+
3568
+ world.addComponent(camera, {
3569
+ component: BarrelDistortion,
3570
+ data: { strength: 0.2, centerX: 0.5, centerY: 0.5 },
3571
+ });
3572
+ ```
3573
+
3574
+ Invalid authoring is a closed error-code path. Narrow on `error.code` and read
3575
+ the structured fields; do not parse `error.message`:
3576
+
3577
+ ```ts
3578
+ import { createBarrelDistortionMapping } from '@forgeax/engine-render';
3579
+
3580
+ const mapping = createBarrelDistortionMapping(1920, 1080, { strength: 0.4 });
3581
+ if (!mapping.ok && mapping.error.code === 'barrel-distortion-invalid-parameter') {
3582
+ const error = mapping.error;
3583
+ const field = error.detail.field;
3584
+ const value = error.detail.value;
3585
+ const detailExpected = error.detail.expected;
3586
+ const expected = error.expected;
3587
+ const hint = error.hint;
3588
+ console.error({ field, value, detailExpected, expected, hint });
3589
+ }
3590
+ ```
3591
+
3592
+ In the design language, `actual` and `bound` map to the public Barrel members
3593
+ `error.detail.value` and `error.detail.expected`. Barrel errors do not expose
3594
+ `error.detail.actual` or `error.detail.bound`; those member names remain owned by
3595
+ `shadow-invalid-config` only.
3596
+
3597
+ `createBarrelDistortionMapping(outputWidth, outputHeight, data)` derives the
3598
+ immutable effective mapping. Reuse the mapping attached to the submitted
3599
+ `FrameReceipt` or the serialized worker frame signal. `mapDisplayToScene` and
3600
+ `mapSceneToDisplay` write continuous output-viewport physical pixels through
3601
+ out-parameters; inverse points cropped outside the display rectangle return
3602
+ `false`. The public `@forgeax/engine-picking` display entrypoints use this
3603
+ mapping once, while legacy picking continues to accept unwarped viewport
3604
+ coordinates. DOM/ShadowRoot HUD layout remains unchanged; world labels and
3605
+ vertex radius sorting must use the inverse mapping in display pixels.
3606
+
3607
+ ## Render publication
3608
+
3609
+ `createRenderPublisher(world, assets, identity, capabilities, features, targets)` projects native
3610
+ StateProjection and GlobalTransform change evidence. `prepare()` returns a
3611
+ candidate: call `accept()` synchronously after successful `postMessage`, or
3612
+ `discard()` if sending fails. Transfer only `renderPublicationTransfers(packet)`;
3613
+ return those buffers through `recycle()` after the receiver's FrameReceipt
3614
+ completes. ECS and asset storage remain owned by the source.
3615
+
3616
+ A Renderer constructed with `publicationSource: { source, epoch }` consumes
3617
+ `draw({ publication })` through its existing PersistentRenderScene and full
3618
+ prepare/record/submit path. It refuses local World leases. Source identity,
3619
+ epoch, baseline, base revision, and numeric columns are checked before CPU
3620
+ acceptance. CPU acceptance advances the publication revision; it is distinct
3621
+ from successful GPU submission/completion. A failed consumer is replaced with
3622
+ a fresh identity epoch and baseline by the App Render Worker owner.
3623
+
3624
+ | Surface | Publication contract |
3625
+ |:--|:--|
3626
+ | Standard mesh/material/texture, camera and light facts | Baseline plus structural, resource, and transform deltas |
3627
+ | Cooked material programs | Immutable shader metadata/artifacts installed in existing receiver registries |
3628
+ | RuntimeMaterialValue / RuntimeMeshVertices | Source-owned updates invalidate receiver residency |
3629
+ | Source assets | Handle dependencies are published before use and retired after the last consumer |
3630
+ | Skin, morph, instances, points/lines, sprites, LOD | CPU source facts cross; receiver allocates palettes and retains instance/history identity |
3631
+ | Glyph text and tilemaps | Source Update derives geometry; edits invalidate the existing receiver mesh residency |
3632
+ | Video | `VideoSourceProvider` supplies native frames; publication clones the decoded backing and receiver releases its owned frame after synchronous upload |
3633
+ | Environment, fog/cloud, camera LUT, light texture modifiers | Frame resource closure is published alongside metadata |
3634
+ | Auxiliary/cube views and probes | Logical target descriptors/references remap to the receiver target owner; resize and recovery preserve logical identity |
3635
+ | Declarative RenderFeatures | Source `extract`, cloneable frame data, receiver `plan`, receiver-local `onFrameSubmitted`, source `onSourceFrameSubmitted` acknowledgment |
3636
+
3637
+ The source retains identity and dependency indexes. The receiver owns the only
3638
+ persistent RenderScene for this path and resolves assets through `AssetReader`.
3639
+ Same-realm Renderer consumers keep their synchronous World path.
3640
+
3641
+ Feature frame data and feedback must be structured-cloneable. Extracted data
3642
+ remains immutable until its submission acknowledgment. A feature's
3643
+ `assetDependencies(data)` declares GUID roots; their catalog closure and cooked
3644
+ material programs enter the same accepted resource namespace as scene assets.
3645
+ The receiver must construct matching feature implementations before drawing.
3646
+ Unknown feature identities, stale/duplicate acknowledgments and foreign GPU
3647
+ skin receipts are rejected. VFX uses this seam to preserve ordered fixed-tick
3648
+ intents and reconstruct retained emitter state after receiver replacement.
3649
+
3650
+ `RenderPublicationTargetOwner.authoring` reuses target descriptor validation
3651
+ without allocating a GPU object. Its receiver creates physical targets through
3652
+ the normal Renderer target host. Direct publication users supply that owner to
3653
+ the publisher; App assembles it automatically for the Render Worker tier.