@forgeax/engine-render 0.1.33 → 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 (518) hide show
  1. package/README.md +1010 -124
  2. package/dist/assembly/backend-contract.d.ts +2 -0
  3. package/dist/assembly/backend-contract.d.ts.map +1 -1
  4. package/dist/assembly/barrel-distortion-inspection.d.ts +3 -0
  5. package/dist/assembly/barrel-distortion-inspection.d.ts.map +1 -0
  6. package/dist/assembly/canvas-draw-guard.d.ts +3 -0
  7. package/dist/assembly/canvas-draw-guard.d.ts.map +1 -0
  8. package/dist/assembly/cloud-shadow-entry.d.ts +4 -0
  9. package/dist/assembly/cloud-shadow-entry.d.ts.map +1 -0
  10. package/dist/assembly/dynamic-geometry-host.d.ts +4 -1
  11. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  12. package/dist/assembly/dynamic-geometry-runtime.d.ts +1 -1
  13. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -1
  14. package/dist/assembly/factory.d.ts +1 -1
  15. package/dist/assembly/factory.d.ts.map +1 -1
  16. package/dist/assembly/host-contract.d.ts +3 -3
  17. package/dist/assembly/host-contract.d.ts.map +1 -1
  18. package/dist/assembly/material/assembly.d.ts.map +1 -1
  19. package/dist/assembly/material/pipeline-helpers.d.ts +5 -0
  20. package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -1
  21. package/dist/assembly/material-shader-policy.d.ts +16 -2
  22. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  23. package/dist/assembly/material-shader-prewarm.d.ts.map +1 -1
  24. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
  25. package/dist/assembly/recovery/renderer-recover.d.ts +2 -0
  26. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  27. package/dist/assembly/render-feature-post-processes.d.ts +6 -0
  28. package/dist/assembly/render-feature-post-processes.d.ts.map +1 -0
  29. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  30. package/dist/assembly/renderer-feature-inspection.d.ts +1 -1
  31. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -1
  32. package/dist/assembly/renderer-frame-transaction.d.ts +12 -0
  33. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  34. package/dist/assembly/renderer-inspection.d.ts +11 -0
  35. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  36. package/dist/assembly/webgpu-pbr-ready.d.ts +1 -0
  37. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -1
  38. package/dist/assembly/webgpu-ready-per-pass.d.ts +39 -0
  39. package/dist/assembly/webgpu-ready-per-pass.d.ts.map +1 -0
  40. package/dist/assembly/webgpu-ready.d.ts +1 -1
  41. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  42. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -1
  43. package/dist/assembly/webgpu-renderer-observation-owner.d.ts +35 -0
  44. package/dist/assembly/webgpu-renderer-observation-owner.d.ts.map +1 -0
  45. package/dist/assembly/webgpu-renderer.d.ts +1 -1
  46. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  47. package/dist/authoring.mjs +1 -1
  48. package/dist/barrel-distortion.d.ts +47 -0
  49. package/dist/barrel-distortion.d.ts.map +1 -0
  50. package/dist/{chunk-PA5DRJNI.mjs → chunk-ELJFJ2BU.mjs} +2 -2
  51. package/dist/chunk-ELJFJ2BU.mjs.map +1 -0
  52. package/dist/{chunk-ZDQWGQW7.mjs → chunk-HEB564WF.mjs} +4 -4
  53. package/dist/{chunk-ZDQWGQW7.mjs.map → chunk-HEB564WF.mjs.map} +1 -1
  54. package/dist/{chunk-VOZWDM3L.mjs → chunk-MKDIT2EM.mjs} +1307 -188
  55. package/dist/chunk-MKDIT2EM.mjs.map +1 -0
  56. package/dist/{chunk-LEAXOMYJ.mjs → chunk-OCDLEI6I.mjs} +3 -3
  57. package/dist/{chunk-LEAXOMYJ.mjs.map → chunk-OCDLEI6I.mjs.map} +1 -1
  58. package/dist/chunk-ODZWR6JQ.mjs +4927 -0
  59. package/dist/chunk-ODZWR6JQ.mjs.map +1 -0
  60. package/dist/{chunk-3RTCJTH4.mjs → chunk-SHFUFJ5N.mjs} +184 -13
  61. package/dist/chunk-SHFUFJ5N.mjs.map +1 -0
  62. package/dist/{chunk-OO3DRYK7.mjs → chunk-UI5SFODX.mjs} +176 -96
  63. package/dist/chunk-UI5SFODX.mjs.map +1 -0
  64. package/dist/{chunk-PLAXOOX5.mjs → chunk-UINHLCZA.mjs} +561 -79
  65. package/dist/chunk-UINHLCZA.mjs.map +1 -0
  66. package/dist/{chunk-NB2S7ZRX.mjs → chunk-WEX45HKH.mjs} +127 -386
  67. package/dist/chunk-WEX45HKH.mjs.map +1 -0
  68. package/dist/cloud/density.d.ts +55 -0
  69. package/dist/cloud/density.d.ts.map +1 -0
  70. package/dist/cloud/extract.d.ts +25 -0
  71. package/dist/cloud/extract.d.ts.map +1 -0
  72. package/dist/cloud/feature.d.ts +57 -0
  73. package/dist/cloud/feature.d.ts.map +1 -0
  74. package/dist/cloud/frame-context.d.ts +8 -0
  75. package/dist/cloud/frame-context.d.ts.map +1 -0
  76. package/dist/cloud/inspection.d.ts +59 -0
  77. package/dist/cloud/inspection.d.ts.map +1 -0
  78. package/dist/cloud/optics.d.ts +56 -0
  79. package/dist/cloud/optics.d.ts.map +1 -0
  80. package/dist/cloud/parameters.d.ts +73 -0
  81. package/dist/cloud/parameters.d.ts.map +1 -0
  82. package/dist/cloud/resources.d.ts +60 -0
  83. package/dist/cloud/resources.d.ts.map +1 -0
  84. package/dist/cloud/shadow.d.ts +42 -0
  85. package/dist/cloud/shadow.d.ts.map +1 -0
  86. package/dist/cloud/temporal.d.ts +74 -0
  87. package/dist/cloud/temporal.d.ts.map +1 -0
  88. package/dist/components/barrel-distortion.d.ts +13 -0
  89. package/dist/components/barrel-distortion.d.ts.map +1 -0
  90. package/dist/components/cloud-layer.d.ts +29 -0
  91. package/dist/components/cloud-layer.d.ts.map +1 -0
  92. package/dist/components/index.d.ts +2 -0
  93. package/dist/components/index.d.ts.map +1 -1
  94. package/dist/components/instances.d.ts +20 -6
  95. package/dist/components/instances.d.ts.map +1 -1
  96. package/dist/construct-renderer.d.ts +1 -1
  97. package/dist/construct-renderer.d.ts.map +1 -1
  98. package/dist/construct-renderer.mjs +6759 -2875
  99. package/dist/construct-renderer.mjs.map +1 -1
  100. package/dist/dynamic-geometry.d.ts +5 -1
  101. package/dist/dynamic-geometry.d.ts.map +1 -1
  102. package/dist/errors/cloud.d.ts +59 -0
  103. package/dist/errors/cloud.d.ts.map +1 -0
  104. package/dist/errors/render.d.ts +18 -4
  105. package/dist/errors/render.d.ts.map +1 -1
  106. package/dist/examples/single-layer-medium-surface.d.ts +47 -0
  107. package/dist/examples/single-layer-medium-surface.d.ts.map +1 -0
  108. package/dist/extract/camera.d.ts.map +1 -1
  109. package/dist/extract/contracts.d.ts +5 -1
  110. package/dist/extract/contracts.d.ts.map +1 -1
  111. package/dist/features/barrel-distortion.d.ts +29 -0
  112. package/dist/features/barrel-distortion.d.ts.map +1 -0
  113. package/dist/features/error-reporting.d.ts +6 -2
  114. package/dist/features/error-reporting.d.ts.map +1 -1
  115. package/dist/features/host.d.ts +10 -0
  116. package/dist/features/host.d.ts.map +1 -1
  117. package/dist/features/plan.d.ts +8 -0
  118. package/dist/features/plan.d.ts.map +1 -1
  119. package/dist/features/render-graph-contribution.d.ts +5 -0
  120. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  121. package/dist/features/render-graph-raster.d.ts +1 -0
  122. package/dist/features/render-graph-raster.d.ts.map +1 -1
  123. package/dist/features/targets.d.ts +2 -0
  124. package/dist/features/targets.d.ts.map +1 -1
  125. package/dist/features/types.d.ts +44 -0
  126. package/dist/features/types.d.ts.map +1 -1
  127. package/dist/fullscreen-post-process-pass.d.ts +15 -7
  128. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  129. package/dist/gpu-dirty-ranges.d.ts +11 -0
  130. package/dist/gpu-dirty-ranges.d.ts.map +1 -0
  131. package/dist/gpu-driven/batch-topology.d.ts +17 -0
  132. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  133. package/dist/gpu-driven/material-artifacts.d.ts +6 -0
  134. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -1
  135. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  136. package/dist/gpu-driven/production-raster-material.d.ts +178 -0
  137. package/dist/gpu-driven/production-raster-material.d.ts.map +1 -0
  138. package/dist/gpu-driven/production-raster.d.ts +47 -3
  139. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  140. package/dist/gpu-driven/resource-allocation.d.ts +31 -0
  141. package/dist/gpu-driven/resource-allocation.d.ts.map +1 -0
  142. package/dist/gpu-driven/shadow-ownership.d.ts +14 -0
  143. package/dist/gpu-driven/shadow-ownership.d.ts.map +1 -0
  144. package/dist/gpu-driven/view-gpu.d.ts +26 -2
  145. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  146. package/dist/gpu-scene.d.ts +5 -0
  147. package/dist/gpu-scene.d.ts.map +1 -1
  148. package/dist/hdrp-buffers.d.ts +2 -2
  149. package/dist/hdrp-buffers.d.ts.map +1 -1
  150. package/dist/ibl/skylight-bind-group.d.ts +10 -1
  151. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  152. package/dist/index.d.ts +27 -7
  153. package/dist/index.d.ts.map +1 -1
  154. package/dist/index.mjs +258 -8
  155. package/dist/index.mjs.map +1 -1
  156. package/dist/inspection-types.d.ts +135 -1
  157. package/dist/inspection-types.d.ts.map +1 -1
  158. package/dist/instance-buffer-cache.d.ts +4 -4
  159. package/dist/instance-buffer-cache.d.ts.map +1 -1
  160. package/dist/instances.d.ts +48 -100
  161. package/dist/instances.d.ts.map +1 -1
  162. package/dist/internal.mjs +7 -7
  163. package/dist/pbr-pipeline.d.ts +19 -1
  164. package/dist/pbr-pipeline.d.ts.map +1 -1
  165. package/dist/pipeline/single-layer-medium-passes.d.ts +73 -0
  166. package/dist/pipeline/single-layer-medium-passes.d.ts.map +1 -0
  167. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  168. package/dist/pipeline/standard-output/graph.d.ts.map +1 -1
  169. package/dist/pipeline/standard-output/types.d.ts +4 -2
  170. package/dist/pipeline/standard-output/types.d.ts.map +1 -1
  171. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  172. package/dist/pipeline/standard-post.d.ts.map +1 -1
  173. package/dist/pipeline-builder.d.ts +1 -1
  174. package/dist/pipeline-builder.d.ts.map +1 -1
  175. package/dist/plugin.d.ts.map +1 -1
  176. package/dist/prepare/prepared-graphics-resolver.d.ts +2 -0
  177. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  178. package/dist/record/barrel-distortion-frame.d.ts +15 -0
  179. package/dist/record/barrel-distortion-frame.d.ts.map +1 -0
  180. package/dist/record/dynamic-geometry-consumption.d.ts +10 -2
  181. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -1
  182. package/dist/record/fold-buckets.d.ts +95 -0
  183. package/dist/record/fold-buckets.d.ts.map +1 -0
  184. package/dist/record/fold-instance-buffer.d.ts +6 -0
  185. package/dist/record/fold-instance-buffer.d.ts.map +1 -0
  186. package/dist/record/frame-lighting.d.ts +1 -0
  187. package/dist/record/frame-lighting.d.ts.map +1 -1
  188. package/dist/record/frame-snapshot.d.ts +37 -3
  189. package/dist/record/frame-snapshot.d.ts.map +1 -1
  190. package/dist/record/frame.d.ts +10 -61
  191. package/dist/record/frame.d.ts.map +1 -1
  192. package/dist/record/gpu-draw-receipts.d.ts +11 -0
  193. package/dist/record/gpu-draw-receipts.d.ts.map +1 -0
  194. package/dist/record/gpu-timing.d.ts +8 -0
  195. package/dist/record/gpu-timing.d.ts.map +1 -1
  196. package/dist/record/main-pass-geometry.d.ts +21 -5
  197. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  198. package/dist/record/main-pass-material.d.ts +7 -1
  199. package/dist/record/main-pass-material.d.ts.map +1 -1
  200. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  201. package/dist/record/main-pass.d.ts +13 -2
  202. package/dist/record/main-pass.d.ts.map +1 -1
  203. package/dist/record/motion-blur-receipt.d.ts +11 -0
  204. package/dist/record/motion-blur-receipt.d.ts.map +1 -0
  205. package/dist/record/probe-blend-buffer.d.ts +16 -0
  206. package/dist/record/probe-blend-buffer.d.ts.map +1 -0
  207. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  208. package/dist/record/render-context.d.ts +22 -3
  209. package/dist/record/render-context.d.ts.map +1 -1
  210. package/dist/record/shadow-pass.d.ts +2 -0
  211. package/dist/record/shadow-pass.d.ts.map +1 -1
  212. package/dist/record/skybox-post-pass.d.ts +1 -1
  213. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  214. package/dist/record/typed-frame-graph.d.ts +22 -2
  215. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  216. package/dist/record/view-ubo.d.ts +11 -3
  217. package/dist/record/view-ubo.d.ts.map +1 -1
  218. package/dist/record/volume-params.d.ts +9 -0
  219. package/dist/record/volume-params.d.ts.map +1 -0
  220. package/dist/recovery/render-system-candidate.d.ts +10 -0
  221. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  222. package/dist/reflection/record-owner.d.ts.map +1 -1
  223. package/dist/render-contract.d.ts +42 -4
  224. package/dist/render-contract.d.ts.map +1 -1
  225. package/dist/render-graph-primitives.d.ts.map +1 -1
  226. package/dist/render-pipeline.d.ts +103 -2
  227. package/dist/render-pipeline.d.ts.map +1 -1
  228. package/dist/render-system-extract-tail.d.ts +3 -2
  229. package/dist/render-system-extract-tail.d.ts.map +1 -1
  230. package/dist/render-system-extract.d.ts +16 -9
  231. package/dist/render-system-extract.d.ts.map +1 -1
  232. package/dist/render-system-presentation.d.ts +23 -0
  233. package/dist/render-system-presentation.d.ts.map +1 -0
  234. package/dist/render-system-producer-roots.d.ts +10 -0
  235. package/dist/render-system-producer-roots.d.ts.map +1 -0
  236. package/dist/render-system-residency.d.ts +12 -0
  237. package/dist/render-system-residency.d.ts.map +1 -0
  238. package/dist/render-system.d.ts +19 -14
  239. package/dist/render-system.d.ts.map +1 -1
  240. package/dist/scene/probe-blend.d.ts +27 -0
  241. package/dist/scene/probe-blend.d.ts.map +1 -1
  242. package/dist/scene/render-scene.d.ts +12 -5
  243. package/dist/scene/render-scene.d.ts.map +1 -1
  244. package/dist/ssao-buffers.d.ts +9 -0
  245. package/dist/ssao-buffers.d.ts.map +1 -1
  246. package/dist/surface/dynamic-input.d.ts +133 -0
  247. package/dist/surface/dynamic-input.d.ts.map +1 -0
  248. package/dist/surface/gpu-driven.d.ts +41 -0
  249. package/dist/surface/gpu-driven.d.ts.map +1 -0
  250. package/dist/surface/submission-observation.d.ts +136 -0
  251. package/dist/surface/submission-observation.d.ts.map +1 -0
  252. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  253. package/dist/temporal/gpu.d.ts +13 -1
  254. package/dist/temporal/gpu.d.ts.map +1 -1
  255. package/dist/temporal/index.mjs +5 -5
  256. package/dist/temporal/inspection.d.ts.map +1 -1
  257. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  258. package/dist/temporal/target-coverage-attachment.d.ts.map +1 -1
  259. package/dist/transmission/single-layer-medium.d.ts +89 -0
  260. package/dist/transmission/single-layer-medium.d.ts.map +1 -0
  261. package/dist/typed-render-graph-primitives.d.ts +14 -2
  262. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  263. package/dist/volume/component.d.ts +9 -4
  264. package/dist/volume/component.d.ts.map +1 -1
  265. package/dist/volume/extract.d.ts +1 -1
  266. package/dist/volume/extract.d.ts.map +1 -1
  267. package/dist/volume/inspection.d.ts +2 -0
  268. package/dist/volume/inspection.d.ts.map +1 -1
  269. package/dist/volume/passes.d.ts +4 -1
  270. package/dist/volume/passes.d.ts.map +1 -1
  271. package/dist/volume/resources.d.ts.map +1 -1
  272. package/package.json +22 -21
  273. package/src/__tests__/atmosphere-daylight.dawn.test.ts +72 -0
  274. package/src/__tests__/barrel-distortion-lkg.integration.test.ts +353 -0
  275. package/src/__tests__/barrel-distortion.dawn.test.ts +41 -0
  276. package/src/__tests__/barrel-distortion.unit.test.ts +364 -0
  277. package/src/__tests__/construct-renderer-identity.unit.test.ts +49 -0
  278. package/src/__tests__/detached-recovery-graph.unit.test.ts +277 -0
  279. package/src/__tests__/device-loss-fanout.unit.test.ts +36 -1
  280. package/src/__tests__/docs-gate-green.unit.test.ts +112 -1
  281. package/src/__tests__/factory-contract.integration.test.ts +295 -45
  282. package/src/__tests__/feature-depth-input.dawn.test.ts +129 -2
  283. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +14 -3
  284. package/src/__tests__/gpu-dirty-ranges.unit.test.ts +22 -0
  285. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +16 -3
  286. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +12 -0
  287. package/src/__tests__/gpu-driven-material-artifacts.unit.test.ts +143 -0
  288. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +2 -2
  289. package/src/__tests__/gpu-driven-production.integration.test.ts +482 -3
  290. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +6 -5
  291. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +94 -6
  292. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +18 -2
  293. package/src/__tests__/gpu-driven-view.browser.test.ts +5 -8
  294. package/src/__tests__/gpu-driven-view.dawn.test.ts +6 -2
  295. package/src/__tests__/gpu-driven-view.unit.test.ts +1 -1
  296. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +1 -0
  297. package/src/__tests__/gpu-pass-timing.browser.test.ts +6 -4
  298. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +1 -0
  299. package/src/__tests__/gpu-scene.unit.test.ts +119 -9
  300. package/src/__tests__/instance-allocation-failure.unit.test.ts +57 -0
  301. package/src/__tests__/instances-culling.integration.test.ts +10 -36
  302. package/src/__tests__/instances-store.unit.test.ts +97 -305
  303. package/src/__tests__/instances-world-ownership.integration.test.ts +62 -0
  304. package/src/__tests__/main-pass-material-medium-selection.unit.test.ts +146 -0
  305. package/src/__tests__/material-shader-prewarm.unit.test.ts +59 -0
  306. package/src/__tests__/material-snapshot-mutation.integration.test.ts +39 -1
  307. package/src/__tests__/morph-culling-reentry.dawn.test.ts +5 -5
  308. package/src/__tests__/morph-visual-evidence.integration.test.ts +8 -7
  309. package/src/__tests__/motion-blur-compute.dawn.test.ts +13 -7
  310. package/src/__tests__/motion-blur.pixel.integration.test.ts +2 -2
  311. package/src/__tests__/nineslice-handle-surface.unit.test.ts +4 -4
  312. package/src/__tests__/pbr-view-layout.unit.test.ts +9 -0
  313. package/src/__tests__/physical-clearcoat.integration.test.ts +58 -0
  314. package/src/__tests__/prepared-graphics-capability.unit.test.ts +66 -2
  315. package/src/__tests__/probe-bindings.unit.test.ts +582 -1
  316. package/src/__tests__/probe-scene-dirty.integration.test.ts +28 -1
  317. package/src/__tests__/receipt-observation-retirement.unit.test.ts +65 -0
  318. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +69 -0
  319. package/src/__tests__/render-error-code-owner.test-d.ts +7 -0
  320. package/src/__tests__/render-feature-diagnostics.unit.test.ts +15 -2
  321. package/src/__tests__/render-feature-error-reporting.unit.test.ts +21 -1
  322. package/src/__tests__/render-feature-prepared-diagnostics.unit.test.ts +10 -1
  323. package/src/__tests__/render-feature-retry.unit.test.ts +12 -1
  324. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +145 -19
  325. package/src/__tests__/render-scene-projection.unit.test.ts +36 -11
  326. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +20 -20
  327. package/src/__tests__/render-target-probe-soak.perf.test.ts +2 -2
  328. package/src/__tests__/render-temporal-environment-consumer.browser.test.ts +3 -3
  329. package/src/__tests__/render-temporal-environment-consumer.dawn.test.ts +5 -2
  330. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +41 -7
  331. package/src/__tests__/renderer-frame-generation-fence.integration.test.ts +16 -0
  332. package/src/__tests__/renderer-frame-transaction.integration.test.ts +26 -10
  333. package/src/__tests__/resource-allocation.unit.test.ts +123 -0
  334. package/src/__tests__/single-layer-medium-passes.unit.test.ts +275 -0
  335. package/src/__tests__/skin-motion-regression.unit.test.ts +70 -1
  336. package/src/__tests__/skin-palette-recycling.dawn.test.ts +97 -0
  337. package/src/__tests__/skinned-shadow-caster.test.ts +15 -0
  338. package/src/__tests__/ssr-carrier-public-contract.unit.test.ts +21 -0
  339. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +4 -4
  340. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +2 -2
  341. package/src/__tests__/ssr-gpu-dispatch.ts +1 -1
  342. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +6 -2
  343. package/src/__tests__/standard-output-chain-contract.unit.test.ts +28 -0
  344. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +46 -1
  345. package/src/__tests__/standard-pipeline.integration.test.ts +182 -1
  346. package/src/__tests__/surface-dynamic-buffer-lifecycle.unit.test.ts +506 -0
  347. package/src/__tests__/surface-submission-observation.unit.test.ts +812 -0
  348. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +32 -0
  349. package/src/__tests__/temporal-performance.integration.test.ts +3 -3
  350. package/src/__tests__/visibility-instances.integration.test.ts +7 -19
  351. package/src/__tests__/visibility-producer-matrix.integration.test.ts +9 -20
  352. package/src/__tests__/volume-optics-oracle.unit.test.ts +4 -2
  353. package/src/__tests__/volumetric-fog-budget-recovery.integration.test.ts +30 -20
  354. package/src/__tests__/volumetric-fog-errors.unit.test.ts +4 -3
  355. package/src/__tests__/volumetric-fog-multiple.unit.test.ts +55 -0
  356. package/src/__tests__/volumetric-fog-spot-frame-optics.integration.test.ts +1 -1
  357. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +113 -9
  358. package/src/__tests__/volumetric-fog-topology.unit.test.ts +5 -3
  359. package/src/assembly/backend-contract.ts +30 -0
  360. package/src/assembly/barrel-distortion-inspection.ts +22 -0
  361. package/src/assembly/canvas-draw-guard.ts +11 -0
  362. package/src/assembly/cloud-shadow-entry.ts +22 -0
  363. package/src/assembly/dynamic-geometry-host.ts +65 -37
  364. package/src/assembly/dynamic-geometry-runtime.ts +7 -1
  365. package/src/assembly/factory.ts +2 -30
  366. package/src/assembly/host-contract.ts +3 -3
  367. package/src/assembly/material/assembly.ts +25 -13
  368. package/src/assembly/material/pipeline-helpers.ts +53 -12
  369. package/src/assembly/material-shader-policy.ts +55 -0
  370. package/src/assembly/material-shader-prewarm.ts +21 -10
  371. package/src/assembly/recovery/device-loss-fanout.ts +13 -0
  372. package/src/assembly/recovery/renderer-recover.ts +41 -18
  373. package/src/assembly/render-feature-post-processes.ts +54 -0
  374. package/src/assembly/renderer-facade.ts +4 -1
  375. package/src/assembly/renderer-feature-inspection.ts +8 -2
  376. package/src/assembly/renderer-frame-transaction.ts +23 -0
  377. package/src/assembly/renderer-inspection.ts +28 -0
  378. package/src/assembly/skin-palette-owner.ts +1 -1
  379. package/src/assembly/temporal-post-process-prewarm.ts +3 -3
  380. package/src/assembly/webgpu-pbr-ready.ts +9 -1
  381. package/src/assembly/webgpu-ready-per-pass.ts +104 -0
  382. package/src/assembly/webgpu-ready.ts +115 -61
  383. package/src/assembly/webgpu-renderer-bootstrap.ts +0 -12
  384. package/src/assembly/webgpu-renderer-observation-owner.ts +222 -0
  385. package/src/assembly/webgpu-renderer.ts +214 -252
  386. package/src/assembly/webgpu-vertex-layouts.ts +1 -1
  387. package/src/barrel-distortion.ts +247 -0
  388. package/src/cloud/__tests__/cloud-density-parity.dawn.test.ts +173 -0
  389. package/src/cloud/__tests__/cloud-layer-framebuffer-y.dawn.test.ts +265 -0
  390. package/src/cloud/__tests__/cloud-layer.browser.test.ts +432 -0
  391. package/src/cloud/__tests__/cloud-layer.unit.test.ts +634 -0
  392. package/src/cloud/density.ts +600 -0
  393. package/src/cloud/extract.ts +110 -0
  394. package/src/cloud/feature.ts +1810 -0
  395. package/src/cloud/frame-context.ts +42 -0
  396. package/src/cloud/inspection.ts +130 -0
  397. package/src/cloud/optics.ts +285 -0
  398. package/src/cloud/parameters.ts +255 -0
  399. package/src/cloud/resources.ts +122 -0
  400. package/src/cloud/shadow.ts +167 -0
  401. package/src/cloud/temporal.ts +226 -0
  402. package/src/components/barrel-distortion.ts +46 -0
  403. package/src/components/cloud-layer.ts +47 -0
  404. package/src/components/index.ts +2 -0
  405. package/src/components/instances.ts +18 -45
  406. package/src/components/sprite-instances.ts +1 -1
  407. package/src/construct-renderer.ts +1 -1
  408. package/src/dynamic-geometry.ts +12 -1
  409. package/src/errors/cloud.ts +103 -0
  410. package/src/errors/render.ts +44 -7
  411. package/src/examples/single-layer-medium-surface.ts +59 -0
  412. package/src/extract/camera.ts +15 -1
  413. package/src/extract/contracts.ts +5 -0
  414. package/src/features/barrel-distortion.ts +172 -0
  415. package/src/features/error-reporting.ts +26 -6
  416. package/src/features/host.ts +115 -14
  417. package/src/features/plan.ts +8 -0
  418. package/src/features/render-graph-contribution.ts +9 -0
  419. package/src/features/render-graph-raster.ts +45 -3
  420. package/src/features/targets.ts +66 -0
  421. package/src/features/types.ts +47 -0
  422. package/src/fullscreen-post-process-pass.ts +41 -9
  423. package/src/gpu-dirty-ranges.ts +48 -0
  424. package/src/gpu-driven/batch-topology.ts +53 -0
  425. package/src/gpu-driven/material-artifacts.ts +33 -25
  426. package/src/gpu-driven/prepared-draw.ts +12 -4
  427. package/src/gpu-driven/production-raster-material.ts +1894 -0
  428. package/src/gpu-driven/production-raster.ts +825 -784
  429. package/src/gpu-driven/resource-allocation.ts +142 -0
  430. package/src/gpu-driven/shadow-ownership.ts +71 -0
  431. package/src/gpu-driven/view-gpu.ts +164 -10
  432. package/src/gpu-scene.ts +75 -84
  433. package/src/hdrp-buffers.ts +4 -3
  434. package/src/ibl/skylight-bind-group.ts +49 -7
  435. package/src/index.ts +196 -9
  436. package/src/inspection-types.ts +162 -0
  437. package/src/instance-buffer-cache.ts +4 -4
  438. package/src/instances.ts +149 -569
  439. package/src/pbr-pipeline.ts +219 -13
  440. package/src/pipeline/single-layer-medium-passes.ts +267 -0
  441. package/src/pipeline/standard-forward-lane.ts +313 -57
  442. package/src/pipeline/standard-output/graph.ts +8 -2
  443. package/src/pipeline/standard-output/types.ts +12 -1
  444. package/src/pipeline/standard-pipeline.ts +291 -18
  445. package/src/pipeline/standard-post.ts +90 -24
  446. package/src/pipeline-builder.ts +1 -1
  447. package/src/plugin.ts +4 -0
  448. package/src/points-lines/__tests__/points-lines.bench.ts +17 -8
  449. package/src/prepare/prepared-graphics-resolver.ts +2 -0
  450. package/src/record/__tests__/main-pass-geometry.unit.test.ts +30 -0
  451. package/src/record/__tests__/sprite-material-abi.unit.test.ts +1 -1
  452. package/src/record/__tests__/temporal-payload.unit.test.ts +1 -1
  453. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +11 -0
  454. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +1 -0
  455. package/src/record/__tests__/view-ubo-layout.unit.test.ts +4 -3
  456. package/src/record/barrel-distortion-frame.ts +110 -0
  457. package/src/record/dynamic-geometry-consumption.ts +39 -9
  458. package/src/record/fold-buckets.ts +852 -0
  459. package/src/record/fold-instance-buffer.ts +51 -0
  460. package/src/record/frame-lighting.ts +36 -2
  461. package/src/record/frame-snapshot.ts +39 -3
  462. package/src/record/frame.ts +217 -1084
  463. package/src/record/gpu-draw-receipts.ts +38 -0
  464. package/src/record/gpu-timing.ts +48 -5
  465. package/src/record/main-pass-geometry.ts +251 -82
  466. package/src/record/main-pass-material.ts +66 -4
  467. package/src/record/main-pass-sprite-draws.ts +7 -65
  468. package/src/record/main-pass.ts +156 -20
  469. package/src/record/motion-blur-receipt.ts +91 -0
  470. package/src/record/probe-blend-buffer.ts +180 -0
  471. package/src/record/recovery-pipeline.ts +35 -3
  472. package/src/record/render-context.ts +39 -3
  473. package/src/record/shadow-pass.ts +30 -1
  474. package/src/record/skybox-post-pass.ts +28 -4
  475. package/src/record/typed-frame-graph.ts +770 -134
  476. package/src/record/view-ubo.ts +26 -3
  477. package/src/record/volume-params.ts +225 -0
  478. package/src/recovery/render-system-candidate.ts +235 -42
  479. package/src/reflection/record-owner.ts +7 -3
  480. package/src/render-contract.ts +48 -4
  481. package/src/render-graph-primitives.ts +7 -1
  482. package/src/render-pipeline.ts +130 -0
  483. package/src/render-system-extract-tail.ts +57 -61
  484. package/src/render-system-extract.ts +74 -76
  485. package/src/render-system-presentation.ts +56 -0
  486. package/src/render-system-producer-roots.ts +96 -0
  487. package/src/render-system-residency.ts +159 -0
  488. package/src/render-system.ts +347 -363
  489. package/src/scene/probe-blend.ts +96 -4
  490. package/src/scene/render-scene.ts +349 -280
  491. package/src/ssao-buffers.ts +95 -2
  492. package/src/surface/__tests__/dynamic-input.unit.test.ts +374 -0
  493. package/src/surface/__tests__/gpu-driven.unit.test.ts +87 -0
  494. package/src/surface/dynamic-input.ts +658 -0
  495. package/src/surface/gpu-driven.ts +147 -0
  496. package/src/surface/submission-observation.ts +517 -0
  497. package/src/systems/skin-palette-allocator.ts +4 -1
  498. package/src/temporal/gpu.ts +101 -17
  499. package/src/temporal/inspection.ts +7 -1
  500. package/src/temporal/standard-scene-data.ts +5 -0
  501. package/src/temporal/target-coverage-attachment.ts +4 -0
  502. package/src/transmission/__tests__/single-layer-medium.unit.test.ts +149 -0
  503. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +12 -11
  504. package/src/transmission/single-layer-medium.ts +322 -0
  505. package/src/typed-render-graph-primitives.ts +70 -2
  506. package/src/volume/component.ts +10 -9
  507. package/src/volume/extract.ts +11 -5
  508. package/src/volume/inspection.ts +3 -0
  509. package/src/volume/passes.ts +87 -4
  510. package/src/volume/resources.ts +2 -1
  511. package/dist/chunk-3RTCJTH4.mjs.map +0 -1
  512. package/dist/chunk-NB2S7ZRX.mjs.map +0 -1
  513. package/dist/chunk-NNQL7CB3.mjs +0 -1713
  514. package/dist/chunk-NNQL7CB3.mjs.map +0 -1
  515. package/dist/chunk-OO3DRYK7.mjs.map +0 -1
  516. package/dist/chunk-PA5DRJNI.mjs.map +0 -1
  517. package/dist/chunk-PLAXOOX5.mjs.map +0 -1
  518. package/dist/chunk-VOZWDM3L.mjs.map +0 -1
@@ -1,15 +1,23 @@
1
1
  import { encodeAnalyticFogParams } from '../environment/analytic-fog';
2
2
  import type { FogFrame } from '../extract/environment';
3
- import { collectGpuDrivenMaterialArtifacts } from '../gpu-driven/material-artifacts';
4
- import { materialBindingKey, projectMaterialBindingClasses } from '../gpu-driven/material-bindings';
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';
5
14
  import { uploadMaterialUniforms } from './material-uniforms';
6
15
  // @forgeax/engine-runtime - RenderSystem record stage: frame.
7
16
  // Extracted from render-system-record.ts (feat-20260704 M3/w17, pure move).
8
17
 
9
- import { HANDLE_NINESLICE_QUAD, resolveAssetHandle } from '@forgeax/engine-assets-runtime';
18
+ import { resolveAssetHandle } from '@forgeax/engine-assets-runtime';
10
19
  import { Time, type World } from '@forgeax/engine-ecs';
11
20
  import type { RenderReadLease } from '@forgeax/engine-ecs/projection';
12
- import { deriveVertexCount } from '@forgeax/engine-geometry';
13
21
  import { frustum, mat4, vec3 } from '@forgeax/engine-math';
14
22
  import {
15
23
  type CurrentFrameObservationLease,
@@ -25,8 +33,12 @@ import {
25
33
  type TextureFormat,
26
34
  type TextureView,
27
35
  } from '@forgeax/engine-rhi';
28
- import type { MaterialRenderState, MeshAsset } from '@forgeax/engine-types';
36
+ import type { MaterialShaderArtifact } from '@forgeax/engine-shader';
37
+ import type { MeshAsset } from '@forgeax/engine-types';
29
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';
30
42
  import { CUBE_CAMERA_FACE_ORDER } from '../components/cube-camera';
31
43
  import type { DirectionalShadowQuality } from '../components/directional-shadow-filter';
32
44
  import type { MeshGpuHandles } from '../device/gpu-residency';
@@ -35,7 +47,7 @@ import {
35
47
  type ObservationUnavailableReason,
36
48
  RendererOperationError,
37
49
  } from '../errors/render';
38
- import { gpuDrivenDrawKey, gpuDrivenSourceDrawItemIndex } from '../extract/gpu-driven';
50
+ import { BARREL_DISTORTION_POST_PROCESS_ID } from '../features/barrel-distortion';
39
51
  import {
40
52
  DEPTH_OF_FIELD_MSAA_POST_PROCESS_IDS,
41
53
  DEPTH_OF_FIELD_POST_PROCESS_IDS,
@@ -47,15 +59,8 @@ import {
47
59
  import type { GpuDrivenProduction, PreparedGpuDrivenFrame } from '../gpu-driven/production-raster';
48
60
  import type { ShadowViewIdentity } from '../gpu-driven/shadow-views';
49
61
  import type { GpuDrivenLodSubmitIdentity } from '../gpu-driven/view-gpu';
50
- import { GpuBuffer } from '../gpu-resource';
51
62
  import { GPU_TEXTURE_USAGE_COPY_SRC } from '../gpu-texture-usage';
52
- import {
53
- GPU_BUFFER_USAGE_COPY_DST,
54
- GPU_BUFFER_USAGE_MAP_READ,
55
- GPU_BUFFER_USAGE_UNIFORM,
56
- GPU_BUFFER_USAGE_VERTEX,
57
- } from '../gpu-usage';
58
- import { disposeTransientInstanceBuffers } from '../instance-buffer-cache';
63
+ import { GPU_BUFFER_USAGE_COPY_DST, GPU_BUFFER_USAGE_MAP_READ } from '../gpu-usage';
59
64
  import { deriveRenderExtent } from '../pipeline/render-extent';
60
65
  import { inspectStandardLighting } from '../pipeline/standard-lighting/inspection';
61
66
  import { standardLightingTopologySignature } from '../pipeline/standard-lighting/topology';
@@ -71,7 +76,6 @@ import type {
71
76
  DispatchEntry,
72
77
  ExtractedLights,
73
78
  ExtractedVolumetricFog,
74
- MaterialSnapshot,
75
79
  RenderableSnapshot,
76
80
  ShadowCasterMembership,
77
81
  SkyboxSnapshot,
@@ -89,14 +93,10 @@ import {
89
93
  type SsrSpatialAdmission,
90
94
  } from '../ssr/admission';
91
95
  import { createSsrHistoryOwner } from '../ssr/history';
92
- import {
93
- getTransparentSortConfig,
94
- TRANSPARENT_SORT_MODE_LAYER_Y,
95
- TRANSPARENT_SORT_MODE_LAYER_Z,
96
- } from '../systems/transparent-sort-config';
96
+ import type { SurfaceDynamicInputFrame } from '../surface/dynamic-input';
97
97
  import type { TransmissionDemand } from '../transmission/projection';
98
98
  import { hasVolumetricFogCapability } from '../volume/capability';
99
- import { inspectVolumetricFogResources, VOLUMETRIC_FOG_PARAMS_BYTES } from '../volume/resources';
99
+ import { inspectVolumetricFogResources } from '../volume/resources';
100
100
  import { resolveVolumeTemporalReset, type VolumeTemporalSignature } from '../volume/temporal';
101
101
  import {
102
102
  buildPerFrameBindGroups,
@@ -114,7 +114,6 @@ import {
114
114
  type DispatchCounts,
115
115
  type FrameObservationSource,
116
116
  getTextureIdentity,
117
- instanceCollectionCacheKey,
118
117
  makeZeroCameraFallbackSnapshot,
119
118
  type ReflectionFallbackReadbackRequest,
120
119
  type RenderFrameState,
@@ -122,28 +121,22 @@ import {
122
121
  validateGraphTargetCaptureReadback,
123
122
  worldEntityKey,
124
123
  } from './frame-snapshot';
125
- // feat-20260622-chunk-gpu-instancing-sprite-tilemap M1 / w4 (D-1 record-stage
126
- // fold operator). Pure linear-scan helper that groups transparent-dispatch
127
- // entries with equal (Layer.value, sortKey, materialHandle) into FoldBucket
128
- // descriptors. The drawIndexed swap (1 instanced draw per bucket vs N
129
- // per-entity draws) hooks into the sprite-pass dispatch loop below
130
- // (w4-record-swap), using {@link buildFoldDispatchPlan} to translate
131
- // renderableIndex-keyed buckets into validatedOrdered-index-keyed
132
- // head/skip maps.
133
- import {
134
- buildFoldDispatchPlan,
135
- evaluateFoldBucketUniformCap,
136
- type FoldBucket,
137
- type FoldDispatchPlan,
138
- foldDispatchBuckets,
139
- incrementFoldedDrawsMetric,
140
- } from './mesh-ssbo';
124
+ import { emitGpuDrivenDrawReceipts, type RenderableDrawReceipt } from './gpu-draw-receipts';
125
+ import { ensureProbeBlendRecordBuffer } from './probe-blend-buffer';
141
126
  import type {
142
127
  _InternalRenderPipelineContext,
143
128
  PipelineState,
144
129
  RenderSystemInternals,
145
130
  } from './render-context';
146
-
131
+ import { prepareVolumetricFogFrame, stageVolumetricFogParams } from './volume-params';
132
+
133
+ export {
134
+ buildDispatchPlan,
135
+ buildMaterialSlotPlan,
136
+ findRenderablePrefixForSlotCapacity,
137
+ materialSlotCountForPrefix,
138
+ validateRenderables,
139
+ } from './fold-buckets';
147
140
  export type { FrameObservationSource } from './frame-snapshot';
148
141
 
149
142
  export type FrameObservationReadback = (
@@ -158,35 +151,10 @@ export type FrameObservationReadback = (
158
151
  function residencyValidationCacheKey(world: World, worlds: readonly World[]): string {
159
152
  return [world.identity, ...worlds.map((candidate) => candidate.identity)].join('\u001f');
160
153
  }
161
-
162
154
  function renderableIdentity(snapshot: Pick<RenderableSnapshot, 'worldId' | 'entityKey'>): string {
163
155
  return `${snapshot.worldId}:${snapshot.entityKey}`;
164
156
  }
165
157
 
166
- /**
167
- * A GPU-owned indirect draw has no CPU submit callback. Keep the same
168
- * record-stage consumption proof for downstream lifecycle owners by checking
169
- * the concrete draw-key projection that was actually encoded for this frame.
170
- */
171
- function gpuDrivenRenderableConsumed(
172
- entry: ValidatedRenderable,
173
- drawKeys: ReadonlySet<string>,
174
- ): boolean {
175
- const draws = entry.source.gpuDrivenDraws;
176
- if (draws === undefined || draws.length === 0) return false;
177
- const worldEntity = worldEntityKey(entry.source.worldId, entry.source.entityKey);
178
- return draws.some((draw, compactIndex) => {
179
- const material = entry.source.materials[draw.materialSlot] ?? entry.source.material;
180
- return drawKeys.has(
181
- gpuDrivenDrawKey(
182
- worldEntity,
183
- material.materialHandle ?? -1,
184
- gpuDrivenSourceDrawItemIndex(draw, compactIndex),
185
- ),
186
- );
187
- });
188
- }
189
-
190
158
  /**
191
159
  * Project one shared validation result into a cull-specific renderable index
192
160
  * domain. The retained ShadowCaster projection is the superset; the primary
@@ -317,6 +285,10 @@ import {
317
285
  } from '../features/motion-blur/motion-blur-params';
318
286
  import { createTemporalView, resolveTemporalReset, type TemporalView } from '../temporal/view';
319
287
  import { inspectVolumetricFog } from '../volume/inspection';
288
+ import {
289
+ packBarrelDistortionParams,
290
+ resolveSubmittedBarrelMapping,
291
+ } from './barrel-distortion-frame';
320
292
  import {
321
293
  acquireSwapChainTarget,
322
294
  graphExecutionPhase,
@@ -329,12 +301,11 @@ import {
329
301
  computeViewMatrix,
330
302
  driveLazyEquirectProjection,
331
303
  selectLazyEquirectHandle,
332
- variantSetFromDefines,
333
304
  warnMultiSkybox,
334
305
  warnMultiSkylight,
335
306
  } from './helpers';
336
307
  import { computeSplitLdrSprite } from './main-pass-sprite-draws';
337
- import { cleanPerEntityCache, ensureMeshSsboCapacity, uploadMeshSsboBatch } from './mesh-ssbo';
308
+ import { uploadMeshSsboBatch } from './mesh-ssbo';
338
309
  import { writeShadowCasterUniforms } from './shadow-pass';
339
310
  import {
340
311
  type CubeCaptureGraphState,
@@ -342,6 +313,7 @@ import {
342
313
  executeCompiledFrameGraph,
343
314
  getRenderFeatureGraphState,
344
315
  type RenderFeatureGraphCandidate,
316
+ settleBarrelDistortionGraphCandidate,
345
317
  settleDepthOfFieldGraphCandidate,
346
318
  settleVolumetricFogGraphCandidate,
347
319
  } from './typed-frame-graph';
@@ -405,110 +377,6 @@ export function gpuDrivenMeshesForFrame(
405
377
  return meshes;
406
378
  }
407
379
 
408
- /**
409
- * A frame-local material table. Repeated snapshot objects share one UBO slot;
410
- * each renderable keeps only the slot index used by each authored material.
411
- */
412
- export interface MaterialSlotPlan<T extends object> {
413
- readonly slotIndices: readonly (readonly number[])[];
414
- readonly slots: readonly T[];
415
- /** Index of the first renderable that referenced each slot. */
416
- readonly slotOwners: readonly number[];
417
- }
418
-
419
- /**
420
- * Intern identical material snapshots into one frame-local slot table.
421
- * Snapshot identity is the extract layer's invalidation token, so this keeps
422
- * the same correctness boundary while avoiding duplicate payload assembly and
423
- * upload for repeated scene instances.
424
- */
425
- export function buildMaterialSlotPlan<T extends object>(
426
- materialGroups: readonly (readonly T[])[],
427
- bindingClass?: (material: T, owner: number) => string,
428
- ): MaterialSlotPlan<T> {
429
- const slotByMaterial = new Map<T, Map<string, number>>();
430
- const slots: T[] = [];
431
- const slotOwners: number[] = [];
432
- const slotIndices = materialGroups.map((materials, ownerIndex) =>
433
- materials.map((material) => {
434
- const resourceClass = bindingClass?.(material, ownerIndex) ?? '';
435
- let materialClasses = slotByMaterial.get(material);
436
- if (materialClasses === undefined) {
437
- materialClasses = new Map();
438
- slotByMaterial.set(material, materialClasses);
439
- }
440
- const cached = materialClasses.get(resourceClass);
441
- if (cached !== undefined) return cached;
442
- const slot = slots.length;
443
- slots.push(material);
444
- slotOwners.push(ownerIndex);
445
- materialClasses.set(resourceClass, slot);
446
- return slot;
447
- }),
448
- );
449
- return { slotIndices, slots, slotOwners };
450
- }
451
-
452
- /**
453
- * Return the largest complete renderable prefix supported by a shared
454
- * mesh/material capacity. Mesh rows consume one slot per renderable; material
455
- * rows use the deduplicated slot indices assigned in first-seen order.
456
- */
457
- export function findRenderablePrefixForSlotCapacity(
458
- materialSlotIndices: readonly (readonly number[])[],
459
- slotCapacity: number,
460
- ): number {
461
- const capacity = Math.max(0, Math.floor(slotCapacity));
462
- const maxRenderableCount = Math.min(materialSlotIndices.length, capacity);
463
- let renderableCount = 0;
464
- while (renderableCount < maxRenderableCount) {
465
- const slots = materialSlotIndices[renderableCount] ?? [];
466
- if (slots.some((slot) => slot >= capacity)) break;
467
- renderableCount += 1;
468
- }
469
- return renderableCount;
470
- }
471
-
472
- /** Count material slots reachable from a prefix of a first-seen slot plan. */
473
- export function materialSlotCountForPrefix(
474
- materialSlotIndices: readonly (readonly number[])[],
475
- renderableCount: number,
476
- ): number {
477
- let maxSlot = -1;
478
- for (let index = 0; index < renderableCount; index += 1) {
479
- for (const slot of materialSlotIndices[index] ?? []) maxSlot = Math.max(maxSlot, slot);
480
- }
481
- return maxSlot + 1;
482
- }
483
-
484
- type MeshSsboCapacityResult = ReturnType<typeof ensureMeshSsboCapacity>;
485
-
486
- /**
487
- * Resolve the actual mesh-SSBO ceiling after the shared grow contract ran.
488
- * A missing grow controller is the legacy/test surface with no renderer-owned
489
- * ceiling; a failed grow explicitly exposes the pre-grow capacity and must
490
- * constrain every row written by this frame, including retained shadow rows.
491
- */
492
- function meshSsboSlotCapacityAfterEnsure(
493
- internals: Pick<RenderSystemInternals, 'growMeshSsbo' | 'meshSsboState'>,
494
- neededSlots: number,
495
- result: MeshSsboCapacityResult,
496
- ): number {
497
- if (internals.growMeshSsbo === undefined) return Number.POSITIVE_INFINITY;
498
- if (!result.ok) return Math.max(0, result.degradedToSlotCount);
499
- return internals.meshSsboState?.slotCount ?? neededSlots;
500
- }
501
-
502
- /**
503
- * Keep a row sequence inside the capacity published by the mesh-SSBO owner.
504
- * The caller has already reserved earlier rows (the primary view), so this
505
- * helper only accepts the remaining slots for the retained shadow sequence.
506
- */
507
- function limitRowsToMeshSsboCapacity<T>(rows: readonly T[], availableSlots: number): readonly T[] {
508
- if (availableSlots === Number.POSITIVE_INFINITY) return rows;
509
- return rows.slice(0, Math.max(0, Math.floor(availableSlots)));
510
- }
511
-
512
380
  export type RecordProfileRunner = <T>(phase: RenderRecordPhase, action: () => T) => T;
513
381
 
514
382
  export interface CubeCaptureFrameInput {
@@ -738,46 +606,6 @@ async function mapReflectionFallbackReadback(
738
606
 
739
607
  const VOLUMETRIC_FOG_PASS_COUNT = 4;
740
608
 
741
- type VolumetricFogParamsSlot = 0 | 1;
742
-
743
- function nextVolumetricFogParamsSlot(frameState: RenderFrameState): VolumetricFogParamsSlot {
744
- const accepted = frameState.volumetricFogParamsAcceptedSlot;
745
- const pending = frameState.volumetricFogParamsPendingSlot;
746
- if (pending !== null && pending !== accepted) return pending;
747
- return accepted === null ? 0 : accepted === 0 ? 1 : 0;
748
- }
749
-
750
- function stageVolumetricFogParams(
751
- internals: RenderSystemInternals,
752
- frameState: RenderFrameState,
753
- params: Float32Array,
754
- ): Buffer | undefined {
755
- const slot = nextVolumetricFogParamsSlot(frameState);
756
- let buffer = frameState.volumetricFogParamsBuffers[slot];
757
- if (buffer === null) {
758
- const created = internals.device.createBuffer({
759
- label: `volumetric-fog-params-${slot}`,
760
- size: VOLUMETRIC_FOG_PARAMS_BYTES,
761
- usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST,
762
- mappedAtCreation: false,
763
- });
764
- if (!created.ok) {
765
- internals.errorRegistry.fire(created.error);
766
- return undefined;
767
- }
768
- buffer = created.value;
769
- frameState.volumetricFogParamsBuffers[slot] = buffer;
770
- }
771
- const written = internals.device.queue.writeBuffer(buffer, 0, params);
772
- if (!written.ok) {
773
- internals.errorRegistry.fire(written.error);
774
- return undefined;
775
- }
776
- frameState.volumetricFogParamsPendingSlot = slot;
777
- frameState.volumetricFogPendingParams = new Float32Array(params);
778
- return buffer;
779
- }
780
-
781
609
  /** Retire parameter buffers only after the submit that stopped using them. */
782
610
  function retireVolumetricFogParams(
783
611
  internals: RenderSystemInternals,
@@ -814,6 +642,19 @@ function volumeRevision(values: readonly number[]): number {
814
642
  return hash;
815
643
  }
816
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
+
817
658
  function volumeTemporalSignature(
818
659
  fog: ExtractedVolumetricFog,
819
660
  prepared: VolumetricFogFrameContext,
@@ -835,7 +676,7 @@ function volumeTemporalSignature(
835
676
  camera.orthoBottom,
836
677
  camera.orthoTop,
837
678
  ]);
838
- const fogRevision =
679
+ let fogRevision =
839
680
  authored === undefined
840
681
  ? 0
841
682
  : volumeRevision([
@@ -846,6 +687,8 @@ function volumeTemporalSignature(
846
687
  ...authored.emission,
847
688
  authored.anisotropy,
848
689
  authored.maxDistance,
690
+ authored.sampling === 'density' ? 1 : 0,
691
+ ...Array.from(`${fog.guid}:${fog.digest}`, (char) => char.charCodeAt(0)),
849
692
  ]);
850
693
  const selectedSpot =
851
694
  fog.spotLightEntity === undefined
@@ -863,7 +706,7 @@ function volumeTemporalSignature(
863
706
  fog.lightKind === 'directional' && directional?.entity === fog.lightEntity
864
707
  ? directional
865
708
  : undefined;
866
- const lightRevision =
709
+ let lightRevision =
867
710
  selectedSpot === undefined && selectedPoint === undefined && selectedDirectional === undefined
868
711
  ? 0
869
712
  : volumeRevision(
@@ -902,12 +745,30 @@ function volumeTemporalSignature(
902
745
  selectedSpot.normalBias ?? 0.05,
903
746
  selectedSpot.pcfKernelSize ?? 3,
904
747
  selectedSpot.shadowAtlasTile,
748
+ Number(selectedSpot.projectorHandle ?? 0),
749
+ selectedSpot.projectorGeneration ?? 0,
750
+ selectedSpot.projectorRevision ?? 0,
905
751
  ...(selectedSpot.lightViewProj === undefined
906
752
  ? []
907
753
  : [...selectedSpot.lightViewProj]),
908
754
  ]),
909
755
  ],
910
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
+ }
911
772
  return {
912
773
  cameraRevision,
913
774
  fogRevision,
@@ -969,137 +830,6 @@ function resolveVolumetricFogHistoryContext(
969
830
  };
970
831
  }
971
832
 
972
- /**
973
- * Resolve the authored density POD into the renderer-owned GPU resources used
974
- * by the graph. This is deliberately a pull operation: a missing/stale
975
- * handle or failed upload produces a structured error and leaves the graph
976
- * candidate disabled for this frame rather than binding a synthetic density.
977
- */
978
- function prepareVolumetricFogFrame(
979
- internals: RenderSystemInternals,
980
- frameState: RenderFrameState,
981
- world: World,
982
- worlds: readonly World[],
983
- fog: ExtractedVolumetricFog | undefined,
984
- lights: ExtractedLights,
985
- volumeCapability: boolean,
986
- projector: SpotLightProjectorFrameContext | undefined,
987
- ): VolumetricFogFrameContext | undefined {
988
- if (
989
- !volumeCapability ||
990
- fog?.status !== 'available' ||
991
- fog.fog === undefined ||
992
- fog.densityHandle === undefined ||
993
- fog.densityAsset === undefined ||
994
- (fog.lightKind === 'spot'
995
- ? lights.spot.find((light) => light.entity === fog.lightEntity) === undefined
996
- : fog.lightKind === 'point'
997
- ? lights.point.find((light) => light.entity === fog.lightEntity) === undefined
998
- : lights.directional === undefined)
999
- ) {
1000
- return undefined;
1001
- }
1002
- const ownerWorld = worlds[fog.worldId ?? 0] ?? world;
1003
- const resident = internals.gpuStore.ensureResident(
1004
- fog.densityHandle,
1005
- fog.densityAsset,
1006
- ownerWorld,
1007
- );
1008
- if (!resident.ok || !('texture' in resident.value)) {
1009
- if (!resident.ok) {
1010
- internals.errorRegistry.fire(
1011
- resident.error instanceof RhiError
1012
- ? resident.error
1013
- : new RhiError({
1014
- code: 'webgpu-runtime-error',
1015
- expected: 'volumetric density residency to resolve a GPU texture',
1016
- hint: 'repair the authored TextureAsset payload and retry the next frame',
1017
- detail: { error: resident.error },
1018
- }),
1019
- );
1020
- }
1021
- return undefined;
1022
- }
1023
- const params = new Float32Array(36);
1024
- params.set(fog.fog.bounds.min, 0);
1025
- params.set(fog.fog.bounds.max, 4);
1026
- params.set(fog.fog.extinction, 8);
1027
- params.set(fog.fog.albedo, 12);
1028
- params.set(fog.fog.emission, 16);
1029
- const selectedPoint =
1030
- fog.pointLightEntity === undefined
1031
- ? undefined
1032
- : lights.point.find((light) => light.entity === fog.pointLightEntity);
1033
- const selectedSpot =
1034
- fog.spotLightEntity === undefined
1035
- ? fog.lightKind === 'spot'
1036
- ? lights.spot.find((light) => light.entity === fog.lightEntity)
1037
- : undefined
1038
- : lights.spot.find((light) => light.entity === fog.spotLightEntity);
1039
- const lightDirection =
1040
- selectedSpot?.direction ?? lights.directional?.direction ?? vec3.create(0, -1, 0);
1041
- const lightColor = selectedSpot?.color ?? lights.directional?.color ?? vec3.create(0, 0, 0);
1042
- if (lightDirection === undefined || lightColor === undefined) return undefined;
1043
- params.set(lightDirection, 20);
1044
- // Reserved padding: render frame identity must not move volume samples.
1045
- params[23] = 0;
1046
- const selectedPointIndex =
1047
- selectedPoint === undefined
1048
- ? -1
1049
- : lights.point.findIndex((light) => light.entity === selectedPoint.entity);
1050
- const selectedSpotIndex =
1051
- selectedSpot === undefined
1052
- ? -1
1053
- : lights.spot.findIndex((light) => light.entity === selectedSpot.entity);
1054
- // Cluster light_data is one point/spot/rect array. Point slots are emitted
1055
- // first, so volume stores the global slot rather than a per-kind array index.
1056
- const selectedSpotClusterIndex =
1057
- selectedSpotIndex < 0 ? -1 : lights.point.length + selectedSpotIndex;
1058
- params.set(lightColor, 24);
1059
- params[19] = selectedPointIndex;
1060
- params[27] = selectedSpotClusterIndex;
1061
- params[28] = fog.fog.maxDistance;
1062
- params[29] = fog.fog.anisotropy;
1063
- params[30] =
1064
- selectedPoint !== undefined && selectedSpot !== undefined
1065
- ? 3
1066
- : selectedPoint !== undefined
1067
- ? 1
1068
- : selectedSpot !== undefined
1069
- ? 2
1070
- : 0;
1071
- // The temporal shader reads this lane after the record stage resolves the
1072
- // accepted graph/slot. It is overwritten on the next queue-visible frame;
1073
- // the first candidate deliberately starts with history disabled.
1074
- params[31] =
1075
- frameState.temporalFrame !== undefined && frameState.temporalFrame.resetReason === 'none'
1076
- ? 1
1077
- : 0;
1078
- // World elapsed time is the sole authored-medium clock. The adjacent
1079
- // frame phase above remains the deterministic sub-froxel jitter identity;
1080
- // it never advances density motion.
1081
- params[32] = fog.worldTimeSeconds ?? 0;
1082
-
1083
- const paramsBuffer = stageVolumetricFogParams(internals, frameState, params);
1084
- if (paramsBuffer === undefined) return undefined;
1085
- return {
1086
- densityTexture: resident.value.texture.handle,
1087
- densityView: resident.value.view,
1088
- paramsBuffer,
1089
- densityGeneration: resident.value.receipt.generation,
1090
- historyReadSlot: null,
1091
- historyWriteSlot: 0,
1092
- historyValid: false,
1093
- ...(projector === undefined
1094
- ? {}
1095
- : {
1096
- projectorTexture: projector.texture,
1097
- projectorView: projector.view,
1098
- projectorSampler: projector.sampler,
1099
- }),
1100
- };
1101
- }
1102
-
1103
833
  /**
1104
834
  * Resolve the SpotLight-owned cookie independently of volumetric fog. Surface
1105
835
  * lighting must keep the authored projector when the fog component is absent;
@@ -1673,6 +1403,10 @@ export function recordFrame(
1673
1403
  readonly telemetryCandidateCount?: number;
1674
1404
  readonly telemetrySubmit?: GpuDrivenLodSubmitIdentity;
1675
1405
  readonly occlusion?: OcclusionFrameProjection;
1406
+ readonly frameTime?: number;
1407
+ readonly deviceGeneration?: number;
1408
+ readonly surfaceDynamicInput?: SurfaceDynamicInputFrame;
1409
+ readonly geometryLane?: 'automatic' | 'direct';
1676
1410
  },
1677
1411
  renderReadLeases?: readonly RenderReadLease[],
1678
1412
  featureGraphCandidate?: RenderFeatureGraphCandidate,
@@ -1691,10 +1425,15 @@ export function recordFrame(
1691
1425
  shadowCasterWorldKeys?: readonly number[],
1692
1426
  preparedLighting?: ReturnType<typeof prepareFrameLighting>,
1693
1427
  ssrDependencies?: SsrDependenciesInspection,
1694
- onRenderableDraw?: (entry: ValidatedRenderable) => void,
1428
+ onRenderableDraw?: (
1429
+ entry: ValidatedRenderable,
1430
+ submeshIndex?: number,
1431
+ receipt?: RenderableDrawReceipt,
1432
+ ) => void,
1695
1433
  analyticFog?: FogFrame,
1696
1434
  sampleTimeSeconds?: number,
1697
1435
  temporalReset = false,
1436
+ cloudLayer?: ExtractedCloudLayer,
1698
1437
  ): boolean {
1699
1438
  void shadowCasterEntityKeys;
1700
1439
  frameState.reflectionFallbackObservationSource = undefined;
@@ -1905,6 +1644,7 @@ export function recordFrame(
1905
1644
  lights,
1906
1645
  volumetricFog,
1907
1646
  );
1647
+ const cloudShadowProjection = cloudShadowProjectionFor(cloudLayer);
1908
1648
 
1909
1649
  const preparedVolumetricFog = prepareVolumetricFogFrame(
1910
1650
  internals,
@@ -1951,21 +1691,29 @@ export function recordFrame(
1951
1691
  (volumetricFog?.status === 'degraded' && acceptedFallback !== undefined);
1952
1692
 
1953
1693
  let preparedGpuDriven: PreparedGpuDrivenFrame | undefined;
1694
+ let acceptedMaterialArtifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined;
1695
+ let acceptedShadowMaterialArtifacts: ReadonlyMap<string, MaterialShaderArtifact> | undefined;
1954
1696
  let materialBindingClasses: ReadonlyMap<string, string> | undefined;
1955
1697
  const abortPreparedGpuDriven = (): void => {
1956
1698
  preparedGpuDriven?._abortResourceReplacement?.();
1957
1699
  };
1958
1700
  const cubeCaptureWork = prepareCubeCapture(internals, cubeCapture, camera);
1959
- 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;
1960
1711
  // Capture views have independent draw ownership; display-camera GPU work
1961
1712
  // remains active while their faces and SSR fallback attachments are recorded.
1962
- // ProbeBlendRecord is an object-level fragment ABI. The scene-index GPU
1963
- // lane has no binding for that record yet, so claiming a probe-bearing
1964
- // row would silently fall back to Skylight and make local diffuse probes
1965
- // visually ineffective. Keep the whole frame on the CPU producer until
1966
- // the scene-index lane carries the same record contract.
1967
- const probeBlendFrame = renderables.some((entry) => entry.probeBlendRecord !== undefined);
1968
- 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.
1969
1717
 
1970
1718
  // Validate first so user meshes are resident in the same frame. The GPU
1971
1719
  // owner consumes this producer-owned map together with the builtin mesh
@@ -2090,16 +1838,37 @@ export function recordFrame(
2090
1838
  frameState.installedPipelineConfig?.ssao?.enabled === true),
2091
1839
  reflectionFallback: frameState.reflectionFallbackDemand === true,
2092
1840
  });
1841
+ acceptedMaterialArtifacts = materialArtifacts;
1842
+ acceptedShadowMaterialArtifacts = shadowMaterialArtifacts;
2093
1843
  const paletteUploadBytes = validatedForResidency.reduce(
2094
1844
  (total, row) => total + (row.source.skin?.uploadBytes ?? 0),
2095
1845
  0,
2096
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;
2097
1857
  const prepared = frameGpuDriven.owner.prepare({
2098
1858
  scene: frameGpuDriven.scene,
2099
1859
  camera,
2100
1860
  meshBySlot: gpuDrivenMeshes,
2101
1861
  viewBindGroupLayout: pipelineState.viewBindGroupLayout,
2102
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',
2103
1872
  materialBindingClasses,
2104
1873
  ...(frameGpuDriven.activeEntityKeys === undefined
2105
1874
  ? {}
@@ -2114,6 +1883,13 @@ export function recordFrame(
2114
1883
  ? {}
2115
1884
  : { telemetrySubmit: frameGpuDriven.telemetrySubmit }),
2116
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 }),
2117
1893
  ...(frameGpuDriven.scene?.structureMetrics === undefined
2118
1894
  ? {}
2119
1895
  : {
@@ -2126,6 +1902,9 @@ export function recordFrame(
2126
1902
  ...(firstSkinArtifact === undefined ? {} : { materialSkinArtifact: firstSkinArtifact }),
2127
1903
  ...(materialArtifacts.size === 0 ? {} : { materialArtifacts }),
2128
1904
  ...(shadowMaterialArtifacts.size === 0 ? {} : { shadowMaterialArtifacts }),
1905
+ ...(gpuProbeBlendBuffer === undefined
1906
+ ? {}
1907
+ : { probeBlendRecordBuffer: gpuProbeBlendBuffer }),
2129
1908
  materialPipelineState: pipelineState,
2130
1909
  ...(internals.getMaterialShaderPipelineEntry === undefined
2131
1910
  ? {}
@@ -2186,7 +1965,12 @@ export function recordFrame(
2186
1965
  }
2187
1966
  }
2188
1967
 
2189
- 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;
2190
1974
  // The GPU lane owns only the primary opaque raster today. Directional,
2191
1975
  // point, and spot shadow views consume the same CPU-validated rows; they do
2192
1976
  // not require a second residency walk because the first validation already
@@ -2203,6 +1987,7 @@ export function recordFrame(
2203
1987
  validated,
2204
1988
  gpuOwnedDrawKeys,
2205
1989
  validationInput.cacheKey,
1990
+ rasterGpuDriven?.worldKeys,
2206
1991
  );
2207
1992
  const cacheValidated =
2208
1993
  validatedShadowForResidency === undefined
@@ -2245,7 +2030,7 @@ export function recordFrame(
2245
2030
  Math.max(1, internals.canvas.width),
2246
2031
  Math.max(1, internals.canvas.height),
2247
2032
  graphShadowMapSize,
2248
- preparedGpuDriven,
2033
+ rasterGpuDriven,
2249
2034
  featureGraphCandidate,
2250
2035
  cubeCapture?.state,
2251
2036
  cameras.length === 0,
@@ -2257,7 +2042,13 @@ export function recordFrame(
2257
2042
  undefined,
2258
2043
  ssrAdmission,
2259
2044
  renderExtent,
2045
+ validated.some((entry) =>
2046
+ entry.source.materials.some(
2047
+ (material) => material.surfaceModel === 'single-layer-medium',
2048
+ ),
2049
+ ),
2260
2050
  analyticFog !== undefined && analyticFog.density > 0 && analyticFog.maxOpacity > 0,
2051
+ cloudShadowProjection?.resolution,
2261
2052
  ),
2262
2053
  );
2263
2054
  if (preflightGraph === null) {
@@ -2334,6 +2125,13 @@ export function recordFrame(
2334
2125
  const view = swapTarget.view;
2335
2126
  const targetW = swapTarget.targetW;
2336
2127
  const targetH = swapTarget.targetH;
2128
+ const submittedBarrelMapping = resolveSubmittedBarrelMapping(
2129
+ frameState,
2130
+ preflightGraph,
2131
+ camera,
2132
+ targetW,
2133
+ targetH,
2134
+ );
2337
2135
 
2338
2136
  const previousTemporalView = frameState.lastSuccessfulTemporalView;
2339
2137
  const motionBlurDemand =
@@ -2642,6 +2440,8 @@ export function recordFrame(
2642
2440
  // View UBO + CSM/spot-shadow matrix pack: assembled + uploaded in one
2643
2441
  // queue.writeBuffer round-trip. Extracted to view-ubo.ts (M3/w18) so
2644
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.
2645
2445
  writeViewUbo(
2646
2446
  internals.device.queue,
2647
2447
  pipelineState.viewUniformBuffer,
@@ -2651,6 +2451,7 @@ export function recordFrame(
2651
2451
  frameState.spotShadowSnapshots,
2652
2452
  temporalView,
2653
2453
  preparedSpotLightProjector?.spotIndex,
2454
+ cloudShadowProjection,
2654
2455
  );
2655
2456
  if (pipelineState.pointsLinesViewBuffer !== undefined) {
2656
2457
  writePointsLinesViewUbo(
@@ -2722,7 +2523,7 @@ export function recordFrame(
2722
2523
  internals,
2723
2524
  frameState,
2724
2525
  pipelineState,
2725
- validated.length > 0 || preparedGpuDriven !== undefined,
2526
+ validated.length > 0 || rasterGpuDriven !== undefined,
2726
2527
  bindGroupCounts,
2727
2528
  undefined,
2728
2529
  true,
@@ -2740,6 +2541,7 @@ export function recordFrame(
2740
2541
  frameState.spotShadowSnapshots,
2741
2542
  capture.viewBindGroupDynamicOffset,
2742
2543
  preparedSpotLightProjector?.spotIndex,
2544
+ cloudShadowProjection,
2743
2545
  );
2744
2546
  }
2745
2547
  for (const probe of cubeCapture?.reflectionProbes?.graph.work ?? []) {
@@ -2754,6 +2556,7 @@ export function recordFrame(
2754
2556
  frameState.spotShadowSnapshots,
2755
2557
  probe.viewBindGroupDynamicOffset,
2756
2558
  preparedSpotLightProjector?.spotIndex,
2559
+ cloudShadowProjection,
2757
2560
  );
2758
2561
  }
2759
2562
 
@@ -2816,7 +2619,18 @@ export function recordFrame(
2816
2619
  framePostProcessParams.set(id, dofParams);
2817
2620
  }
2818
2621
  }
2622
+ if (submittedBarrelMapping !== undefined && submittedBarrelMapping.strength > 0) {
2623
+ framePostProcessParams.set(
2624
+ BARREL_DISTORTION_POST_PROCESS_ID,
2625
+ packBarrelDistortionParams(submittedBarrelMapping),
2626
+ );
2627
+ }
2819
2628
 
2629
+ if (preparedGpuDriven !== undefined) {
2630
+ frameState.surfaceSubmissionObservation?.setResourceGeneration(
2631
+ preparedGpuDriven.standardPbrFrameResources.resourceGeneration,
2632
+ );
2633
+ }
2820
2634
  const passCtx: _InternalRenderPipelineContext = {
2821
2635
  assets: internals.assets,
2822
2636
  world,
@@ -2879,12 +2693,22 @@ export function recordFrame(
2879
2693
  ...(preparedGpuDriven === undefined
2880
2694
  ? {}
2881
2695
  : {
2882
- gpuDrivenStandardPbrFrameResources: preparedGpuDriven.standardPbrFrameResources,
2883
- gpuDrivenDrawKeys: preparedGpuDriven.drawKeys,
2884
- gpuDrivenShadowViews: preparedGpuDriven.shadowViewPool,
2885
- gpuDrivenShadowBatchProjections: preparedGpuDriven.shadowBatchProjections,
2886
- gpuDrivenShadowDrawKeys: preparedGpuDriven.shadowDrawKeys,
2887
- 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
+ }),
2888
2712
  }),
2889
2713
  ...(frameGpuDriven?.scene === undefined
2890
2714
  ? {}
@@ -2958,6 +2782,12 @@ export function recordFrame(
2958
2782
  frameState.ssrHistoryCandidate = undefined;
2959
2783
  }
2960
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
+ }
2961
2791
  frameGpuDriven?.occlusion?.commit(true, frameState.frameNumber, profilePhase);
2962
2792
  },
2963
2793
  onAborted: () => {
@@ -2982,6 +2812,7 @@ export function recordFrame(
2982
2812
  cubeCaptureWork.length === 0 ? undefined : cubeCaptureWork.map((work) => work.faceIndex),
2983
2813
  (done) => {
2984
2814
  completion = done;
2815
+ frameState.surfaceSubmissionObservation?.submit(done, frameState.graphGeneration);
2985
2816
  if (fallbackReadbackRequest !== undefined) {
2986
2817
  fallbackReadback = done.then(
2987
2818
  () =>
@@ -3000,14 +2831,24 @@ export function recordFrame(
3000
2831
  timingCapture,
3001
2832
  ),
3002
2833
  );
3003
- if (submitted && preparedGpuDriven?.drawKeys !== undefined && onRenderableDraw !== undefined) {
2834
+ if (submitted && onRenderableDraw !== undefined && rasterGpuDriven?.drawKeys !== undefined) {
3004
2835
  for (const entry of validated) {
3005
- if (gpuDrivenRenderableConsumed(entry, preparedGpuDriven.drawKeys)) {
3006
- onRenderableDraw(entry);
3007
- }
2836
+ emitGpuDrivenDrawReceipts(entry, rasterGpuDriven.drawKeys, onRenderableDraw);
3008
2837
  }
3009
2838
  }
3010
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
+ }
3011
2852
  // Feature plans are submission-sensitive: VFX keeps queued fixed ticks
3012
2853
  // until the graph that consumed them is actually submitted. Acknowledge
3013
2854
  // the exact accepted graph here so successful frames do not replay every
@@ -3046,6 +2887,8 @@ export function recordFrame(
3046
2887
  : null;
3047
2888
  }
3048
2889
  if (submitted) {
2890
+ frameState.recoveryMaterialArtifacts = acceptedMaterialArtifacts;
2891
+ frameState.recoveryShadowMaterialArtifacts = acceptedShadowMaterialArtifacts;
3049
2892
  if (graphVolumetricFog === undefined) {
3050
2893
  frameState.volumetricFogAccepted = undefined;
3051
2894
  frameState.volumetricFogAcceptedContext = undefined;
@@ -3098,12 +2941,13 @@ export function recordFrame(
3098
2941
  frameState.ssrHistoryOwner?.abortFrame(frameState.ssrHistoryCandidate, 'submit');
3099
2942
  frameState.ssrHistoryCandidate = undefined;
3100
2943
  }
3101
- // DoF and volume topology replacements are provisional until the queue
3102
- // submission succeeds. This runs for every early-return path as well, so a
3103
- // swapchain, encoder, or graph failure cannot leave a non-submitted
3104
- // 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.
3105
2948
  settleVolumetricFogGraphCandidate(frameState, submitted);
3106
2949
  settleDepthOfFieldGraphCandidate(frameState, submitted);
2950
+ settleBarrelDistortionGraphCandidate(frameState, submitted);
3107
2951
  if (volumeInspectionReady && (submitted || volumeSubmissionAttempted)) {
3108
2952
  updateVolumetricFogInspection(
3109
2953
  frameState,
@@ -3158,8 +3002,10 @@ function sameWorldState(
3158
3002
  // atlas rather than reaching through World for a second mutation clock.
3159
3003
  return false;
3160
3004
  }
3161
- if (changes.status !== 'ok') return false;
3162
- 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
+ ) {
3163
3009
  return false;
3164
3010
  }
3165
3011
  }
@@ -3277,716 +3123,3 @@ function prepareDirectionalShadowCache(
3277
3123
  },
3278
3124
  };
3279
3125
  }
3280
-
3281
- /**
3282
- * feat-20260704 M3/w18: validate renderable handles + collect the render plan,
3283
- * extracted verbatim from `recordFrame`. Empty `renderables` input or all-
3284
- * unregistered handles both yield an empty result (the Case E clear-pass-only
3285
- * path). Builds per-entry renderState / stencilReference overlays from the
3286
- * transparent-dispatch entries, resolves each MeshFilter.assetHandle through
3287
- * the AssetRegistry + GPU store (with the sprite 9-slice mesh swap), and fires
3288
- * structured `asset-not-registered` errors for handles that fail to resolve.
3289
- *
3290
- * @internal
3291
- */
3292
- export function validateRenderables(
3293
- internals: RenderSystemInternals,
3294
- world: World,
3295
- // feat-20260709-editor-world-partition ENGINE-fix-round2 (defect 2): the full
3296
- // worlds[] list. Each renderable's mesh handle is a user-tier slot in its OWN
3297
- // world's sharedRefs; resolving against a foreign world either misses
3298
- // (asset-not-registered) or resolves the wrong slot payload. `world` (the
3299
- // resource-owner) is retained as the fallback for renderables whose worldId
3300
- // is out of range (defensive; extractFrames always stamps a valid index).
3301
- worlds: readonly World[],
3302
- pipelineState: PipelineState,
3303
- frameState: RenderFrameState,
3304
- renderables: readonly RenderableSnapshot[],
3305
- transparentDispatch: readonly DispatchEntry[],
3306
- excludedEntityKeys: ReadonlySet<number> = new Set<number>(),
3307
- worldKeys?: readonly number[],
3308
- ): ValidatedRenderable[] {
3309
- // bug-20260527-renderstate-pipeline-dispatch-gap D-4:
3310
- // build a renderableIndex -> renderState map from dispatch entries
3311
- // so each ValidatedRenderable carries its per-material renderState
3312
- // override without an O(n^2) back-scan in the draw loop.
3313
- const renderStateByRenderableIdx = new Map<number, MaterialRenderState | undefined>();
3314
- // w10: also build a renderableIndex -> stencilReference map from
3315
- // dispatch entries for per-draw setStencilReference calls.
3316
- const stencilRefByRenderableIdx = new Map<number, number | undefined>();
3317
- const variantSetByRenderableIdx = new Map<number, string | undefined>();
3318
- // Keep these three independent maps because each overlay has its own
3319
- // consumer and the last dispatch entry remains authoritative. Populate
3320
- // them in one pass: the old code walked the same dispatch list three times
3321
- // and repeated the same renderableIndex branch / hash writes.
3322
- for (const de of transparentDispatch) {
3323
- const renderableIndex = de.renderableIndex;
3324
- if (renderableIndex === undefined) continue;
3325
- renderStateByRenderableIdx.set(renderableIndex, de.renderState);
3326
- stencilRefByRenderableIdx.set(renderableIndex, de.stencilReference);
3327
- variantSetByRenderableIdx.set(renderableIndex, variantSetFromDefines(de.defines));
3328
- }
3329
- const validated: ValidatedRenderable[] = [];
3330
- for (let rIdx = 0; rIdx < renderables.length; rIdx++) {
3331
- const r = renderables[rIdx];
3332
- if (r === undefined) continue;
3333
- if (excludedEntityKeys.has(worldEntityKey(worldKeys?.[r.worldId] ?? r.worldId, r.entityKey)))
3334
- continue;
3335
- // feat-20260709-editor-world-partition ENGINE-fix-round2 (defect 2): resolve
3336
- // this renderable's mesh against the world it was EXTRACTED from
3337
- // (worlds[r.worldId]) — NOT the single resource-owner `world`. Builtin mesh
3338
- // slots (< BUILTIN_BASE) resolve process-statically regardless of world, so
3339
- // the per-world pick only matters for user-tier handles, but selecting it
3340
- // unconditionally keeps a single code path. Falls back to the resource-owner
3341
- // world if worldId is out of range (defensive; extractFrames always stamps
3342
- // a valid index into worlds[]).
3343
- const renderableWorld = worlds[r.worldId] ?? world;
3344
- const assetRes = resolveAssetHandle<MeshAsset>(
3345
- renderableWorld,
3346
- toShared<'MeshAsset'>(r.assetHandle),
3347
- );
3348
- if (!assetRes.ok) {
3349
- internals.errorRegistry.fire(
3350
- new RhiError({
3351
- code: 'asset-not-registered',
3352
- expected: 'MeshFilter.assetHandle in AssetRegistry',
3353
- hint: 'use HANDLE_CUBE / HANDLE_TRIANGLE imports; custom mesh register path: feat-future-asset-system',
3354
- detail: { assetHandle: r.assetHandle },
3355
- }),
3356
- );
3357
- continue;
3358
- }
3359
- // feat-20260601-device/gpu-residency-extraction M1 (D-1): builtin meshes
3360
- // (slots through HANDLE_NINESLICE_QUAD) keep the createRenderer step-3
3361
- // direct-upload + `pipelineState.meshes` path -- they are NOT routed
3362
- // through `ensureResident`. User-registered meshes pull through the store
3363
- // on first access (the register->upload push was severed in this M1);
3364
- // the POD fetched above (assetRes.value) is passed in, store holds no
3365
- // registry ref (D-2). A first-access miss builds the GPU buffers; later
3366
- // frames hit the O(1) cache.
3367
- const meshAssetHandle = toShared<'MeshAsset'>(r.assetHandle);
3368
- let meshHandles = internals.gpuStore.getMeshGpuHandles(meshAssetHandle, renderableWorld);
3369
- const lodMeshes = r.lods?.map((lod) => internals.assets.lookup<MeshAsset>(lod.mesh));
3370
- const resolvedLodMeshes =
3371
- lodMeshes?.every((lod): lod is MeshAsset => lod?.kind === 'mesh') === true
3372
- ? lodMeshes
3373
- : undefined;
3374
- if (meshHandles === undefined && r.assetHandle > handleSlot(HANDLE_NINESLICE_QUAD)) {
3375
- const residentRes = internals.gpuStore.ensureResident(
3376
- meshAssetHandle,
3377
- assetRes.value,
3378
- renderableWorld,
3379
- resolvedLodMeshes,
3380
- );
3381
- if (residentRes.ok) {
3382
- meshHandles = residentRes.value;
3383
- } else if (residentRes.error instanceof RhiError) {
3384
- internals.errorRegistry.fire(residentRes.error);
3385
- }
3386
- }
3387
- meshHandles = meshHandles ?? pipelineState.meshes.get(r.assetHandle);
3388
- if (meshHandles === undefined) {
3389
- internals.errorRegistry.fire(
3390
- new RhiError({
3391
- code: 'asset-not-registered',
3392
- expected: 'GPU mesh buffers uploaded for assetHandle',
3393
- hint: 'await renderer.initialization before draw([world], { cameraOwner: 0, resourceOwner: 0 }); ensure AssetRegistry.configureGpuDevice ran so user meshes are uploaded',
3394
- detail: { assetHandle: r.assetHandle },
3395
- }),
3396
- );
3397
- continue;
3398
- }
3399
- // feat-20260527-sprite-nineslice M2 / w11 (plan-strategy section D-2):
3400
- // sprite branch with non-zero `slicesAndMode` (post-w12 paramSnapshot
3401
- // entry name) overrides the user-supplied mesh handle (typically
3402
- // HANDLE_QUAD = 3) with the 16-vertex / 54-index HANDLE_NINESLICE_QUAD
3403
- // (id=5) topology so the vertex shader sees the 4x4 grid required for
3404
- // 9-region anchor mapping. Default slicesAndMode ([0, 0, 0, 0]) keeps
3405
- // the legacy HANDLE_QUAD path; a flip from zero to non-zero on the
3406
- // same entity routes here per-frame so AI users can toggle 9-slice
3407
- // on the fly without re-spawning the entity (charter F1 minimum
3408
- // surface). The HANDLE_NINESLICE_QUAD GPU buffers are seeded by
3409
- // createRenderer step-3.
3410
- //
3411
- // feat-20260625-refactor-sprite-as-transparent-mesh M3 / w13: judgement
3412
- // key migrated from `shadingModel === 'sprite'` to
3413
- // `materialShaderId === 'forgeax::sprite'` (plan-strategy D-10); slices
3414
- // sourced from `paramSnapshot.slicesAndMode` (post-w12 UBO-aligned
3415
- // overlay path).
3416
- //
3417
- // feat-20260624-sprite-lit-shading-model-pure-2d-lighting M1' / t7:
3418
- // sprite-lit shares the sprite paramSchema (5 fields, t4 mirror) so
3419
- // the 9-slices mesh swap applies identically.
3420
- let effectiveMeshHandles = meshHandles;
3421
- if (
3422
- r.material.materialShaderId === 'forgeax::sprite' ||
3423
- r.material.materialShaderId === 'forgeax::sprite-lit'
3424
- ) {
3425
- const slicesArr = r.material.paramSnapshot?.slicesAndMode as readonly number[] | undefined;
3426
- if (
3427
- slicesArr !== undefined &&
3428
- slicesArr.length >= 4 &&
3429
- (slicesArr[0] !== 0 || slicesArr[1] !== 0 || slicesArr[2] !== 0 || slicesArr[3] !== 0)
3430
- ) {
3431
- const nineSliceHandles = pipelineState.meshes.get(handleSlot(HANDLE_NINESLICE_QUAD));
3432
- if (nineSliceHandles !== undefined) {
3433
- effectiveMeshHandles = nineSliceHandles;
3434
- }
3435
- }
3436
- }
3437
- if (r.morph !== undefined) {
3438
- const morphed = prepareMorphMesh(
3439
- internals,
3440
- frameState,
3441
- worldEntityKey(r.worldId, r.entityKey),
3442
- effectiveMeshHandles,
3443
- assetRes.value,
3444
- r.morph,
3445
- );
3446
- if (morphed !== undefined) effectiveMeshHandles = morphed;
3447
- }
3448
- validated.push({
3449
- source: r,
3450
- world: renderableWorld,
3451
- mesh: effectiveMeshHandles,
3452
- renderableIndex: rIdx,
3453
- renderState: renderStateByRenderableIdx.get(rIdx),
3454
- variantSet: variantSetByRenderableIdx.get(rIdx),
3455
- stencilReference: stencilRefByRenderableIdx.get(rIdx),
3456
- });
3457
- }
3458
- return validated;
3459
- }
3460
-
3461
- // Keep the primary record-stage name explicit at its call site while sharing
3462
- // the same owner implementation for the complete retained ShadowCaster view.
3463
-
3464
- function prepareMorphMesh(
3465
- internals: RenderSystemInternals,
3466
- frameState: RenderFrameState,
3467
- cacheKey: number,
3468
- base: MeshGpuHandles,
3469
- mesh: MeshAsset,
3470
- morph: RenderableSnapshot['morph'],
3471
- ): MeshGpuHandles | undefined {
3472
- if (morph === undefined || frameState.morphBuffers === undefined) return undefined;
3473
- const targets = mesh.morphTargets;
3474
- if (targets === undefined || targets.length !== morph.targetCount) return undefined;
3475
- const position = base.layoutProjection.attributes.find(
3476
- (attribute) => attribute.key === 'position',
3477
- );
3478
- if (
3479
- position === undefined ||
3480
- position.offset !== 0 ||
3481
- position.shaderLocation !== 0 ||
3482
- position.format !== 'float32x3' ||
3483
- position.byteLength !== 12
3484
- ) {
3485
- return undefined;
3486
- }
3487
- const vertexCount = deriveVertexCount(mesh.vertices, base.layoutProjection);
3488
- if (vertexCount === undefined || vertexCount !== base.vertexCount || vertexCount <= 0) {
3489
- return undefined;
3490
- }
3491
- const stride = base.layoutProjection.arrayStride / Float32Array.BYTES_PER_ELEMENT;
3492
-
3493
- let active = false;
3494
- for (const weight of morph.weights) {
3495
- if (weight !== 0) {
3496
- active = true;
3497
- break;
3498
- }
3499
- }
3500
- if (!active) {
3501
- frameState.morphBuffers.get(cacheKey)?.buffer.destroy();
3502
- frameState.morphBuffers.delete(cacheKey);
3503
- return undefined;
3504
- }
3505
-
3506
- const vertices = new Float32Array(mesh.vertices);
3507
- for (let targetIndex = 0; targetIndex < targets.length; targetIndex += 1) {
3508
- const weight = morph.weights[targetIndex] ?? 0;
3509
- const positions = targets[targetIndex]?.position;
3510
- if (positions === undefined || positions.length !== base.vertexCount * 3) return undefined;
3511
- for (let vertex = 0; vertex < base.vertexCount; vertex += 1) {
3512
- const vertexBase = vertex * stride;
3513
- const positionBase = vertex * 3;
3514
- vertices[vertexBase] = (vertices[vertexBase] ?? 0) + (positions[positionBase] ?? 0) * weight;
3515
- vertices[vertexBase + 1] =
3516
- (vertices[vertexBase + 1] ?? 0) + (positions[positionBase + 1] ?? 0) * weight;
3517
- vertices[vertexBase + 2] =
3518
- (vertices[vertexBase + 2] ?? 0) + (positions[positionBase + 2] ?? 0) * weight;
3519
- }
3520
- }
3521
-
3522
- let cached = frameState.morphBuffers.get(cacheKey);
3523
- if (cached === undefined || cached.byteLength !== vertices.byteLength) {
3524
- if (cached !== undefined && !cached.buffer.isDestroyed) {
3525
- const destroyed = cached.buffer.destroy();
3526
- if (!destroyed.ok) internals.errorRegistry.fire(destroyed.error);
3527
- }
3528
- const created = internals.device.createBuffer({
3529
- label: `morph-${cacheKey}-vbo`,
3530
- size: vertices.byteLength,
3531
- usage: GPU_BUFFER_USAGE_VERTEX | GPU_BUFFER_USAGE_COPY_DST,
3532
- mappedAtCreation: false,
3533
- });
3534
- if (!created.ok) {
3535
- internals.errorRegistry.fire(created.error);
3536
- frameState.morphBuffers.delete(cacheKey);
3537
- return undefined;
3538
- }
3539
- const buffer = new GpuBuffer(internals.device, created.value);
3540
- internals.deviceScope._adopt('buffer', buffer, (owned) => {
3541
- if (!owned.isDestroyed) owned.destroy();
3542
- });
3543
- cached = {
3544
- buffer,
3545
- byteLength: vertices.byteLength,
3546
- };
3547
- frameState.morphBuffers.set(cacheKey, cached);
3548
- }
3549
- const written = internals.device.queue.writeBuffer(cached.buffer.handle, 0, vertices);
3550
- if (!written.ok) {
3551
- internals.errorRegistry.fire(written.error);
3552
- return undefined;
3553
- }
3554
- return { ...base, vertexBuffer: cached.buffer };
3555
- }
3556
-
3557
- /**
3558
- * feat-20260704 M3/w18: build the dispatch-ordered render plan, extracted
3559
- * verbatim from `recordFrame`. (1) M3/w26 dispatch-ordered reorder: reorder
3560
- * `validated` to follow the transparent-dispatch order (extract-order fallback
3561
- * for unmatched entries). (2) feat-20260608 mesh-SSBO capacity gate: size the
3562
- * mesh-SSBO + material-UBO pair to the larger of entity vs cumulative
3563
- * material-slot count, truncating on ceiling (graceful degradation). (3)
3564
- * feat-20260622 fold dispatch plan: build + apply the WebGL2 uniform-cap
3565
- * fallback + bump the folded-draws metric.
3566
- *
3567
- * @internal
3568
- */
3569
- export function buildDispatchPlan(
3570
- internals: RenderSystemInternals,
3571
- validated: readonly ValidatedRenderable[],
3572
- transparentDispatch: readonly DispatchEntry[],
3573
- foldBuckets: readonly FoldBucket[],
3574
- shadowValidated: readonly ValidatedRenderable[] = [],
3575
- materialBindingClasses?: ReadonlyMap<string, string>,
3576
- ): {
3577
- validatedOrdered: readonly ValidatedRenderable[];
3578
- shadowValidatedOrdered: readonly ValidatedRenderable[];
3579
- foldDispatchPlan: FoldDispatchPlan | null;
3580
- materialSlotIndices: readonly (readonly number[])[];
3581
- materialSlots: readonly MaterialSnapshot[];
3582
- materialSlotOwners: readonly ValidatedRenderable[];
3583
- materialSlotCount: number;
3584
- } {
3585
- // A production GPU-owned frame has no CPU validated rows by design. The
3586
- // dispatch list can still contain one entry per authored entity, but no
3587
- // CPU plan, material table, or fold bucket is consumed in that lane. Return
3588
- // the empty plan without walking that list so a steady 100k scene remains
3589
- // bounded by the persistent GPU batches rather than per-entity record work.
3590
- if (validated.length === 0 && shadowValidated.length === 0) {
3591
- const meshSsboCapResult = ensureMeshSsboCapacity(internals, shadowValidated.length);
3592
- const meshSsboSlotCapacity = meshSsboSlotCapacityAfterEnsure(
3593
- internals,
3594
- shadowValidated.length,
3595
- meshSsboCapResult,
3596
- );
3597
- return {
3598
- validatedOrdered: [],
3599
- shadowValidatedOrdered: limitRowsToMeshSsboCapacity(shadowValidated, meshSsboSlotCapacity),
3600
- foldDispatchPlan: null,
3601
- materialSlotIndices: [],
3602
- materialSlots: [],
3603
- materialSlotOwners: [],
3604
- materialSlotCount: 0,
3605
- };
3606
- }
3607
- // M3 / w26: dispatch-ordered render. The dispatch list is pre-sorted
3608
- // by queue (ascending, stable) by the extract stage per plan-strategy D-3.
3609
- // Reorder validated renderables to follow the dispatch order, falling
3610
- // back to extract order for renderables with no matching dispatch entry.
3611
- let dispatchOrderMatchesValidated = transparentDispatch.length === validated.length;
3612
- if (dispatchOrderMatchesValidated) {
3613
- for (let i = 0; i < validated.length; i++) {
3614
- if (transparentDispatch[i]?.renderableIndex !== validated[i]?.renderableIndex) {
3615
- dispatchOrderMatchesValidated = false;
3616
- break;
3617
- }
3618
- }
3619
- }
3620
- let validatedOrdered: readonly ValidatedRenderable[] = dispatchOrderMatchesValidated
3621
- ? validated
3622
- : [...validated];
3623
- if (transparentDispatch.length > 0 && !dispatchOrderMatchesValidated) {
3624
- const validatedByRenderableIdx = new Map<number, ValidatedRenderable>();
3625
- const seen = new Set<number>();
3626
- for (const v of validated) {
3627
- validatedByRenderableIdx.set(v.renderableIndex, v);
3628
- }
3629
- const ordered: ValidatedRenderable[] = [];
3630
- for (const de of transparentDispatch) {
3631
- if (de.renderableIndex === undefined) continue;
3632
- const v = validatedByRenderableIdx.get(de.renderableIndex);
3633
- if (v !== undefined && !seen.has(de.renderableIndex)) {
3634
- seen.add(de.renderableIndex);
3635
- ordered.push(v);
3636
- }
3637
- }
3638
- // Append renderables not in the dispatch list (e.g. default-material entities)
3639
- for (const v of validated) {
3640
- if (!seen.has(v.renderableIndex)) {
3641
- ordered.push(v);
3642
- }
3643
- }
3644
- validatedOrdered = ordered;
3645
- }
3646
-
3647
- // Shadow-only/off-camera CPU residuals consume the same material UBO as
3648
- // visible draws. Their preparation must not depend on main-view visibility.
3649
- const materialRows = [...validatedOrdered, ...shadowValidated];
3650
- const materialGroups = materialRows.map((entry): readonly MaterialSnapshot[] => {
3651
- const shaderId = entry.source.material.materialShaderId;
3652
- if (shaderId === 'forgeax::sprite' || shaderId === 'forgeax::sprite-lit') {
3653
- return [entry.source.material];
3654
- }
3655
- return entry.source.materials.length > 0 ? entry.source.materials : [entry.source.material];
3656
- });
3657
- const materialSlotPlan = buildMaterialSlotPlan(materialGroups, (material, owner) => {
3658
- const row = materialRows[owner];
3659
- return row === undefined
3660
- ? ''
3661
- : (materialBindingClasses?.get(
3662
- materialBindingKey(row.source, material.materialHandle ?? -1),
3663
- ) ?? '');
3664
- });
3665
- let materialSlotIndices = materialSlotPlan.slotIndices;
3666
- let materialSlots = materialSlotPlan.slots;
3667
- let materialSlotOwners = materialSlotPlan.slotOwners.map((index) => {
3668
- const owner = materialRows[index];
3669
- if (owner === undefined) throw new Error('material slot has no validated owner');
3670
- return owner;
3671
- });
3672
- let materialSlotCount = materialSlots.length;
3673
- let materialRenderableCount = materialRows.length;
3674
-
3675
- // feat-20260608-mesh-ssbo-dynamic-grow-l1-lift-1024-entity-cap M3 / T-M3-04:
3676
- // ensure the mesh-SSBO + material-UBO buffer pair is large enough to hold
3677
- // the render plan BEFORE the first per-entity writeBuffer.
3678
- // On `ok:false` the controller has already fired a structured RuntimeError
3679
- // (`mesh-ssbo-ceiling-reached` / `mesh-ssbo-capacity-exceeded`); we truncate
3680
- // the draw list to the largest complete entity prefix whose cumulative
3681
- // material slots fit `degradedToSlotCount` (graceful degradation per
3682
- // plan-strategy D-2): render the subset that fits, discard overflow, no
3683
- // black frame. The helper is idempotent across same-frame re-calls (AC-09)
3684
- // and short-circuits on length=0 / length<=slotCount (boundary table).
3685
- //
3686
- // bug-20260609: feat-20260608 M5 amend made the material UBO indexed by
3687
- // cumulative *material-slot* count (one slot per submesh material),
3688
- // which is >= entity count once an entity carries `materials.length>1`.
3689
- // The mesh + material buffer pair share `slotCount` (single allocator),
3690
- // so we size against the larger of the two requirements: entity count
3691
- // (mesh-SSBO consumer) vs cumulative material-slot count (material-UBO
3692
- // consumer). Sprite entities collapse to 1 slot in the material table,
3693
- // mirroring the same rule (sprite per-submesh OOS-1;
3694
- // post-w13 judgement key migrated to materialShaderId).
3695
- //
3696
- // feat-20260624 M1' / t7: sprite-lit treated identically to sprite
3697
- // for material-slot accounting (paramSchema mirror, t4).
3698
- const neededSlots = Math.max(validatedOrdered.length + shadowValidated.length, materialSlotCount);
3699
- const meshSsboCapResult = ensureMeshSsboCapacity(internals, neededSlots);
3700
- const meshSsboSlotCapacity = meshSsboSlotCapacityAfterEnsure(
3701
- internals,
3702
- neededSlots,
3703
- meshSsboCapResult,
3704
- );
3705
- if (!meshSsboCapResult.ok) {
3706
- // Graceful degradation: the controller reports slots, but this stage
3707
- // consumes entities. Find a complete prefix instead of slicing at the
3708
- // numeric slot count; a multi-material entity can consume several slots.
3709
- const degradedRenderableCount = findRenderablePrefixForSlotCapacity(
3710
- materialSlotIndices,
3711
- meshSsboCapResult.degradedToSlotCount,
3712
- );
3713
- materialRenderableCount = degradedRenderableCount;
3714
- materialSlotCount = materialSlotCountForPrefix(materialSlotIndices, degradedRenderableCount);
3715
- materialSlotIndices = materialSlotIndices.slice(0, degradedRenderableCount);
3716
- materialSlots = materialSlots.slice(0, materialSlotCount);
3717
- materialSlotOwners = materialSlotOwners.slice(0, materialSlotCount);
3718
- validatedOrdered = validatedOrdered.slice(0, degradedRenderableCount);
3719
- }
3720
- const shadowValidatedOrdered = limitRowsToMeshSsboCapacity(
3721
- shadowValidated,
3722
- Math.min(meshSsboSlotCapacity, materialRenderableCount) - validatedOrdered.length,
3723
- );
3724
-
3725
- // feat-20260622-chunk-gpu-instancing-sprite-tilemap M1 / w4-record-swap
3726
- // (D-1): build the fold dispatch plan once `validatedOrdered` is final
3727
- // (post truncation by mesh-SSBO capacity gate). The plan re-keys each
3728
- // non-singleton bucket from `renderableIndex` to the validated-ordered
3729
- // index `i` consumed by the dispatch loops; the loops use it to skip
3730
- // non-head bucket members and emit one instanced drawIndexed per
3731
- // bucket head. Empty plan (no fold-eligible buckets) is a byte-
3732
- // identical no-op for the dispatch loops below (charter P3: silent
3733
- // pass-through, no error path).
3734
- let renderableToValidatedIdx: Map<number, number> | null = null;
3735
- let foldDispatchPlan: FoldDispatchPlan | null = null;
3736
- if (foldBuckets.length > 0) {
3737
- renderableToValidatedIdx = new Map<number, number>();
3738
- for (let i = 0; i < validatedOrdered.length; i++) {
3739
- const e = validatedOrdered[i];
3740
- if (e === undefined) continue;
3741
- renderableToValidatedIdx.set(e.renderableIndex, i);
3742
- }
3743
- foldDispatchPlan = buildFoldDispatchPlan(foldBuckets, renderableToValidatedIdx);
3744
-
3745
- // feat-20260622 M2 / w11 (D-2 + D-9 + AC-05): WebGL2 uniform-fallback
3746
- // per-bucket instance-count cap. When caps.storageBuffer===false AND
3747
- // a fold bucket carries more than FOLD_UNIFORM_INSTANCE_CAP (128)
3748
- // instances, fire RhiError({code:'instancing-exceeds-uniform-cap'})
3749
- // AND remove the bucket from the dispatch plan so its members fall
3750
- // through to the per-entity drawIndexed exit (the same exit the
3751
- // mode-gate bypass uses — D-9 "shared fallback exit"). The frame
3752
- // stays visually correct (charter proposition 9 graceful
3753
- // degradation: no identity-collapse / black screen) while the cap
3754
- // event surfaces structurally for AI users (proposition 4 explicit
3755
- // failure on .code).
3756
- //
3757
- // Scope discrimination: tilemap-chunk-extract-system encodes
3758
- // Layer.value = (layerOrder<<20) | (chunkIndex & 0xfffff), so a
3759
- // bucket whose head entry carries non-zero low-20-bits is
3760
- // definitively a tilemap-chunk dispatch site. Plain sprite buckets
3761
- // use SPRITE_LAYER_VALUE = layerOrder<<20 (low-20 zero) by the
3762
- // documented convention (apps/hello/asi-world main.ts pattern).
3763
- // The chunkIndex===0 edge case maps to 'sprite' (the helper's
3764
- // default branch) — a one-bucket ambiguity per layerOrder that is
3765
- // acceptable for the AI-user affordance level (the error semantics
3766
- // — "this bucket exceeded the cap" — is the actionable signal;
3767
- // scope=sprite vs tilemap-chunk only refines the recovery hint).
3768
- if (foldDispatchPlan.headBuckets.size > 0 && !internals.device.caps.storageBuffer) {
3769
- const filteredHeads = new Map<number, FoldBucket>(foldDispatchPlan.headBuckets);
3770
- const filteredSkips = new Set<number>(foldDispatchPlan.skipIndices);
3771
- let filteredCount = foldDispatchPlan.foldedBucketCount;
3772
- for (const [headIdx, bucket] of foldDispatchPlan.headBuckets) {
3773
- const scope: 'sprite' | 'tilemap-chunk' =
3774
- (bucket.layer & 0xfffff) !== 0 ? 'tilemap-chunk' : 'sprite';
3775
- const decision = evaluateFoldBucketUniformCap(bucket, internals.device.caps, scope);
3776
- if (decision.fallback && decision.error !== undefined) {
3777
- internals.errorRegistry.fire(decision.error);
3778
- filteredHeads.delete(headIdx);
3779
- filteredCount -= 1;
3780
- for (let j = 1; j < bucket.entries.length; j++) {
3781
- const memberEntry = bucket.entries[j];
3782
- if (memberEntry === undefined) continue;
3783
- const memberValidatedIdx = renderableToValidatedIdx.get(memberEntry.renderableIndex);
3784
- if (memberValidatedIdx !== undefined) {
3785
- filteredSkips.delete(memberValidatedIdx);
3786
- }
3787
- }
3788
- }
3789
- }
3790
- if (filteredCount !== foldDispatchPlan.foldedBucketCount) {
3791
- foldDispatchPlan = {
3792
- headBuckets: filteredHeads,
3793
- skipIndices: filteredSkips,
3794
- foldedBucketCount: filteredCount,
3795
- };
3796
- }
3797
- }
3798
-
3799
- // feat-20260622-chunk-gpu-instancing-sprite-tilemap M3 / w13 (D-3 +
3800
- // AC-06): increment `render.instancing.foldedDraws` once per fold-
3801
- // eligible head bucket retained after the cap-fallback filter above.
3802
- // The metric tracks instanced drawIndexed call count for this frame
3803
- // — cap-overrun buckets routed through the per-entity fallback exit
3804
- // are removed from `foldDispatchPlan` and therefore not counted, by
3805
- // construction (M2 / w11 cap-fallback + plan-strategy D-3 semantics).
3806
- // Singleton buckets (mode-bypass under D-5, or non-foldable under
3807
- // mode 0) carry `bucketSize === 1` and never enter `headBuckets`, so
3808
- // the per-entity drawIndexed path correctly does not count.
3809
- // SSOT helper lives in this record owner — engine code never
3810
- // hardcodes the metric key string.
3811
- incrementFoldedDrawsMetric(foldDispatchPlan, internals.metrics);
3812
- }
3813
-
3814
- return {
3815
- validatedOrdered,
3816
- shadowValidatedOrdered,
3817
- foldDispatchPlan,
3818
- materialSlotIndices,
3819
- materialSlots,
3820
- materialSlotOwners,
3821
- materialSlotCount,
3822
- };
3823
- }
3824
-
3825
- /**
3826
- * feat-20260704 M3/w18: per-frame cache clean-up (despawn eviction), extracted
3827
- * verbatim from `recordFrame`.
3828
- *
3829
- * feat-20260531-per-frame-bind-group-cache M4 / w14 (D-5): drop per-entity BG
3830
- * cache entries (materialBgPerEntity / instancesBgPerEntity) + instance buffers
3831
- * whose outer-Map entityKey (packed Entity u32) is absent from the current
3832
- * validated set, preventing unbounded growth after entity despawn. view + mesh
3833
- * caches are frame-shared (keyed by GPU resource handle objects, WeakMap chains
3834
- * naturally bounded) and are not touched. feat-20260619 M4 / F11: destroy the
3835
- * GPU instance buffer before Map.delete (D-6 symmetric release); failure fires
3836
- * errorRegistry + continues the sweep.
3837
- *
3838
- * @internal
3839
- */
3840
- function cleanPerFrameCaches(
3841
- internals: RenderSystemInternals,
3842
- frameState: RenderFrameState,
3843
- validated: readonly ValidatedRenderable[],
3844
- retainedEntityKeys: ReadonlySet<number> = new Set<number>(),
3845
- ): void {
3846
- if (frameState.transientInstanceBuffers !== undefined) {
3847
- disposeTransientInstanceBuffers(frameState.transientInstanceBuffers, internals.errorRegistry);
3848
- }
3849
- // Build a Set<number> of worldEntityKey composites from the validated
3850
- // renderables. D-1a #4: validatedEntityKeys are worldEntityKey(worldId, entityKey)
3851
- // composites matching the write-side keys of #1-#3 — cross-world false eviction
3852
- // is prevented because worldEntityKey(0, k) !== worldEntityKey(1, k).
3853
- const validatedEntityKeys = new Set<number>(retainedEntityKeys);
3854
- const validatedInstanceKeys = new Set<number>(retainedEntityKeys);
3855
- for (const v of validated) {
3856
- const entityKey = worldEntityKey(v.source.worldId, v.source.entityKey);
3857
- validatedEntityKeys.add(entityKey);
3858
- validatedInstanceKeys.add(entityKey);
3859
- if (v.source.instances !== undefined) {
3860
- validatedInstanceKeys.add(instanceCollectionCacheKey(v.source.worldId, v.source.instances));
3861
- }
3862
- }
3863
-
3864
- // Clean per-entity material BG cache: drop outer-Map entries whose
3865
- // entityKey is absent from the current validated set. The shared and
3866
- // singleton material caches have no entityKey and are not touched here.
3867
- cleanPerEntityCache(frameState.materialBgPerEntity, validatedEntityKeys);
3868
-
3869
- // Clean per-entity instances BG cache.
3870
- cleanPerEntityCache(frameState.instancesBgPerEntity, validatedEntityKeys);
3871
-
3872
- // D-5 retrofit: instanceBuffers clean-up. The instanceBuffers Map is
3873
- // keyed by cacheKey (packed Entity u32, same as entityKey on
3874
- // RenderableSnapshot). Drop entries whose key is no longer in the
3875
- // validated set (OQ-3 / R-4). feat-20260619 M4 / F11: destroy the GPU
3876
- // buffer before Map.delete so despawned entities release their
3877
- // instance-buffer backing memory symmetrically (D-6).
3878
- //
3879
- // D-1a #1: instanceBuffers keys on the positive half (>= 0) are
3880
- // worldEntityKey composites matching the write side. Negative half
3881
- // fold-bucket keys (< 0) are NOT worldEntityKey; they are
3882
- // material-handle-based and cross-world collision is semantically
3883
- // correct (same material renders in same fold bucket).
3884
- for (const [key, entry] of frameState.instanceBuffers.entries()) {
3885
- if (!validatedInstanceKeys.has(key)) {
3886
- if (!entry.buffer.isDestroyed) {
3887
- const r = entry.buffer.destroy();
3888
- if (!r.ok) internals.errorRegistry.fire(r.error);
3889
- }
3890
- frameState.instanceBuffers.delete(key);
3891
- }
3892
- }
3893
-
3894
- // Chunk keys are `${worldEntityKey}:${chunkStart}`. Keep the key opaque to
3895
- // the authoring API, but retain the numeric owner prefix for eviction.
3896
- if (frameState.instanceBufferChunks !== undefined) {
3897
- for (const [key, entry] of frameState.instanceBufferChunks.entries()) {
3898
- const separator = key.indexOf(':');
3899
- const ownerKey = separator < 0 ? Number.NaN : Number(key.slice(0, separator));
3900
- if (validatedInstanceKeys.has(ownerKey)) continue;
3901
- if (!entry.buffer.isDestroyed) {
3902
- const r = entry.buffer.destroy();
3903
- if (!r.ok) internals.errorRegistry.fire(r.error);
3904
- }
3905
- frameState.instanceBufferChunks.delete(key);
3906
- }
3907
- }
3908
-
3909
- if (frameState.morphBuffers !== undefined) {
3910
- for (const [key, entry] of frameState.morphBuffers.entries()) {
3911
- if (validatedEntityKeys.has(key)) continue;
3912
- if (!entry.buffer.isDestroyed) {
3913
- const result = entry.buffer.destroy();
3914
- if (!result.ok) internals.errorRegistry.fire(result.error);
3915
- }
3916
- frameState.morphBuffers.delete(key);
3917
- }
3918
- }
3919
- }
3920
-
3921
- /**
3922
- * feat-20260704 M3/w18: record-stage fold operator linear scan, extracted
3923
- * verbatim from `recordFrame`.
3924
- *
3925
- * feat-20260622-chunk-gpu-instancing-sprite-tilemap M1 / w4 + w5 (D-1, D-5):
3926
- * groups transparent-sort-ordered dispatch entries with equal (Layer.value,
3927
- * sortKey, materialHandle) into fold buckets. Mode-gate (D-5 extended): modes 0
3928
- * (LAYER_Z) and 1 (LAYER_Y) fold using the pos z/y lanes; modes 2/3 bypass per-entity
3929
- * (each entry a singleton bucket). Records `frameState.lastFoldBucketCount` =
3930
- * fold-eligible buckets (bucketSize > 1) for the AC-06 metric. Empty dispatch
3931
- * short-circuits (test fixtures pass null world).
3932
- *
3933
- * @internal
3934
- */
3935
- function computeFoldBuckets(
3936
- world: World,
3937
- frameState: RenderFrameState,
3938
- transparentDispatch: readonly DispatchEntry[],
3939
- renderables: readonly RenderableSnapshot[],
3940
- ): readonly FoldBucket[] {
3941
- if (transparentDispatch.length === 0) {
3942
- frameState.lastFoldBucketCount = 0;
3943
- return [];
3944
- }
3945
- const transparentSortCfg = getTransparentSortConfig(world);
3946
-
3947
- // Only layer-Z / layer-Y modes can produce non-singleton fold buckets.
3948
- // The other modes make one inert singleton per entry, which is discarded by
3949
- // buildFoldDispatchPlan. Returning the same empty plan here avoids allocating
3950
- // a transform matrix for every renderable when folding is disabled by mode.
3951
- if (
3952
- transparentSortCfg.mode !== TRANSPARENT_SORT_MODE_LAYER_Z &&
3953
- transparentSortCfg.mode !== TRANSPARENT_SORT_MODE_LAYER_Y
3954
- ) {
3955
- frameState.lastFoldBucketCount = 0;
3956
- return [];
3957
- }
3958
-
3959
- // `transparentDispatch` is the legacy name for the complete sorted dispatch
3960
- // list. Opaque entries can never participate in the transparent-only fold
3961
- // plan, yet foldDispatchBuckets must preserve its general helper contract and
3962
- // therefore materializes an inert singleton (including a Float32Array(16))
3963
- // for each one. Filter only at this private production call site so the fold
3964
- // helper keeps its testable semantics while record avoids work whose result
3965
- // is provably discarded.
3966
- let transparentCount = 0;
3967
- for (const entry of transparentDispatch) {
3968
- if (renderables[entry.renderableIndex]?.material.transparent === true) {
3969
- transparentCount += 1;
3970
- }
3971
- }
3972
- if (transparentCount === 0) {
3973
- frameState.lastFoldBucketCount = 0;
3974
- return [];
3975
- }
3976
- const foldCandidates =
3977
- transparentCount === transparentDispatch.length
3978
- ? transparentDispatch
3979
- : transparentDispatch.filter(
3980
- (entry) => renderables[entry.renderableIndex]?.material.transparent === true,
3981
- );
3982
- const foldBuckets = foldDispatchBuckets(foldCandidates, transparentSortCfg.mode, renderables);
3983
- // Count only fold-eligible buckets (bucketSize > 1) so the metric
3984
- // surfaces fold actually reducing draws — singleton buckets under
3985
- // mode bypass do not change draw count, so they do not contribute.
3986
- let foldEligibleCount = 0;
3987
- for (const b of foldBuckets) {
3988
- if (b.bucketSize > 1) foldEligibleCount += 1;
3989
- }
3990
- frameState.lastFoldBucketCount = foldEligibleCount;
3991
- return foldBuckets;
3992
- }