@forgeax/engine-render 0.1.32 → 0.1.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (624) hide show
  1. package/README.md +1374 -142
  2. package/dist/assembly/analytic-fog-registration.d.ts +3 -0
  3. package/dist/assembly/analytic-fog-registration.d.ts.map +1 -0
  4. package/dist/assembly/backend-contract.d.ts +2 -0
  5. package/dist/assembly/backend-contract.d.ts.map +1 -1
  6. package/dist/assembly/barrel-distortion-inspection.d.ts +3 -0
  7. package/dist/assembly/barrel-distortion-inspection.d.ts.map +1 -0
  8. package/dist/assembly/canvas-draw-guard.d.ts +3 -0
  9. package/dist/assembly/canvas-draw-guard.d.ts.map +1 -0
  10. package/dist/assembly/cloud-shadow-entry.d.ts +4 -0
  11. package/dist/assembly/cloud-shadow-entry.d.ts.map +1 -0
  12. package/dist/assembly/dynamic-geometry-host.d.ts +4 -1
  13. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  14. package/dist/assembly/dynamic-geometry-runtime.d.ts +1 -1
  15. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -1
  16. package/dist/assembly/factory.d.ts +1 -1
  17. package/dist/assembly/factory.d.ts.map +1 -1
  18. package/dist/assembly/fxaa-registration.d.ts +4 -0
  19. package/dist/assembly/fxaa-registration.d.ts.map +1 -0
  20. package/dist/assembly/host-contract.d.ts +3 -3
  21. package/dist/assembly/host-contract.d.ts.map +1 -1
  22. package/dist/assembly/lod-observation.d.ts +4 -0
  23. package/dist/assembly/lod-observation.d.ts.map +1 -0
  24. package/dist/assembly/material/assembly.d.ts.map +1 -1
  25. package/dist/assembly/material/pipeline-helpers.d.ts +5 -0
  26. package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -1
  27. package/dist/assembly/material-shader-policy.d.ts +16 -2
  28. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  29. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -1
  30. package/dist/assembly/motion-blur-registration.d.ts +3 -0
  31. package/dist/assembly/motion-blur-registration.d.ts.map +1 -0
  32. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
  33. package/dist/assembly/recovery/renderer-recover.d.ts +2 -0
  34. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  35. package/dist/assembly/render-feature-post-processes.d.ts +6 -0
  36. package/dist/assembly/render-feature-post-processes.d.ts.map +1 -0
  37. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  38. package/dist/assembly/renderer-feature-inspection.d.ts +1 -1
  39. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -1
  40. package/dist/assembly/renderer-frame-transaction.d.ts +12 -0
  41. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  42. package/dist/assembly/renderer-inspection.d.ts +11 -0
  43. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  44. package/dist/assembly/temporal-post-process-prewarm.d.ts +13 -0
  45. package/dist/assembly/temporal-post-process-prewarm.d.ts.map +1 -0
  46. package/dist/assembly/webgpu-pbr-ready.d.ts +1 -0
  47. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -1
  48. package/dist/assembly/webgpu-ready-contract.d.ts +4 -3
  49. package/dist/assembly/webgpu-ready-contract.d.ts.map +1 -1
  50. package/dist/assembly/webgpu-ready-per-pass.d.ts +39 -0
  51. package/dist/assembly/webgpu-ready-per-pass.d.ts.map +1 -0
  52. package/dist/assembly/webgpu-ready.d.ts +2 -2
  53. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  54. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -1
  55. package/dist/assembly/webgpu-renderer-observation-owner.d.ts +35 -0
  56. package/dist/assembly/webgpu-renderer-observation-owner.d.ts.map +1 -0
  57. package/dist/assembly/webgpu-renderer.d.ts +1 -1
  58. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  59. package/dist/authoring.mjs +1 -1
  60. package/dist/barrel-distortion.d.ts +47 -0
  61. package/dist/barrel-distortion.d.ts.map +1 -0
  62. package/dist/bloom-admission.d.ts +13 -0
  63. package/dist/bloom-admission.d.ts.map +1 -0
  64. package/dist/{chunk-PA5DRJNI.mjs → chunk-ELJFJ2BU.mjs} +2 -2
  65. package/dist/chunk-ELJFJ2BU.mjs.map +1 -0
  66. package/dist/{chunk-JCIRYQ4V.mjs → chunk-HEB564WF.mjs} +10 -4
  67. package/dist/chunk-HEB564WF.mjs.map +1 -0
  68. package/dist/{chunk-JRIPCMYC.mjs → chunk-MKDIT2EM.mjs} +1584 -375
  69. package/dist/chunk-MKDIT2EM.mjs.map +1 -0
  70. package/dist/{chunk-S665NJNF.mjs → chunk-OCDLEI6I.mjs} +3 -3
  71. package/dist/{chunk-S665NJNF.mjs.map → chunk-OCDLEI6I.mjs.map} +1 -1
  72. package/dist/chunk-ODZWR6JQ.mjs +4927 -0
  73. package/dist/chunk-ODZWR6JQ.mjs.map +1 -0
  74. package/dist/{chunk-DRR7D3KX.mjs → chunk-SHFUFJ5N.mjs} +192 -18
  75. package/dist/chunk-SHFUFJ5N.mjs.map +1 -0
  76. package/dist/{chunk-4NNZQCL3.mjs → chunk-UI5SFODX.mjs} +223 -277
  77. package/dist/chunk-UI5SFODX.mjs.map +1 -0
  78. package/dist/{chunk-GWCEMDVR.mjs → chunk-UINHLCZA.mjs} +787 -80
  79. package/dist/chunk-UINHLCZA.mjs.map +1 -0
  80. package/dist/{chunk-6TFBXLRJ.mjs → chunk-WEX45HKH.mjs} +222 -337
  81. package/dist/chunk-WEX45HKH.mjs.map +1 -0
  82. package/dist/cloud/density.d.ts +55 -0
  83. package/dist/cloud/density.d.ts.map +1 -0
  84. package/dist/cloud/extract.d.ts +25 -0
  85. package/dist/cloud/extract.d.ts.map +1 -0
  86. package/dist/cloud/feature.d.ts +57 -0
  87. package/dist/cloud/feature.d.ts.map +1 -0
  88. package/dist/cloud/frame-context.d.ts +8 -0
  89. package/dist/cloud/frame-context.d.ts.map +1 -0
  90. package/dist/cloud/inspection.d.ts +59 -0
  91. package/dist/cloud/inspection.d.ts.map +1 -0
  92. package/dist/cloud/optics.d.ts +56 -0
  93. package/dist/cloud/optics.d.ts.map +1 -0
  94. package/dist/cloud/parameters.d.ts +73 -0
  95. package/dist/cloud/parameters.d.ts.map +1 -0
  96. package/dist/cloud/resources.d.ts +60 -0
  97. package/dist/cloud/resources.d.ts.map +1 -0
  98. package/dist/cloud/shadow.d.ts +42 -0
  99. package/dist/cloud/shadow.d.ts.map +1 -0
  100. package/dist/cloud/temporal.d.ts +74 -0
  101. package/dist/cloud/temporal.d.ts.map +1 -0
  102. package/dist/components/atmosphere.d.ts +2 -0
  103. package/dist/components/atmosphere.d.ts.map +1 -1
  104. package/dist/components/barrel-distortion.d.ts +13 -0
  105. package/dist/components/barrel-distortion.d.ts.map +1 -0
  106. package/dist/components/camera.d.ts +20 -4
  107. package/dist/components/camera.d.ts.map +1 -1
  108. package/dist/components/cloud-layer.d.ts +29 -0
  109. package/dist/components/cloud-layer.d.ts.map +1 -0
  110. package/dist/components/index.d.ts +2 -0
  111. package/dist/components/index.d.ts.map +1 -1
  112. package/dist/components/instances.d.ts +20 -6
  113. package/dist/components/instances.d.ts.map +1 -1
  114. package/dist/components/mesh-filter.d.ts +6 -5
  115. package/dist/components/mesh-filter.d.ts.map +1 -1
  116. package/dist/components/motion-blur.d.ts +1 -0
  117. package/dist/components/motion-blur.d.ts.map +1 -1
  118. package/dist/construct-renderer.d.ts +1 -1
  119. package/dist/construct-renderer.d.ts.map +1 -1
  120. package/dist/construct-renderer.mjs +9751 -3528
  121. package/dist/construct-renderer.mjs.map +1 -1
  122. package/dist/device/gpu-residency.d.ts +6 -0
  123. package/dist/device/gpu-residency.d.ts.map +1 -1
  124. package/dist/dynamic-geometry.d.ts +5 -1
  125. package/dist/dynamic-geometry.d.ts.map +1 -1
  126. package/dist/environment/analytic-fog.d.ts +6 -0
  127. package/dist/environment/analytic-fog.d.ts.map +1 -0
  128. package/dist/errors/cloud.d.ts +59 -0
  129. package/dist/errors/cloud.d.ts.map +1 -0
  130. package/dist/errors/render.d.ts +23 -5
  131. package/dist/errors/render.d.ts.map +1 -1
  132. package/dist/examples/single-layer-medium-surface.d.ts +47 -0
  133. package/dist/examples/single-layer-medium-surface.d.ts.map +1 -0
  134. package/dist/extract/camera.d.ts.map +1 -1
  135. package/dist/extract/contracts.d.ts +12 -1
  136. package/dist/extract/contracts.d.ts.map +1 -1
  137. package/dist/extract/environment.d.ts +2 -0
  138. package/dist/extract/environment.d.ts.map +1 -1
  139. package/dist/features/barrel-distortion.d.ts +29 -0
  140. package/dist/features/barrel-distortion.d.ts.map +1 -0
  141. package/dist/features/error-reporting.d.ts +17 -0
  142. package/dist/features/error-reporting.d.ts.map +1 -0
  143. package/dist/features/host.d.ts +15 -0
  144. package/dist/features/host.d.ts.map +1 -1
  145. package/dist/features/motion-blur/motion-blur-feature.d.ts +20 -2
  146. package/dist/features/motion-blur/motion-blur-feature.d.ts.map +1 -1
  147. package/dist/features/motion-blur/motion-blur-params.d.ts +21 -0
  148. package/dist/features/motion-blur/motion-blur-params.d.ts.map +1 -1
  149. package/dist/features/motion-blur/motion-blur-runtime.d.ts +54 -0
  150. package/dist/features/motion-blur/motion-blur-runtime.d.ts.map +1 -0
  151. package/dist/features/plan.d.ts +15 -4
  152. package/dist/features/plan.d.ts.map +1 -1
  153. package/dist/features/prepared-gpu-work.d.ts +9 -5
  154. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  155. package/dist/features/render-graph-compute.d.ts.map +1 -1
  156. package/dist/features/render-graph-contribution.d.ts +5 -0
  157. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  158. package/dist/features/render-graph-raster.d.ts +1 -0
  159. package/dist/features/render-graph-raster.d.ts.map +1 -1
  160. package/dist/features/targets.d.ts +2 -0
  161. package/dist/features/targets.d.ts.map +1 -1
  162. package/dist/features/types.d.ts +44 -0
  163. package/dist/features/types.d.ts.map +1 -1
  164. package/dist/fullscreen-post-process-pass.d.ts +15 -7
  165. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  166. package/dist/gpu-dirty-ranges.d.ts +11 -0
  167. package/dist/gpu-dirty-ranges.d.ts.map +1 -0
  168. package/dist/gpu-driven/batch-topology.d.ts +17 -0
  169. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  170. package/dist/gpu-driven/material-artifacts.d.ts +6 -0
  171. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
  172. package/dist/gpu-driven/prepared-draw.d.ts +7 -0
  173. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  174. package/dist/gpu-driven/production-raster-material.d.ts +178 -0
  175. package/dist/gpu-driven/production-raster-material.d.ts.map +1 -0
  176. package/dist/gpu-driven/production-raster.d.ts +50 -4
  177. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  178. package/dist/gpu-driven/resource-allocation.d.ts +31 -0
  179. package/dist/gpu-driven/resource-allocation.d.ts.map +1 -0
  180. package/dist/gpu-driven/shadow-ownership.d.ts +14 -0
  181. package/dist/gpu-driven/shadow-ownership.d.ts.map +1 -0
  182. package/dist/gpu-driven/view-gpu.d.ts +35 -2
  183. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  184. package/dist/gpu-scene.d.ts +11 -1
  185. package/dist/gpu-scene.d.ts.map +1 -1
  186. package/dist/hdrp-buffers.d.ts +2 -2
  187. package/dist/hdrp-buffers.d.ts.map +1 -1
  188. package/dist/ibl/skylight-bind-group.d.ts +10 -1
  189. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  190. package/dist/index.d.ts +31 -8
  191. package/dist/index.d.ts.map +1 -1
  192. package/dist/index.mjs +258 -8
  193. package/dist/index.mjs.map +1 -1
  194. package/dist/inspection-types.d.ts +178 -3
  195. package/dist/inspection-types.d.ts.map +1 -1
  196. package/dist/instance-buffer-cache.d.ts +4 -4
  197. package/dist/instance-buffer-cache.d.ts.map +1 -1
  198. package/dist/instances.d.ts +49 -94
  199. package/dist/instances.d.ts.map +1 -1
  200. package/dist/internal.mjs +7 -7
  201. package/dist/pbr-pipeline.d.ts +19 -1
  202. package/dist/pbr-pipeline.d.ts.map +1 -1
  203. package/dist/pipeline/single-layer-medium-passes.d.ts +73 -0
  204. package/dist/pipeline/single-layer-medium-passes.d.ts.map +1 -0
  205. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  206. package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
  207. package/dist/pipeline/standard-output/types.d.ts +4 -2
  208. package/dist/pipeline/standard-output/types.d.ts.map +1 -1
  209. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  210. package/dist/pipeline/standard-post.d.ts +28 -19
  211. package/dist/pipeline/standard-post.d.ts.map +1 -1
  212. package/dist/pipeline-builder.d.ts +1 -1
  213. package/dist/pipeline-builder.d.ts.map +1 -1
  214. package/dist/pipeline-spec.d.ts +2 -2
  215. package/dist/plugin.d.ts.map +1 -1
  216. package/dist/prepare/prepared-graphics-resolver.d.ts +2 -0
  217. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  218. package/dist/record/barrel-distortion-frame.d.ts +15 -0
  219. package/dist/record/barrel-distortion-frame.d.ts.map +1 -0
  220. package/dist/record/dynamic-geometry-consumption.d.ts +10 -2
  221. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -1
  222. package/dist/record/fold-buckets.d.ts +95 -0
  223. package/dist/record/fold-buckets.d.ts.map +1 -0
  224. package/dist/record/fold-instance-buffer.d.ts +6 -0
  225. package/dist/record/fold-instance-buffer.d.ts.map +1 -0
  226. package/dist/record/frame-lighting.d.ts +1 -0
  227. package/dist/record/frame-lighting.d.ts.map +1 -1
  228. package/dist/record/frame-snapshot.d.ts +40 -3
  229. package/dist/record/frame-snapshot.d.ts.map +1 -1
  230. package/dist/record/frame-targets.d.ts.map +1 -1
  231. package/dist/record/frame.d.ts +14 -62
  232. package/dist/record/frame.d.ts.map +1 -1
  233. package/dist/record/gpu-draw-receipts.d.ts +11 -0
  234. package/dist/record/gpu-draw-receipts.d.ts.map +1 -0
  235. package/dist/record/gpu-timing.d.ts +8 -0
  236. package/dist/record/gpu-timing.d.ts.map +1 -1
  237. package/dist/record/main-pass-geometry.d.ts +21 -5
  238. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  239. package/dist/record/main-pass-material.d.ts +7 -1
  240. package/dist/record/main-pass-material.d.ts.map +1 -1
  241. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  242. package/dist/record/main-pass.d.ts +13 -2
  243. package/dist/record/main-pass.d.ts.map +1 -1
  244. package/dist/record/mesh-ssbo.d.ts +6 -5
  245. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  246. package/dist/record/motion-blur-receipt.d.ts +11 -0
  247. package/dist/record/motion-blur-receipt.d.ts.map +1 -0
  248. package/dist/record/probe-blend-buffer.d.ts +16 -0
  249. package/dist/record/probe-blend-buffer.d.ts.map +1 -0
  250. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  251. package/dist/record/render-context.d.ts +34 -31
  252. package/dist/record/render-context.d.ts.map +1 -1
  253. package/dist/record/shadow-pass.d.ts +2 -0
  254. package/dist/record/shadow-pass.d.ts.map +1 -1
  255. package/dist/record/skybox-post-pass.d.ts +6 -4
  256. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  257. package/dist/record/typed-frame-graph.d.ts +22 -2
  258. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  259. package/dist/record/view-ubo.d.ts +11 -3
  260. package/dist/record/view-ubo.d.ts.map +1 -1
  261. package/dist/record/volume-params.d.ts +9 -0
  262. package/dist/record/volume-params.d.ts.map +1 -0
  263. package/dist/recovery/render-system-candidate.d.ts +10 -0
  264. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  265. package/dist/reflection/record-owner.d.ts.map +1 -1
  266. package/dist/render-contract.d.ts +66 -8
  267. package/dist/render-contract.d.ts.map +1 -1
  268. package/dist/render-graph-primitives.d.ts.map +1 -1
  269. package/dist/render-pipeline.d.ts +106 -3
  270. package/dist/render-pipeline.d.ts.map +1 -1
  271. package/dist/render-system-extract-tail.d.ts +3 -2
  272. package/dist/render-system-extract-tail.d.ts.map +1 -1
  273. package/dist/render-system-extract.d.ts +24 -10
  274. package/dist/render-system-extract.d.ts.map +1 -1
  275. package/dist/render-system-presentation.d.ts +23 -0
  276. package/dist/render-system-presentation.d.ts.map +1 -0
  277. package/dist/render-system-producer-roots.d.ts +10 -0
  278. package/dist/render-system-producer-roots.d.ts.map +1 -0
  279. package/dist/render-system-residency.d.ts +12 -0
  280. package/dist/render-system-residency.d.ts.map +1 -0
  281. package/dist/render-system.d.ts +26 -15
  282. package/dist/render-system.d.ts.map +1 -1
  283. package/dist/scene/probe-blend.d.ts +27 -0
  284. package/dist/scene/probe-blend.d.ts.map +1 -1
  285. package/dist/scene/render-scene.d.ts +14 -5
  286. package/dist/scene/render-scene.d.ts.map +1 -1
  287. package/dist/ssao-buffers.d.ts +9 -0
  288. package/dist/ssao-buffers.d.ts.map +1 -1
  289. package/dist/surface/dynamic-input.d.ts +133 -0
  290. package/dist/surface/dynamic-input.d.ts.map +1 -0
  291. package/dist/surface/gpu-driven.d.ts +41 -0
  292. package/dist/surface/gpu-driven.d.ts.map +1 -0
  293. package/dist/surface/submission-observation.d.ts +136 -0
  294. package/dist/surface/submission-observation.d.ts.map +1 -0
  295. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  296. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  297. package/dist/temporal/frame.d.ts +7 -0
  298. package/dist/temporal/frame.d.ts.map +1 -1
  299. package/dist/temporal/gpu.d.ts +13 -1
  300. package/dist/temporal/gpu.d.ts.map +1 -1
  301. package/dist/temporal/index.mjs +5 -5
  302. package/dist/temporal/inspection.d.ts.map +1 -1
  303. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  304. package/dist/temporal/target-coverage-attachment.d.ts.map +1 -1
  305. package/dist/temporal/view.d.ts +6 -1
  306. package/dist/temporal/view.d.ts.map +1 -1
  307. package/dist/transmission/single-layer-medium.d.ts +89 -0
  308. package/dist/transmission/single-layer-medium.d.ts.map +1 -0
  309. package/dist/typed-render-graph-primitives.d.ts +24 -9
  310. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  311. package/dist/volume/component.d.ts +9 -5
  312. package/dist/volume/component.d.ts.map +1 -1
  313. package/dist/volume/extract.d.ts +1 -1
  314. package/dist/volume/extract.d.ts.map +1 -1
  315. package/dist/volume/inspection.d.ts +2 -0
  316. package/dist/volume/inspection.d.ts.map +1 -1
  317. package/dist/volume/passes.d.ts +4 -1
  318. package/dist/volume/passes.d.ts.map +1 -1
  319. package/dist/volume/resources.d.ts.map +1 -1
  320. package/dist/volume/temporal.d.ts +3 -1
  321. package/dist/volume/temporal.d.ts.map +1 -1
  322. package/package.json +22 -21
  323. package/src/__tests__/analytic-fog-graph.unit.test.ts +199 -0
  324. package/src/__tests__/analytic-fog-optics.unit.test.ts +38 -0
  325. package/src/__tests__/atmosphere-daylight.dawn.test.ts +72 -0
  326. package/src/__tests__/barrel-distortion-lkg.integration.test.ts +353 -0
  327. package/src/__tests__/barrel-distortion.dawn.test.ts +41 -0
  328. package/src/__tests__/barrel-distortion.unit.test.ts +364 -0
  329. package/src/__tests__/bloom-exact-zero.unit.test.ts +112 -18
  330. package/src/__tests__/bloom-lifecycle.integration.test.ts +17 -4
  331. package/src/__tests__/construct-renderer-identity.unit.test.ts +49 -0
  332. package/src/__tests__/detached-recovery-graph.unit.test.ts +277 -0
  333. package/src/__tests__/device-loss-fanout.unit.test.ts +36 -1
  334. package/src/__tests__/docs-gate-green.unit.test.ts +112 -1
  335. package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +20 -0
  336. package/src/__tests__/environment-fog-lanes.integration.test.ts +14 -0
  337. package/src/__tests__/environment-selection.unit.test.ts +12 -0
  338. package/src/__tests__/factory-contract.integration.test.ts +626 -113
  339. package/src/__tests__/feature-depth-input.dawn.test.ts +129 -2
  340. package/src/__tests__/frame-camera-selection.unit.test.ts +39 -1
  341. package/src/__tests__/frame-plan-contract.unit.test.ts +2 -0
  342. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +14 -3
  343. package/src/__tests__/gpu-dirty-ranges.unit.test.ts +22 -0
  344. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +16 -3
  345. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +12 -0
  346. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +2 -1
  347. package/src/__tests__/gpu-driven-material-artifacts.unit.test.ts +143 -0
  348. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +2 -2
  349. package/src/__tests__/gpu-driven-production.integration.test.ts +482 -3
  350. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +7 -6
  351. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +1 -1
  352. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +103 -4
  353. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +18 -2
  354. package/src/__tests__/gpu-driven-view.browser.test.ts +5 -8
  355. package/src/__tests__/gpu-driven-view.dawn.test.ts +6 -2
  356. package/src/__tests__/gpu-driven-view.unit.test.ts +1 -1
  357. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +1 -0
  358. package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -4
  359. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +1 -0
  360. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +12 -0
  361. package/src/__tests__/gpu-scene.unit.test.ts +210 -9
  362. package/src/__tests__/instance-allocation-failure.unit.test.ts +57 -0
  363. package/src/__tests__/instances-culling.integration.test.ts +10 -36
  364. package/src/__tests__/instances-store.unit.test.ts +105 -238
  365. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  366. package/src/__tests__/main-pass-material-medium-selection.unit.test.ts +146 -0
  367. package/src/__tests__/material-shader-prewarm.unit.test.ts +59 -0
  368. package/src/__tests__/material-snapshot-mutation.integration.test.ts +39 -1
  369. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +31 -0
  370. package/src/__tests__/morph-culling-reentry.dawn.test.ts +5 -5
  371. package/src/__tests__/morph-visual-evidence.integration.test.ts +8 -7
  372. package/src/__tests__/motion-blur-compute.dawn.test.ts +589 -0
  373. package/src/__tests__/motion-blur.pixel.integration.test.ts +2 -2
  374. package/src/__tests__/nineslice-handle-surface.unit.test.ts +4 -4
  375. package/src/__tests__/pbr-view-layout.unit.test.ts +9 -0
  376. package/src/__tests__/physical-clearcoat.integration.test.ts +58 -0
  377. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +21 -0
  378. package/src/__tests__/prepared-graphics-capability.unit.test.ts +66 -2
  379. package/src/__tests__/prepared-graphics-lifecycle.unit.test.ts +86 -0
  380. package/src/__tests__/probe-bindings.unit.test.ts +582 -1
  381. package/src/__tests__/probe-scene-dirty.integration.test.ts +28 -1
  382. package/src/__tests__/public-surface.test-d.ts +8 -0
  383. package/src/__tests__/receipt-observation-retirement.unit.test.ts +65 -0
  384. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +69 -0
  385. package/src/__tests__/render-error-code-owner.test-d.ts +9 -0
  386. package/src/__tests__/render-feature-diagnostics.unit.test.ts +15 -2
  387. package/src/__tests__/render-feature-error-reporting.unit.test.ts +44 -0
  388. package/src/__tests__/render-feature-prepared-diagnostics.unit.test.ts +10 -1
  389. package/src/__tests__/render-feature-retry.unit.test.ts +12 -1
  390. package/src/__tests__/render-feature-stage-fault-recovery.unit.test.ts +23 -0
  391. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +145 -19
  392. package/src/__tests__/render-scene-projection.unit.test.ts +36 -11
  393. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +37 -6
  394. package/src/__tests__/render-target-probe-soak.perf.test.ts +2 -2
  395. package/src/__tests__/render-temporal-environment-consumer.browser.test.ts +3 -3
  396. package/src/__tests__/render-temporal-environment-consumer.dawn.test.ts +5 -2
  397. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +41 -7
  398. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +16 -0
  399. package/src/__tests__/renderer-frame-transaction.integration.test.ts +33 -11
  400. package/src/__tests__/resource-allocation.unit.test.ts +123 -0
  401. package/src/__tests__/shader-manifest-fixture.ts +4 -4
  402. package/src/__tests__/single-layer-medium-passes.unit.test.ts +275 -0
  403. package/src/__tests__/skin-motion-regression.unit.test.ts +70 -1
  404. package/src/__tests__/skin-palette-recycling.dawn.test.ts +97 -0
  405. package/src/__tests__/skinned-shadow-caster.test.ts +17 -0
  406. package/src/__tests__/ssr-carrier-public-contract.unit.test.ts +21 -0
  407. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +4 -4
  408. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +2 -2
  409. package/src/__tests__/ssr-gpu-dispatch.ts +1 -1
  410. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +6 -2
  411. package/src/__tests__/standard-lighting-write.unit.test.ts +2 -1
  412. package/src/__tests__/standard-output-chain-contract.unit.test.ts +28 -0
  413. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +46 -1
  414. package/src/__tests__/standard-pipeline.dawn.test.ts +1 -1
  415. package/src/__tests__/standard-pipeline.integration.test.ts +200 -9
  416. package/src/__tests__/surface-dynamic-buffer-lifecycle.unit.test.ts +506 -0
  417. package/src/__tests__/surface-submission-observation.unit.test.ts +812 -0
  418. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +32 -0
  419. package/src/__tests__/temporal-performance.integration.test.ts +3 -3
  420. package/src/__tests__/temporal-reset-matrix.unit.test.ts +10 -0
  421. package/src/__tests__/temporal-view.unit.test.ts +2 -1
  422. package/src/__tests__/typed-pipeline-topology.unit.test.ts +20 -4
  423. package/src/__tests__/visibility-instances.integration.test.ts +7 -19
  424. package/src/__tests__/visibility-producer-matrix.integration.test.ts +9 -20
  425. package/src/__tests__/volume-optics-oracle.unit.test.ts +7 -3
  426. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +189 -15
  427. package/src/__tests__/volumetric-fog-errors.unit.test.ts +4 -3
  428. package/src/__tests__/volumetric-fog-multiple.unit.test.ts +55 -0
  429. package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +1 -1
  430. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +558 -68
  431. package/src/__tests__/volumetric-fog-temporal.unit.test.ts +27 -0
  432. package/src/__tests__/volumetric-fog-topology.unit.test.ts +23 -5
  433. package/src/assembly/analytic-fog-registration.ts +14 -0
  434. package/src/assembly/backend-contract.ts +30 -0
  435. package/src/assembly/barrel-distortion-inspection.ts +22 -0
  436. package/src/assembly/canvas-draw-guard.ts +11 -0
  437. package/src/assembly/cloud-shadow-entry.ts +22 -0
  438. package/src/assembly/dynamic-geometry-host.ts +65 -37
  439. package/src/assembly/dynamic-geometry-runtime.ts +7 -1
  440. package/src/assembly/factory.ts +2 -30
  441. package/src/assembly/fxaa-registration.ts +17 -0
  442. package/src/assembly/host-contract.ts +3 -3
  443. package/src/assembly/lod-observation.ts +12 -0
  444. package/src/assembly/material/assembly.ts +25 -13
  445. package/src/assembly/material/pipeline-helpers.ts +53 -12
  446. package/src/assembly/material-shader-policy.ts +55 -0
  447. package/src/assembly/material-shader-prewarm.ts +21 -10
  448. package/src/assembly/motion-blur-registration.ts +17 -0
  449. package/src/assembly/recovery/device-loss-fanout.ts +13 -0
  450. package/src/assembly/recovery/renderer-recover.ts +41 -18
  451. package/src/assembly/render-feature-post-processes.ts +54 -0
  452. package/src/assembly/renderer-facade.ts +6 -1
  453. package/src/assembly/renderer-feature-inspection.ts +8 -2
  454. package/src/assembly/renderer-frame-transaction.ts +23 -0
  455. package/src/assembly/renderer-inspection.ts +28 -0
  456. package/src/assembly/skin-palette-owner.ts +1 -1
  457. package/src/assembly/temporal-post-process-prewarm.ts +40 -0
  458. package/src/assembly/webgpu-pbr-ready.ts +9 -1
  459. package/src/assembly/webgpu-ready-contract.ts +8 -3
  460. package/src/assembly/webgpu-ready-per-pass.ts +104 -0
  461. package/src/assembly/webgpu-ready.ts +296 -306
  462. package/src/assembly/webgpu-renderer-bootstrap.ts +0 -12
  463. package/src/assembly/webgpu-renderer-observation-owner.ts +222 -0
  464. package/src/assembly/webgpu-renderer.ts +228 -281
  465. package/src/assembly/webgpu-vertex-layouts.ts +1 -1
  466. package/src/barrel-distortion.ts +247 -0
  467. package/src/bloom-admission.ts +21 -0
  468. package/src/cloud/__tests__/cloud-density-parity.dawn.test.ts +173 -0
  469. package/src/cloud/__tests__/cloud-layer-framebuffer-y.dawn.test.ts +265 -0
  470. package/src/cloud/__tests__/cloud-layer.browser.test.ts +432 -0
  471. package/src/cloud/__tests__/cloud-layer.unit.test.ts +634 -0
  472. package/src/cloud/density.ts +600 -0
  473. package/src/cloud/extract.ts +110 -0
  474. package/src/cloud/feature.ts +1810 -0
  475. package/src/cloud/frame-context.ts +42 -0
  476. package/src/cloud/inspection.ts +130 -0
  477. package/src/cloud/optics.ts +285 -0
  478. package/src/cloud/parameters.ts +255 -0
  479. package/src/cloud/resources.ts +122 -0
  480. package/src/cloud/shadow.ts +167 -0
  481. package/src/cloud/temporal.ts +226 -0
  482. package/src/components/__tests__/motion-blur.unit.test.ts +1 -0
  483. package/src/components/atmosphere.ts +4 -0
  484. package/src/components/barrel-distortion.ts +46 -0
  485. package/src/components/camera.ts +66 -10
  486. package/src/components/cloud-layer.ts +47 -0
  487. package/src/components/index.ts +2 -0
  488. package/src/components/instances.ts +18 -45
  489. package/src/components/mesh-filter.ts +11 -9
  490. package/src/components/motion-blur.ts +1 -0
  491. package/src/components/sprite-instances.ts +1 -1
  492. package/src/construct-renderer.ts +1 -1
  493. package/src/device/gpu-residency.ts +56 -12
  494. package/src/dynamic-geometry.ts +12 -1
  495. package/src/environment/analytic-fog.ts +44 -0
  496. package/src/environment/background.ts +2 -2
  497. package/src/errors/cloud.ts +103 -0
  498. package/src/errors/render.ts +58 -8
  499. package/src/examples/single-layer-medium-surface.ts +59 -0
  500. package/src/extract/camera.ts +35 -8
  501. package/src/extract/contracts.ts +12 -0
  502. package/src/extract/environment.ts +4 -0
  503. package/src/features/barrel-distortion.ts +172 -0
  504. package/src/features/error-reporting.ts +98 -0
  505. package/src/features/host.ts +138 -24
  506. package/src/features/motion-blur/__tests__/motion-blur-feature.unit.test.ts +111 -1
  507. package/src/features/motion-blur/__tests__/motion-blur-params.unit.test.ts +58 -3
  508. package/src/features/motion-blur/motion-blur-feature.ts +1519 -15
  509. package/src/features/motion-blur/motion-blur-params.ts +70 -2
  510. package/src/features/motion-blur/motion-blur-runtime.ts +176 -0
  511. package/src/features/plan.ts +33 -8
  512. package/src/features/prepared-gpu-work.ts +86 -35
  513. package/src/features/render-graph-compute.ts +13 -5
  514. package/src/features/render-graph-contribution.ts +11 -0
  515. package/src/features/render-graph-raster.ts +45 -3
  516. package/src/features/targets.ts +66 -0
  517. package/src/features/types.ts +47 -0
  518. package/src/fullscreen-post-process-pass.ts +41 -9
  519. package/src/gpu-dirty-ranges.ts +48 -0
  520. package/src/gpu-driven/batch-topology.ts +53 -0
  521. package/src/gpu-driven/material-artifacts.ts +35 -32
  522. package/src/gpu-driven/prepared-draw.ts +27 -14
  523. package/src/gpu-driven/production-raster-material.ts +1894 -0
  524. package/src/gpu-driven/production-raster.ts +904 -787
  525. package/src/gpu-driven/resource-allocation.ts +142 -0
  526. package/src/gpu-driven/shadow-ownership.ts +71 -0
  527. package/src/gpu-driven/view-gpu.ts +181 -10
  528. package/src/gpu-scene.ts +259 -101
  529. package/src/hdrp-buffers.ts +4 -3
  530. package/src/ibl/skylight-bind-group.ts +49 -7
  531. package/src/index.ts +228 -9
  532. package/src/inspection-types.ts +222 -2
  533. package/src/instance-buffer-cache.ts +4 -4
  534. package/src/instances.ts +151 -492
  535. package/src/pbr-pipeline.ts +219 -13
  536. package/src/pipeline/single-layer-medium-passes.ts +267 -0
  537. package/src/pipeline/standard-forward-lane.ts +313 -57
  538. package/src/pipeline/standard-output/graph.ts +8 -2
  539. package/src/pipeline/standard-output/types.ts +12 -1
  540. package/src/pipeline/standard-pipeline.ts +291 -18
  541. package/src/pipeline/standard-post.ts +397 -102
  542. package/src/pipeline-builder.ts +1 -1
  543. package/src/pipeline-spec.ts +5 -5
  544. package/src/plugin.ts +4 -0
  545. package/src/points-lines/__tests__/points-lines.bench.ts +17 -8
  546. package/src/points-lines/__tests__/view-ubo.unit.test.ts +2 -1
  547. package/src/prepare/prepared-graphics-resolver.ts +2 -0
  548. package/src/record/__tests__/main-pass-geometry.unit.test.ts +30 -0
  549. package/src/record/__tests__/sprite-material-abi.unit.test.ts +1 -1
  550. package/src/record/__tests__/temporal-payload.unit.test.ts +3 -2
  551. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +11 -0
  552. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -0
  553. package/src/record/__tests__/view-ubo-layout.unit.test.ts +4 -3
  554. package/src/record/barrel-distortion-frame.ts +110 -0
  555. package/src/record/dynamic-geometry-consumption.ts +39 -9
  556. package/src/record/fold-buckets.ts +852 -0
  557. package/src/record/fold-instance-buffer.ts +51 -0
  558. package/src/record/frame-lighting.ts +36 -2
  559. package/src/record/frame-snapshot.ts +44 -4
  560. package/src/record/frame-targets.ts +10 -6
  561. package/src/record/frame.ts +373 -1099
  562. package/src/record/gpu-draw-receipts.ts +38 -0
  563. package/src/record/gpu-timing.ts +48 -5
  564. package/src/record/main-pass-geometry.ts +281 -83
  565. package/src/record/main-pass-material.ts +66 -4
  566. package/src/record/main-pass-sprite-draws.ts +13 -66
  567. package/src/record/main-pass.ts +156 -20
  568. package/src/record/mesh-ssbo.ts +43 -6
  569. package/src/record/motion-blur-receipt.ts +91 -0
  570. package/src/record/probe-blend-buffer.ts +180 -0
  571. package/src/record/recovery-pipeline.ts +35 -3
  572. package/src/record/render-context.ts +51 -31
  573. package/src/record/shadow-pass.ts +36 -2
  574. package/src/record/skybox-post-pass.ts +150 -200
  575. package/src/record/typed-frame-graph.ts +824 -44
  576. package/src/record/view-ubo.ts +26 -3
  577. package/src/record/volume-params.ts +225 -0
  578. package/src/recovery/render-system-candidate.ts +257 -52
  579. package/src/reflection/record-owner.ts +7 -3
  580. package/src/render-contract.ts +71 -8
  581. package/src/render-graph-primitives.ts +7 -1
  582. package/src/render-pipeline.ts +136 -1
  583. package/src/render-system-extract-tail.ts +72 -60
  584. package/src/render-system-extract.ts +117 -87
  585. package/src/render-system-presentation.ts +56 -0
  586. package/src/render-system-producer-roots.ts +96 -0
  587. package/src/render-system-residency.ts +159 -0
  588. package/src/render-system.ts +601 -482
  589. package/src/scene/probe-blend.ts +96 -4
  590. package/src/scene/render-scene.ts +431 -281
  591. package/src/ssao-buffers.ts +95 -2
  592. package/src/surface/__tests__/dynamic-input.unit.test.ts +374 -0
  593. package/src/surface/__tests__/gpu-driven.unit.test.ts +87 -0
  594. package/src/surface/dynamic-input.ts +658 -0
  595. package/src/surface/gpu-driven.ts +147 -0
  596. package/src/surface/submission-observation.ts +517 -0
  597. package/src/systems/skin-palette-allocator.ts +4 -1
  598. package/src/temporal/__tests__/temporal-frame-transaction.unit.test.ts +36 -0
  599. package/src/temporal/frame-coordinator.ts +8 -1
  600. package/src/temporal/frame.ts +20 -0
  601. package/src/temporal/gpu.ts +101 -17
  602. package/src/temporal/inspection.ts +7 -1
  603. package/src/temporal/standard-scene-data.ts +5 -0
  604. package/src/temporal/target-coverage-attachment.ts +4 -0
  605. package/src/temporal/view.ts +13 -2
  606. package/src/transmission/__tests__/single-layer-medium.unit.test.ts +149 -0
  607. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +12 -11
  608. package/src/transmission/single-layer-medium.ts +322 -0
  609. package/src/typed-render-graph-primitives.ts +191 -50
  610. package/src/volume/component.ts +10 -10
  611. package/src/volume/extract.ts +11 -5
  612. package/src/volume/inspection.ts +3 -0
  613. package/src/volume/passes.ts +95 -8
  614. package/src/volume/resources.ts +2 -1
  615. package/src/volume/temporal.ts +11 -0
  616. package/dist/chunk-4NNZQCL3.mjs.map +0 -1
  617. package/dist/chunk-6TFBXLRJ.mjs.map +0 -1
  618. package/dist/chunk-DRR7D3KX.mjs.map +0 -1
  619. package/dist/chunk-GWCEMDVR.mjs.map +0 -1
  620. package/dist/chunk-JCIRYQ4V.mjs.map +0 -1
  621. package/dist/chunk-JRIPCMYC.mjs.map +0 -1
  622. package/dist/chunk-LULCA3OF.mjs +0 -1713
  623. package/dist/chunk-LULCA3OF.mjs.map +0 -1
  624. package/dist/chunk-PA5DRJNI.mjs.map +0 -1
@@ -1,13 +1,23 @@
1
- import { collectGpuDrivenMaterialArtifacts } from '../gpu-driven/material-artifacts';
2
- import { materialBindingKey, projectMaterialBindingClasses } from '../gpu-driven/material-bindings';
1
+ import { encodeAnalyticFogParams } from '../environment/analytic-fog';
2
+ import type { FogFrame } from '../extract/environment';
3
+ import {
4
+ collectGpuDrivenMaterialArtifacts,
5
+ requiresProbeBlendRecord,
6
+ } from '../gpu-driven/material-artifacts';
7
+ import { projectMaterialBindingClasses } from '../gpu-driven/material-bindings';
8
+ import {
9
+ buildDispatchPlan,
10
+ cleanPerFrameCaches,
11
+ computeFoldBuckets,
12
+ validateRenderables,
13
+ } from './fold-buckets';
3
14
  import { uploadMaterialUniforms } from './material-uniforms';
4
15
  // @forgeax/engine-runtime - RenderSystem record stage: frame.
5
16
  // Extracted from render-system-record.ts (feat-20260704 M3/w17, pure move).
6
17
 
7
- import { HANDLE_NINESLICE_QUAD, resolveAssetHandle } from '@forgeax/engine-assets-runtime';
8
- import type { World } from '@forgeax/engine-ecs';
18
+ import { resolveAssetHandle } from '@forgeax/engine-assets-runtime';
19
+ import { Time, type World } from '@forgeax/engine-ecs';
9
20
  import type { RenderReadLease } from '@forgeax/engine-ecs/projection';
10
- import { deriveVertexCount } from '@forgeax/engine-geometry';
11
21
  import { frustum, mat4, vec3 } from '@forgeax/engine-math';
12
22
  import {
13
23
  type CurrentFrameObservationLease,
@@ -23,8 +33,12 @@ import {
23
33
  type TextureFormat,
24
34
  type TextureView,
25
35
  } from '@forgeax/engine-rhi';
26
- import type { MaterialRenderState, MeshAsset } from '@forgeax/engine-types';
36
+ import type { MaterialShaderArtifact } from '@forgeax/engine-shader';
37
+ import type { MeshAsset } from '@forgeax/engine-types';
27
38
  import { BUILTIN_BASE, handleSlot, toShared } from '@forgeax/engine-types';
39
+ import type { ExtractedCloudLayer } from '../cloud/extract';
40
+ import { cloudShadowResolutionForQuality } from '../cloud/parameters';
41
+ import { type CloudShadowProjection, createCloudShadowProjection } from '../cloud/shadow';
28
42
  import { CUBE_CAMERA_FACE_ORDER } from '../components/cube-camera';
29
43
  import type { DirectionalShadowQuality } from '../components/directional-shadow-filter';
30
44
  import type { MeshGpuHandles } from '../device/gpu-residency';
@@ -33,7 +47,7 @@ import {
33
47
  type ObservationUnavailableReason,
34
48
  RendererOperationError,
35
49
  } from '../errors/render';
36
- import { gpuDrivenDrawKey, gpuDrivenSourceDrawItemIndex } from '../extract/gpu-driven';
50
+ import { BARREL_DISTORTION_POST_PROCESS_ID } from '../features/barrel-distortion';
37
51
  import {
38
52
  DEPTH_OF_FIELD_MSAA_POST_PROCESS_IDS,
39
53
  DEPTH_OF_FIELD_POST_PROCESS_IDS,
@@ -44,15 +58,9 @@ import {
44
58
  } from '../features/depth-of-field/depth-of-field-params';
45
59
  import type { GpuDrivenProduction, PreparedGpuDrivenFrame } from '../gpu-driven/production-raster';
46
60
  import type { ShadowViewIdentity } from '../gpu-driven/shadow-views';
47
- import { GpuBuffer } from '../gpu-resource';
61
+ import type { GpuDrivenLodSubmitIdentity } from '../gpu-driven/view-gpu';
48
62
  import { GPU_TEXTURE_USAGE_COPY_SRC } from '../gpu-texture-usage';
49
- import {
50
- GPU_BUFFER_USAGE_COPY_DST,
51
- GPU_BUFFER_USAGE_MAP_READ,
52
- GPU_BUFFER_USAGE_UNIFORM,
53
- GPU_BUFFER_USAGE_VERTEX,
54
- } from '../gpu-usage';
55
- import { disposeTransientInstanceBuffers } from '../instance-buffer-cache';
63
+ import { GPU_BUFFER_USAGE_COPY_DST, GPU_BUFFER_USAGE_MAP_READ } from '../gpu-usage';
56
64
  import { deriveRenderExtent } from '../pipeline/render-extent';
57
65
  import { inspectStandardLighting } from '../pipeline/standard-lighting/inspection';
58
66
  import { standardLightingTopologySignature } from '../pipeline/standard-lighting/topology';
@@ -68,7 +76,6 @@ import type {
68
76
  DispatchEntry,
69
77
  ExtractedLights,
70
78
  ExtractedVolumetricFog,
71
- MaterialSnapshot,
72
79
  RenderableSnapshot,
73
80
  ShadowCasterMembership,
74
81
  SkyboxSnapshot,
@@ -86,14 +93,10 @@ import {
86
93
  type SsrSpatialAdmission,
87
94
  } from '../ssr/admission';
88
95
  import { createSsrHistoryOwner } from '../ssr/history';
89
- import {
90
- getTransparentSortConfig,
91
- TRANSPARENT_SORT_MODE_LAYER_Y,
92
- TRANSPARENT_SORT_MODE_LAYER_Z,
93
- } from '../systems/transparent-sort-config';
96
+ import type { SurfaceDynamicInputFrame } from '../surface/dynamic-input';
94
97
  import type { TransmissionDemand } from '../transmission/projection';
95
98
  import { hasVolumetricFogCapability } from '../volume/capability';
96
- import { inspectVolumetricFogResources, VOLUMETRIC_FOG_PARAMS_BYTES } from '../volume/resources';
99
+ import { inspectVolumetricFogResources } from '../volume/resources';
97
100
  import { resolveVolumeTemporalReset, type VolumeTemporalSignature } from '../volume/temporal';
98
101
  import {
99
102
  buildPerFrameBindGroups,
@@ -111,7 +114,6 @@ import {
111
114
  type DispatchCounts,
112
115
  type FrameObservationSource,
113
116
  getTextureIdentity,
114
- instanceCollectionCacheKey,
115
117
  makeZeroCameraFallbackSnapshot,
116
118
  type ReflectionFallbackReadbackRequest,
117
119
  type RenderFrameState,
@@ -119,28 +121,22 @@ import {
119
121
  validateGraphTargetCaptureReadback,
120
122
  worldEntityKey,
121
123
  } from './frame-snapshot';
122
- // feat-20260622-chunk-gpu-instancing-sprite-tilemap M1 / w4 (D-1 record-stage
123
- // fold operator). Pure linear-scan helper that groups transparent-dispatch
124
- // entries with equal (Layer.value, sortKey, materialHandle) into FoldBucket
125
- // descriptors. The drawIndexed swap (1 instanced draw per bucket vs N
126
- // per-entity draws) hooks into the sprite-pass dispatch loop below
127
- // (w4-record-swap), using {@link buildFoldDispatchPlan} to translate
128
- // renderableIndex-keyed buckets into validatedOrdered-index-keyed
129
- // head/skip maps.
130
- import {
131
- buildFoldDispatchPlan,
132
- evaluateFoldBucketUniformCap,
133
- type FoldBucket,
134
- type FoldDispatchPlan,
135
- foldDispatchBuckets,
136
- incrementFoldedDrawsMetric,
137
- } from './mesh-ssbo';
124
+ import { emitGpuDrivenDrawReceipts, type RenderableDrawReceipt } from './gpu-draw-receipts';
125
+ import { ensureProbeBlendRecordBuffer } from './probe-blend-buffer';
138
126
  import type {
139
127
  _InternalRenderPipelineContext,
140
128
  PipelineState,
141
129
  RenderSystemInternals,
142
130
  } from './render-context';
143
-
131
+ import { prepareVolumetricFogFrame, stageVolumetricFogParams } from './volume-params';
132
+
133
+ export {
134
+ buildDispatchPlan,
135
+ buildMaterialSlotPlan,
136
+ findRenderablePrefixForSlotCapacity,
137
+ materialSlotCountForPrefix,
138
+ validateRenderables,
139
+ } from './fold-buckets';
144
140
  export type { FrameObservationSource } from './frame-snapshot';
145
141
 
146
142
  export type FrameObservationReadback = (
@@ -155,35 +151,10 @@ export type FrameObservationReadback = (
155
151
  function residencyValidationCacheKey(world: World, worlds: readonly World[]): string {
156
152
  return [world.identity, ...worlds.map((candidate) => candidate.identity)].join('\u001f');
157
153
  }
158
-
159
154
  function renderableIdentity(snapshot: Pick<RenderableSnapshot, 'worldId' | 'entityKey'>): string {
160
155
  return `${snapshot.worldId}:${snapshot.entityKey}`;
161
156
  }
162
157
 
163
- /**
164
- * A GPU-owned indirect draw has no CPU submit callback. Keep the same
165
- * record-stage consumption proof for downstream lifecycle owners by checking
166
- * the concrete draw-key projection that was actually encoded for this frame.
167
- */
168
- function gpuDrivenRenderableConsumed(
169
- entry: ValidatedRenderable,
170
- drawKeys: ReadonlySet<string>,
171
- ): boolean {
172
- const draws = entry.source.gpuDrivenDraws;
173
- if (draws === undefined || draws.length === 0) return false;
174
- const worldEntity = worldEntityKey(entry.source.worldId, entry.source.entityKey);
175
- return draws.some((draw, compactIndex) => {
176
- const material = entry.source.materials[draw.materialSlot] ?? entry.source.material;
177
- return drawKeys.has(
178
- gpuDrivenDrawKey(
179
- worldEntity,
180
- material.materialHandle ?? -1,
181
- gpuDrivenSourceDrawItemIndex(draw, compactIndex),
182
- ),
183
- );
184
- });
185
- }
186
-
187
158
  /**
188
159
  * Project one shared validation result into a cull-specific renderable index
189
160
  * domain. The retained ShadowCaster projection is the superset; the primary
@@ -304,8 +275,20 @@ export async function observeCurrentFrame(
304
275
  }
305
276
 
306
277
  import { acknowledgeRenderFeatureSubmission } from '../features/host';
278
+ import {
279
+ effectiveMotionBlurSampleCount,
280
+ isMotionBlurIntervalValid,
281
+ MOTION_BLUR_PARAMS_BYTE_SIZE,
282
+ motionBlurExposureScale,
283
+ motionBlurSampleDelta,
284
+ motionBlurTemporalDemand,
285
+ } from '../features/motion-blur/motion-blur-params';
307
286
  import { createTemporalView, resolveTemporalReset, type TemporalView } from '../temporal/view';
308
287
  import { inspectVolumetricFog } from '../volume/inspection';
288
+ import {
289
+ packBarrelDistortionParams,
290
+ resolveSubmittedBarrelMapping,
291
+ } from './barrel-distortion-frame';
309
292
  import {
310
293
  acquireSwapChainTarget,
311
294
  graphExecutionPhase,
@@ -318,12 +301,11 @@ import {
318
301
  computeViewMatrix,
319
302
  driveLazyEquirectProjection,
320
303
  selectLazyEquirectHandle,
321
- variantSetFromDefines,
322
304
  warnMultiSkybox,
323
305
  warnMultiSkylight,
324
306
  } from './helpers';
325
307
  import { computeSplitLdrSprite } from './main-pass-sprite-draws';
326
- import { cleanPerEntityCache, ensureMeshSsboCapacity, uploadMeshSsboBatch } from './mesh-ssbo';
308
+ import { uploadMeshSsboBatch } from './mesh-ssbo';
327
309
  import { writeShadowCasterUniforms } from './shadow-pass';
328
310
  import {
329
311
  type CubeCaptureGraphState,
@@ -331,6 +313,7 @@ import {
331
313
  executeCompiledFrameGraph,
332
314
  getRenderFeatureGraphState,
333
315
  type RenderFeatureGraphCandidate,
316
+ settleBarrelDistortionGraphCandidate,
334
317
  settleDepthOfFieldGraphCandidate,
335
318
  settleVolumetricFogGraphCandidate,
336
319
  } from './typed-frame-graph';
@@ -394,110 +377,6 @@ export function gpuDrivenMeshesForFrame(
394
377
  return meshes;
395
378
  }
396
379
 
397
- /**
398
- * A frame-local material table. Repeated snapshot objects share one UBO slot;
399
- * each renderable keeps only the slot index used by each authored material.
400
- */
401
- export interface MaterialSlotPlan<T extends object> {
402
- readonly slotIndices: readonly (readonly number[])[];
403
- readonly slots: readonly T[];
404
- /** Index of the first renderable that referenced each slot. */
405
- readonly slotOwners: readonly number[];
406
- }
407
-
408
- /**
409
- * Intern identical material snapshots into one frame-local slot table.
410
- * Snapshot identity is the extract layer's invalidation token, so this keeps
411
- * the same correctness boundary while avoiding duplicate payload assembly and
412
- * upload for repeated scene instances.
413
- */
414
- export function buildMaterialSlotPlan<T extends object>(
415
- materialGroups: readonly (readonly T[])[],
416
- bindingClass?: (material: T, owner: number) => string,
417
- ): MaterialSlotPlan<T> {
418
- const slotByMaterial = new Map<T, Map<string, number>>();
419
- const slots: T[] = [];
420
- const slotOwners: number[] = [];
421
- const slotIndices = materialGroups.map((materials, ownerIndex) =>
422
- materials.map((material) => {
423
- const resourceClass = bindingClass?.(material, ownerIndex) ?? '';
424
- let materialClasses = slotByMaterial.get(material);
425
- if (materialClasses === undefined) {
426
- materialClasses = new Map();
427
- slotByMaterial.set(material, materialClasses);
428
- }
429
- const cached = materialClasses.get(resourceClass);
430
- if (cached !== undefined) return cached;
431
- const slot = slots.length;
432
- slots.push(material);
433
- slotOwners.push(ownerIndex);
434
- materialClasses.set(resourceClass, slot);
435
- return slot;
436
- }),
437
- );
438
- return { slotIndices, slots, slotOwners };
439
- }
440
-
441
- /**
442
- * Return the largest complete renderable prefix supported by a shared
443
- * mesh/material capacity. Mesh rows consume one slot per renderable; material
444
- * rows use the deduplicated slot indices assigned in first-seen order.
445
- */
446
- export function findRenderablePrefixForSlotCapacity(
447
- materialSlotIndices: readonly (readonly number[])[],
448
- slotCapacity: number,
449
- ): number {
450
- const capacity = Math.max(0, Math.floor(slotCapacity));
451
- const maxRenderableCount = Math.min(materialSlotIndices.length, capacity);
452
- let renderableCount = 0;
453
- while (renderableCount < maxRenderableCount) {
454
- const slots = materialSlotIndices[renderableCount] ?? [];
455
- if (slots.some((slot) => slot >= capacity)) break;
456
- renderableCount += 1;
457
- }
458
- return renderableCount;
459
- }
460
-
461
- /** Count material slots reachable from a prefix of a first-seen slot plan. */
462
- export function materialSlotCountForPrefix(
463
- materialSlotIndices: readonly (readonly number[])[],
464
- renderableCount: number,
465
- ): number {
466
- let maxSlot = -1;
467
- for (let index = 0; index < renderableCount; index += 1) {
468
- for (const slot of materialSlotIndices[index] ?? []) maxSlot = Math.max(maxSlot, slot);
469
- }
470
- return maxSlot + 1;
471
- }
472
-
473
- type MeshSsboCapacityResult = ReturnType<typeof ensureMeshSsboCapacity>;
474
-
475
- /**
476
- * Resolve the actual mesh-SSBO ceiling after the shared grow contract ran.
477
- * A missing grow controller is the legacy/test surface with no renderer-owned
478
- * ceiling; a failed grow explicitly exposes the pre-grow capacity and must
479
- * constrain every row written by this frame, including retained shadow rows.
480
- */
481
- function meshSsboSlotCapacityAfterEnsure(
482
- internals: Pick<RenderSystemInternals, 'growMeshSsbo' | 'meshSsboState'>,
483
- neededSlots: number,
484
- result: MeshSsboCapacityResult,
485
- ): number {
486
- if (internals.growMeshSsbo === undefined) return Number.POSITIVE_INFINITY;
487
- if (!result.ok) return Math.max(0, result.degradedToSlotCount);
488
- return internals.meshSsboState?.slotCount ?? neededSlots;
489
- }
490
-
491
- /**
492
- * Keep a row sequence inside the capacity published by the mesh-SSBO owner.
493
- * The caller has already reserved earlier rows (the primary view), so this
494
- * helper only accepts the remaining slots for the retained shadow sequence.
495
- */
496
- function limitRowsToMeshSsboCapacity<T>(rows: readonly T[], availableSlots: number): readonly T[] {
497
- if (availableSlots === Number.POSITIVE_INFINITY) return rows;
498
- return rows.slice(0, Math.max(0, Math.floor(availableSlots)));
499
- }
500
-
501
380
  export type RecordProfileRunner = <T>(phase: RenderRecordPhase, action: () => T) => T;
502
381
 
503
382
  export interface CubeCaptureFrameInput {
@@ -727,46 +606,6 @@ async function mapReflectionFallbackReadback(
727
606
 
728
607
  const VOLUMETRIC_FOG_PASS_COUNT = 4;
729
608
 
730
- type VolumetricFogParamsSlot = 0 | 1;
731
-
732
- function nextVolumetricFogParamsSlot(frameState: RenderFrameState): VolumetricFogParamsSlot {
733
- const accepted = frameState.volumetricFogParamsAcceptedSlot;
734
- const pending = frameState.volumetricFogParamsPendingSlot;
735
- if (pending !== null && pending !== accepted) return pending;
736
- return accepted === null ? 0 : accepted === 0 ? 1 : 0;
737
- }
738
-
739
- function stageVolumetricFogParams(
740
- internals: RenderSystemInternals,
741
- frameState: RenderFrameState,
742
- params: Float32Array,
743
- ): Buffer | undefined {
744
- const slot = nextVolumetricFogParamsSlot(frameState);
745
- let buffer = frameState.volumetricFogParamsBuffers[slot];
746
- if (buffer === null) {
747
- const created = internals.device.createBuffer({
748
- label: `volumetric-fog-params-${slot}`,
749
- size: VOLUMETRIC_FOG_PARAMS_BYTES,
750
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
751
- mappedAtCreation: false,
752
- });
753
- if (!created.ok) {
754
- internals.errorRegistry.fire(created.error);
755
- return undefined;
756
- }
757
- buffer = created.value;
758
- frameState.volumetricFogParamsBuffers[slot] = buffer;
759
- }
760
- const written = internals.device.queue.writeBuffer(buffer, 0, params);
761
- if (!written.ok) {
762
- internals.errorRegistry.fire(written.error);
763
- return undefined;
764
- }
765
- frameState.volumetricFogParamsPendingSlot = slot;
766
- frameState.volumetricFogPendingParams = new Float32Array(params);
767
- return buffer;
768
- }
769
-
770
609
  /** Retire parameter buffers only after the submit that stopped using them. */
771
610
  function retireVolumetricFogParams(
772
611
  internals: RenderSystemInternals,
@@ -803,6 +642,19 @@ function volumeRevision(values: readonly number[]): number {
803
642
  return hash;
804
643
  }
805
644
 
645
+ /** Resolve the stable world-space projection shared by cloud and lighting. */
646
+ function cloudShadowProjectionFor(
647
+ cloudLayer: ExtractedCloudLayer | undefined,
648
+ ): CloudShadowProjection | undefined {
649
+ if (cloudLayer?.sunDirection === undefined) return undefined;
650
+ return createCloudShadowProjection({
651
+ center: [0, 0, 0],
652
+ sunDirection: cloudLayer.sunDirection,
653
+ range: cloudLayer.params.shadowRange,
654
+ resolution: cloudShadowResolutionForQuality(cloudLayer.params.quality),
655
+ });
656
+ }
657
+
806
658
  function volumeTemporalSignature(
807
659
  fog: ExtractedVolumetricFog,
808
660
  prepared: VolumetricFogFrameContext,
@@ -824,7 +676,7 @@ function volumeTemporalSignature(
824
676
  camera.orthoBottom,
825
677
  camera.orthoTop,
826
678
  ]);
827
- const fogRevision =
679
+ let fogRevision =
828
680
  authored === undefined
829
681
  ? 0
830
682
  : volumeRevision([
@@ -835,6 +687,8 @@ function volumeTemporalSignature(
835
687
  ...authored.emission,
836
688
  authored.anisotropy,
837
689
  authored.maxDistance,
690
+ authored.sampling === 'density' ? 1 : 0,
691
+ ...Array.from(`${fog.guid}:${fog.digest}`, (char) => char.charCodeAt(0)),
838
692
  ]);
839
693
  const selectedSpot =
840
694
  fog.spotLightEntity === undefined
@@ -852,7 +706,7 @@ function volumeTemporalSignature(
852
706
  fog.lightKind === 'directional' && directional?.entity === fog.lightEntity
853
707
  ? directional
854
708
  : undefined;
855
- const lightRevision =
709
+ let lightRevision =
856
710
  selectedSpot === undefined && selectedPoint === undefined && selectedDirectional === undefined
857
711
  ? 0
858
712
  : volumeRevision(
@@ -891,12 +745,30 @@ function volumeTemporalSignature(
891
745
  selectedSpot.normalBias ?? 0.05,
892
746
  selectedSpot.pcfKernelSize ?? 3,
893
747
  selectedSpot.shadowAtlasTile,
748
+ Number(selectedSpot.projectorHandle ?? 0),
749
+ selectedSpot.projectorGeneration ?? 0,
750
+ selectedSpot.projectorRevision ?? 0,
894
751
  ...(selectedSpot.lightViewProj === undefined
895
752
  ? []
896
753
  : [...selectedSpot.lightViewProj]),
897
754
  ]),
898
755
  ],
899
756
  );
757
+ for (const [index, member] of (fog.additional ?? []).entries()) {
758
+ const signature = volumeTemporalSignature(
759
+ member,
760
+ {
761
+ ...prepared,
762
+ densityGeneration: prepared.additional?.[index]?.densityGeneration ?? 0,
763
+ },
764
+ camera,
765
+ lights,
766
+ width,
767
+ height,
768
+ );
769
+ fogRevision = volumeRevision([fogRevision, signature.fogRevision, signature.densityGeneration]);
770
+ lightRevision = volumeRevision([lightRevision, signature.lightRevision]);
771
+ }
900
772
  return {
901
773
  cameraRevision,
902
774
  fogRevision,
@@ -904,6 +776,7 @@ function volumeTemporalSignature(
904
776
  densityGeneration: prepared.densityGeneration,
905
777
  width,
906
778
  height,
779
+ ...(fog.worldTimeSeconds === undefined ? {} : { worldTimeSeconds: fog.worldTimeSeconds }),
907
780
  ...(fog.lightKind === undefined ? {} : { lightKind: fog.lightKind }),
908
781
  ...(fog.lightEntity === undefined ? {} : { lightEntity: fog.lightEntity }),
909
782
  ...(fog.pointLightEntity === undefined ? {} : { pointLightEntity: fog.pointLightEntity }),
@@ -957,135 +830,6 @@ function resolveVolumetricFogHistoryContext(
957
830
  };
958
831
  }
959
832
 
960
- /**
961
- * Resolve the authored density POD into the renderer-owned GPU resources used
962
- * by the graph. This is deliberately a pull operation: a missing/stale
963
- * handle or failed upload produces a structured error and leaves the graph
964
- * candidate disabled for this frame rather than binding a synthetic density.
965
- */
966
- function prepareVolumetricFogFrame(
967
- internals: RenderSystemInternals,
968
- frameState: RenderFrameState,
969
- world: World,
970
- worlds: readonly World[],
971
- fog: ExtractedVolumetricFog | undefined,
972
- lights: ExtractedLights,
973
- volumeCapability: boolean,
974
- projector: SpotLightProjectorFrameContext | undefined,
975
- ): VolumetricFogFrameContext | undefined {
976
- if (
977
- !volumeCapability ||
978
- fog?.status !== 'available' ||
979
- fog.fog === undefined ||
980
- fog.densityHandle === undefined ||
981
- fog.densityAsset === undefined ||
982
- (fog.lightKind === 'spot'
983
- ? lights.spot.find((light) => light.entity === fog.lightEntity) === undefined
984
- : fog.lightKind === 'point'
985
- ? lights.point.find((light) => light.entity === fog.lightEntity) === undefined
986
- : lights.directional === undefined)
987
- ) {
988
- return undefined;
989
- }
990
- const ownerWorld = worlds[fog.worldId ?? 0] ?? world;
991
- const resident = internals.gpuStore.ensureResident(
992
- fog.densityHandle,
993
- fog.densityAsset,
994
- ownerWorld,
995
- );
996
- if (!resident.ok || !('texture' in resident.value)) {
997
- if (!resident.ok) {
998
- internals.errorRegistry.fire(
999
- resident.error instanceof RhiError
1000
- ? resident.error
1001
- : new RhiError({
1002
- code: 'webgpu-runtime-error',
1003
- expected: 'volumetric density residency to resolve a GPU texture',
1004
- hint: 'repair the authored TextureAsset payload and retry the next frame',
1005
- detail: { error: resident.error },
1006
- }),
1007
- );
1008
- }
1009
- return undefined;
1010
- }
1011
- const params = new Float32Array(32);
1012
- params.set(fog.fog.bounds.min, 0);
1013
- params.set(fog.fog.bounds.max, 4);
1014
- params.set(fog.fog.extinction, 8);
1015
- params.set(fog.fog.albedo, 12);
1016
- params.set(fog.fog.emission, 16);
1017
- const selectedPoint =
1018
- fog.pointLightEntity === undefined
1019
- ? undefined
1020
- : lights.point.find((light) => light.entity === fog.pointLightEntity);
1021
- const selectedSpot =
1022
- fog.spotLightEntity === undefined
1023
- ? fog.lightKind === 'spot'
1024
- ? lights.spot.find((light) => light.entity === fog.lightEntity)
1025
- : undefined
1026
- : lights.spot.find((light) => light.entity === fog.spotLightEntity);
1027
- const lightDirection =
1028
- selectedSpot?.direction ?? lights.directional?.direction ?? vec3.create(0, -1, 0);
1029
- const lightColor = selectedSpot?.color ?? lights.directional?.color ?? vec3.create(0, 0, 0);
1030
- if (lightDirection === undefined || lightColor === undefined) return undefined;
1031
- params.set(lightDirection, 20);
1032
- // Reuse the vec4 padding lane as the renderer-owned frame phase for
1033
- // sub-froxel jitter. The temporal resolver consumes it without growing the
1034
- // public fog component or allocating another uniform buffer.
1035
- params[23] = frameState.frameNumber % 1024;
1036
- const selectedPointIndex =
1037
- selectedPoint === undefined
1038
- ? -1
1039
- : lights.point.findIndex((light) => light.entity === selectedPoint.entity);
1040
- const selectedSpotIndex =
1041
- selectedSpot === undefined
1042
- ? -1
1043
- : lights.spot.findIndex((light) => light.entity === selectedSpot.entity);
1044
- // Cluster light_data is one point/spot/rect array. Point slots are emitted
1045
- // first, so volume stores the global slot rather than a per-kind array index.
1046
- const selectedSpotClusterIndex =
1047
- selectedSpotIndex < 0 ? -1 : lights.point.length + selectedSpotIndex;
1048
- params.set(lightColor, 24);
1049
- params[19] = selectedPointIndex;
1050
- params[27] = selectedSpotClusterIndex;
1051
- params[28] = fog.fog.maxDistance;
1052
- params[29] = fog.fog.anisotropy;
1053
- params[30] =
1054
- selectedPoint !== undefined && selectedSpot !== undefined
1055
- ? 3
1056
- : selectedPoint !== undefined
1057
- ? 1
1058
- : selectedSpot !== undefined
1059
- ? 2
1060
- : 0;
1061
- // The temporal shader reads this lane after the record stage resolves the
1062
- // accepted graph/slot. It is overwritten on the next queue-visible frame;
1063
- // the first candidate deliberately starts with history disabled.
1064
- params[31] =
1065
- frameState.temporalFrame !== undefined && frameState.temporalFrame.resetReason === 'none'
1066
- ? 1
1067
- : 0;
1068
-
1069
- const paramsBuffer = stageVolumetricFogParams(internals, frameState, params);
1070
- if (paramsBuffer === undefined) return undefined;
1071
- return {
1072
- densityTexture: resident.value.texture.handle,
1073
- densityView: resident.value.view,
1074
- paramsBuffer,
1075
- densityGeneration: resident.value.receipt.generation,
1076
- historyReadSlot: null,
1077
- historyWriteSlot: 0,
1078
- historyValid: false,
1079
- ...(projector === undefined
1080
- ? {}
1081
- : {
1082
- projectorTexture: projector.texture,
1083
- projectorView: projector.view,
1084
- projectorSampler: projector.sampler,
1085
- }),
1086
- };
1087
- }
1088
-
1089
833
  /**
1090
834
  * Resolve the SpotLight-owned cookie independently of volumetric fog. Surface
1091
835
  * lighting must keep the authored projector when the fog component is absent;
@@ -1219,6 +963,8 @@ function mapSsrHistoryResetReason(
1219
963
  case 'environment-change':
1220
964
  case 'fog-change':
1221
965
  return 'reflection-generation';
966
+ case 'time-discontinuity':
967
+ return 'camera-cut';
1222
968
  case 'device-recover':
1223
969
  return 'device-recovery';
1224
970
  case 'submit-failure':
@@ -1227,6 +973,50 @@ function mapSsrHistoryResetReason(
1227
973
  }
1228
974
  }
1229
975
 
976
+ interface FrameClockSample {
977
+ readonly deltaSeconds: number;
978
+ readonly elapsedSeconds: number | undefined;
979
+ /** Explicit host/replay clock was present but invalid. */
980
+ readonly invalidSampleTime: boolean;
981
+ }
982
+
983
+ /** Read the render sample clock from the camera's composition world once per frame. */
984
+ function frameClockSample(
985
+ world: World,
986
+ worlds: readonly World[],
987
+ camera: CameraSnapshot,
988
+ sampleTimeSeconds?: number,
989
+ ): FrameClockSample {
990
+ const owner = worlds[camera.worldId ?? 0] ?? world;
991
+ const invalidSampleTime = sampleTimeSeconds !== undefined && !Number.isFinite(sampleTimeSeconds);
992
+ try {
993
+ const clock = owner.getResource(Time);
994
+ return {
995
+ deltaSeconds: Number.isFinite(clock.delta) ? Math.max(0, clock.delta) : 0,
996
+ // Keep the raw host sample even when ECS accepted only a clamped
997
+ // simulation delta. Temporal admission compares this value with the
998
+ // last successfully submitted sample and therefore sees real hitches.
999
+ elapsedSeconds:
1000
+ sampleTimeSeconds === undefined
1001
+ ? Number.isFinite(clock.elapsed)
1002
+ ? clock.elapsed
1003
+ : undefined
1004
+ : Number.isFinite(sampleTimeSeconds)
1005
+ ? sampleTimeSeconds
1006
+ : undefined,
1007
+ invalidSampleTime,
1008
+ };
1009
+ } catch {
1010
+ // Test and recovery callers may provide a minimal World-shaped owner. An
1011
+ // absent clock is an invalid temporal pair and therefore safely resets.
1012
+ return {
1013
+ deltaSeconds: 0,
1014
+ elapsedSeconds: Number.isFinite(sampleTimeSeconds) ? sampleTimeSeconds : undefined,
1015
+ invalidSampleTime,
1016
+ };
1017
+ }
1018
+ }
1019
+
1230
1020
  function stageSsrHistoryCandidate(
1231
1021
  internals: RenderSystemInternals,
1232
1022
  frameState: RenderFrameState,
@@ -1611,7 +1401,12 @@ export function recordFrame(
1611
1401
  readonly activeEntityKeys?: ReadonlySet<number>;
1612
1402
  readonly activeEntityRevision?: number;
1613
1403
  readonly telemetryCandidateCount?: number;
1404
+ readonly telemetrySubmit?: GpuDrivenLodSubmitIdentity;
1614
1405
  readonly occlusion?: OcclusionFrameProjection;
1406
+ readonly frameTime?: number;
1407
+ readonly deviceGeneration?: number;
1408
+ readonly surfaceDynamicInput?: SurfaceDynamicInputFrame;
1409
+ readonly geometryLane?: 'automatic' | 'direct';
1615
1410
  },
1616
1411
  renderReadLeases?: readonly RenderReadLease[],
1617
1412
  featureGraphCandidate?: RenderFeatureGraphCandidate,
@@ -1630,7 +1425,15 @@ export function recordFrame(
1630
1425
  shadowCasterWorldKeys?: readonly number[],
1631
1426
  preparedLighting?: ReturnType<typeof prepareFrameLighting>,
1632
1427
  ssrDependencies?: SsrDependenciesInspection,
1633
- onRenderableDraw?: (entry: ValidatedRenderable) => void,
1428
+ onRenderableDraw?: (
1429
+ entry: ValidatedRenderable,
1430
+ submeshIndex?: number,
1431
+ receipt?: RenderableDrawReceipt,
1432
+ ) => void,
1433
+ analyticFog?: FogFrame,
1434
+ sampleTimeSeconds?: number,
1435
+ temporalReset = false,
1436
+ cloudLayer?: ExtractedCloudLayer,
1634
1437
  ): boolean {
1635
1438
  void shadowCasterEntityKeys;
1636
1439
  frameState.reflectionFallbackObservationSource = undefined;
@@ -1841,6 +1644,7 @@ export function recordFrame(
1841
1644
  lights,
1842
1645
  volumetricFog,
1843
1646
  );
1647
+ const cloudShadowProjection = cloudShadowProjectionFor(cloudLayer);
1844
1648
 
1845
1649
  const preparedVolumetricFog = prepareVolumetricFogFrame(
1846
1650
  internals,
@@ -1887,21 +1691,29 @@ export function recordFrame(
1887
1691
  (volumetricFog?.status === 'degraded' && acceptedFallback !== undefined);
1888
1692
 
1889
1693
  let preparedGpuDriven: PreparedGpuDrivenFrame | undefined;
1694
+ let acceptedMaterialArtifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined;
1695
+ let acceptedShadowMaterialArtifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined;
1890
1696
  let materialBindingClasses: ReadonlyMap<string, string> | undefined;
1891
1697
  const abortPreparedGpuDriven = (): void => {
1892
1698
  preparedGpuDriven?._abortResourceReplacement?.();
1893
1699
  };
1894
1700
  const cubeCaptureWork = prepareCubeCapture(internals, cubeCapture, camera);
1895
- const reflectionFallbackCandidate = cubeCapture?.reflectionProbes?.fallbackDemand === true;
1701
+ const reflectionFallbackCandidate =
1702
+ cubeCapture?.reflectionProbes?.fallbackDemand === true ||
1703
+ (ssrDependencies?.requested === true && ssrDependencies.reflectionFallback !== undefined);
1704
+ frameState.surfaceSubmissionObservation?.setActualLaneReason(
1705
+ reflectionFallbackCandidate ? 'reflection-fallback-mrt' : undefined,
1706
+ );
1707
+ // The Standard PBR GPU raster owns one color target. Reflection fallback
1708
+ // adds an MRT attachment, so keep this frame on the ordinary CPU producer
1709
+ // instead of claiming rows that the one-target GPU pipeline cannot draw.
1710
+ const frameGpuDriven = reflectionFallbackCandidate ? undefined : gpuDriven;
1896
1711
  // Capture views have independent draw ownership; display-camera GPU work
1897
1712
  // remains active while their faces and SSR fallback attachments are recorded.
1898
- // ProbeBlendRecord is an object-level fragment ABI. The scene-index GPU
1899
- // lane has no binding for that record yet, so claiming a probe-bearing
1900
- // row would silently fall back to Skylight and make local diffuse probes
1901
- // visually ineffective. Keep the whole frame on the CPU producer until
1902
- // the scene-index lane carries the same record contract.
1903
- const probeBlendFrame = renderables.some((entry) => entry.probeBlendRecord !== undefined);
1904
- const frameGpuDriven = probeBlendFrame ? undefined : gpuDriven;
1713
+ // ProbeBlendRecord is a shared object-level fragment ABI. The prepared
1714
+ // GPU owner receives the renderer-owned record buffer below and binds it
1715
+ // at the same slot used by the direct material path, so probe-bearing rows
1716
+ // remain eligible for the GPU lane.
1905
1717
 
1906
1718
  // Validate first so user meshes are resident in the same frame. The GPU
1907
1719
  // owner consumes this producer-owned map together with the builtin mesh
@@ -2026,16 +1838,37 @@ export function recordFrame(
2026
1838
  frameState.installedPipelineConfig?.ssao?.enabled === true),
2027
1839
  reflectionFallback: frameState.reflectionFallbackDemand === true,
2028
1840
  });
1841
+ acceptedMaterialArtifacts = materialArtifacts;
1842
+ acceptedShadowMaterialArtifacts = shadowMaterialArtifacts;
2029
1843
  const paletteUploadBytes = validatedForResidency.reduce(
2030
1844
  (total, row) => total + (row.source.skin?.uploadBytes ?? 0),
2031
1845
  0,
2032
1846
  );
1847
+ const gpuProbeBlendRequested = [...materialArtifacts.values()].some((artifact) =>
1848
+ requiresProbeBlendRecord(artifact),
1849
+ );
1850
+ const gpuProbeBlendBuffer = gpuProbeBlendRequested
1851
+ ? ensureProbeBlendRecordBuffer(
1852
+ internals.device,
1853
+ frameState,
1854
+ frameGpuDriven.scene?.probeBlend,
1855
+ )
1856
+ : undefined;
2033
1857
  const prepared = frameGpuDriven.owner.prepare({
2034
1858
  scene: frameGpuDriven.scene,
2035
1859
  camera,
2036
1860
  meshBySlot: gpuDrivenMeshes,
2037
1861
  viewBindGroupLayout: pipelineState.viewBindGroupLayout,
2038
1862
  meshResidencyEpoch: internals.gpuStore.meshResidencyEpoch,
1863
+ // `hdrp` remains a compatibility stop for callers that cannot provide
1864
+ // the clustered topology. The production frame supplies the explicit
1865
+ // Standard topology below, so the GPU owner can select a matching
1866
+ // published group(2) artifact and bind the existing cluster owner.
1867
+ hdrp:
1868
+ standard.kind === 'clustered' &&
1869
+ (standard.prepared.local.length > 0 ||
1870
+ frameState.installedPipelineConfig?.ssao?.enabled === true),
1871
+ clustered: standard.kind === 'clustered',
2039
1872
  materialBindingClasses,
2040
1873
  ...(frameGpuDriven.activeEntityKeys === undefined
2041
1874
  ? {}
@@ -2046,7 +1879,17 @@ export function recordFrame(
2046
1879
  ...(frameGpuDriven.telemetryCandidateCount === undefined
2047
1880
  ? {}
2048
1881
  : { telemetryCandidateCount: frameGpuDriven.telemetryCandidateCount }),
1882
+ ...(frameGpuDriven.telemetrySubmit === undefined
1883
+ ? {}
1884
+ : { telemetrySubmit: frameGpuDriven.telemetrySubmit }),
2049
1885
  ...(frameGpuDriven.occlusion === undefined ? {} : { occlusion: frameGpuDriven.occlusion }),
1886
+ ...(frameGpuDriven.frameTime === undefined ? {} : { frameTime: frameGpuDriven.frameTime }),
1887
+ ...(frameGpuDriven.deviceGeneration === undefined
1888
+ ? {}
1889
+ : { deviceGeneration: frameGpuDriven.deviceGeneration }),
1890
+ ...(frameGpuDriven.surfaceDynamicInput === undefined
1891
+ ? {}
1892
+ : { surfaceDynamicInput: frameGpuDriven.surfaceDynamicInput }),
2050
1893
  ...(frameGpuDriven.scene?.structureMetrics === undefined
2051
1894
  ? {}
2052
1895
  : {
@@ -2059,6 +1902,9 @@ export function recordFrame(
2059
1902
  ...(firstSkinArtifact === undefined ? {} : { materialSkinArtifact: firstSkinArtifact }),
2060
1903
  ...(materialArtifacts.size === 0 ? {} : { materialArtifacts }),
2061
1904
  ...(shadowMaterialArtifacts.size === 0 ? {} : { shadowMaterialArtifacts }),
1905
+ ...(gpuProbeBlendBuffer === undefined
1906
+ ? {}
1907
+ : { probeBlendRecordBuffer: gpuProbeBlendBuffer }),
2062
1908
  materialPipelineState: pipelineState,
2063
1909
  ...(internals.getMaterialShaderPipelineEntry === undefined
2064
1910
  ? {}
@@ -2119,7 +1965,12 @@ export function recordFrame(
2119
1965
  }
2120
1966
  }
2121
1967
 
2122
- const gpuOwnedDrawKeys = preparedGpuDriven?.drawKeys ?? EMPTY_DRAW_KEYS;
1968
+ // Direct acceptance keeps producer preparation/residency active so its
1969
+ // page/frame/shared-time candidate is validated and committed through the
1970
+ // same submit hooks. Only raster projection changes lanes.
1971
+ const rasterGpuDriven =
1972
+ frameGpuDriven?.geometryLane === 'direct' ? undefined : preparedGpuDriven;
1973
+ const gpuOwnedDrawKeys = rasterGpuDriven?.drawKeys ?? EMPTY_DRAW_KEYS;
2123
1974
  // The GPU lane owns only the primary opaque raster today. Directional,
2124
1975
  // point, and spot shadow views consume the same CPU-validated rows; they do
2125
1976
  // not require a second residency walk because the first validation already
@@ -2136,6 +1987,7 @@ export function recordFrame(
2136
1987
  validated,
2137
1988
  gpuOwnedDrawKeys,
2138
1989
  validationInput.cacheKey,
1990
+ rasterGpuDriven?.worldKeys,
2139
1991
  );
2140
1992
  const cacheValidated =
2141
1993
  validatedShadowForResidency === undefined
@@ -2178,7 +2030,7 @@ export function recordFrame(
2178
2030
  Math.max(1, internals.canvas.width),
2179
2031
  Math.max(1, internals.canvas.height),
2180
2032
  graphShadowMapSize,
2181
- preparedGpuDriven,
2033
+ rasterGpuDriven,
2182
2034
  featureGraphCandidate,
2183
2035
  cubeCapture?.state,
2184
2036
  cameras.length === 0,
@@ -2190,6 +2042,13 @@ export function recordFrame(
2190
2042
  undefined,
2191
2043
  ssrAdmission,
2192
2044
  renderExtent,
2045
+ validated.some((entry) =>
2046
+ entry.source.materials.some(
2047
+ (material) => material.surfaceModel === 'single-layer-medium',
2048
+ ),
2049
+ ),
2050
+ analyticFog !== undefined && analyticFog.density > 0 && analyticFog.maxOpacity > 0,
2051
+ cloudShadowProjection?.resolution,
2193
2052
  ),
2194
2053
  );
2195
2054
  if (preflightGraph === null) {
@@ -2266,8 +2125,47 @@ export function recordFrame(
2266
2125
  const view = swapTarget.view;
2267
2126
  const targetW = swapTarget.targetW;
2268
2127
  const targetH = swapTarget.targetH;
2128
+ const submittedBarrelMapping = resolveSubmittedBarrelMapping(
2129
+ frameState,
2130
+ preflightGraph,
2131
+ camera,
2132
+ targetW,
2133
+ targetH,
2134
+ );
2269
2135
 
2270
2136
  const previousTemporalView = frameState.lastSuccessfulTemporalView;
2137
+ const motionBlurDemand =
2138
+ camera.motionBlur === undefined
2139
+ ? false
2140
+ : motionBlurTemporalDemand({
2141
+ shutterAngle: camera.motionBlur.shutterAngle,
2142
+ maxRadiusPixels: camera.motionBlur.maxRadiusPixels,
2143
+ sampleCount: camera.motionBlur.sampleCount,
2144
+ targetFps: camera.motionBlur.targetFps ?? 60,
2145
+ });
2146
+ const temporalDemand = camera.antialias === 'taa' || motionBlurDemand;
2147
+ const clock = frameClockSample(world, worlds, camera, sampleTimeSeconds);
2148
+ const deltaSeconds =
2149
+ temporalReset || clock.invalidSampleTime
2150
+ ? Number.NaN
2151
+ : motionBlurSampleDelta(
2152
+ clock.elapsedSeconds,
2153
+ previousTemporalView?.sampleTimeSeconds,
2154
+ clock.deltaSeconds,
2155
+ );
2156
+ const intervalValid = isMotionBlurIntervalValid(deltaSeconds);
2157
+ // An off MB-only frame still carries an unjittered temporal view so the
2158
+ // accepted sample clock remains observable. Its history flag is false,
2159
+ // however, and must form a new baseline when the effect is enabled again.
2160
+ // A reset frame already has an explicit reason and is the valid baseline
2161
+ // for the following pair, so it is deliberately excluded here.
2162
+ const motionBlurFirstFrame =
2163
+ motionBlurDemand &&
2164
+ camera.antialias !== 'taa' &&
2165
+ (previousTemporalView === undefined ||
2166
+ (previousTemporalView.mode === 'off' &&
2167
+ !previousTemporalView.historyValid &&
2168
+ previousTemporalView.resetReason === undefined));
2271
2169
  const currentUnjitteredViewProjection = mat4.multiply(
2272
2170
  mat4.create(),
2273
2171
  computeProjectionMatrix(camera),
@@ -2294,19 +2192,32 @@ export function recordFrame(
2294
2192
  internalHeight: renderExtent.internalHeight,
2295
2193
  }),
2296
2194
  ...(camera.historyVersion === undefined ? {} : { historyVersion: camera.historyVersion }),
2195
+ ...(clock.elapsedSeconds === undefined ? {} : { sampleTimeSeconds: clock.elapsedSeconds }),
2297
2196
  historyValid: false,
2298
2197
  currentUnjitteredViewProjection,
2299
2198
  currentCameraPosition,
2300
2199
  });
2301
2200
  const resetReason =
2302
- camera.antialias === 'taa' && environmentReady
2303
- ? resolveTemporalReset(previousTemporalView, temporalCandidate)
2201
+ temporalDemand && environmentReady
2202
+ ? (resolveTemporalReset(previousTemporalView, temporalCandidate) ??
2203
+ (temporalReset || (motionBlurDemand && !intervalValid)
2204
+ ? 'time-discontinuity'
2205
+ : motionBlurFirstFrame
2206
+ ? 'first-frame'
2207
+ : undefined))
2304
2208
  : undefined;
2305
2209
  const canReusePrevious = previousTemporalView !== undefined && resetReason === undefined;
2306
2210
  const temporalView = createTemporalView({
2307
2211
  ...temporalCandidate.input,
2308
2212
  frameIndex: resetReason === undefined ? temporalCandidate.input.frameIndex : 0,
2309
- historyValid: camera.antialias === 'taa' && environmentReady && canReusePrevious,
2213
+ // Motion Blur needs a finite positive pair before it can read the
2214
+ // baseline. TAA-only frames retain their existing temporal contract and
2215
+ // do not depend on the simulation clock being present in a test host.
2216
+ historyValid:
2217
+ temporalDemand &&
2218
+ environmentReady &&
2219
+ canReusePrevious &&
2220
+ (!motionBlurDemand || intervalValid),
2310
2221
  resetReason,
2311
2222
  ...(canReusePrevious && previousTemporalView.currentUnjitteredViewProjection !== undefined
2312
2223
  ? {
@@ -2357,17 +2268,26 @@ export function recordFrame(
2357
2268
  );
2358
2269
  activeVolumetricFogContext = resolvedHistory.context;
2359
2270
  activeVolumetricFogSignature = resolvedHistory.signature;
2360
- const historyFlag = new Float32Array([activeVolumetricFogContext.historyValid ? 1 : 0]);
2271
+ const previousTime = frameState.volumetricFogHistorySignature?.worldTimeSeconds;
2272
+ const currentTime = resolvedHistory.signature.worldTimeSeconds;
2273
+ // Adjacent UBO lanes: validity, World time, elapsed time since accepted history.
2274
+ const historyParams = new Float32Array([
2275
+ activeVolumetricFogContext.historyValid ? 1 : 0,
2276
+ currentTime ?? 0,
2277
+ previousTime === undefined || currentTime === undefined
2278
+ ? 0
2279
+ : Math.max(0, currentTime - previousTime),
2280
+ ]);
2361
2281
  if (frameState.volumetricFogPendingParams !== undefined) {
2362
- frameState.volumetricFogPendingParams[31] = historyFlag[0] ?? 0;
2282
+ frameState.volumetricFogPendingParams.set(historyParams, 31);
2363
2283
  }
2364
- const historyFlagWritten = internals.device.queue.writeBuffer(
2284
+ const historyParamsWritten = internals.device.queue.writeBuffer(
2365
2285
  activeVolumetricFogContext.paramsBuffer,
2366
2286
  31 * Float32Array.BYTES_PER_ELEMENT,
2367
- historyFlag,
2287
+ historyParams,
2368
2288
  );
2369
- if (!historyFlagWritten.ok) {
2370
- internals.errorRegistry.fire(historyFlagWritten.error);
2289
+ if (!historyParamsWritten.ok) {
2290
+ internals.errorRegistry.fire(historyParamsWritten.error);
2371
2291
  abortPreparedGpuDriven();
2372
2292
  return false;
2373
2293
  }
@@ -2520,6 +2440,8 @@ export function recordFrame(
2520
2440
  // View UBO + CSM/spot-shadow matrix pack: assembled + uploaded in one
2521
2441
  // queue.writeBuffer round-trip. Extracted to view-ubo.ts (M3/w18) so
2522
2442
  // recordFrame stays an orchestration skeleton (D-2).
2443
+ // The stable projection is carried in the View UBO. Direct-sun shader
2444
+ // consumers sample the renderer-owned map at each receiver position.
2523
2445
  writeViewUbo(
2524
2446
  internals.device.queue,
2525
2447
  pipelineState.viewUniformBuffer,
@@ -2529,6 +2451,7 @@ export function recordFrame(
2529
2451
  frameState.spotShadowSnapshots,
2530
2452
  temporalView,
2531
2453
  preparedSpotLightProjector?.spotIndex,
2454
+ cloudShadowProjection,
2532
2455
  );
2533
2456
  if (pipelineState.pointsLinesViewBuffer !== undefined) {
2534
2457
  writePointsLinesViewUbo(
@@ -2600,7 +2523,7 @@ export function recordFrame(
2600
2523
  internals,
2601
2524
  frameState,
2602
2525
  pipelineState,
2603
- validated.length > 0 || preparedGpuDriven !== undefined,
2526
+ validated.length > 0 || rasterGpuDriven !== undefined,
2604
2527
  bindGroupCounts,
2605
2528
  undefined,
2606
2529
  true,
@@ -2618,6 +2541,7 @@ export function recordFrame(
2618
2541
  frameState.spotShadowSnapshots,
2619
2542
  capture.viewBindGroupDynamicOffset,
2620
2543
  preparedSpotLightProjector?.spotIndex,
2544
+ cloudShadowProjection,
2621
2545
  );
2622
2546
  }
2623
2547
  for (const probe of cubeCapture?.reflectionProbes?.graph.work ?? []) {
@@ -2632,6 +2556,7 @@ export function recordFrame(
2632
2556
  frameState.spotShadowSnapshots,
2633
2557
  probe.viewBindGroupDynamicOffset,
2634
2558
  preparedSpotLightProjector?.spotIndex,
2559
+ cloudShadowProjection,
2635
2560
  );
2636
2561
  }
2637
2562
 
@@ -2652,14 +2577,33 @@ export function recordFrame(
2652
2577
  // builtin motion-blur pass receives its all-zero registration default and
2653
2578
  // becomes a no-op even though the temporal scene target is populated.
2654
2579
  const framePostProcessParams = new Map(postProcessParams);
2580
+ if (analyticFog !== undefined)
2581
+ framePostProcessParams.set(
2582
+ 'forgeax.analytic-fog',
2583
+ encodeAnalyticFogParams(analyticFog, recordCamera),
2584
+ );
2655
2585
  if (camera.motionBlur !== undefined) {
2656
- const motionBlurParams = new Uint8Array(16);
2657
- const motionBlurView = new DataView(motionBlurParams.buffer);
2658
- motionBlurView.setFloat32(0, camera.motionBlur.shutterAngle, true);
2659
- motionBlurView.setFloat32(4, camera.motionBlur.maxRadiusPixels, true);
2660
- motionBlurView.setUint32(8, camera.motionBlur.sampleCount, true);
2661
- motionBlurView.setUint32(12, temporalView?.historyValid === true ? 0 : 1, true);
2662
- framePostProcessParams.set('forgeax.motion-blur', motionBlurParams);
2586
+ if (motionBlurDemand) {
2587
+ const motionBlurParams = new Uint8Array(MOTION_BLUR_PARAMS_BYTE_SIZE);
2588
+ const motionBlurView = new DataView(motionBlurParams.buffer);
2589
+ const uniformDeltaSeconds = Number.isFinite(deltaSeconds) ? deltaSeconds : 0;
2590
+ motionBlurView.setFloat32(0, camera.motionBlur.shutterAngle, true);
2591
+ motionBlurView.setFloat32(4, camera.motionBlur.maxRadiusPixels, true);
2592
+ motionBlurView.setUint32(
2593
+ 8,
2594
+ effectiveMotionBlurSampleCount(camera.motionBlur.sampleCount),
2595
+ true,
2596
+ );
2597
+ motionBlurView.setUint32(12, temporalView?.historyValid === true ? 0 : 1, true);
2598
+ motionBlurView.setFloat32(16, camera.motionBlur.targetFps ?? 60, true);
2599
+ motionBlurView.setFloat32(
2600
+ 20,
2601
+ motionBlurExposureScale(uniformDeltaSeconds, camera.motionBlur.targetFps ?? 60),
2602
+ true,
2603
+ );
2604
+ motionBlurView.setFloat32(24, uniformDeltaSeconds, true);
2605
+ framePostProcessParams.set('forgeax.motion-blur', motionBlurParams);
2606
+ }
2663
2607
  }
2664
2608
  if (camera.depthOfField !== undefined) {
2665
2609
  const dofParams = packDepthOfFieldParams(camera.depthOfField, {
@@ -2675,7 +2619,18 @@ export function recordFrame(
2675
2619
  framePostProcessParams.set(id, dofParams);
2676
2620
  }
2677
2621
  }
2622
+ if (submittedBarrelMapping !== undefined && submittedBarrelMapping.strength > 0) {
2623
+ framePostProcessParams.set(
2624
+ BARREL_DISTORTION_POST_PROCESS_ID,
2625
+ packBarrelDistortionParams(submittedBarrelMapping),
2626
+ );
2627
+ }
2678
2628
 
2629
+ if (preparedGpuDriven !== undefined) {
2630
+ frameState.surfaceSubmissionObservation?.setResourceGeneration(
2631
+ preparedGpuDriven.standardPbrFrameResources.resourceGeneration,
2632
+ );
2633
+ }
2679
2634
  const passCtx: _InternalRenderPipelineContext = {
2680
2635
  assets: internals.assets,
2681
2636
  world,
@@ -2738,12 +2693,22 @@ export function recordFrame(
2738
2693
  ...(preparedGpuDriven === undefined
2739
2694
  ? {}
2740
2695
  : {
2741
- gpuDrivenStandardPbrFrameResources: preparedGpuDriven.standardPbrFrameResources,
2742
- gpuDrivenDrawKeys: preparedGpuDriven.drawKeys,
2743
- gpuDrivenShadowViews: preparedGpuDriven.shadowViewPool,
2744
- gpuDrivenShadowBatchProjections: preparedGpuDriven.shadowBatchProjections,
2745
- gpuDrivenShadowDrawKeys: preparedGpuDriven.shadowDrawKeys,
2746
- gpuDrivenShadowDrawKeysByView: preparedGpuDriven.shadowDrawKeysByView,
2696
+ gpuDrivenStandardPbrFrameResources: {
2697
+ ...preparedGpuDriven.standardPbrFrameResources,
2698
+ ...(frameState.surfaceSubmissionObservation === undefined
2699
+ ? {}
2700
+ : { surfaceSubmissionObservation: frameState.surfaceSubmissionObservation }),
2701
+ },
2702
+ ...(rasterGpuDriven === undefined
2703
+ ? {}
2704
+ : {
2705
+ gpuDrivenDrawKeys: rasterGpuDriven.drawKeys,
2706
+ gpuDrivenWorldKeys: rasterGpuDriven.worldKeys,
2707
+ gpuDrivenShadowViews: rasterGpuDriven.shadowViewPool,
2708
+ gpuDrivenShadowBatchProjections: rasterGpuDriven.shadowBatchProjections,
2709
+ gpuDrivenShadowDrawKeys: rasterGpuDriven.shadowDrawKeys,
2710
+ gpuDrivenShadowDrawKeysByView: rasterGpuDriven.shadowDrawKeysByView,
2711
+ }),
2747
2712
  }),
2748
2713
  ...(frameGpuDriven?.scene === undefined
2749
2714
  ? {}
@@ -2817,6 +2782,12 @@ export function recordFrame(
2817
2782
  frameState.ssrHistoryCandidate = undefined;
2818
2783
  }
2819
2784
  preparedGpuDriven?._commitResourceReplacement();
2785
+ const consumed = preparedGpuDriven?._consumeSurfaceDynamicInput?.(
2786
+ frameState.frameNumber,
2787
+ );
2788
+ if (consumed !== undefined && !consumed.ok) {
2789
+ internals.errorRegistry.fire(consumed.error);
2790
+ }
2820
2791
  frameGpuDriven?.occlusion?.commit(true, frameState.frameNumber, profilePhase);
2821
2792
  },
2822
2793
  onAborted: () => {
@@ -2841,6 +2812,7 @@ export function recordFrame(
2841
2812
  cubeCaptureWork.length === 0 ? undefined : cubeCaptureWork.map((work) => work.faceIndex),
2842
2813
  (done) => {
2843
2814
  completion = done;
2815
+ frameState.surfaceSubmissionObservation?.submit(done, frameState.graphGeneration);
2844
2816
  if (fallbackReadbackRequest !== undefined) {
2845
2817
  fallbackReadback = done.then(
2846
2818
  () =>
@@ -2859,14 +2831,24 @@ export function recordFrame(
2859
2831
  timingCapture,
2860
2832
  ),
2861
2833
  );
2862
- if (submitted && preparedGpuDriven?.drawKeys !== undefined && onRenderableDraw !== undefined) {
2834
+ if (submitted && onRenderableDraw !== undefined && rasterGpuDriven?.drawKeys !== undefined) {
2863
2835
  for (const entry of validated) {
2864
- if (gpuDrivenRenderableConsumed(entry, preparedGpuDriven.drawKeys)) {
2865
- onRenderableDraw(entry);
2866
- }
2836
+ emitGpuDrivenDrawReceipts(entry, rasterGpuDriven.drawKeys, onRenderableDraw);
2867
2837
  }
2868
2838
  }
2869
2839
  if (submitted) {
2840
+ // `ensureCompiledFrameGraph` can deliberately return the accepted LKG
2841
+ // while a topology candidate is still preparing. The submitted graph is
2842
+ // the picture authority: an identity frame publishes an identity
2843
+ // mapping, while a failed enable/disable candidate retains the previous
2844
+ // mapping together with its old picture. Never pair an LKG graph with
2845
+ // the live World's newer camera component.
2846
+ // The exact mapping used to populate the accepted graph's UBO is the
2847
+ // only mapping promoted with its receipt. This keeps an identity frame,
2848
+ // a retained barrel LKG and a normal nonzero frame on one context.
2849
+ if (submittedBarrelMapping !== undefined) {
2850
+ frameState.lastSuccessfulBarrelDistortion = submittedBarrelMapping;
2851
+ }
2870
2852
  // Feature plans are submission-sensitive: VFX keeps queued fixed ticks
2871
2853
  // until the graph that consumed them is actually submitted. Acknowledge
2872
2854
  // the exact accepted graph here so successful frames do not replay every
@@ -2905,6 +2887,8 @@ export function recordFrame(
2905
2887
  : null;
2906
2888
  }
2907
2889
  if (submitted) {
2890
+ frameState.recoveryMaterialArtifacts = acceptedMaterialArtifacts;
2891
+ frameState.recoveryShadowMaterialArtifacts = acceptedShadowMaterialArtifacts;
2908
2892
  if (graphVolumetricFog === undefined) {
2909
2893
  frameState.volumetricFogAccepted = undefined;
2910
2894
  frameState.volumetricFogAcceptedContext = undefined;
@@ -2957,12 +2941,13 @@ export function recordFrame(
2957
2941
  frameState.ssrHistoryOwner?.abortFrame(frameState.ssrHistoryCandidate, 'submit');
2958
2942
  frameState.ssrHistoryCandidate = undefined;
2959
2943
  }
2960
- // DoF and volume topology replacements are provisional until the queue
2961
- // submission succeeds. This runs for every early-return path as well, so a
2962
- // swapchain, encoder, or graph failure cannot leave a non-submitted
2963
- // candidate as the next frame's accepted graph.
2944
+ // Output, DoF, and volume topology replacements are provisional until the
2945
+ // queue submission succeeds. This runs for every early-return path as
2946
+ // well, so a swapchain, encoder, or graph failure cannot leave a
2947
+ // non-submitted candidate as the next frame's accepted graph.
2964
2948
  settleVolumetricFogGraphCandidate(frameState, submitted);
2965
2949
  settleDepthOfFieldGraphCandidate(frameState, submitted);
2950
+ settleBarrelDistortionGraphCandidate(frameState, submitted);
2966
2951
  if (volumeInspectionReady && (submitted || volumeSubmissionAttempted)) {
2967
2952
  updateVolumetricFogInspection(
2968
2953
  frameState,
@@ -3017,8 +3002,10 @@ function sameWorldState(
3017
3002
  // atlas rather than reaching through World for a second mutation clock.
3018
3003
  return false;
3019
3004
  }
3020
- if (changes.status !== 'ok') return false;
3021
- if (changes.world.changedComponentIds.length !== 0 || changes.sharedRefs.records.length !== 0) {
3005
+ if (
3006
+ changes.version.structureEpoch !== cachedState.version.structureEpoch ||
3007
+ changes.world.changedComponentIds.length !== 0
3008
+ ) {
3022
3009
  return false;
3023
3010
  }
3024
3011
  }
@@ -3136,716 +3123,3 @@ function prepareDirectionalShadowCache(
3136
3123
  },
3137
3124
  };
3138
3125
  }
3139
-
3140
- /**
3141
- * feat-20260704 M3/w18: validate renderable handles + collect the render plan,
3142
- * extracted verbatim from `recordFrame`. Empty `renderables` input or all-
3143
- * unregistered handles both yield an empty result (the Case E clear-pass-only
3144
- * path). Builds per-entry renderState / stencilReference overlays from the
3145
- * transparent-dispatch entries, resolves each MeshFilter.assetHandle through
3146
- * the AssetRegistry + GPU store (with the sprite 9-slice mesh swap), and fires
3147
- * structured `asset-not-registered` errors for handles that fail to resolve.
3148
- *
3149
- * @internal
3150
- */
3151
- export function validateRenderables(
3152
- internals: RenderSystemInternals,
3153
- world: World,
3154
- // feat-20260709-editor-world-partition ENGINE-fix-round2 (defect 2): the full
3155
- // worlds[] list. Each renderable's mesh handle is a user-tier slot in its OWN
3156
- // world's sharedRefs; resolving against a foreign world either misses
3157
- // (asset-not-registered) or resolves the wrong slot payload. `world` (the
3158
- // resource-owner) is retained as the fallback for renderables whose worldId
3159
- // is out of range (defensive; extractFrames always stamps a valid index).
3160
- worlds: readonly World[],
3161
- pipelineState: PipelineState,
3162
- frameState: RenderFrameState,
3163
- renderables: readonly RenderableSnapshot[],
3164
- transparentDispatch: readonly DispatchEntry[],
3165
- excludedEntityKeys: ReadonlySet<number> = new Set<number>(),
3166
- worldKeys?: readonly number[],
3167
- ): ValidatedRenderable[] {
3168
- // bug-20260527-renderstate-pipeline-dispatch-gap D-4:
3169
- // build a renderableIndex -> renderState map from dispatch entries
3170
- // so each ValidatedRenderable carries its per-material renderState
3171
- // override without an O(n^2) back-scan in the draw loop.
3172
- const renderStateByRenderableIdx = new Map<number, MaterialRenderState | undefined>();
3173
- // w10: also build a renderableIndex -> stencilReference map from
3174
- // dispatch entries for per-draw setStencilReference calls.
3175
- const stencilRefByRenderableIdx = new Map<number, number | undefined>();
3176
- const variantSetByRenderableIdx = new Map<number, string | undefined>();
3177
- // Keep these three independent maps because each overlay has its own
3178
- // consumer and the last dispatch entry remains authoritative. Populate
3179
- // them in one pass: the old code walked the same dispatch list three times
3180
- // and repeated the same renderableIndex branch / hash writes.
3181
- for (const de of transparentDispatch) {
3182
- const renderableIndex = de.renderableIndex;
3183
- if (renderableIndex === undefined) continue;
3184
- renderStateByRenderableIdx.set(renderableIndex, de.renderState);
3185
- stencilRefByRenderableIdx.set(renderableIndex, de.stencilReference);
3186
- variantSetByRenderableIdx.set(renderableIndex, variantSetFromDefines(de.defines));
3187
- }
3188
- const validated: ValidatedRenderable[] = [];
3189
- for (let rIdx = 0; rIdx < renderables.length; rIdx++) {
3190
- const r = renderables[rIdx];
3191
- if (r === undefined) continue;
3192
- if (excludedEntityKeys.has(worldEntityKey(worldKeys?.[r.worldId] ?? r.worldId, r.entityKey)))
3193
- continue;
3194
- // feat-20260709-editor-world-partition ENGINE-fix-round2 (defect 2): resolve
3195
- // this renderable's mesh against the world it was EXTRACTED from
3196
- // (worlds[r.worldId]) — NOT the single resource-owner `world`. Builtin mesh
3197
- // slots (< BUILTIN_BASE) resolve process-statically regardless of world, so
3198
- // the per-world pick only matters for user-tier handles, but selecting it
3199
- // unconditionally keeps a single code path. Falls back to the resource-owner
3200
- // world if worldId is out of range (defensive; extractFrames always stamps
3201
- // a valid index into worlds[]).
3202
- const renderableWorld = worlds[r.worldId] ?? world;
3203
- const assetRes = resolveAssetHandle<MeshAsset>(
3204
- renderableWorld,
3205
- toShared<'MeshAsset'>(r.assetHandle),
3206
- );
3207
- if (!assetRes.ok) {
3208
- internals.errorRegistry.fire(
3209
- new RhiError({
3210
- code: 'asset-not-registered',
3211
- expected: 'MeshFilter.assetHandle in AssetRegistry',
3212
- hint: 'use HANDLE_CUBE / HANDLE_TRIANGLE imports; custom mesh register path: feat-future-asset-system',
3213
- detail: { assetHandle: r.assetHandle },
3214
- }),
3215
- );
3216
- continue;
3217
- }
3218
- // feat-20260601-device/gpu-residency-extraction M1 (D-1): builtin meshes
3219
- // (slots through HANDLE_NINESLICE_QUAD) keep the createRenderer step-3
3220
- // direct-upload + `pipelineState.meshes` path -- they are NOT routed
3221
- // through `ensureResident`. User-registered meshes pull through the store
3222
- // on first access (the register->upload push was severed in this M1);
3223
- // the POD fetched above (assetRes.value) is passed in, store holds no
3224
- // registry ref (D-2). A first-access miss builds the GPU buffers; later
3225
- // frames hit the O(1) cache.
3226
- const meshAssetHandle = toShared<'MeshAsset'>(r.assetHandle);
3227
- let meshHandles = internals.gpuStore.getMeshGpuHandles(meshAssetHandle, renderableWorld);
3228
- const lodMeshes = r.lods?.map((lod) => internals.assets.lookup<MeshAsset>(lod.mesh));
3229
- const resolvedLodMeshes =
3230
- lodMeshes?.every((lod): lod is MeshAsset => lod?.kind === 'mesh') === true
3231
- ? lodMeshes
3232
- : undefined;
3233
- if (meshHandles === undefined && r.assetHandle > handleSlot(HANDLE_NINESLICE_QUAD)) {
3234
- const residentRes = internals.gpuStore.ensureResident(
3235
- meshAssetHandle,
3236
- assetRes.value,
3237
- renderableWorld,
3238
- resolvedLodMeshes,
3239
- );
3240
- if (residentRes.ok) {
3241
- meshHandles = residentRes.value;
3242
- } else if (residentRes.error instanceof RhiError) {
3243
- internals.errorRegistry.fire(residentRes.error);
3244
- }
3245
- }
3246
- meshHandles = meshHandles ?? pipelineState.meshes.get(r.assetHandle);
3247
- if (meshHandles === undefined) {
3248
- internals.errorRegistry.fire(
3249
- new RhiError({
3250
- code: 'asset-not-registered',
3251
- expected: 'GPU mesh buffers uploaded for assetHandle',
3252
- hint: 'await renderer.initialization before draw([world], { cameraOwner: 0, resourceOwner: 0 }); ensure AssetRegistry.configureGpuDevice ran so user meshes are uploaded',
3253
- detail: { assetHandle: r.assetHandle },
3254
- }),
3255
- );
3256
- continue;
3257
- }
3258
- // feat-20260527-sprite-nineslice M2 / w11 (plan-strategy section D-2):
3259
- // sprite branch with non-zero `slicesAndMode` (post-w12 paramSnapshot
3260
- // entry name) overrides the user-supplied mesh handle (typically
3261
- // HANDLE_QUAD = 3) with the 16-vertex / 54-index HANDLE_NINESLICE_QUAD
3262
- // (id=5) topology so the vertex shader sees the 4x4 grid required for
3263
- // 9-region anchor mapping. Default slicesAndMode ([0, 0, 0, 0]) keeps
3264
- // the legacy HANDLE_QUAD path; a flip from zero to non-zero on the
3265
- // same entity routes here per-frame so AI users can toggle 9-slice
3266
- // on the fly without re-spawning the entity (charter F1 minimum
3267
- // surface). The HANDLE_NINESLICE_QUAD GPU buffers are seeded by
3268
- // createRenderer step-3.
3269
- //
3270
- // feat-20260625-refactor-sprite-as-transparent-mesh M3 / w13: judgement
3271
- // key migrated from `shadingModel === 'sprite'` to
3272
- // `materialShaderId === 'forgeax::sprite'` (plan-strategy D-10); slices
3273
- // sourced from `paramSnapshot.slicesAndMode` (post-w12 UBO-aligned
3274
- // overlay path).
3275
- //
3276
- // feat-20260624-sprite-lit-shading-model-pure-2d-lighting M1' / t7:
3277
- // sprite-lit shares the sprite paramSchema (5 fields, t4 mirror) so
3278
- // the 9-slices mesh swap applies identically.
3279
- let effectiveMeshHandles = meshHandles;
3280
- if (
3281
- r.material.materialShaderId === 'forgeax::sprite' ||
3282
- r.material.materialShaderId === 'forgeax::sprite-lit'
3283
- ) {
3284
- const slicesArr = r.material.paramSnapshot?.slicesAndMode as readonly number[] | undefined;
3285
- if (
3286
- slicesArr !== undefined &&
3287
- slicesArr.length >= 4 &&
3288
- (slicesArr[0] !== 0 || slicesArr[1] !== 0 || slicesArr[2] !== 0 || slicesArr[3] !== 0)
3289
- ) {
3290
- const nineSliceHandles = pipelineState.meshes.get(handleSlot(HANDLE_NINESLICE_QUAD));
3291
- if (nineSliceHandles !== undefined) {
3292
- effectiveMeshHandles = nineSliceHandles;
3293
- }
3294
- }
3295
- }
3296
- if (r.morph !== undefined) {
3297
- const morphed = prepareMorphMesh(
3298
- internals,
3299
- frameState,
3300
- worldEntityKey(r.worldId, r.entityKey),
3301
- effectiveMeshHandles,
3302
- assetRes.value,
3303
- r.morph,
3304
- );
3305
- if (morphed !== undefined) effectiveMeshHandles = morphed;
3306
- }
3307
- validated.push({
3308
- source: r,
3309
- world: renderableWorld,
3310
- mesh: effectiveMeshHandles,
3311
- renderableIndex: rIdx,
3312
- renderState: renderStateByRenderableIdx.get(rIdx),
3313
- variantSet: variantSetByRenderableIdx.get(rIdx),
3314
- stencilReference: stencilRefByRenderableIdx.get(rIdx),
3315
- });
3316
- }
3317
- return validated;
3318
- }
3319
-
3320
- // Keep the primary record-stage name explicit at its call site while sharing
3321
- // the same owner implementation for the complete retained ShadowCaster view.
3322
-
3323
- function prepareMorphMesh(
3324
- internals: RenderSystemInternals,
3325
- frameState: RenderFrameState,
3326
- cacheKey: number,
3327
- base: MeshGpuHandles,
3328
- mesh: MeshAsset,
3329
- morph: RenderableSnapshot['morph'],
3330
- ): MeshGpuHandles | undefined {
3331
- if (morph === undefined || frameState.morphBuffers === undefined) return undefined;
3332
- const targets = mesh.morphTargets;
3333
- if (targets === undefined || targets.length !== morph.targetCount) return undefined;
3334
- const position = base.layoutProjection.attributes.find(
3335
- (attribute) => attribute.key === 'position',
3336
- );
3337
- if (
3338
- position === undefined ||
3339
- position.offset !== 0 ||
3340
- position.shaderLocation !== 0 ||
3341
- position.format !== 'float32x3' ||
3342
- position.byteLength !== 12
3343
- ) {
3344
- return undefined;
3345
- }
3346
- const vertexCount = deriveVertexCount(mesh.vertices, base.layoutProjection);
3347
- if (vertexCount === undefined || vertexCount !== base.vertexCount || vertexCount <= 0) {
3348
- return undefined;
3349
- }
3350
- const stride = base.layoutProjection.arrayStride / Float32Array.BYTES_PER_ELEMENT;
3351
-
3352
- let active = false;
3353
- for (const weight of morph.weights) {
3354
- if (weight !== 0) {
3355
- active = true;
3356
- break;
3357
- }
3358
- }
3359
- if (!active) {
3360
- frameState.morphBuffers.get(cacheKey)?.buffer.destroy();
3361
- frameState.morphBuffers.delete(cacheKey);
3362
- return undefined;
3363
- }
3364
-
3365
- const vertices = new Float32Array(mesh.vertices);
3366
- for (let targetIndex = 0; targetIndex < targets.length; targetIndex += 1) {
3367
- const weight = morph.weights[targetIndex] ?? 0;
3368
- const positions = targets[targetIndex]?.position;
3369
- if (positions === undefined || positions.length !== base.vertexCount * 3) return undefined;
3370
- for (let vertex = 0; vertex < base.vertexCount; vertex += 1) {
3371
- const vertexBase = vertex * stride;
3372
- const positionBase = vertex * 3;
3373
- vertices[vertexBase] = (vertices[vertexBase] ?? 0) + (positions[positionBase] ?? 0) * weight;
3374
- vertices[vertexBase + 1] =
3375
- (vertices[vertexBase + 1] ?? 0) + (positions[positionBase + 1] ?? 0) * weight;
3376
- vertices[vertexBase + 2] =
3377
- (vertices[vertexBase + 2] ?? 0) + (positions[positionBase + 2] ?? 0) * weight;
3378
- }
3379
- }
3380
-
3381
- let cached = frameState.morphBuffers.get(cacheKey);
3382
- if (cached === undefined || cached.byteLength !== vertices.byteLength) {
3383
- if (cached !== undefined && !cached.buffer.isDestroyed) {
3384
- const destroyed = cached.buffer.destroy();
3385
- if (!destroyed.ok) internals.errorRegistry.fire(destroyed.error);
3386
- }
3387
- const created = internals.device.createBuffer({
3388
- label: `morph-${cacheKey}-vbo`,
3389
- size: vertices.byteLength,
3390
- usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
3391
- mappedAtCreation: false,
3392
- });
3393
- if (!created.ok) {
3394
- internals.errorRegistry.fire(created.error);
3395
- frameState.morphBuffers.delete(cacheKey);
3396
- return undefined;
3397
- }
3398
- const buffer = new GpuBuffer(internals.device, created.value);
3399
- internals.deviceScope._adopt('buffer', buffer, (owned) => {
3400
- if (!owned.isDestroyed) owned.destroy();
3401
- });
3402
- cached = {
3403
- buffer,
3404
- byteLength: vertices.byteLength,
3405
- };
3406
- frameState.morphBuffers.set(cacheKey, cached);
3407
- }
3408
- const written = internals.device.queue.writeBuffer(cached.buffer.handle, 0, vertices);
3409
- if (!written.ok) {
3410
- internals.errorRegistry.fire(written.error);
3411
- return undefined;
3412
- }
3413
- return { ...base, vertexBuffer: cached.buffer };
3414
- }
3415
-
3416
- /**
3417
- * feat-20260704 M3/w18: build the dispatch-ordered render plan, extracted
3418
- * verbatim from `recordFrame`. (1) M3/w26 dispatch-ordered reorder: reorder
3419
- * `validated` to follow the transparent-dispatch order (extract-order fallback
3420
- * for unmatched entries). (2) feat-20260608 mesh-SSBO capacity gate: size the
3421
- * mesh-SSBO + material-UBO pair to the larger of entity vs cumulative
3422
- * material-slot count, truncating on ceiling (graceful degradation). (3)
3423
- * feat-20260622 fold dispatch plan: build + apply the WebGL2 uniform-cap
3424
- * fallback + bump the folded-draws metric.
3425
- *
3426
- * @internal
3427
- */
3428
- export function buildDispatchPlan(
3429
- internals: RenderSystemInternals,
3430
- validated: readonly ValidatedRenderable[],
3431
- transparentDispatch: readonly DispatchEntry[],
3432
- foldBuckets: readonly FoldBucket[],
3433
- shadowValidated: readonly ValidatedRenderable[] = [],
3434
- materialBindingClasses?: ReadonlyMap<string, string>,
3435
- ): {
3436
- validatedOrdered: readonly ValidatedRenderable[];
3437
- shadowValidatedOrdered: readonly ValidatedRenderable[];
3438
- foldDispatchPlan: FoldDispatchPlan | null;
3439
- materialSlotIndices: readonly (readonly number[])[];
3440
- materialSlots: readonly MaterialSnapshot[];
3441
- materialSlotOwners: readonly ValidatedRenderable[];
3442
- materialSlotCount: number;
3443
- } {
3444
- // A production GPU-owned frame has no CPU validated rows by design. The
3445
- // dispatch list can still contain one entry per authored entity, but no
3446
- // CPU plan, material table, or fold bucket is consumed in that lane. Return
3447
- // the empty plan without walking that list so a steady 100k scene remains
3448
- // bounded by the persistent GPU batches rather than per-entity record work.
3449
- if (validated.length === 0 && shadowValidated.length === 0) {
3450
- const meshSsboCapResult = ensureMeshSsboCapacity(internals, shadowValidated.length);
3451
- const meshSsboSlotCapacity = meshSsboSlotCapacityAfterEnsure(
3452
- internals,
3453
- shadowValidated.length,
3454
- meshSsboCapResult,
3455
- );
3456
- return {
3457
- validatedOrdered: [],
3458
- shadowValidatedOrdered: limitRowsToMeshSsboCapacity(shadowValidated, meshSsboSlotCapacity),
3459
- foldDispatchPlan: null,
3460
- materialSlotIndices: [],
3461
- materialSlots: [],
3462
- materialSlotOwners: [],
3463
- materialSlotCount: 0,
3464
- };
3465
- }
3466
- // M3 / w26: dispatch-ordered render. The dispatch list is pre-sorted
3467
- // by queue (ascending, stable) by the extract stage per plan-strategy D-3.
3468
- // Reorder validated renderables to follow the dispatch order, falling
3469
- // back to extract order for renderables with no matching dispatch entry.
3470
- let dispatchOrderMatchesValidated = transparentDispatch.length === validated.length;
3471
- if (dispatchOrderMatchesValidated) {
3472
- for (let i = 0; i < validated.length; i++) {
3473
- if (transparentDispatch[i]?.renderableIndex !== validated[i]?.renderableIndex) {
3474
- dispatchOrderMatchesValidated = false;
3475
- break;
3476
- }
3477
- }
3478
- }
3479
- let validatedOrdered: readonly ValidatedRenderable[] = dispatchOrderMatchesValidated
3480
- ? validated
3481
- : [...validated];
3482
- if (transparentDispatch.length > 0 && !dispatchOrderMatchesValidated) {
3483
- const validatedByRenderableIdx = new Map<number, ValidatedRenderable>();
3484
- const seen = new Set<number>();
3485
- for (const v of validated) {
3486
- validatedByRenderableIdx.set(v.renderableIndex, v);
3487
- }
3488
- const ordered: ValidatedRenderable[] = [];
3489
- for (const de of transparentDispatch) {
3490
- if (de.renderableIndex === undefined) continue;
3491
- const v = validatedByRenderableIdx.get(de.renderableIndex);
3492
- if (v !== undefined && !seen.has(de.renderableIndex)) {
3493
- seen.add(de.renderableIndex);
3494
- ordered.push(v);
3495
- }
3496
- }
3497
- // Append renderables not in the dispatch list (e.g. default-material entities)
3498
- for (const v of validated) {
3499
- if (!seen.has(v.renderableIndex)) {
3500
- ordered.push(v);
3501
- }
3502
- }
3503
- validatedOrdered = ordered;
3504
- }
3505
-
3506
- // Shadow-only/off-camera CPU residuals consume the same material UBO as
3507
- // visible draws. Their preparation must not depend on main-view visibility.
3508
- const materialRows = [...validatedOrdered, ...shadowValidated];
3509
- const materialGroups = materialRows.map((entry): readonly MaterialSnapshot[] => {
3510
- const shaderId = entry.source.material.materialShaderId;
3511
- if (shaderId === 'forgeax::sprite' || shaderId === 'forgeax::sprite-lit') {
3512
- return [entry.source.material];
3513
- }
3514
- return entry.source.materials.length > 0 ? entry.source.materials : [entry.source.material];
3515
- });
3516
- const materialSlotPlan = buildMaterialSlotPlan(materialGroups, (material, owner) => {
3517
- const row = materialRows[owner];
3518
- return row === undefined
3519
- ? ''
3520
- : (materialBindingClasses?.get(
3521
- materialBindingKey(row.source, material.materialHandle ?? -1),
3522
- ) ?? '');
3523
- });
3524
- let materialSlotIndices = materialSlotPlan.slotIndices;
3525
- let materialSlots = materialSlotPlan.slots;
3526
- let materialSlotOwners = materialSlotPlan.slotOwners.map((index) => {
3527
- const owner = materialRows[index];
3528
- if (owner === undefined) throw new Error('material slot has no validated owner');
3529
- return owner;
3530
- });
3531
- let materialSlotCount = materialSlots.length;
3532
- let materialRenderableCount = materialRows.length;
3533
-
3534
- // feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M3 / T-M3-04:
3535
- // ensure the mesh-SSBO + material-UBO buffer pair is large enough to hold
3536
- // the render plan BEFORE the first per-entity writeBuffer.
3537
- // On `ok:false` the controller has already fired a structured RuntimeError
3538
- // (`mesh-ssbo-ceiling-reached` / `mesh-ssbo-capacity-exceeded`); we truncate
3539
- // the draw list to the largest complete entity prefix whose cumulative
3540
- // material slots fit `degradedToSlotCount` (graceful degradation per
3541
- // plan-strategy D-2): render the subset that fits, discard overflow, no
3542
- // black frame. The helper is idempotent across same-frame re-calls (AC-09)
3543
- // and short-circuits on length=0 / length<=slotCount (boundary table).
3544
- //
3545
- // bug-20260609: feat-20260608 M5 amend made the material UBO indexed by
3546
- // cumulative *material-slot* count (one slot per submesh material),
3547
- // which is >= entity count once an entity carries `materials.length>1`.
3548
- // The mesh + material buffer pair share `slotCount` (single allocator),
3549
- // so we size against the larger of the two requirements: entity count
3550
- // (mesh-SSBO consumer) vs cumulative material-slot count (material-UBO
3551
- // consumer). Sprite entities collapse to 1 slot in the material table,
3552
- // mirroring the same rule (sprite per-submesh OOS-1;
3553
- // post-w13 judgement key migrated to materialShaderId).
3554
- //
3555
- // feat-20260624 M1' / t7: sprite-lit treated identically to sprite
3556
- // for material-slot accounting (paramSchema mirror, t4).
3557
- const neededSlots = Math.max(validatedOrdered.length + shadowValidated.length, materialSlotCount);
3558
- const meshSsboCapResult = ensureMeshSsboCapacity(internals, neededSlots);
3559
- const meshSsboSlotCapacity = meshSsboSlotCapacityAfterEnsure(
3560
- internals,
3561
- neededSlots,
3562
- meshSsboCapResult,
3563
- );
3564
- if (!meshSsboCapResult.ok) {
3565
- // Graceful degradation: the controller reports slots, but this stage
3566
- // consumes entities. Find a complete prefix instead of slicing at the
3567
- // numeric slot count; a multi-material entity can consume several slots.
3568
- const degradedRenderableCount = findRenderablePrefixForSlotCapacity(
3569
- materialSlotIndices,
3570
- meshSsboCapResult.degradedToSlotCount,
3571
- );
3572
- materialRenderableCount = degradedRenderableCount;
3573
- materialSlotCount = materialSlotCountForPrefix(materialSlotIndices, degradedRenderableCount);
3574
- materialSlotIndices = materialSlotIndices.slice(0, degradedRenderableCount);
3575
- materialSlots = materialSlots.slice(0, materialSlotCount);
3576
- materialSlotOwners = materialSlotOwners.slice(0, materialSlotCount);
3577
- validatedOrdered = validatedOrdered.slice(0, degradedRenderableCount);
3578
- }
3579
- const shadowValidatedOrdered = limitRowsToMeshSsboCapacity(
3580
- shadowValidated,
3581
- Math.min(meshSsboSlotCapacity, materialRenderableCount) - validatedOrdered.length,
3582
- );
3583
-
3584
- // feat-20260622-chunk-gpu-instancing-sprite-tilemap M1 / w4-record-swap
3585
- // (D-1): build the fold dispatch plan once `validatedOrdered` is final
3586
- // (post truncation by mesh-SSBO capacity gate). The plan re-keys each
3587
- // non-singleton bucket from `renderableIndex` to the validated-ordered
3588
- // index `i` consumed by the dispatch loops; the loops use it to skip
3589
- // non-head bucket members and emit one instanced drawIndexed per
3590
- // bucket head. Empty plan (no fold-eligible buckets) is a byte-
3591
- // identical no-op for the dispatch loops below (charter P3: silent
3592
- // pass-through, no error path).
3593
- let renderableToValidatedIdx: Map<number, number> | null = null;
3594
- let foldDispatchPlan: FoldDispatchPlan | null = null;
3595
- if (foldBuckets.length > 0) {
3596
- renderableToValidatedIdx = new Map<number, number>();
3597
- for (let i = 0; i < validatedOrdered.length; i++) {
3598
- const e = validatedOrdered[i];
3599
- if (e === undefined) continue;
3600
- renderableToValidatedIdx.set(e.renderableIndex, i);
3601
- }
3602
- foldDispatchPlan = buildFoldDispatchPlan(foldBuckets, renderableToValidatedIdx);
3603
-
3604
- // feat-20260622 M2 / w11 (D-2 + D-9 + AC-05): WebGL2 uniform-fallback
3605
- // per-bucket instance-count cap. When caps.storageBuffer===false AND
3606
- // a fold bucket carries more than FOLD_UNIFORM_INSTANCE_CAP (128)
3607
- // instances, fire RhiError({code:'instancing-exceeds-uniform-cap'})
3608
- // AND remove the bucket from the dispatch plan so its members fall
3609
- // through to the per-entity drawIndexed exit (the same exit the
3610
- // mode-gate bypass uses — D-9 "shared fallback exit"). The frame
3611
- // stays visually correct (charter proposition 9 graceful
3612
- // degradation: no identity-collapse / black screen) while the cap
3613
- // event surfaces structurally for AI users (proposition 4 explicit
3614
- // failure on .code).
3615
- //
3616
- // Scope discrimination: tilemap-chunk-extract-system encodes
3617
- // Layer.value = (layerOrder<<20) | (chunkIndex & 0xfffff), so a
3618
- // bucket whose head entry carries non-zero low-20-bits is
3619
- // definitively a tilemap-chunk dispatch site. Plain sprite buckets
3620
- // use SPRITE_LAYER_VALUE = layerOrder<<20 (low-20 zero) by the
3621
- // documented convention (apps/hello/asi-world main.ts pattern).
3622
- // The chunkIndex===0 edge case maps to 'sprite' (the helper's
3623
- // default branch) — a one-bucket ambiguity per layerOrder that is
3624
- // acceptable for the AI-user affordance level (the error semantics
3625
- // — "this bucket exceeded the cap" — is the actionable signal;
3626
- // scope=sprite vs tilemap-chunk only refines the recovery hint).
3627
- if (foldDispatchPlan.headBuckets.size > 0 && !internals.device.caps.storageBuffer) {
3628
- const filteredHeads = new Map<number, FoldBucket>(foldDispatchPlan.headBuckets);
3629
- const filteredSkips = new Set<number>(foldDispatchPlan.skipIndices);
3630
- let filteredCount = foldDispatchPlan.foldedBucketCount;
3631
- for (const [headIdx, bucket] of foldDispatchPlan.headBuckets) {
3632
- const scope: 'sprite' | 'tilemap-chunk' =
3633
- (bucket.layer & 0xfffff) !== 0 ? 'tilemap-chunk' : 'sprite';
3634
- const decision = evaluateFoldBucketUniformCap(bucket, internals.device.caps, scope);
3635
- if (decision.fallback && decision.error !== undefined) {
3636
- internals.errorRegistry.fire(decision.error);
3637
- filteredHeads.delete(headIdx);
3638
- filteredCount -= 1;
3639
- for (let j = 1; j < bucket.entries.length; j++) {
3640
- const memberEntry = bucket.entries[j];
3641
- if (memberEntry === undefined) continue;
3642
- const memberValidatedIdx = renderableToValidatedIdx.get(memberEntry.renderableIndex);
3643
- if (memberValidatedIdx !== undefined) {
3644
- filteredSkips.delete(memberValidatedIdx);
3645
- }
3646
- }
3647
- }
3648
- }
3649
- if (filteredCount !== foldDispatchPlan.foldedBucketCount) {
3650
- foldDispatchPlan = {
3651
- headBuckets: filteredHeads,
3652
- skipIndices: filteredSkips,
3653
- foldedBucketCount: filteredCount,
3654
- };
3655
- }
3656
- }
3657
-
3658
- // feat-20260622-chunk-gpu-instancing-sprite-tilemap M3 / w13 (D-3 +
3659
- // AC-06): increment `render.instancing.foldedDraws` once per fold-
3660
- // eligible head bucket retained after the cap-fallback filter above.
3661
- // The metric tracks instanced drawIndexed call count for this frame
3662
- // — cap-overrun buckets routed through the per-entity fallback exit
3663
- // are removed from `foldDispatchPlan` and therefore not counted, by
3664
- // construction (M2 / w11 cap-fallback + plan-strategy D-3 semantics).
3665
- // Singleton buckets (mode-bypass under D-5, or non-foldable under
3666
- // mode 0) carry `bucketSize === 1` and never enter `headBuckets`, so
3667
- // the per-entity drawIndexed path correctly does not count.
3668
- // SSOT helper lives in this record owner — engine code never
3669
- // hardcodes the metric key string.
3670
- incrementFoldedDrawsMetric(foldDispatchPlan, internals.metrics);
3671
- }
3672
-
3673
- return {
3674
- validatedOrdered,
3675
- shadowValidatedOrdered,
3676
- foldDispatchPlan,
3677
- materialSlotIndices,
3678
- materialSlots,
3679
- materialSlotOwners,
3680
- materialSlotCount,
3681
- };
3682
- }
3683
-
3684
- /**
3685
- * feat-20260704 M3/w18: per-frame cache clean-up (despawn eviction), extracted
3686
- * verbatim from `recordFrame`.
3687
- *
3688
- * feat-20260531-per-frame-bind-group-cache M4 / w14 (D-5): drop per-entity BG
3689
- * cache entries (materialBgPerEntity / instancesBgPerEntity) + instance buffers
3690
- * whose outer-Map entityKey (packed Entity u32) is absent from the current
3691
- * validated set, preventing unbounded growth after entity despawn. view + mesh
3692
- * caches are frame-shared (keyed by GPU resource handle objects, WeakMap chains
3693
- * naturally bounded) and are not touched. feat-20260619 M4 / F11: destroy the
3694
- * GPU instance buffer before Map.delete (D-6 symmetric release); failure fires
3695
- * errorRegistry + continues the sweep.
3696
- *
3697
- * @internal
3698
- */
3699
- function cleanPerFrameCaches(
3700
- internals: RenderSystemInternals,
3701
- frameState: RenderFrameState,
3702
- validated: readonly ValidatedRenderable[],
3703
- retainedEntityKeys: ReadonlySet<number> = new Set<number>(),
3704
- ): void {
3705
- if (frameState.transientInstanceBuffers !== undefined) {
3706
- disposeTransientInstanceBuffers(frameState.transientInstanceBuffers, internals.errorRegistry);
3707
- }
3708
- // Build a Set<number> of worldEntityKey composites from the validated
3709
- // renderables. D-1a #4: validatedEntityKeys are worldEntityKey(worldId, entityKey)
3710
- // composites matching the write-side keys of #1-#3 — cross-world false eviction
3711
- // is prevented because worldEntityKey(0, k) !== worldEntityKey(1, k).
3712
- const validatedEntityKeys = new Set<number>(retainedEntityKeys);
3713
- const validatedInstanceKeys = new Set<number>(retainedEntityKeys);
3714
- for (const v of validated) {
3715
- const entityKey = worldEntityKey(v.source.worldId, v.source.entityKey);
3716
- validatedEntityKeys.add(entityKey);
3717
- validatedInstanceKeys.add(entityKey);
3718
- if (v.source.instances !== undefined) {
3719
- validatedInstanceKeys.add(instanceCollectionCacheKey(v.source.worldId, v.source.instances));
3720
- }
3721
- }
3722
-
3723
- // Clean per-entity material BG cache: drop outer-Map entries whose
3724
- // entityKey is absent from the current validated set. The shared and
3725
- // singleton material caches have no entityKey and are not touched here.
3726
- cleanPerEntityCache(frameState.materialBgPerEntity, validatedEntityKeys);
3727
-
3728
- // Clean per-entity instances BG cache.
3729
- cleanPerEntityCache(frameState.instancesBgPerEntity, validatedEntityKeys);
3730
-
3731
- // D-5 retrofit: instanceBuffers clean-up. The instanceBuffers Map is
3732
- // keyed by cacheKey (packed Entity u32, same as entityKey on
3733
- // RenderableSnapshot). Drop entries whose key is no longer in the
3734
- // validated set (OQ-3 / R-4). feat-20260619 M4 / F11: destroy the GPU
3735
- // buffer before Map.delete so despawned entities release their
3736
- // instance-buffer backing memory symmetrically (D-6).
3737
- //
3738
- // D-1a #1: instanceBuffers keys on the positive half (>= 0) are
3739
- // worldEntityKey composites matching the write side. Negative half
3740
- // fold-bucket keys (< 0) are NOT worldEntityKey; they are
3741
- // material-handle-based and cross-world collision is semantically
3742
- // correct (same material renders in same fold bucket).
3743
- for (const [key, entry] of frameState.instanceBuffers.entries()) {
3744
- if (!validatedInstanceKeys.has(key)) {
3745
- if (!entry.buffer.isDestroyed) {
3746
- const r = entry.buffer.destroy();
3747
- if (!r.ok) internals.errorRegistry.fire(r.error);
3748
- }
3749
- frameState.instanceBuffers.delete(key);
3750
- }
3751
- }
3752
-
3753
- // Chunk keys are `${worldEntityKey}:${chunkStart}`. Keep the key opaque to
3754
- // the authoring API, but retain the numeric owner prefix for eviction.
3755
- if (frameState.instanceBufferChunks !== undefined) {
3756
- for (const [key, entry] of frameState.instanceBufferChunks.entries()) {
3757
- const separator = key.indexOf(':');
3758
- const ownerKey = separator < 0 ? Number.NaN : Number(key.slice(0, separator));
3759
- if (validatedInstanceKeys.has(ownerKey)) continue;
3760
- if (!entry.buffer.isDestroyed) {
3761
- const r = entry.buffer.destroy();
3762
- if (!r.ok) internals.errorRegistry.fire(r.error);
3763
- }
3764
- frameState.instanceBufferChunks.delete(key);
3765
- }
3766
- }
3767
-
3768
- if (frameState.morphBuffers !== undefined) {
3769
- for (const [key, entry] of frameState.morphBuffers.entries()) {
3770
- if (validatedEntityKeys.has(key)) continue;
3771
- if (!entry.buffer.isDestroyed) {
3772
- const result = entry.buffer.destroy();
3773
- if (!result.ok) internals.errorRegistry.fire(result.error);
3774
- }
3775
- frameState.morphBuffers.delete(key);
3776
- }
3777
- }
3778
- }
3779
-
3780
- /**
3781
- * feat-20260704 M3/w18: record-stage fold operator linear scan, extracted
3782
- * verbatim from `recordFrame`.
3783
- *
3784
- * feat-20260622-chunk-gpu-instancing-sprite-tilemap M1 / w4 + w5 (D-1, D-5):
3785
- * groups transparent-sort-ordered dispatch entries with equal (Layer.value,
3786
- * sortKey, materialHandle) into fold buckets. Mode-gate (D-5 extended): modes 0
3787
- * (LAYER_Z) and 1 (LAYER_Y) fold using the pos z/y lanes; modes 2/3 bypass per-entity
3788
- * (each entry a singleton bucket). Records `frameState.lastFoldBucketCount` =
3789
- * fold-eligible buckets (bucketSize > 1) for the AC-06 metric. Empty dispatch
3790
- * short-circuits (test fixtures pass null world).
3791
- *
3792
- * @internal
3793
- */
3794
- function computeFoldBuckets(
3795
- world: World,
3796
- frameState: RenderFrameState,
3797
- transparentDispatch: readonly DispatchEntry[],
3798
- renderables: readonly RenderableSnapshot[],
3799
- ): readonly FoldBucket[] {
3800
- if (transparentDispatch.length === 0) {
3801
- frameState.lastFoldBucketCount = 0;
3802
- return [];
3803
- }
3804
- const transparentSortCfg = getTransparentSortConfig(world);
3805
-
3806
- // Only layer-Z / layer-Y modes can produce non-singleton fold buckets.
3807
- // The other modes make one inert singleton per entry, which is discarded by
3808
- // buildFoldDispatchPlan. Returning the same empty plan here avoids allocating
3809
- // a transform matrix for every renderable when folding is disabled by mode.
3810
- if (
3811
- transparentSortCfg.mode !== TRANSPARENT_SORT_MODE_LAYER_Z &&
3812
- transparentSortCfg.mode !== TRANSPARENT_SORT_MODE_LAYER_Y
3813
- ) {
3814
- frameState.lastFoldBucketCount = 0;
3815
- return [];
3816
- }
3817
-
3818
- // `transparentDispatch` is the legacy name for the complete sorted dispatch
3819
- // list. Opaque entries can never participate in the transparent-only fold
3820
- // plan, yet foldDispatchBuckets must preserve its general helper contract and
3821
- // therefore materializes an inert singleton (including a Float32Array(16))
3822
- // for each one. Filter only at this private production call site so the fold
3823
- // helper keeps its testable semantics while record avoids work whose result
3824
- // is provably discarded.
3825
- let transparentCount = 0;
3826
- for (const entry of transparentDispatch) {
3827
- if (renderables[entry.renderableIndex]?.material.transparent === true) {
3828
- transparentCount += 1;
3829
- }
3830
- }
3831
- if (transparentCount === 0) {
3832
- frameState.lastFoldBucketCount = 0;
3833
- return [];
3834
- }
3835
- const foldCandidates =
3836
- transparentCount === transparentDispatch.length
3837
- ? transparentDispatch
3838
- : transparentDispatch.filter(
3839
- (entry) => renderables[entry.renderableIndex]?.material.transparent === true,
3840
- );
3841
- const foldBuckets = foldDispatchBuckets(foldCandidates, transparentSortCfg.mode, renderables);
3842
- // Count only fold-eligible buckets (bucketSize > 1) so the metric
3843
- // surfaces fold actually reducing draws — singleton buckets under
3844
- // mode bypass do not change draw count, so they do not contribute.
3845
- let foldEligibleCount = 0;
3846
- for (const b of foldBuckets) {
3847
- if (b.bucketSize > 1) foldEligibleCount += 1;
3848
- }
3849
- frameState.lastFoldBucketCount = foldEligibleCount;
3850
- return foldBuckets;
3851
- }