@forgeax/engine-render 0.1.28 → 0.1.30

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 (768) hide show
  1. package/README.md +856 -126
  2. package/dist/assembly/atmosphere-shader-prewarm.d.ts +6 -0
  3. package/dist/assembly/atmosphere-shader-prewarm.d.ts.map +1 -0
  4. package/dist/assembly/device-feature-admission.d.ts +4 -3
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -1
  6. package/dist/assembly/dynamic-geometry-host.d.ts +27 -0
  7. package/dist/assembly/dynamic-geometry-host.d.ts.map +1 -1
  8. package/dist/assembly/dynamic-geometry-runtime.d.ts +20 -0
  9. package/dist/assembly/dynamic-geometry-runtime.d.ts.map +1 -0
  10. package/dist/assembly/factory.d.ts +3 -1
  11. package/dist/assembly/factory.d.ts.map +1 -1
  12. package/dist/assembly/gpu-driven-owner.d.ts +10 -0
  13. package/dist/assembly/gpu-driven-owner.d.ts.map +1 -0
  14. package/dist/assembly/host-contract.d.ts +14 -0
  15. package/dist/assembly/host-contract.d.ts.map +1 -1
  16. package/dist/assembly/material/assembly.d.ts +31 -1
  17. package/dist/assembly/material/assembly.d.ts.map +1 -1
  18. package/dist/assembly/material/pipeline-helpers.d.ts +24 -0
  19. package/dist/assembly/material/pipeline-helpers.d.ts.map +1 -0
  20. package/dist/assembly/material-shader-policy.d.ts +3 -1
  21. package/dist/assembly/material-shader-policy.d.ts.map +1 -1
  22. package/dist/assembly/recovery/device-loss-fanout.d.ts.map +1 -1
  23. package/dist/assembly/recovery/failure-location.d.ts +12 -0
  24. package/dist/assembly/recovery/failure-location.d.ts.map +1 -0
  25. package/dist/assembly/recovery/recovery-attempt.d.ts +1 -6
  26. package/dist/assembly/recovery/recovery-attempt.d.ts.map +1 -1
  27. package/dist/assembly/recovery/renderer-recover.d.ts +7 -4
  28. package/dist/assembly/recovery/renderer-recover.d.ts.map +1 -1
  29. package/dist/assembly/renderer-facade.d.ts.map +1 -1
  30. package/dist/assembly/renderer-feature-inspection.d.ts +5 -0
  31. package/dist/assembly/renderer-feature-inspection.d.ts.map +1 -0
  32. package/dist/assembly/renderer-frame-transaction.d.ts +19 -0
  33. package/dist/assembly/renderer-frame-transaction.d.ts.map +1 -1
  34. package/dist/assembly/renderer-helpers.d.ts +3 -0
  35. package/dist/assembly/renderer-helpers.d.ts.map +1 -1
  36. package/dist/assembly/renderer-inspection.d.ts +4 -0
  37. package/dist/assembly/renderer-inspection.d.ts.map +1 -1
  38. package/dist/assembly/shader-prewarm-policy.d.ts +7 -1
  39. package/dist/assembly/shader-prewarm-policy.d.ts.map +1 -1
  40. package/dist/assembly/skin-palette-owner.d.ts +8 -0
  41. package/dist/assembly/skin-palette-owner.d.ts.map +1 -0
  42. package/dist/assembly/ssr-shader-prewarm.d.ts +6 -0
  43. package/dist/assembly/ssr-shader-prewarm.d.ts.map +1 -0
  44. package/dist/assembly/webgpu-device-loss.d.ts +15 -0
  45. package/dist/assembly/webgpu-device-loss.d.ts.map +1 -0
  46. package/dist/assembly/webgpu-pbr-ready.d.ts +38 -0
  47. package/dist/assembly/webgpu-pbr-ready.d.ts.map +1 -0
  48. package/dist/assembly/webgpu-ready-mipmap.d.ts +11 -0
  49. package/dist/assembly/webgpu-ready-mipmap.d.ts.map +1 -0
  50. package/dist/assembly/webgpu-ready.d.ts +7 -3
  51. package/dist/assembly/webgpu-ready.d.ts.map +1 -1
  52. package/dist/assembly/webgpu-renderer-bootstrap.d.ts +10 -0
  53. package/dist/assembly/webgpu-renderer-bootstrap.d.ts.map +1 -0
  54. package/dist/assembly/webgpu-renderer-contract.d.ts +62 -0
  55. package/dist/assembly/webgpu-renderer-contract.d.ts.map +1 -0
  56. package/dist/assembly/webgpu-renderer-guards.d.ts +9 -0
  57. package/dist/assembly/webgpu-renderer-guards.d.ts.map +1 -0
  58. package/dist/assembly/webgpu-renderer-observation.d.ts +6 -0
  59. package/dist/assembly/webgpu-renderer-observation.d.ts.map +1 -0
  60. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts +12 -0
  61. package/dist/assembly/webgpu-renderer-recovery-failure.d.ts.map +1 -0
  62. package/dist/assembly/webgpu-renderer.d.ts +7 -141
  63. package/dist/assembly/webgpu-renderer.d.ts.map +1 -1
  64. package/dist/assembly/webgpu-vertex-layouts.d.ts +28 -84
  65. package/dist/assembly/webgpu-vertex-layouts.d.ts.map +1 -1
  66. package/dist/authoring.mjs +2 -2
  67. package/dist/capture/scene-projection.d.ts +41 -0
  68. package/dist/capture/scene-projection.d.ts.map +1 -0
  69. package/dist/{chunk-J52NMQKR.mjs → chunk-7MQ3XBSP.mjs} +1098 -844
  70. package/dist/chunk-7MQ3XBSP.mjs.map +1 -0
  71. package/dist/{chunk-GF523LHF.mjs → chunk-BLXGONR2.mjs} +30 -12
  72. package/dist/chunk-BLXGONR2.mjs.map +1 -0
  73. package/dist/{chunk-VSL23LZE.mjs → chunk-C3DDSXYK.mjs} +1188 -463
  74. package/dist/chunk-C3DDSXYK.mjs.map +1 -0
  75. package/dist/{chunk-IQMKJLNU.mjs → chunk-FUUO4UND.mjs} +443 -30
  76. package/dist/chunk-FUUO4UND.mjs.map +1 -0
  77. package/dist/{chunk-J6KACNOH.mjs → chunk-JRBDO2KB.mjs} +617 -7
  78. package/dist/chunk-JRBDO2KB.mjs.map +1 -0
  79. package/dist/{chunk-GE3SDD72.mjs → chunk-MDQJPY3T.mjs} +20 -40
  80. package/dist/chunk-MDQJPY3T.mjs.map +1 -0
  81. package/dist/{chunk-MYJ5RPJO.mjs → chunk-NO4WZCAH.mjs} +3 -3
  82. package/dist/chunk-NO4WZCAH.mjs.map +1 -0
  83. package/dist/{chunk-RBDBZ5R7.mjs → chunk-PA5DRJNI.mjs} +2 -2
  84. package/dist/chunk-PA5DRJNI.mjs.map +1 -0
  85. package/dist/{chunk-MBU4ZVE7.mjs → chunk-PSEQ6QPH.mjs} +4655 -892
  86. package/dist/chunk-PSEQ6QPH.mjs.map +1 -0
  87. package/dist/{chunk-JAASCMXA.mjs → chunk-S5UE2T5P.mjs} +289 -41
  88. package/dist/chunk-S5UE2T5P.mjs.map +1 -0
  89. package/dist/components/camera.d.ts +62 -2
  90. package/dist/components/camera.d.ts.map +1 -1
  91. package/dist/components/directional-light.d.ts +2 -2
  92. package/dist/components/dynamic-resolution.d.ts +15 -0
  93. package/dist/components/dynamic-resolution.d.ts.map +1 -0
  94. package/dist/components/index.d.ts +2 -0
  95. package/dist/components/index.d.ts.map +1 -1
  96. package/dist/components/instances.d.ts +6 -20
  97. package/dist/components/instances.d.ts.map +1 -1
  98. package/dist/components/light-helpers.d.ts.map +1 -1
  99. package/dist/components/reflection-probe.d.ts +1 -0
  100. package/dist/components/reflection-probe.d.ts.map +1 -1
  101. package/dist/components/scene-instance.d.ts +2 -1
  102. package/dist/components/scene-instance.d.ts.map +1 -1
  103. package/dist/components/screen-space-reflection.d.ts +14 -0
  104. package/dist/components/screen-space-reflection.d.ts.map +1 -0
  105. package/dist/components/skylight.d.ts +4 -1
  106. package/dist/components/skylight.d.ts.map +1 -1
  107. package/dist/construct-renderer.mjs +14809 -7570
  108. package/dist/construct-renderer.mjs.map +1 -1
  109. package/dist/device/device-scope.d.ts +2 -0
  110. package/dist/device/device-scope.d.ts.map +1 -1
  111. package/dist/device/gpu-residency.d.ts +3 -14
  112. package/dist/device/gpu-residency.d.ts.map +1 -1
  113. package/dist/dynamic-geometry.d.ts.map +1 -1
  114. package/dist/environment/background.d.ts +7 -0
  115. package/dist/environment/background.d.ts.map +1 -0
  116. package/dist/environment/ibl.d.ts +11 -0
  117. package/dist/environment/ibl.d.ts.map +1 -0
  118. package/dist/environment/storage.d.ts +14 -0
  119. package/dist/environment/storage.d.ts.map +1 -0
  120. package/dist/errors/gpu-driven.d.ts +6 -3
  121. package/dist/errors/gpu-driven.d.ts.map +1 -1
  122. package/dist/errors/render.d.ts +41 -4
  123. package/dist/errors/render.d.ts.map +1 -1
  124. package/dist/extract/camera.d.ts.map +1 -1
  125. package/dist/extract/contracts.d.ts +7 -0
  126. package/dist/extract/contracts.d.ts.map +1 -1
  127. package/dist/extract/gpu-driven.d.ts +50 -3
  128. package/dist/extract/gpu-driven.d.ts.map +1 -1
  129. package/dist/features/host.d.ts +23 -1
  130. package/dist/features/host.d.ts.map +1 -1
  131. package/dist/features/noise-texture.d.ts +17 -0
  132. package/dist/features/noise-texture.d.ts.map +1 -0
  133. package/dist/features/plan.d.ts +83 -2
  134. package/dist/features/plan.d.ts.map +1 -1
  135. package/dist/features/prepared-gpu-work.d.ts +37 -4
  136. package/dist/features/prepared-gpu-work.d.ts.map +1 -1
  137. package/dist/features/prepared-graphics-store.d.ts.map +1 -1
  138. package/dist/features/prepared-graphics.d.ts +5 -0
  139. package/dist/features/prepared-graphics.d.ts.map +1 -1
  140. package/dist/features/render-graph-compute.d.ts +5 -3
  141. package/dist/features/render-graph-compute.d.ts.map +1 -1
  142. package/dist/features/render-graph-contribution.d.ts +11 -3
  143. package/dist/features/render-graph-contribution.d.ts.map +1 -1
  144. package/dist/features/render-graph-raster.d.ts +19 -3
  145. package/dist/features/render-graph-raster.d.ts.map +1 -1
  146. package/dist/features/types.d.ts +11 -0
  147. package/dist/features/types.d.ts.map +1 -1
  148. package/dist/fullscreen-post-process-pass.d.ts +2 -0
  149. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  150. package/dist/gpu-driven/batch-topology.d.ts +34 -1
  151. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  152. package/dist/gpu-driven/material-artifacts.d.ts +24 -0
  153. package/dist/gpu-driven/material-artifacts.d.ts.map +1 -0
  154. package/dist/gpu-driven/material-bindings.d.ts +7 -0
  155. package/dist/gpu-driven/material-bindings.d.ts.map +1 -0
  156. package/dist/gpu-driven/pbr-program.d.ts +10 -0
  157. package/dist/gpu-driven/pbr-program.d.ts.map +1 -0
  158. package/dist/gpu-driven/prepared-draw.d.ts +2 -0
  159. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -1
  160. package/dist/gpu-driven/production-raster.d.ts +169 -27
  161. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  162. package/dist/gpu-driven/shadow-views.d.ts +126 -0
  163. package/dist/gpu-driven/shadow-views.d.ts.map +1 -0
  164. package/dist/gpu-driven/view-gpu.d.ts +18 -2
  165. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  166. package/dist/gpu-resource.d.ts +5 -2
  167. package/dist/gpu-resource.d.ts.map +1 -1
  168. package/dist/gpu-scene-schema.d.ts +2 -8
  169. package/dist/gpu-scene-schema.d.ts.map +1 -1
  170. package/dist/gpu-scene.d.ts +14 -0
  171. package/dist/gpu-scene.d.ts.map +1 -1
  172. package/dist/gpu-texture-usage.d.ts +1 -0
  173. package/dist/gpu-texture-usage.d.ts.map +1 -1
  174. package/dist/hdrp-buffers.d.ts +9 -0
  175. package/dist/hdrp-buffers.d.ts.map +1 -1
  176. package/dist/ibl/IblPipelineCache.d.ts +2 -0
  177. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  178. package/dist/ibl/frame-resources.d.ts +8 -0
  179. package/dist/ibl/frame-resources.d.ts.map +1 -0
  180. package/dist/ibl/skylight-bind-group.d.ts +3 -1
  181. package/dist/ibl/skylight-bind-group.d.ts.map +1 -1
  182. package/dist/index.d.ts +87 -75
  183. package/dist/index.d.ts.map +1 -1
  184. package/dist/index.mjs +10 -8
  185. package/dist/index.mjs.map +1 -1
  186. package/dist/inspection-types.d.ts +180 -1
  187. package/dist/inspection-types.d.ts.map +1 -1
  188. package/dist/instances-derived-bounds.d.ts +11 -0
  189. package/dist/instances-derived-bounds.d.ts.map +1 -1
  190. package/dist/instances.d.ts +89 -44
  191. package/dist/instances.d.ts.map +1 -1
  192. package/dist/internal.d.ts +3 -0
  193. package/dist/internal.d.ts.map +1 -1
  194. package/dist/internal.mjs +117 -9
  195. package/dist/internal.mjs.map +1 -1
  196. package/dist/lifecycle.d.ts +2 -1
  197. package/dist/lifecycle.d.ts.map +1 -1
  198. package/dist/material-row.d.ts +15 -0
  199. package/dist/material-row.d.ts.map +1 -0
  200. package/dist/materials.d.ts +15 -0
  201. package/dist/materials.d.ts.map +1 -1
  202. package/dist/pbr-pipeline.d.ts +23 -5
  203. package/dist/pbr-pipeline.d.ts.map +1 -1
  204. package/dist/pipeline/render-extent.d.ts +51 -0
  205. package/dist/pipeline/render-extent.d.ts.map +1 -0
  206. package/dist/pipeline/standard-build-context.d.ts +7 -0
  207. package/dist/pipeline/standard-build-context.d.ts.map +1 -0
  208. package/dist/pipeline/standard-forward-lane.d.ts +3 -2
  209. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  210. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts +12 -0
  211. package/dist/pipeline/standard-output/auto-exposure/capability.d.ts.map +1 -0
  212. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts +70 -0
  213. package/dist/pipeline/standard-output/auto-exposure/gpu.d.ts.map +1 -0
  214. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts +75 -0
  215. package/dist/pipeline/standard-output/auto-exposure/graph.d.ts.map +1 -0
  216. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts +111 -0
  217. package/dist/pipeline/standard-output/auto-exposure/inspection.d.ts.map +1 -0
  218. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts +16 -0
  219. package/dist/pipeline/standard-output/auto-exposure/oracle.d.ts.map +1 -0
  220. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts +14 -0
  221. package/dist/pipeline/standard-output/auto-exposure/preset.d.ts.map +1 -0
  222. package/dist/pipeline/standard-output/auto-exposure/state.d.ts +46 -0
  223. package/dist/pipeline/standard-output/auto-exposure/state.d.ts.map +1 -0
  224. package/dist/pipeline/standard-output/color-transform.d.ts +27 -0
  225. package/dist/pipeline/standard-output/color-transform.d.ts.map +1 -0
  226. package/dist/pipeline/standard-output/graph.d.ts +15 -0
  227. package/dist/pipeline/standard-output/graph.d.ts.map +1 -0
  228. package/dist/pipeline/standard-output/lut-admission.d.ts +29 -0
  229. package/dist/pipeline/standard-output/lut-admission.d.ts.map +1 -0
  230. package/dist/pipeline/standard-output/lut-gpu.d.ts +48 -0
  231. package/dist/pipeline/standard-output/lut-gpu.d.ts.map +1 -0
  232. package/dist/pipeline/standard-output/lut-state.d.ts +68 -0
  233. package/dist/pipeline/standard-output/lut-state.d.ts.map +1 -0
  234. package/dist/pipeline/standard-output/resources.d.ts +10 -0
  235. package/dist/pipeline/standard-output/resources.d.ts.map +1 -0
  236. package/dist/pipeline/standard-output/types.d.ts +39 -0
  237. package/dist/pipeline/standard-output/types.d.ts.map +1 -0
  238. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  239. package/dist/pipeline/standard-post.d.ts +2 -2
  240. package/dist/pipeline/standard-post.d.ts.map +1 -1
  241. package/dist/pipeline/standard-profile.d.ts +2 -1
  242. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  243. package/dist/pipeline-builder.d.ts +5 -2
  244. package/dist/pipeline-builder.d.ts.map +1 -1
  245. package/dist/pipeline-spec-types.d.ts +4 -0
  246. package/dist/pipeline-spec-types.d.ts.map +1 -1
  247. package/dist/pipeline-spec.d.ts +1 -9
  248. package/dist/pipeline-spec.d.ts.map +1 -1
  249. package/dist/plugin.d.ts.map +1 -1
  250. package/dist/points-lines/standard-owner.d.ts +1 -1
  251. package/dist/points-lines/standard-owner.d.ts.map +1 -1
  252. package/dist/post-process-errors.d.ts +9 -2
  253. package/dist/post-process-errors.d.ts.map +1 -1
  254. package/dist/prepare/extended-lighting/resources.d.ts +3 -3
  255. package/dist/prepare/prepared-graphics-resolver.d.ts +6 -1
  256. package/dist/prepare/prepared-graphics-resolver.d.ts.map +1 -1
  257. package/dist/record/dynamic-geometry-consumption.d.ts +15 -0
  258. package/dist/record/dynamic-geometry-consumption.d.ts.map +1 -0
  259. package/dist/record/frame-lighting.d.ts +3 -3
  260. package/dist/record/frame-lighting.d.ts.map +1 -1
  261. package/dist/record/frame-snapshot.d.ts +109 -13
  262. package/dist/record/frame-snapshot.d.ts.map +1 -1
  263. package/dist/record/frame-targets.d.ts.map +1 -1
  264. package/dist/record/frame.d.ts +41 -5
  265. package/dist/record/frame.d.ts.map +1 -1
  266. package/dist/record/main-pass-geometry.d.ts +13 -9
  267. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  268. package/dist/record/main-pass-material.d.ts +43 -3
  269. package/dist/record/main-pass-material.d.ts.map +1 -1
  270. package/dist/record/main-pass-sprite-draws.d.ts +2 -10
  271. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  272. package/dist/record/main-pass.d.ts +14 -2
  273. package/dist/record/main-pass.d.ts.map +1 -1
  274. package/dist/record/mesh-ssbo.d.ts +15 -6
  275. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  276. package/dist/record/prepared-material-bindings.d.ts +19 -0
  277. package/dist/record/prepared-material-bindings.d.ts.map +1 -0
  278. package/dist/record/recovery-pipeline.d.ts +1 -0
  279. package/dist/record/recovery-pipeline.d.ts.map +1 -1
  280. package/dist/record/render-context.d.ts +101 -22
  281. package/dist/record/render-context.d.ts.map +1 -1
  282. package/dist/record/shadow-pass.d.ts +62 -7
  283. package/dist/record/shadow-pass.d.ts.map +1 -1
  284. package/dist/record/skybox-post-pass.d.ts +1 -1
  285. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  286. package/dist/record/typed-frame-graph.d.ts +39 -8
  287. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  288. package/dist/record/view-ubo.d.ts +1 -0
  289. package/dist/record/view-ubo.d.ts.map +1 -1
  290. package/dist/recovery/render-system-candidate.d.ts +10 -2
  291. package/dist/recovery/render-system-candidate.d.ts.map +1 -1
  292. package/dist/recovery/render-system-roots.d.ts +11 -0
  293. package/dist/recovery/render-system-roots.d.ts.map +1 -0
  294. package/dist/recovery/types.d.ts +76 -0
  295. package/dist/recovery/types.d.ts.map +1 -0
  296. package/dist/reflection/background.d.ts +14 -0
  297. package/dist/reflection/background.d.ts.map +1 -0
  298. package/dist/reflection/filter.d.ts.map +1 -1
  299. package/dist/reflection/projection.d.ts +1 -0
  300. package/dist/reflection/projection.d.ts.map +1 -1
  301. package/dist/reflection/record-owner.d.ts +4 -4
  302. package/dist/reflection/record-owner.d.ts.map +1 -1
  303. package/dist/render-contract.d.ts +211 -53
  304. package/dist/render-contract.d.ts.map +1 -1
  305. package/dist/render-graph-primitives.d.ts +1 -0
  306. package/dist/render-graph-primitives.d.ts.map +1 -1
  307. package/dist/render-pipeline.d.ts +81 -6
  308. package/dist/render-pipeline.d.ts.map +1 -1
  309. package/dist/render-system-extract-tail.d.ts +15 -6
  310. package/dist/render-system-extract-tail.d.ts.map +1 -1
  311. package/dist/render-system-extract.d.ts +128 -41
  312. package/dist/render-system-extract.d.ts.map +1 -1
  313. package/dist/render-system-projections.d.ts +22 -0
  314. package/dist/render-system-projections.d.ts.map +1 -0
  315. package/dist/render-system.d.ts +27 -77
  316. package/dist/render-system.d.ts.map +1 -1
  317. package/dist/scene/render-scene-types.d.ts +14 -7
  318. package/dist/scene/render-scene-types.d.ts.map +1 -1
  319. package/dist/scene/render-scene.d.ts +31 -11
  320. package/dist/scene/render-scene.d.ts.map +1 -1
  321. package/dist/scene/shadow-visibility.d.ts +5 -0
  322. package/dist/scene/shadow-visibility.d.ts.map +1 -0
  323. package/dist/ssao-config.d.ts +8 -6
  324. package/dist/ssao-config.d.ts.map +1 -1
  325. package/dist/ssr/admission.d.ts +70 -1
  326. package/dist/ssr/admission.d.ts.map +1 -1
  327. package/dist/ssr/compose.d.ts +13 -0
  328. package/dist/ssr/compose.d.ts.map +1 -0
  329. package/dist/ssr/errors.d.ts +35 -0
  330. package/dist/ssr/errors.d.ts.map +1 -1
  331. package/dist/ssr/graph.d.ts +60 -0
  332. package/dist/ssr/graph.d.ts.map +1 -0
  333. package/dist/ssr/history.d.ts +97 -0
  334. package/dist/ssr/history.d.ts.map +1 -0
  335. package/dist/ssr/hiz.d.ts +37 -0
  336. package/dist/ssr/hiz.d.ts.map +1 -0
  337. package/dist/ssr/inspection.d.ts +42 -0
  338. package/dist/ssr/inspection.d.ts.map +1 -0
  339. package/dist/ssr/resources.d.ts +58 -0
  340. package/dist/ssr/resources.d.ts.map +1 -0
  341. package/dist/ssr/temporal.d.ts +38 -0
  342. package/dist/ssr/temporal.d.ts.map +1 -0
  343. package/dist/systems/skin-palette-allocator.d.ts +32 -10
  344. package/dist/systems/skin-palette-allocator.d.ts.map +1 -1
  345. package/dist/systems/skin-palette-types.d.ts +15 -0
  346. package/dist/systems/skin-palette-types.d.ts.map +1 -1
  347. package/dist/systems/transparent-dispatch.d.ts +9 -0
  348. package/dist/systems/transparent-dispatch.d.ts.map +1 -0
  349. package/dist/targets/material-source.d.ts +7 -0
  350. package/dist/targets/material-source.d.ts.map +1 -1
  351. package/dist/temporal/frame-coordinator.d.ts +7 -0
  352. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  353. package/dist/temporal/gpu.d.ts +8 -0
  354. package/dist/temporal/gpu.d.ts.map +1 -1
  355. package/dist/temporal/history.d.ts.map +1 -1
  356. package/dist/temporal/index.mjs +6 -6
  357. package/dist/temporal/inspection.d.ts.map +1 -1
  358. package/dist/temporal/standard-scene-data.d.ts +4 -2
  359. package/dist/temporal/standard-scene-data.d.ts.map +1 -1
  360. package/dist/temporal/target-coverage-attachment.d.ts +15 -0
  361. package/dist/temporal/target-coverage-attachment.d.ts.map +1 -0
  362. package/dist/temporal/temporal-view.d.ts +6 -0
  363. package/dist/temporal/temporal-view.d.ts.map +1 -1
  364. package/dist/temporal/view.d.ts +6 -0
  365. package/dist/temporal/view.d.ts.map +1 -1
  366. package/dist/typed-render-graph-primitives.d.ts +41 -1
  367. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  368. package/dist/typed-shadow-passes.d.ts +3 -1
  369. package/dist/typed-shadow-passes.d.ts.map +1 -1
  370. package/dist/volume/component.d.ts +1 -0
  371. package/dist/volume/component.d.ts.map +1 -1
  372. package/dist/volume/temporal.d.ts +6 -0
  373. package/dist/volume/temporal.d.ts.map +1 -1
  374. package/package.json +21 -20
  375. package/src/__tests__/auto-exposure-gpu-evidence.ts +291 -0
  376. package/src/__tests__/auto-exposure-inspection.unit.test.ts +80 -0
  377. package/src/__tests__/auto-exposure-transaction.integration.test.ts +99 -0
  378. package/src/__tests__/auto-exposure.browser.test.ts +33 -0
  379. package/src/__tests__/auto-exposure.dawn.test.ts +33 -0
  380. package/src/__tests__/capture-scene-projection.unit.test.ts +24 -0
  381. package/src/__tests__/detached-inspection.integration.test.ts +5 -0
  382. package/src/__tests__/device-feature-admission.unit.test.ts +15 -24
  383. package/src/__tests__/device-loss-fanout.unit.test.ts +43 -0
  384. package/src/__tests__/device-scope-lifecycle.unit.test.ts +18 -0
  385. package/src/__tests__/device-scope-stale-matrix.unit.test.ts +18 -0
  386. package/src/__tests__/docs-gate-green.unit.test.ts +3 -3
  387. package/src/__tests__/dynamic-geometry-batch.unit.test.ts +157 -0
  388. package/src/__tests__/dynamic-geometry.unit.test.ts +36 -0
  389. package/src/__tests__/dynamic-resolution-plugin.integration.test.ts +81 -0
  390. package/src/__tests__/factory-contract.integration.test.ts +231 -9
  391. package/src/__tests__/fallback-row-stride-surface.unit.test.ts +19 -0
  392. package/src/__tests__/feature-depth-input.dawn.test.ts +121 -0
  393. package/src/__tests__/feature-noise.unit.test.ts +38 -0
  394. package/src/__tests__/fixtures/taa-resolve-pinned-baseline.wgsl +301 -0
  395. package/src/__tests__/frame-camera-selection.unit.test.ts +131 -0
  396. package/src/__tests__/frame-observation.contract.test.ts +21 -0
  397. package/src/__tests__/frame-plan-contract.unit.test.ts +24 -0
  398. package/src/__tests__/frame-targets.unit.test.ts +24 -0
  399. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +3 -0
  400. package/src/__tests__/gpu-driven-baseline.characterization.test.ts +79 -0
  401. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +307 -7
  402. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +122 -142
  403. package/src/__tests__/gpu-driven-indirect-raster.browser.test.ts +10 -1
  404. package/src/__tests__/gpu-driven-lod-cache.unit.test.ts +261 -0
  405. package/src/__tests__/gpu-driven-mesh-world-identity.unit.test.ts +346 -0
  406. package/src/__tests__/gpu-driven-pbr.dawn.test.ts +1422 -0
  407. package/src/__tests__/gpu-driven-pbr.integration.test.ts +21 -0
  408. package/src/__tests__/gpu-driven-production-fixture.ts +96 -0
  409. package/src/__tests__/gpu-driven-production.integration.test.ts +1076 -106
  410. package/src/__tests__/gpu-driven-scaling.unit.test.ts +1 -3
  411. package/src/__tests__/gpu-driven-shadow-views.browser.test.ts +63 -0
  412. package/src/__tests__/gpu-driven-shadow-views.dawn.test.ts +294 -0
  413. package/src/__tests__/gpu-driven-shadow-views.integration.test.ts +97 -0
  414. package/src/__tests__/gpu-driven-shadow-views.unit.test.ts +168 -0
  415. package/src/__tests__/gpu-driven-skin-contract.test-d.ts +11 -0
  416. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +72 -20
  417. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +30 -9
  418. package/src/__tests__/gpu-driven-view.unit.test.ts +160 -52
  419. package/src/__tests__/gpu-pass-timing-browser-runner.ts +15 -0
  420. package/src/__tests__/gpu-pass-timing.browser.test.ts +20 -2
  421. package/src/__tests__/gpu-resource-scope.unit.test.ts +71 -0
  422. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +42 -0
  423. package/src/__tests__/gpu-scene-render-graph-gpu.ts +19 -5
  424. package/src/__tests__/gpu-scene.dawn.test.ts +40 -13
  425. package/src/__tests__/gpu-scene.unit.test.ts +613 -33
  426. package/src/__tests__/instances-culling.integration.test.ts +36 -10
  427. package/src/__tests__/instances-derived-bounds.unit.test.ts +54 -0
  428. package/src/__tests__/instances-store.unit.test.ts +242 -39
  429. package/src/__tests__/ktx2-basis-gpu-consumer.dawn.test.ts +6 -0
  430. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +13 -16
  431. package/src/__tests__/lut-lkg-transaction.integration.test.ts +122 -0
  432. package/src/__tests__/material-contract-inventory.unit.test.ts +5 -6
  433. package/src/__tests__/material-dispatch-shader-id.unit.test.ts +51 -0
  434. package/src/__tests__/material-lane-selection.unit.test.ts +60 -0
  435. package/src/__tests__/material-numeric-upload.unit.test.ts +53 -0
  436. package/src/__tests__/material-texture-handles.unit.test.ts +18 -0
  437. package/src/__tests__/material-texture-source.unit.test.ts +87 -0
  438. package/src/__tests__/materials-authoring-validation.unit.test.ts +41 -0
  439. package/src/__tests__/materials-standard-contract.unit.test.ts +57 -3
  440. package/src/__tests__/mesh-buffer-usage-surface.unit.test.ts +4 -0
  441. package/src/__tests__/mesh-submission-lifetime.unit.test.ts +8 -0
  442. package/src/__tests__/morph-neutral-admission.unit.test.ts +19 -0
  443. package/src/__tests__/normal-fallback.dawn.test.ts +122 -0
  444. package/src/__tests__/particle-material-input-layout.unit.test.ts +104 -0
  445. package/src/__tests__/physical-clearcoat.integration.test.ts +42 -3
  446. package/src/__tests__/pipeline-texture-specialization.unit.test.ts +138 -0
  447. package/src/__tests__/point-shadow-inspection.unit.test.ts +2 -0
  448. package/src/__tests__/post-process-error-policy-owner.unit.test.ts +6 -5
  449. package/src/__tests__/prepared-gpu-driven-pbr-contract.test-d.ts +31 -0
  450. package/src/__tests__/prepared-gpu-driven-pbr-contract.unit.test.ts +39 -0
  451. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +251 -9
  452. package/src/__tests__/prepared-graphics-capability.unit.test.ts +21 -0
  453. package/src/__tests__/prepared-graphics-pipeline-warmup.unit.test.ts +6 -2
  454. package/src/__tests__/profiler-phase-catalog.test.ts +5 -0
  455. package/src/__tests__/public-surface.test-d.ts +3 -0
  456. package/src/__tests__/recovery-candidate-prepare.contract.test.ts +30 -12
  457. package/src/__tests__/reflection-fallback-async-publication.unit.test.ts +171 -0
  458. package/src/__tests__/reflection-probe-authoring.unit.test.ts +12 -10
  459. package/src/__tests__/reflection-probe-box.dawn.test.ts +71 -0
  460. package/src/__tests__/reflection-probe-filter.unit.test.ts +9 -0
  461. package/src/__tests__/reflection-probe-orientation.dawn.test.ts +294 -0
  462. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +5 -14
  463. package/src/__tests__/reflection-probe-updates.unit.test.ts +129 -0
  464. package/src/__tests__/render-error-code-owner.test-d.ts +7 -0
  465. package/src/__tests__/render-error-detail-ssot.test-d.ts +27 -1
  466. package/src/__tests__/render-error-exhaustive.test-d.ts +15 -0
  467. package/src/__tests__/render-extent.unit.test.ts +63 -0
  468. package/src/__tests__/render-feature-gpu-work.integration.test.ts +285 -2
  469. package/src/__tests__/render-feature-plan.unit.test.ts +245 -0
  470. package/src/__tests__/render-feature-shadow-order.unit.test.ts +228 -0
  471. package/src/__tests__/render-feature-zero-work.unit.test.ts +35 -1
  472. package/src/__tests__/render-graph-fallback.integration.test.ts +2 -2
  473. package/src/__tests__/render-owner-cohesion.unit.test.ts +55 -0
  474. package/src/__tests__/render-scene-mixed-updates.integration.test.ts +426 -0
  475. package/src/__tests__/render-scene-projection.unit.test.ts +915 -23
  476. package/src/__tests__/render-scene-resource-updates.integration.test.ts +214 -0
  477. package/src/__tests__/render-scene-skin-lifecycle.integration.test.ts +173 -0
  478. package/src/__tests__/render-scene-temporal-retry.unit.test.ts +133 -0
  479. package/src/__tests__/render-target-material-source.test-d.ts +27 -3
  480. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +233 -9
  481. package/src/__tests__/renderer-frame-transaction.integration.test.ts +923 -5
  482. package/src/__tests__/shader-manifest-fixture.ts +45 -0
  483. package/src/__tests__/skin-motion-regression.unit.test.ts +0 -33
  484. package/src/__tests__/skin-palette-persistent.integration.test.ts +28 -0
  485. package/src/__tests__/skin-palette-persistent.unit.test.ts +247 -0
  486. package/src/__tests__/skinned-shadow-caster.test.ts +125 -14
  487. package/src/__tests__/specular-environment-scale.dawn.test.ts +158 -0
  488. package/src/__tests__/ssao-config.unit.test.ts +7 -0
  489. package/src/__tests__/ssao-surface.dawn.test.ts +252 -0
  490. package/src/__tests__/ssr-admission-matrix.integration.test.ts +7 -1
  491. package/src/__tests__/ssr-confidence-compose.unit.test.ts +84 -0
  492. package/src/__tests__/ssr-coordinate-oracle.unit.test.ts +28 -0
  493. package/src/__tests__/ssr-cube-silhouette.dawn.test.ts +270 -0
  494. package/src/__tests__/ssr-format-probe-gating.integration.test.ts +82 -0
  495. package/src/__tests__/ssr-gpu-dispatch.browser.test.ts +99 -0
  496. package/src/__tests__/ssr-gpu-dispatch.dawn.test.ts +116 -0
  497. package/src/__tests__/ssr-gpu-dispatch.ts +951 -0
  498. package/src/__tests__/ssr-graph-topology.unit.test.ts +104 -0
  499. package/src/__tests__/ssr-history-filter.dawn.test.ts +198 -0
  500. package/src/__tests__/ssr-hit-color.dawn.test.ts +273 -0
  501. package/src/__tests__/ssr-hiz-odd-reduction.dawn.test.ts +569 -0
  502. package/src/__tests__/ssr-hiz-rescue.dawn.test.ts +279 -0
  503. package/src/__tests__/ssr-hiz.browser.test.ts +150 -0
  504. package/src/__tests__/ssr-hiz.dawn.test.ts +150 -0
  505. package/src/__tests__/ssr-hiz.unit.test.ts +56 -0
  506. package/src/__tests__/ssr-inspection.unit.test.ts +105 -0
  507. package/src/__tests__/ssr-memory-budget.unit.test.ts +45 -0
  508. package/src/__tests__/ssr-performance-sequence-aggregation.unit.test.ts +15 -0
  509. package/src/__tests__/ssr-public-surface.test-d.ts +37 -0
  510. package/src/__tests__/ssr-receiver-coverage.dawn.test.ts +387 -0
  511. package/src/__tests__/ssr-reflection-mip-reducer.dawn.test.ts +309 -0
  512. package/src/__tests__/ssr-reprojection-probe.ts +90 -0
  513. package/src/__tests__/ssr-roughness-mip.dawn.test.ts +163 -0
  514. package/src/__tests__/ssr-shader-prewarm.unit.test.ts +59 -0
  515. package/src/__tests__/ssr-spatial.integration.test.ts +141 -0
  516. package/src/__tests__/ssr-stable-lattice.dawn.test.ts +475 -0
  517. package/src/__tests__/ssr-temporal-history.unit.test.ts +202 -0
  518. package/src/__tests__/ssr-texel-crossing.dawn.test.ts +448 -0
  519. package/src/__tests__/ssr-zero-work.integration.test.ts +87 -0
  520. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +84 -12
  521. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +29 -0
  522. package/src/__tests__/standard-output-chain-contract.unit.test.ts +99 -0
  523. package/src/__tests__/standard-output-chain.integration.test.ts +73 -0
  524. package/src/__tests__/standard-output-gpu-evidence.ts +205 -0
  525. package/src/__tests__/standard-output.browser.test.ts +15 -0
  526. package/src/__tests__/standard-output.dawn.test.ts +15 -0
  527. package/src/__tests__/standard-pbr-artifact-assembly.unit.test.ts +275 -0
  528. package/src/__tests__/standard-pipeline.integration.test.ts +320 -3
  529. package/src/__tests__/standard-profile-oracle.unit.test.ts +49 -0
  530. package/src/__tests__/standard-surface-lighting.unit.test.ts +41 -0
  531. package/src/__tests__/standard-texture-specialization.dawn.test.ts +141 -0
  532. package/src/__tests__/surface-adjacent-regressions.integration.test.ts +6 -3
  533. package/src/__tests__/taa-coverage-edge.dawn.test.ts +199 -0
  534. package/src/__tests__/taa-history-clipping.dawn.test.ts +328 -0
  535. package/src/__tests__/taa-neighborhood-equivalence.dawn.test.ts +782 -0
  536. package/src/__tests__/taa-stability-feedback.browser.test.ts +23 -0
  537. package/src/__tests__/taa-stability-feedback.dawn.test.ts +68 -0
  538. package/src/__tests__/taa-stability-feedback.ts +342 -0
  539. package/src/__tests__/temporal-history-lifecycle.unit.test.ts +49 -7
  540. package/src/__tests__/temporal-reset-matrix.unit.test.ts +9 -0
  541. package/src/__tests__/temporal-transaction.integration.test.ts +28 -0
  542. package/src/__tests__/temporal-view.unit.test.ts +17 -0
  543. package/src/__tests__/texture-usage-surface.unit.test.ts +4 -2
  544. package/src/__tests__/transmission-prewarm.unit.test.ts +57 -0
  545. package/src/__tests__/typed-pipeline-topology.unit.test.ts +109 -4
  546. package/src/__tests__/typed-scene-pass-material-cache.integration.test.ts +126 -0
  547. package/src/__tests__/vertex-layouts-ssot.unit.test.ts +47 -0
  548. package/src/__tests__/visibility-extract.perf.test.ts +20 -8
  549. package/src/__tests__/visibility-instances.integration.test.ts +19 -7
  550. package/src/__tests__/visibility-producer-matrix.integration.test.ts +20 -9
  551. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +62 -13
  552. package/src/__tests__/volumetric-fog-topology.unit.test.ts +7 -7
  553. package/src/assembly/atmosphere-shader-prewarm.ts +39 -0
  554. package/src/assembly/device-feature-admission.ts +9 -4
  555. package/src/assembly/dynamic-geometry-host.ts +284 -163
  556. package/src/assembly/dynamic-geometry-runtime.ts +71 -0
  557. package/src/assembly/factory.ts +4 -0
  558. package/src/assembly/gpu-driven-owner.ts +29 -0
  559. package/src/assembly/host-contract.ts +19 -0
  560. package/src/assembly/material/assembly.ts +383 -1
  561. package/src/assembly/material/pipeline-helpers.ts +100 -0
  562. package/src/assembly/material-shader-policy.ts +110 -46
  563. package/src/assembly/recovery/device-loss-fanout.ts +5 -0
  564. package/src/assembly/recovery/failure-location.ts +62 -0
  565. package/src/assembly/recovery/recovery-attempt.ts +2 -29
  566. package/src/assembly/recovery/renderer-recover.ts +16 -17
  567. package/src/assembly/renderer-facade.ts +34 -1
  568. package/src/assembly/renderer-feature-inspection.ts +13 -0
  569. package/src/assembly/renderer-frame-transaction.ts +63 -0
  570. package/src/assembly/renderer-helpers.ts +26 -0
  571. package/src/assembly/renderer-inspection.ts +6 -0
  572. package/src/assembly/shader-prewarm-policy.ts +46 -1
  573. package/src/assembly/skin-palette-owner.ts +21 -0
  574. package/src/assembly/ssr-shader-prewarm.ts +41 -0
  575. package/src/assembly/webgpu-device-loss.ts +75 -0
  576. package/src/assembly/webgpu-pbr-ready.ts +232 -0
  577. package/src/assembly/webgpu-ready-mipmap.ts +26 -0
  578. package/src/assembly/webgpu-ready.ts +161 -131
  579. package/src/assembly/webgpu-renderer-bootstrap.ts +85 -0
  580. package/src/assembly/webgpu-renderer-contract.ts +69 -0
  581. package/src/assembly/webgpu-renderer-guards.ts +32 -0
  582. package/src/assembly/webgpu-renderer-observation.ts +43 -0
  583. package/src/assembly/webgpu-renderer-recovery-failure.ts +62 -0
  584. package/src/assembly/webgpu-renderer.ts +744 -730
  585. package/src/assembly/webgpu-vertex-layouts.ts +233 -87
  586. package/src/capture/scene-projection.ts +37 -0
  587. package/src/components/__tests__/camera-exposure.contract.unit.test.ts +153 -0
  588. package/src/components/__tests__/dynamic-resolution-schema.unit.test.ts +65 -0
  589. package/src/components/__tests__/light-radius.integration.test.ts +59 -0
  590. package/src/components/camera.ts +287 -7
  591. package/src/components/directional-light.ts +3 -3
  592. package/src/components/dynamic-resolution.ts +67 -0
  593. package/src/components/index.ts +2 -0
  594. package/src/components/instances.ts +45 -18
  595. package/src/components/light-helpers.ts +6 -1
  596. package/src/components/reflection-probe.ts +1 -0
  597. package/src/components/scene-instance.ts +2 -1
  598. package/src/components/screen-space-reflection.ts +21 -0
  599. package/src/components/skylight.ts +4 -1
  600. package/src/components/sprite-animation.ts +2 -2
  601. package/src/components/sprite-instances.ts +1 -1
  602. package/src/device/device-scope.ts +13 -7
  603. package/src/device/gpu-residency.ts +45 -42
  604. package/src/dynamic-geometry.ts +13 -8
  605. package/src/environment/background.ts +281 -0
  606. package/src/environment/ibl.ts +211 -0
  607. package/src/environment/storage.ts +63 -0
  608. package/src/errors/gpu-driven.ts +49 -8
  609. package/src/errors/render.ts +91 -3
  610. package/src/extract/camera.ts +37 -1
  611. package/src/extract/contracts.ts +10 -0
  612. package/src/extract/gpu-driven.ts +240 -10
  613. package/src/features/host.ts +237 -19
  614. package/src/features/noise-texture.ts +91 -0
  615. package/src/features/plan.ts +553 -24
  616. package/src/features/prepared-gpu-work.ts +631 -49
  617. package/src/features/prepared-graphics-store.ts +31 -0
  618. package/src/features/prepared-graphics.ts +5 -0
  619. package/src/features/render-graph-compute.ts +41 -8
  620. package/src/features/render-graph-contribution.ts +141 -3
  621. package/src/features/render-graph-raster.ts +143 -30
  622. package/src/features/types.ts +11 -0
  623. package/src/fullscreen-post-process-pass.ts +3 -0
  624. package/src/gpu-driven/batch-topology.ts +452 -108
  625. package/src/gpu-driven/material-artifacts.ts +179 -0
  626. package/src/gpu-driven/material-bindings.ts +41 -0
  627. package/src/gpu-driven/pbr-program.ts +31 -0
  628. package/src/gpu-driven/prepared-draw.ts +42 -6
  629. package/src/gpu-driven/production-raster.ts +2806 -716
  630. package/src/gpu-driven/shadow-views.ts +539 -0
  631. package/src/gpu-driven/view-gpu.ts +188 -41
  632. package/src/gpu-resource.ts +21 -2
  633. package/src/gpu-scene-schema.ts +61 -11
  634. package/src/gpu-scene.ts +197 -99
  635. package/src/gpu-texture-usage.ts +1 -0
  636. package/src/hdrp-buffers.ts +141 -36
  637. package/src/ibl/IblPipelineCache.ts +5 -0
  638. package/src/ibl/frame-resources.ts +38 -0
  639. package/src/ibl/skylight-bind-group.ts +32 -21
  640. package/src/index.ts +194 -75
  641. package/src/inspection-types.ts +207 -1
  642. package/src/instances-derived-bounds.ts +258 -34
  643. package/src/instances.ts +459 -150
  644. package/src/internal.ts +22 -0
  645. package/src/lifecycle.ts +2 -0
  646. package/src/material-row.ts +194 -0
  647. package/src/materials.ts +44 -8
  648. package/src/pbr-pipeline.ts +172 -19
  649. package/src/pipeline/render-extent.ts +111 -0
  650. package/src/pipeline/standard-build-context.ts +7 -0
  651. package/src/pipeline/standard-forward-lane.ts +74 -29
  652. package/src/pipeline/standard-output/__tests__/lut-admission.unit.test.ts +103 -0
  653. package/src/pipeline/standard-output/auto-exposure/__tests__/capability.unit.test.ts +40 -0
  654. package/src/pipeline/standard-output/auto-exposure/__tests__/gpu.contract.unit.test.ts +125 -0
  655. package/src/pipeline/standard-output/auto-exposure/__tests__/graph.contract.unit.test.ts +94 -0
  656. package/src/pipeline/standard-output/auto-exposure/__tests__/oracle.test.ts +108 -0
  657. package/src/pipeline/standard-output/auto-exposure/__tests__/preset.test.ts +26 -0
  658. package/src/pipeline/standard-output/auto-exposure/capability.ts +38 -0
  659. package/src/pipeline/standard-output/auto-exposure/gpu.ts +567 -0
  660. package/src/pipeline/standard-output/auto-exposure/graph.ts +221 -0
  661. package/src/pipeline/standard-output/auto-exposure/inspection.ts +231 -0
  662. package/src/pipeline/standard-output/auto-exposure/oracle.ts +291 -0
  663. package/src/pipeline/standard-output/auto-exposure/preset.ts +28 -0
  664. package/src/pipeline/standard-output/auto-exposure/state.ts +156 -0
  665. package/src/pipeline/standard-output/color-transform.ts +124 -0
  666. package/src/pipeline/standard-output/graph.ts +139 -0
  667. package/src/pipeline/standard-output/lut-admission.ts +121 -0
  668. package/src/pipeline/standard-output/lut-gpu.ts +399 -0
  669. package/src/pipeline/standard-output/lut-state.ts +176 -0
  670. package/src/pipeline/standard-output/resources.ts +121 -0
  671. package/src/pipeline/standard-output/types.ts +58 -0
  672. package/src/pipeline/standard-pipeline.ts +232 -70
  673. package/src/pipeline/standard-post.ts +190 -27
  674. package/src/pipeline/standard-profile.ts +2 -1
  675. package/src/pipeline-builder.ts +28 -9
  676. package/src/pipeline-spec-types.ts +4 -0
  677. package/src/pipeline-spec.ts +37 -29
  678. package/src/plugin.ts +4 -0
  679. package/src/points-lines/standard-owner.ts +6 -14
  680. package/src/post-process-errors.ts +26 -18
  681. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +2 -2
  682. package/src/prepare/extended-lighting/resources.ts +3 -3
  683. package/src/prepare/prepared-graphics-resolver.ts +9 -2
  684. package/src/record/__tests__/main-pass-geometry.unit.test.ts +68 -0
  685. package/src/record/__tests__/material-bg-cache.unit.test.ts +45 -0
  686. package/src/record/__tests__/material-slot-plan.unit.test.ts +57 -0
  687. package/src/record/__tests__/mesh-ssbo.unit.test.ts +40 -1
  688. package/src/record/__tests__/recovery-pipeline.unit.test.ts +20 -1
  689. package/src/record/__tests__/sprite-material-abi.unit.test.ts +41 -6
  690. package/src/record/__tests__/temporal-payload.unit.test.ts +2 -0
  691. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +45 -0
  692. package/src/record/__tests__/view-ubo-layout.unit.test.ts +5 -0
  693. package/src/record/dynamic-geometry-consumption.ts +59 -0
  694. package/src/record/frame-lighting.ts +15 -12
  695. package/src/record/frame-snapshot.ts +146 -34
  696. package/src/record/frame-targets.ts +9 -0
  697. package/src/record/frame.ts +838 -170
  698. package/src/record/main-pass-geometry.ts +209 -103
  699. package/src/record/main-pass-material.ts +157 -40
  700. package/src/record/main-pass-sprite-draws.ts +16 -107
  701. package/src/record/main-pass.ts +141 -12
  702. package/src/record/material-uniforms.ts +1 -1
  703. package/src/record/mesh-ssbo.ts +59 -15
  704. package/src/record/prepared-material-bindings.ts +238 -0
  705. package/src/record/recovery-pipeline.ts +69 -90
  706. package/src/record/render-context.ts +123 -24
  707. package/src/record/shadow-pass.ts +1389 -230
  708. package/src/record/skybox-post-pass.ts +14 -3
  709. package/src/record/typed-frame-graph.ts +1064 -47
  710. package/src/record/view-ubo.ts +12 -1
  711. package/src/recovery/render-system-candidate.ts +135 -68
  712. package/src/recovery/render-system-roots.ts +113 -0
  713. package/src/recovery/types.ts +77 -0
  714. package/src/reflection/background.ts +151 -0
  715. package/src/reflection/filter.ts +3 -0
  716. package/src/reflection/projection.ts +16 -6
  717. package/src/reflection/record-owner.ts +308 -140
  718. package/src/render-contract.ts +237 -49
  719. package/src/render-graph-primitives.ts +34 -5
  720. package/src/render-pipeline.ts +90 -2
  721. package/src/render-system-extract-tail.ts +431 -162
  722. package/src/render-system-extract.ts +478 -119
  723. package/src/render-system-projections.ts +241 -0
  724. package/src/render-system.ts +715 -828
  725. package/src/scene/render-scene-types.ts +13 -4
  726. package/src/scene/render-scene.ts +577 -355
  727. package/src/scene/shadow-visibility.ts +21 -0
  728. package/src/ssao-config.ts +25 -8
  729. package/src/ssr/admission.ts +380 -14
  730. package/src/ssr/compose.ts +144 -0
  731. package/src/ssr/errors.ts +48 -0
  732. package/src/ssr/graph.ts +797 -0
  733. package/src/ssr/history.ts +436 -0
  734. package/src/ssr/hiz.ts +124 -0
  735. package/src/ssr/inspection.ts +124 -0
  736. package/src/ssr/resources.ts +202 -0
  737. package/src/ssr/temporal.ts +164 -0
  738. package/src/systems/skin-palette-allocator.ts +495 -148
  739. package/src/systems/skin-palette-types.ts +17 -0
  740. package/src/systems/transparent-dispatch.ts +90 -0
  741. package/src/targets/material-source.ts +34 -0
  742. package/src/temporal/coverage.ts +1 -1
  743. package/src/temporal/frame-coordinator.ts +23 -2
  744. package/src/temporal/gpu.ts +79 -12
  745. package/src/temporal/history.ts +3 -1
  746. package/src/temporal/inspection.ts +2 -1
  747. package/src/temporal/standard-scene-data.ts +10 -2
  748. package/src/temporal/target-coverage-attachment.ts +72 -0
  749. package/src/temporal/temporal-view.ts +11 -2
  750. package/src/temporal/view.ts +12 -2
  751. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +219 -139
  752. package/src/typed-render-graph-primitives.ts +304 -74
  753. package/src/typed-shadow-passes.ts +122 -12
  754. package/src/volume/component.ts +1 -0
  755. package/src/volume/passes.ts +3 -3
  756. package/src/volume/temporal.ts +10 -0
  757. package/dist/chunk-GE3SDD72.mjs.map +0 -1
  758. package/dist/chunk-GF523LHF.mjs.map +0 -1
  759. package/dist/chunk-IQMKJLNU.mjs.map +0 -1
  760. package/dist/chunk-J52NMQKR.mjs.map +0 -1
  761. package/dist/chunk-J6KACNOH.mjs.map +0 -1
  762. package/dist/chunk-JAASCMXA.mjs.map +0 -1
  763. package/dist/chunk-MBU4ZVE7.mjs.map +0 -1
  764. package/dist/chunk-MYJ5RPJO.mjs.map +0 -1
  765. package/dist/chunk-RBDBZ5R7.mjs.map +0 -1
  766. package/dist/chunk-VSL23LZE.mjs.map +0 -1
  767. package/src/__tests__/instances-world-ownership.integration.test.ts +0 -62
  768. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +0 -36
@@ -3,20 +3,22 @@ import { World } from '@forgeax/engine-ecs';
3
3
  import { deriveVertexLayoutProjection } from '@forgeax/engine-geometry';
4
4
  import { vec3 } from '@forgeax/engine-math';
5
5
  import { RenderGraphBuilder } from '@forgeax/engine-render-graph';
6
- import type { BindGroup } from '@forgeax/engine-rhi';
6
+ import { type BindGroup, RhiError, type RhiRenderPassEncoder } from '@forgeax/engine-rhi';
7
7
  import { type RhiNullDevice, rhi } from '@forgeax/engine-rhi-null';
8
- import { Transform } from '@forgeax/engine-scene';
8
+ import { propagateTransforms, Transform } from '@forgeax/engine-scene';
9
9
  import {
10
10
  createStandardPbrArtifactReceipt,
11
11
  DEFAULT_STANDARD_PBR_PARAM_SCHEMA,
12
12
  DEFAULT_UNLIT_PARAM_SCHEMA,
13
13
  } from '@forgeax/engine-shader';
14
14
  import { ok } from '@forgeax/engine-types';
15
- import { describe, expect, it } from 'vitest';
15
+ import { describe, expect, it, vi } from 'vitest';
16
16
  import { createRenderer as constructRenderer } from '../assembly/factory';
17
17
  import { Camera, MeshFilter, MeshRenderer, PointLight } from '../components';
18
18
  import type { MeshGpuHandles } from '../device/gpu-residency';
19
+ import { gpuDrivenDrawKey, gpuDrivenShadowDrawKey } from '../extract/gpu-driven';
19
20
  import { BatchTopology } from '../gpu-driven/batch-topology';
21
+ import { standardPbrProgramKey } from '../gpu-driven/pbr-program';
20
22
  import {
21
23
  adaptStandardPbrFrameResources,
22
24
  GpuDrivenProduction,
@@ -31,10 +33,28 @@ import {
31
33
  GPU_BUFFER_USAGE_UNIFORM,
32
34
  GPU_BUFFER_USAGE_VERTEX,
33
35
  } from '../gpu-usage';
36
+ import { buildDispatchPlan } from '../record/frame';
34
37
  import { makeZeroCameraFallbackSnapshot, worldEntityKey } from '../record/frame-snapshot';
38
+ import {
39
+ encodeDirectionalShadowPass,
40
+ encodePointShadowPass,
41
+ encodeSpotShadowPass,
42
+ recordShadowCasterDraws,
43
+ shadowShaderMap,
44
+ } from '../record/shadow-pass';
35
45
  import type { RenderPipelineFrame } from '../render-pipeline';
36
- import type { MaterialSnapshot, RenderableSnapshot } from '../render-system-extract';
37
- import { RenderScene } from '../scene/render-scene';
46
+ import type {
47
+ DispatchEntry,
48
+ MaterialSnapshot,
49
+ RenderableSnapshot,
50
+ ShadowCasterMembership,
51
+ } from '../render-system-extract';
52
+ import { shadowCasterCpuReason } from '../render-system-extract';
53
+ import { PersistentRenderScene, RenderScene } from '../scene/render-scene';
54
+ import {
55
+ preparedPbrSnapshot as preparedPbrFixture,
56
+ standardPbrInputs,
57
+ } from './gpu-driven-production-fixture';
38
58
 
39
59
  const STANDARD_PBR_RECEIPT = createStandardPbrArtifactReceipt();
40
60
 
@@ -136,6 +156,27 @@ function snapshot(entityKey: number, assetHandle: number): RenderableSnapshot {
136
156
  };
137
157
  }
138
158
 
159
+ function preparedSnapshot(entityKey: number, assetHandle: number): RenderableSnapshot {
160
+ return preparedPbrFixture(snapshot(entityKey, assetHandle));
161
+ }
162
+
163
+ function withoutPrepared(
164
+ draw: NonNullable<RenderableSnapshot['gpuDrivenDraws']>[number],
165
+ ): Omit<typeof draw, 'prepared'> {
166
+ const { prepared, ...rest } = draw;
167
+ void prepared;
168
+ return rest;
169
+ }
170
+
171
+ function updateSnapshot(value: RenderableSnapshot) {
172
+ return {
173
+ kind: 'update' as const,
174
+ worldId: value.worldId,
175
+ entityKey: value.entityKey,
176
+ snapshot: value,
177
+ };
178
+ }
179
+
139
180
  function mesh(device: RhiNullDevice, indexed = true, withLod = false): MeshGpuHandles {
140
181
  const vertex = device
141
182
  .createBuffer({
@@ -158,7 +199,6 @@ function mesh(device: RhiNullDevice, indexed = true, withLod = false): MeshGpuHa
158
199
  iboBytes: indexed ? 8 : 0,
159
200
  indexCount: indexed ? 3 : 0,
160
201
  indexFormat: 'uint16',
161
- layout: '12F',
162
202
  layoutProjection: deriveVertexLayoutProjection({
163
203
  position: new Float32Array(9),
164
204
  normal: new Float32Array(9),
@@ -199,10 +239,11 @@ describe('GPU-driven production projection', () => {
199
239
  entries: [{ binding: 0, visibility: 0x1, buffer: { type: 'uniform' } }],
200
240
  })
201
241
  .unwrap();
242
+ const pbrInputs = standardPbrInputs(device, shader, viewLayout);
202
243
  const projection = new RenderScene();
203
244
  const lodMeshGuid = '00000000-0000-7000-8000-000000000001' as never;
204
245
  const makeLodSnapshot = (entityKey: number): RenderableSnapshot => {
205
- const base = snapshot(entityKey, 3);
246
+ const base = preparedPbrFixture(snapshot(entityKey, 3));
206
247
  const draw = base.gpuDrivenDraws?.[0];
207
248
  if (draw === undefined) throw new Error('LOD test snapshot has no draw');
208
249
  return {
@@ -212,8 +253,8 @@ describe('GPU-driven production projection', () => {
212
253
  };
213
254
  };
214
255
  const delta = projection.apply([
215
- { kind: 'create', snapshot: makeLodSnapshot(1) },
216
- { kind: 'create', snapshot: makeLodSnapshot(2) },
256
+ updateSnapshot(makeLodSnapshot(1)),
257
+ updateSnapshot(makeLodSnapshot(2)),
217
258
  ]);
218
259
  const availability = GpuScene.create(device, 2).unwrap();
219
260
  expect(availability.status).toBe('available');
@@ -224,23 +265,28 @@ describe('GPU-driven production projection', () => {
224
265
  const production = new GpuDrivenProduction(device, {
225
266
  createShaderModule: () => ok(shader),
226
267
  });
268
+ const lodGpuMesh = mesh(device, true, true);
227
269
  const prepared = production
228
270
  .prepare({
271
+ ...pbrInputs,
229
272
  scene: {
230
273
  scene: availability.scene,
231
274
  plan: topology.plan(),
232
275
  slots: projection.slotsSnapshot(),
233
276
  },
234
277
  camera: makeZeroCameraFallbackSnapshot(),
235
- meshes: new Map([[3, mesh(device, true, true)]]),
278
+ meshBySlot: new Map([
279
+ [0, lodGpuMesh],
280
+ [1, lodGpuMesh],
281
+ ]),
236
282
  viewBindGroupLayout: viewLayout,
237
283
  meshResidencyEpoch: 1,
238
- hdrp: false,
284
+
239
285
  activeEntityKeys: new Set([worldEntityKey(0, 1)]),
240
286
  activeEntityRevision: 1,
241
287
  })
242
288
  .unwrap();
243
- expect(prepared?.entityKeys).toEqual(new Set([worldEntityKey(0, 1)]));
289
+ expect(prepared?.drawKeys).toEqual(new Set(['1:-1:0', '2:-1:0']));
244
290
  // Both candidates are uploaded to the selector (2 * 304 bytes), while
245
291
  // only the active entity is admitted into the compacted draw stream.
246
292
  expect(production.inspect()).toMatchObject({
@@ -254,7 +300,7 @@ describe('GPU-driven production projection', () => {
254
300
  .unwrap();
255
301
  expect(projected.accesses.length).toBeGreaterThan(0);
256
302
  expect(production.inspect()).toMatchObject({
257
- batchBindGroupCreates: 1,
303
+ batchBindGroupCreates: 0,
258
304
  indirectDrawCount: 1,
259
305
  });
260
306
  expect(production.inspect().indirectDrawCount).toBeGreaterThan(0);
@@ -262,18 +308,50 @@ describe('GPU-driven production projection', () => {
262
308
  production.inspect().batchCount,
263
309
  );
264
310
 
311
+ // The scene/view buffers and visible range are unchanged on a warm
312
+ // projection. The raster owner must reuse the physical batch bind group;
313
+ // a new group is only valid after one of those handles or the range
314
+ // changes (resize/recovery/LOD admission).
315
+ const warm = production
316
+ .prepare({
317
+ ...pbrInputs,
318
+ scene: {
319
+ scene: availability.scene,
320
+ plan: topology.plan(),
321
+ slots: projection.slotsSnapshot(),
322
+ },
323
+ camera: makeZeroCameraFallbackSnapshot(),
324
+ meshBySlot: new Map([
325
+ [0, lodGpuMesh],
326
+ [1, lodGpuMesh],
327
+ ]),
328
+ viewBindGroupLayout: viewLayout,
329
+ meshResidencyEpoch: 1,
330
+
331
+ activeEntityKeys: new Set([worldEntityKey(0, 1)]),
332
+ activeEntityRevision: 1,
333
+ })
334
+ .unwrap();
335
+ expect(warm?.topologySignature).toBe(prepared.topologySignature);
336
+ warm?.project(new RenderGraphBuilder<RenderPipelineFrame>(), 'rgba8unorm', 1).unwrap();
337
+ expect(production.inspect()).toMatchObject({ batchBindGroupCreates: 0 });
338
+
265
339
  const suppressed = production
266
340
  .prepare({
341
+ ...pbrInputs,
267
342
  scene: {
268
343
  scene: availability.scene,
269
344
  plan: topology.plan(),
270
345
  slots: projection.slotsSnapshot(),
271
346
  },
272
347
  camera: makeZeroCameraFallbackSnapshot(),
273
- meshes: new Map([[3, mesh(device, true, true)]]),
348
+ meshBySlot: new Map([
349
+ [0, lodGpuMesh],
350
+ [1, lodGpuMesh],
351
+ ]),
274
352
  viewBindGroupLayout: viewLayout,
275
353
  meshResidencyEpoch: 1,
276
- hdrp: false,
354
+
277
355
  activeEntityKeys: new Set(),
278
356
  activeEntityRevision: 2,
279
357
  })
@@ -286,16 +364,20 @@ describe('GPU-driven production projection', () => {
286
364
 
287
365
  const restored = production
288
366
  .prepare({
367
+ ...pbrInputs,
289
368
  scene: {
290
369
  scene: availability.scene,
291
370
  plan: topology.plan(),
292
371
  slots: projection.slotsSnapshot(),
293
372
  },
294
373
  camera: makeZeroCameraFallbackSnapshot(),
295
- meshes: new Map([[3, mesh(device, true, true)]]),
374
+ meshBySlot: new Map([
375
+ [0, lodGpuMesh],
376
+ [1, lodGpuMesh],
377
+ ]),
296
378
  viewBindGroupLayout: viewLayout,
297
379
  meshResidencyEpoch: 1,
298
- hdrp: false,
380
+
299
381
  activeEntityKeys: new Set([worldEntityKey(0, 1)]),
300
382
  activeEntityRevision: 3,
301
383
  })
@@ -306,16 +388,18 @@ describe('GPU-driven production projection', () => {
306
388
  indirectDrawCount: 1,
307
389
  });
308
390
  expect(GPU_DRIVEN_VIEW_WGSL.indexOf('atomicAdd(&counters[lodCounterIndex')).toBeLessThan(
309
- GPU_DRIVEN_VIEW_WGSL.indexOf('if (candidate.submitAdmission == 0u)'),
391
+ GPU_DRIVEN_VIEW_WGSL.indexOf('if ((candidate.submitAdmission & 1u) == 0u)'),
310
392
  );
311
- expect(GPU_DRIVEN_VIEW_WGSL.indexOf('if (candidate.submitAdmission == 0u)')).toBeLessThan(
393
+ expect(
394
+ GPU_DRIVEN_VIEW_WGSL.indexOf('if ((candidate.submitAdmission & 1u) == 0u)'),
395
+ ).toBeLessThan(
312
396
  GPU_DRIVEN_VIEW_WGSL.indexOf('atomicAdd(&counters[counterIndex(candidate.batchIndex)], 1u)'),
313
397
  );
314
398
  production.dispose();
315
399
  availability.scene.dispose();
316
400
  });
317
401
 
318
- it('keeps a suppressed-only batch in selector telemetry without raster resources', async () => {
402
+ it('keeps an all-suppressed candidate out of the GPU raster frame', async () => {
319
403
  const adapter = (await rhi.requestAdapter()).unwrap();
320
404
  const device = (await adapter.requestDevice()).unwrap() as RhiNullDevice;
321
405
  const shader = (await rhi.createShaderModule(device, { code: 'synthetic' })).unwrap();
@@ -324,9 +408,10 @@ describe('GPU-driven production projection', () => {
324
408
  entries: [{ binding: 0, visibility: 0x1, buffer: { type: 'uniform' } }],
325
409
  })
326
410
  .unwrap();
411
+ const pbrInputs = standardPbrInputs(device, shader, viewLayout);
327
412
  const projection = new RenderScene();
328
- const candidate = snapshot(1, 3);
329
- const delta = projection.apply([{ kind: 'create', snapshot: candidate }]);
413
+ const candidate = preparedPbrFixture(snapshot(1, 3));
414
+ const delta = projection.apply([updateSnapshot(candidate)]);
330
415
  const availability = GpuScene.create(device, 1).unwrap();
331
416
  expect(availability.status).toBe('available');
332
417
  if (availability.status !== 'available') return;
@@ -338,31 +423,29 @@ describe('GPU-driven production projection', () => {
338
423
  });
339
424
  const prepared = production
340
425
  .prepare({
426
+ ...pbrInputs,
341
427
  scene: {
342
428
  scene: availability.scene,
343
429
  plan: topology.plan(),
344
430
  slots: projection.slotsSnapshot(),
345
431
  },
346
432
  camera: makeZeroCameraFallbackSnapshot(),
347
- meshes: new Map([[3, mesh(device)]]),
433
+ meshBySlot: new Map([[0, mesh(device)]]),
348
434
  viewBindGroupLayout: viewLayout,
349
435
  meshResidencyEpoch: 1,
350
- hdrp: false,
436
+
351
437
  activeEntityKeys: new Set(),
352
438
  activeEntityRevision: 1,
353
439
  })
354
440
  .unwrap();
355
- expect(prepared).toBeDefined();
356
- expect(production.requiresSceneRows()).toBe(true);
441
+ expect(prepared).toBeUndefined();
357
442
  expect(production.inspect()).toMatchObject({
358
- batchCount: 1,
443
+ batchCount: 0,
359
444
  indirectDrawCount: 0,
360
445
  batchBindGroupCreates: 0,
361
446
  });
362
- if (prepared === undefined) return;
363
- prepared.project(new RenderGraphBuilder<RenderPipelineFrame>(), 'rgba8unorm', 1).unwrap();
364
447
  expect(production.inspect()).toMatchObject({
365
- batchCount: 1,
448
+ batchCount: 0,
366
449
  indirectDrawCount: 0,
367
450
  batchBindGroupCreates: 0,
368
451
  });
@@ -379,13 +462,11 @@ describe('GPU-driven production projection', () => {
379
462
  entries: [{ binding: 0, visibility: 0x1, buffer: { type: 'uniform' } }],
380
463
  })
381
464
  .unwrap();
465
+ const pbrInputs = standardPbrInputs(device, shader, viewLayout);
382
466
  const projection = new RenderScene();
383
- const first = snapshot(1, 3);
384
- const second = { ...snapshot(2, 3), worldId: 1 };
385
- const delta = projection.apply([
386
- { kind: 'create', snapshot: first },
387
- { kind: 'create', snapshot: second },
388
- ]);
467
+ const first = preparedPbrFixture(snapshot(1, 3));
468
+ const second = { ...preparedPbrFixture(snapshot(2, 3)), worldId: 1 };
469
+ const delta = projection.apply([updateSnapshot(first), updateSnapshot(second)]);
389
470
  const availability = GpuScene.create(device, 2).unwrap();
390
471
  expect(availability.status).toBe('available');
391
472
  if (availability.status !== 'available') return;
@@ -397,16 +478,19 @@ describe('GPU-driven production projection', () => {
397
478
  });
398
479
  const prepared = production
399
480
  .prepare({
481
+ ...pbrInputs,
400
482
  scene: {
401
483
  scene: availability.scene,
402
484
  plan: topology.plan(),
403
485
  slots: projection.slotsSnapshot(),
404
486
  },
405
487
  camera: makeZeroCameraFallbackSnapshot(),
406
- meshes: new Map([[3, mesh(device)]]),
488
+ meshBySlot: new Map([
489
+ [0, mesh(device)],
490
+ [1, mesh(device)],
491
+ ]),
407
492
  viewBindGroupLayout: viewLayout,
408
493
  meshResidencyEpoch: 1,
409
- hdrp: false,
410
494
  })
411
495
  .unwrap();
412
496
  expect(prepared).toBeDefined();
@@ -415,7 +499,7 @@ describe('GPU-driven production projection', () => {
415
499
  availability.scene.dispose();
416
500
  });
417
501
 
418
- it('uses renderer-local World keys for CPU validation telemetry', async () => {
502
+ it('uses concrete draw keys for CPU validation telemetry', async () => {
419
503
  const adapter = (await rhi.requestAdapter()).unwrap();
420
504
  const device = (await adapter.requestDevice()).unwrap() as RhiNullDevice;
421
505
  const shader = (await rhi.createShaderModule(device, { code: 'synthetic' })).unwrap();
@@ -428,24 +512,24 @@ describe('GPU-driven production projection', () => {
428
512
  lods: [{ mesh: '00000000-0000-7000-8000-000000000001' as never, screenCoverage: 0.5 }],
429
513
  };
430
514
  const plainFallbackRow = { ...snapshot(2, 3), worldId: 0 };
431
- const gpuOwned = new Set([worldEntityKey(101, gpuLodRow.entityKey)]);
432
- const worldKeys = [202, 101];
515
+ const gpuOwned = new Set([
516
+ gpuDrivenDrawKey(worldEntityKey(gpuLodRow.worldId, gpuLodRow.entityKey), -1, 0),
517
+ ]);
433
518
 
434
519
  // A GPU-owned row is removed from validation before this telemetry hook;
435
520
  // the remaining plain row must still be counted as the one CPU fallback.
436
- production.recordCpuValidation([{ source: plainFallbackRow }], gpuOwned, worldKeys);
521
+ production.recordCpuValidation([{ source: plainFallbackRow }], gpuOwned);
437
522
  expect(production.inspect()).toMatchObject({
438
523
  cpuFallbackDrawItems: 1,
439
524
  validatedGpuOwnedRows: 0,
440
525
  });
441
526
 
442
- // Prove the same renderer-local namespace recognizes the GPU row when a
443
- // diagnostic caller supplies it directly, even though its extracted
444
- // RenderableSnapshot still carries the reordered array index.
445
- production.recordCpuValidation([{ source: gpuLodRow }], gpuOwned, worldKeys);
527
+ // A GPU-only row contributes ownership telemetry without inflating the
528
+ // CPU residual row count.
529
+ production.recordCpuValidation([{ source: gpuLodRow }], gpuOwned);
446
530
  expect(production.inspect()).toMatchObject({
447
- cpuFallbackDrawItems: 1,
448
- validatedGpuOwnedRows: 1,
531
+ cpuFallbackDrawItems: 0,
532
+ validatedGpuOwnedRows: 0,
449
533
  });
450
534
  production.dispose();
451
535
  });
@@ -459,10 +543,11 @@ describe('GPU-driven production projection', () => {
459
543
  entries: [{ binding: 0, visibility: 0x1, buffer: { type: 'uniform' } }],
460
544
  })
461
545
  .unwrap();
546
+ const pbrInputs = standardPbrInputs(device, shader, viewLayout);
462
547
  const projection = new RenderScene();
463
548
  const lodMeshGuid = '00000000-0000-7000-8000-000000000001' as never;
464
549
  const makeLodSnapshot = (entityKey: number): RenderableSnapshot => {
465
- const base = snapshot(entityKey, 3);
550
+ const base = preparedPbrFixture(snapshot(entityKey, 3));
466
551
  const draw = base.gpuDrivenDraws?.[0];
467
552
  if (draw === undefined) throw new Error('LOD test snapshot has no draw');
468
553
  return {
@@ -472,8 +557,8 @@ describe('GPU-driven production projection', () => {
472
557
  };
473
558
  };
474
559
  const delta = projection.apply([
475
- { kind: 'create', snapshot: makeLodSnapshot(1) },
476
- { kind: 'create', snapshot: makeLodSnapshot(2) },
560
+ updateSnapshot(makeLodSnapshot(1)),
561
+ updateSnapshot(makeLodSnapshot(2)),
477
562
  ]);
478
563
  const availability = GpuScene.create(device, 2).unwrap();
479
564
  expect(availability.status).toBe('available');
@@ -484,18 +569,22 @@ describe('GPU-driven production projection', () => {
484
569
  const production = new GpuDrivenProduction(device, {
485
570
  createShaderModule: () => ok(shader),
486
571
  });
572
+ const lodGpuMesh = mesh(device, true, true);
487
573
  const prepared = production
488
574
  .prepare({
575
+ ...pbrInputs,
489
576
  scene: {
490
577
  scene: availability.scene,
491
578
  plan: topology.plan(),
492
579
  slots: projection.slotsSnapshot(),
493
580
  },
494
581
  camera: { ...makeZeroCameraFallbackSnapshot(), position: vec3.create(0, 0, 5) },
495
- meshes: new Map([[3, mesh(device, true, true)]]),
582
+ meshBySlot: new Map([
583
+ [0, lodGpuMesh],
584
+ [1, lodGpuMesh],
585
+ ]),
496
586
  viewBindGroupLayout: viewLayout,
497
587
  meshResidencyEpoch: 1,
498
- hdrp: false,
499
588
  })
500
589
  .unwrap();
501
590
  expect(prepared).toBeDefined();
@@ -582,6 +671,7 @@ describe('GPU-driven production projection', () => {
582
671
  entries: [{ binding: 0, visibility: 0x1, buffer: { type: 'uniform' } }],
583
672
  })
584
673
  .unwrap();
674
+ const pbrInputs = standardPbrInputs(device, shader, viewLayout);
585
675
  const viewUniform = device
586
676
  .createBuffer({ size: 784, usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST })
587
677
  .unwrap();
@@ -591,11 +681,11 @@ describe('GPU-driven production projection', () => {
591
681
  entries: [{ binding: 0, resource: { kind: 'buffer', value: { buffer: viewUniform } } }],
592
682
  })
593
683
  .unwrap();
594
- const first = snapshot(1, 3);
684
+ const first = preparedPbrFixture(snapshot(1, 3));
595
685
  const firstDraw = first.gpuDrivenDraws?.[0];
596
686
  expect(firstDraw).toBeDefined();
597
687
  if (firstDraw === undefined) return;
598
- const second = { ...snapshot(2, 4), worldId: 1 };
688
+ const second = { ...preparedPbrFixture(snapshot(2, 4)), worldId: 1 };
599
689
  const secondDraw = second.gpuDrivenDraws?.[0];
600
690
  expect(secondDraw).toBeDefined();
601
691
  if (secondDraw === undefined) return;
@@ -621,9 +711,7 @@ describe('GPU-driven production projection', () => {
621
711
  },
622
712
  ];
623
713
  const projection = new RenderScene();
624
- const delta = projection.apply(
625
- renderables.map((renderable) => ({ kind: 'create' as const, snapshot: renderable })),
626
- );
714
+ const delta = projection.apply(renderables.map((renderable) => updateSnapshot(renderable)));
627
715
  const availability = GpuScene.create(device, 2).unwrap();
628
716
  expect(availability.status).toBe('available');
629
717
  if (availability.status !== 'available') return;
@@ -632,53 +720,59 @@ describe('GPU-driven production projection', () => {
632
720
  topology.rebuild(projection.slotsSnapshot());
633
721
  expect(topology.plan()).toMatchObject({
634
722
  candidateCount: 3,
635
- visibleCapacity: 129,
636
- batches: [{ visibleBase: 0 }, { visibleBase: 64 }, { visibleBase: 128 }],
723
+ visibleCapacity: 65,
724
+ batches: [{ visibleBase: 0 }, { visibleBase: 64 }],
637
725
  });
638
726
  const production = new GpuDrivenProduction(device, {
639
727
  createShaderModule: () => ok(shader),
640
728
  });
641
729
  const meshes = new Map([
642
- [3, mesh(device)],
643
- [4, mesh(device, false)],
730
+ [0, mesh(device)],
731
+ [1, mesh(device, false)],
644
732
  ]);
645
733
  const prepared = production
646
734
  .prepare({
735
+ ...pbrInputs,
647
736
  scene: {
648
737
  scene: availability.scene,
649
738
  plan: topology.plan(),
650
739
  slots: projection.slotsSnapshot(),
651
740
  },
652
741
  camera: makeZeroCameraFallbackSnapshot(),
653
- meshes,
742
+ meshBySlot: meshes,
654
743
  viewBindGroupLayout: viewLayout,
655
744
  meshResidencyEpoch: 1,
656
- hdrp: false,
657
745
  })
658
746
  .unwrap();
659
747
  expect(prepared).toBeDefined();
660
748
  if (prepared === undefined) return;
661
- expect([...prepared.entityKeys]).toEqual([worldEntityKey(0, 1), worldEntityKey(1, 2)]);
749
+ expect(prepared.drawKeys).toEqual(
750
+ new Set([
751
+ gpuDrivenDrawKey(worldEntityKey(0, 1), -1, 0),
752
+ gpuDrivenDrawKey(worldEntityKey(0, 1), -1, 1),
753
+ gpuDrivenDrawKey(worldEntityKey(1, 2), 42, 0),
754
+ ]),
755
+ );
662
756
  expect(production.inspect()).toMatchObject({
663
757
  gpuOwnedSnapshotsMaterialized: 2,
664
758
  filteredPlanBuilds: 1,
665
759
  candidateUploadBytes: 912,
666
- batchUploadBytes: 96,
760
+ batchUploadBytes: 64,
667
761
  viewBindGroupCreates: 1,
668
762
  });
669
763
 
670
764
  const stable = production
671
765
  .prepare({
766
+ ...pbrInputs,
672
767
  scene: {
673
768
  scene: availability.scene,
674
769
  plan: topology.plan(),
675
770
  slots: projection.slotsSnapshot(),
676
771
  },
677
772
  camera: makeZeroCameraFallbackSnapshot(),
678
- meshes,
773
+ meshBySlot: meshes,
679
774
  viewBindGroupLayout: viewLayout,
680
775
  meshResidencyEpoch: 1,
681
- hdrp: false,
682
776
  })
683
777
  .unwrap();
684
778
  expect(stable?.topologySignature).toBe(prepared.topologySignature);
@@ -690,40 +784,46 @@ describe('GPU-driven production projection', () => {
690
784
  viewConstantsUploadBytes: 112,
691
785
  viewBindGroupCreates: 0,
692
786
  batchBindGroupCreates: 0,
693
- indirectDrawCount: 3,
787
+ indirectDrawCount: 2,
694
788
  });
695
789
 
696
- // The scene topology is unchanged, but projectedHeight is camera-owned
697
- // input to the GPU LOD selector. A moved camera must rebuild the filtered
698
- // candidate plan instead of reusing the previous view's heights.
790
+ // This mixed batch has no LOD relation. Camera movement updates view
791
+ // constants while retaining the scene topology and candidate uploads.
699
792
  const movedCamera = {
700
793
  ...makeZeroCameraFallbackSnapshot(),
701
794
  position: vec3.create(0, 0, 6),
702
795
  };
703
796
  const moved = production
704
797
  .prepare({
798
+ ...pbrInputs,
705
799
  scene: {
706
800
  scene: availability.scene,
707
801
  plan: topology.plan(),
708
802
  slots: projection.slotsSnapshot(),
709
803
  },
710
804
  camera: movedCamera,
711
- meshes,
805
+ meshBySlot: meshes,
712
806
  viewBindGroupLayout: viewLayout,
713
807
  meshResidencyEpoch: 1,
714
- hdrp: false,
715
808
  })
716
809
  .unwrap();
717
810
  expect(moved).toBeDefined();
718
811
  expect(production.inspect()).toMatchObject({
719
- filteredPlanBuilds: 1,
720
- candidateUploadBytes: 912,
721
- batchUploadBytes: 96,
812
+ filteredPlanBuilds: 0,
813
+ candidateUploadBytes: 0,
814
+ batchUploadBytes: 0,
722
815
  });
723
816
  if (moved === undefined) return;
724
817
 
725
818
  const graph = new RenderGraphBuilder<RenderPipelineFrame>();
726
819
  const gpu = moved.project(graph, 'rgba8unorm', 1).unwrap();
820
+ const materialBindGroup = device.createBindGroupLayout({ entries: [] }).unwrap();
821
+ const frameResources = {
822
+ ...moved.standardPbrFrameResources,
823
+ materialBindGroups: [
824
+ device.createBindGroup({ layout: materialBindGroup, entries: [] }).unwrap(),
825
+ ],
826
+ };
727
827
  const color = graph
728
828
  .createTexture('gpu-driven-production-color', {
729
829
  format: 'rgba8unorm',
@@ -742,7 +842,7 @@ describe('GPU-driven production projection', () => {
742
842
  clearValue: { r: 0, g: 0, b: 0, a: 1 },
743
843
  },
744
844
  ],
745
- encode: ({ pass, resources }) => gpu.encode(viewBindGroup, pass, resources),
845
+ encode: ({ pass, resources }) => gpu.encode(viewBindGroup, pass, resources, frameResources),
746
846
  })
747
847
  .unwrap();
748
848
  const compiled = graph.compile({ device, surfaceSize: { width: 1, height: 1 } }).unwrap();
@@ -751,7 +851,13 @@ describe('GPU-driven production projection', () => {
751
851
  dependencies: ['gpu-driven.frustum-compact', 'gpu-driven.finalize-indirect'],
752
852
  });
753
853
  const encoder = device.createCommandEncoder({ label: 'gpu-driven-production' }).unwrap();
754
- compiled.execute({ viewBindGroup, encoder } as unknown as RenderPipelineFrame).unwrap();
854
+ compiled
855
+ .execute({
856
+ viewBindGroup,
857
+ encoder,
858
+ gpuDrivenStandardPbrFrameResources: frameResources,
859
+ } as unknown as RenderPipelineFrame)
860
+ .unwrap();
755
861
  encoder.finish().unwrap();
756
862
  expect(device.framePassNames).toEqual([
757
863
  'gpu-driven.view-reset',
@@ -760,8 +866,8 @@ describe('GPU-driven production projection', () => {
760
866
  'main',
761
867
  ]);
762
868
  expect(device.totalDispatchCount).toBe(3);
763
- expect(device.totalDrawCount).toBe(3);
764
- expect(production.inspect().indirectDrawCount).toBe(3);
869
+ expect(device.totalDrawCount).toBe(2);
870
+ expect(production.inspect().indirectDrawCount).toBe(2);
765
871
  expect(production.inspect().indirectDrawCount).toBeLessThanOrEqual(
766
872
  production.inspect().batchCount,
767
873
  );
@@ -771,40 +877,42 @@ describe('GPU-driven production projection', () => {
771
877
  // plan without making the remaining entity fall back to CPU validation.
772
878
  const activeOnly = production
773
879
  .prepare({
880
+ ...pbrInputs,
774
881
  scene: {
775
882
  scene: availability.scene,
776
883
  plan: topology.plan(),
777
884
  slots: projection.slotsSnapshot(),
778
885
  },
779
886
  camera: movedCamera,
780
- meshes,
887
+ meshBySlot: meshes,
781
888
  viewBindGroupLayout: viewLayout,
782
889
  meshResidencyEpoch: 1,
783
- hdrp: false,
890
+
784
891
  activeEntityKeys: new Set([worldEntityKey(0, 1)]),
785
892
  activeEntityRevision: 1,
786
893
  })
787
894
  .unwrap();
788
- expect(activeOnly?.entityKeys).toEqual(new Set([1]));
789
- expect(activeOnly?.ownsAllRenderables).toBe(true);
895
+ expect(activeOnly?.drawKeys?.size).toBe(3);
896
+ expect(activeOnly?.ownsAllDrawItems).toBe(true);
790
897
 
791
898
  const changedActive = production
792
899
  .prepare({
900
+ ...pbrInputs,
793
901
  scene: {
794
902
  scene: availability.scene,
795
903
  plan: topology.plan(),
796
904
  slots: projection.slotsSnapshot(),
797
905
  },
798
906
  camera: movedCamera,
799
- meshes,
907
+ meshBySlot: meshes,
800
908
  viewBindGroupLayout: viewLayout,
801
909
  meshResidencyEpoch: 1,
802
- hdrp: false,
910
+
803
911
  activeEntityKeys: new Set([worldEntityKey(1, 2)]),
804
912
  activeEntityRevision: 2,
805
913
  })
806
914
  .unwrap();
807
- expect(changedActive?.entityKeys).toEqual(new Set([worldEntityKey(1, 2)]));
915
+ expect(changedActive?.drawKeys?.size).toBe(3);
808
916
  // Production inspection counters are per-prepare-frame facts. The changed
809
917
  // revision must trigger exactly one rebuild for this frame, not accumulate
810
918
  // the prior frame's count.
@@ -816,16 +924,16 @@ describe('GPU-driven production projection', () => {
816
924
  replacement.scene.sync(delta).unwrap();
817
925
  const replacementPrepared = production
818
926
  .prepare({
927
+ ...pbrInputs,
819
928
  scene: {
820
929
  scene: replacement.scene,
821
930
  plan: topology.plan(),
822
931
  slots: projection.slotsSnapshot(),
823
932
  },
824
933
  camera: makeZeroCameraFallbackSnapshot(),
825
- meshes,
934
+ meshBySlot: meshes,
826
935
  viewBindGroupLayout: viewLayout,
827
936
  meshResidencyEpoch: 1,
828
- hdrp: false,
829
937
  })
830
938
  .unwrap();
831
939
  expect(replacementPrepared?.topologySignature).not.toBe(prepared.topologySignature);
@@ -850,9 +958,7 @@ describe('GPU-driven production projection', () => {
850
958
  materialShaderId: 'forgeax::default-standard-pbr',
851
959
  };
852
960
  const projection = new RenderScene();
853
- const delta = projection.apply([
854
- { kind: 'create', snapshot: { ...base, material, materials: [material] } },
855
- ]);
961
+ const delta = projection.apply([updateSnapshot({ ...base, material, materials: [material] })]);
856
962
  const availability = GpuScene.create(device, 1).unwrap();
857
963
  expect(availability.status).toBe('available');
858
964
  if (availability.status !== 'available') return;
@@ -868,9 +974,8 @@ describe('GPU-driven production projection', () => {
868
974
  plan: topology.plan(),
869
975
  slots: projection.slotsSnapshot(),
870
976
  },
871
- meshes: new Map([[3, mesh(device)]]),
977
+ meshBySlot: new Map([[0, mesh(device)]]),
872
978
  viewBindGroupLayout: viewLayout,
873
- hdrp: false,
874
979
  } as const;
875
980
 
876
981
  expect(
@@ -882,8 +987,7 @@ describe('GPU-driven production projection', () => {
882
987
  })
883
988
  .unwrap(),
884
989
  ).toBeUndefined();
885
- expect(production.requiresSceneRows()).toBe(false);
886
- expect(production.inspect().filteredPlanBuilds).toBe(1);
990
+ expect(production.inspect().filteredPlanBuilds).toBe(0);
887
991
  expect(
888
992
  production
889
993
  .prepare({
@@ -893,7 +997,6 @@ describe('GPU-driven production projection', () => {
893
997
  })
894
998
  .unwrap(),
895
999
  ).toBeUndefined();
896
- expect(production.requiresSceneRows()).toBe(false);
897
1000
  expect(production.inspect()).toMatchObject({
898
1001
  filteredPlanBuilds: 0,
899
1002
  gpuOwnedSnapshotsMaterialized: 0,
@@ -904,7 +1007,7 @@ describe('GPU-driven production projection', () => {
904
1007
  availability.scene.dispose();
905
1008
  });
906
1009
 
907
- it('activates inside the renderer-owned clustered Standard graph and removes the CPU draw', async () => {
1010
+ it('keeps an ordinary mesh on the common CPU residual while the Standard graph stays active', async () => {
908
1011
  const canvas = {
909
1012
  width: 1,
910
1013
  height: 1,
@@ -954,11 +1057,9 @@ describe('GPU-driven production projection', () => {
954
1057
  );
955
1058
  expect(renderer.perFramePassNames).toContain('output-transform');
956
1059
  // Storage-buffer LDR frames keep the scene linear until the one output
957
- // transform writes the encoded surface; the GPU-driven indirect raster
958
- // draw and that fullscreen pass are the only draws in this topology.
959
- // A plain mesh has no LOD relation, so it stays on the CPU visibility
960
- // path for occlusion while GPU-driven raster + output-transform remain
961
- // the only draws in this graph.
1060
+ // transform writes the encoded surface. The ordinary unprepared mesh
1061
+ // remains the CPU residual under common visibility projection, while the
1062
+ // clustered Standard graph and output transform stay active.
962
1063
  expect(device.totalDrawCount).toBe(2);
963
1064
  expect(renderer.inspect().lodOcclusion).toMatchObject({
964
1065
  fallback: { active: false },
@@ -979,7 +1080,7 @@ describe('GPU-driven production projection', () => {
979
1080
  batchBindGroupCreates: 0,
980
1081
  viewBindGroupCreates: 0,
981
1082
  validatedGpuOwnedRows: 0,
982
- cpuFallbackDrawItems: 0,
1083
+ cpuFallbackDrawItems: 1,
983
1084
  },
984
1085
  });
985
1086
  renderer.dispose();
@@ -1030,7 +1131,7 @@ describe('GPU-driven production projection', () => {
1030
1131
  projectionRecords: 1,
1031
1132
  topology: { batchCount: 1, candidateCount: 1 },
1032
1133
  gpu: { status: 'resident' },
1033
- gpuDriven: { cpuFallbackDrawItems: 0, validatedGpuOwnedRows: 0 },
1134
+ gpuDriven: { cpuFallbackDrawItems: 1, validatedGpuOwnedRows: 0 },
1034
1135
  });
1035
1136
 
1036
1137
  sceneWorld.set(rendered, Transform, { pos: [1, 0, 0] }).unwrap();
@@ -1047,4 +1148,873 @@ describe('GPU-driven production projection', () => {
1047
1148
  });
1048
1149
  renderer.dispose();
1049
1150
  });
1151
+
1152
+ it('routes mixed ordinary camera and light edits through common visibility projection', async () => {
1153
+ const canvas = {
1154
+ width: 1,
1155
+ height: 1,
1156
+ getContext: () => null,
1157
+ addEventListener: () => undefined,
1158
+ removeEventListener: () => undefined,
1159
+ } as unknown as HTMLCanvasElement;
1160
+ const renderer = await constructRenderer(
1161
+ canvas,
1162
+ { rhi },
1163
+ { shaderManifestUrl: manifestDataUrl() },
1164
+ );
1165
+ expect((await renderer.initialization).ok).toBe(true);
1166
+ const world = new World();
1167
+ expect(renderer.attach(world).ok).toBe(true);
1168
+ const camera = world
1169
+ .spawn(
1170
+ { component: Transform, data: { pos: [0, 0, 5] } },
1171
+ { component: Camera, data: { fov: Math.PI / 4, aspect: 1, near: 0.1, far: 100 } },
1172
+ )
1173
+ .unwrap();
1174
+ world
1175
+ .spawn(
1176
+ { component: Transform, data: {} },
1177
+ { component: MeshFilter, data: { assetHandle: HANDLE_CUBE } },
1178
+ { component: MeshRenderer, data: {} },
1179
+ )
1180
+ .unwrap();
1181
+ const lamp = world
1182
+ .spawn(
1183
+ { component: Transform, data: { pos: [0, 0, 1] } },
1184
+ { component: PointLight, data: { intensity: 1, range: 10 } },
1185
+ )
1186
+ .unwrap();
1187
+ const updateVisibility = vi.spyOn(PersistentRenderScene.prototype, 'updateVisibilityFacet');
1188
+ const projectVisibility = vi.spyOn(PersistentRenderScene.prototype, 'projectVisibility');
1189
+ try {
1190
+ world.update(1 / 60).unwrap();
1191
+ expect(renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 }).ok).toBe(true);
1192
+ const warmUpdateCalls = updateVisibility.mock.calls.length;
1193
+ const warmProjectCalls = projectVisibility.mock.calls.length;
1194
+ const before = renderer.inspect().renderScene;
1195
+
1196
+ world.set(camera, Transform, { pos: [0, 1, 5] }).unwrap();
1197
+ world.set(lamp, PointLight, { intensity: 3, range: 8 }).unwrap();
1198
+ propagateTransforms(world).unwrap();
1199
+ world.update(1 / 60).unwrap();
1200
+ expect(renderer.draw([world], { cameraOwner: 0, resourceOwner: 0 }).ok).toBe(true);
1201
+
1202
+ expect(updateVisibility.mock.calls.length).toBeGreaterThan(warmUpdateCalls);
1203
+ expect(projectVisibility.mock.calls.length).toBeGreaterThan(warmProjectCalls);
1204
+ const after = renderer.inspect().renderScene;
1205
+ expect(after).toMatchObject({
1206
+ fullRebuilds: 1,
1207
+ projectionRecords: before.projectionRecords,
1208
+ });
1209
+ expect(after.topology.revision).toBe(before.topology.revision);
1210
+ } finally {
1211
+ updateVisibility.mockRestore();
1212
+ projectVisibility.mockRestore();
1213
+ renderer.dispose();
1214
+ }
1215
+ });
1216
+
1217
+ it('admits prepared Alpha Mask draws to the GPU shadow lane', () => {
1218
+ const source = preparedSnapshot(10, 3);
1219
+ const alphaMaterial = {
1220
+ ...source.material,
1221
+ paramSnapshot: { alphaCutoff: 0.5 },
1222
+ textureHandles: new Map([['baseColorTexture', 1]]) as never,
1223
+ samplerHandles: new Map([['baseColorTexture', 2]]) as never,
1224
+ } as MaterialSnapshot;
1225
+ const draw = source.gpuDrivenDraws?.[0];
1226
+ expect(draw?.prepared).toBeDefined();
1227
+ if (draw === undefined || draw.prepared === undefined) return;
1228
+
1229
+ // Alpha Mask is a prepared material fact: it keeps the opaque depth
1230
+ // state, carries an authored cutoff, and owns the base-color resource
1231
+ // needed by the shadow fragment discard. Alpha Blend is covered by the
1232
+ // adjacent residual-lane regression and must remain CPU-owned.
1233
+ expect(
1234
+ shadowCasterCpuReason(
1235
+ alphaMaterial,
1236
+ draw,
1237
+ [{ materialShaderId: 'forgeax::default-shadow-caster' } as DispatchEntry],
1238
+ undefined,
1239
+ undefined,
1240
+ ),
1241
+ ).toBeUndefined();
1242
+
1243
+ const projection = new RenderScene();
1244
+ projection.apply([
1245
+ updateSnapshot({ ...source, material: alphaMaterial, materials: [alphaMaterial] }),
1246
+ ]);
1247
+ const topology = new BatchTopology();
1248
+ topology.rebuild(projection.slotsSnapshot());
1249
+ expect(topology.plan()).toMatchObject({
1250
+ candidateCount: 1,
1251
+ batches: [{ key: { admission: 'alpha-mask' } }],
1252
+ });
1253
+ });
1254
+
1255
+ it('does not admit Alpha Blend or CPU transmission draw plans as GPU-owned', async () => {
1256
+ const adapter = (await rhi.requestAdapter()).unwrap();
1257
+ const device = (await adapter.requestDevice()).unwrap() as RhiNullDevice;
1258
+ const shader = (await rhi.createShaderModule(device, { code: 'synthetic' })).unwrap();
1259
+ const viewLayout = device
1260
+ .createBindGroupLayout({
1261
+ entries: [{ binding: 0, visibility: 0x1, buffer: { type: 'uniform' } }],
1262
+ })
1263
+ .unwrap();
1264
+ const materialLayout = device.createBindGroupLayout({ entries: [] }).unwrap();
1265
+ const meshLayout = device
1266
+ .createBindGroupLayout({
1267
+ entries: [{ binding: 0, visibility: 0x1, buffer: { type: 'read-only-storage' } }],
1268
+ })
1269
+ .unwrap();
1270
+ const instancesLayout = device
1271
+ .createBindGroupLayout({
1272
+ entries: [
1273
+ { binding: 0, visibility: 0x1, buffer: { type: 'read-only-storage' } },
1274
+ { binding: 1, visibility: 0x1, buffer: { type: 'read-only-storage' } },
1275
+ ],
1276
+ })
1277
+ .unwrap();
1278
+ const pbrPipelineLayout = device
1279
+ .createPipelineLayout({
1280
+ bindGroupLayouts: [viewLayout, materialLayout, meshLayout, instancesLayout],
1281
+ })
1282
+ .unwrap();
1283
+ const opaque = preparedSnapshot(1, 3);
1284
+ const blendState = {
1285
+ color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' },
1286
+ alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
1287
+ } as const;
1288
+ const alphaMaterial = {
1289
+ ...opaque.material,
1290
+ renderState: { cullMode: 'none' as const, blend: blendState, depthWriteEnabled: false },
1291
+ } as MaterialSnapshot;
1292
+ const alphaSnapshots = [2, 3].map(
1293
+ (entityKey): RenderableSnapshot => ({
1294
+ ...preparedSnapshot(entityKey, 3),
1295
+ material: alphaMaterial,
1296
+ materials: [alphaMaterial],
1297
+ gpuDrivenDraws: [],
1298
+ }),
1299
+ );
1300
+ const transmissionMaterial = {
1301
+ ...opaque.material,
1302
+ paramSnapshot: { transmission: 0.5 },
1303
+ } as MaterialSnapshot;
1304
+ const transmissionSnapshot: RenderableSnapshot = {
1305
+ ...preparedSnapshot(4, 3),
1306
+ material: transmissionMaterial,
1307
+ materials: [transmissionMaterial],
1308
+ };
1309
+ const sceneProjection = new RenderScene();
1310
+ const delta = sceneProjection.apply([
1311
+ updateSnapshot(opaque),
1312
+ ...alphaSnapshots.map((snapshot) => updateSnapshot(snapshot)),
1313
+ updateSnapshot(transmissionSnapshot),
1314
+ ]);
1315
+ const availability = GpuScene.create(device, 3).unwrap();
1316
+ expect(availability.status).toBe('available');
1317
+ if (availability.status !== 'available') return;
1318
+ availability.scene.sync(delta).unwrap();
1319
+ const topology = new BatchTopology();
1320
+ topology.rebuild(sceneProjection.slotsSnapshot());
1321
+ const production = new GpuDrivenProduction(device, {
1322
+ createShaderModule: () => ok(shader),
1323
+ });
1324
+ const artifact = {
1325
+ material: 'forgeax::default-standard-pbr',
1326
+ pass: 'forward',
1327
+ wgsl: 'synthetic',
1328
+ layoutIdentity: STANDARD_PBR_RECEIPT.reflection.layoutIdentity,
1329
+ bindings: [],
1330
+ deps: [],
1331
+ vertexInputs: STANDARD_PBR_RECEIPT.vertexInputs as unknown as readonly Readonly<
1332
+ Record<string, unknown>
1333
+ >[],
1334
+ receipt: STANDARD_PBR_RECEIPT,
1335
+ };
1336
+ const programKey = standardPbrProgramKey(artifact.material);
1337
+ expect(programKey).toBeDefined();
1338
+ if (programKey === undefined) return;
1339
+ const prepared = production.prepare({
1340
+ scene: {
1341
+ scene: availability.scene,
1342
+ plan: topology.plan(),
1343
+ slots: sceneProjection.slotsSnapshot(),
1344
+ },
1345
+ camera: makeZeroCameraFallbackSnapshot(),
1346
+ meshBySlot: new Map([[0, mesh(device)]]),
1347
+ viewBindGroupLayout: viewLayout,
1348
+ meshResidencyEpoch: 1,
1349
+
1350
+ standardPbrArtifact: artifact,
1351
+ standardPbrPipelineState: {
1352
+ pbrPipelineLayout,
1353
+ meshBindGroupLayout: meshLayout,
1354
+ instancesBindGroupLayout: instancesLayout,
1355
+ gpuDrivenInstancesBindGroupLayout: instancesLayout,
1356
+ gpuDrivenPbrPipelineLayout: pbrPipelineLayout,
1357
+ standardPbrShaderModule: shader,
1358
+ gpuDrivenPbrPrograms: new Map([[programKey, { module: shader, artifact }]]),
1359
+ } as never,
1360
+ });
1361
+ expect(prepared.ok).toBe(true);
1362
+ if (!prepared.ok) return;
1363
+ expect(prepared.value?.drawKeys).toEqual(new Set(['1:-1:0']));
1364
+ expect(prepared.value?.ownsAllDrawItems).toBe(false);
1365
+ production.dispose();
1366
+ availability.scene.dispose();
1367
+ });
1368
+
1369
+ it('projects concrete ShadowCaster candidates and leaves the CPU residual disjoint', async () => {
1370
+ const adapter = (await rhi.requestAdapter()).unwrap();
1371
+ const device = (await adapter.requestDevice()).unwrap() as RhiNullDevice;
1372
+ const shader = (await rhi.createShaderModule(device, { code: 'synthetic' })).unwrap();
1373
+ const viewLayout = device
1374
+ .createBindGroupLayout({
1375
+ entries: [{ binding: 0, visibility: 0x1, buffer: { type: 'uniform' } }],
1376
+ })
1377
+ .unwrap();
1378
+ const materialLayout = device.createBindGroupLayout({ entries: [] }).unwrap();
1379
+ const meshLayout = device
1380
+ .createBindGroupLayout({
1381
+ entries: [{ binding: 0, visibility: 0x1, buffer: { type: 'read-only-storage' } }],
1382
+ })
1383
+ .unwrap();
1384
+ const instancesLayout = device
1385
+ .createBindGroupLayout({
1386
+ entries: [
1387
+ { binding: 0, visibility: 0x1, buffer: { type: 'read-only-storage' } },
1388
+ { binding: 1, visibility: 0x1, buffer: { type: 'read-only-storage' } },
1389
+ ],
1390
+ })
1391
+ .unwrap();
1392
+ const pbrPipelineLayout = device
1393
+ .createPipelineLayout({
1394
+ bindGroupLayouts: [viewLayout, materialLayout, meshLayout, instancesLayout],
1395
+ })
1396
+ .unwrap();
1397
+ const source = preparedSnapshot(1, 3);
1398
+ const firstDraw = source.gpuDrivenDraws?.[0];
1399
+ expect(firstDraw).toBeDefined();
1400
+ if (firstDraw === undefined) return;
1401
+ const firstPrepared = firstDraw.prepared;
1402
+ expect(firstPrepared).toBeDefined();
1403
+ if (firstPrepared === undefined) return;
1404
+ const blendState = {
1405
+ color: { srcFactor: 'src-alpha', dstFactor: 'one-minus-src-alpha', operation: 'add' },
1406
+ alpha: { srcFactor: 'one', dstFactor: 'one-minus-src-alpha', operation: 'add' },
1407
+ } as const;
1408
+ const residualMaterial = {
1409
+ ...source.material,
1410
+ materialHandle: 22,
1411
+ transparent: true,
1412
+ renderState: { cullMode: 'none' as const, blend: blendState, depthWriteEnabled: false },
1413
+ } as MaterialSnapshot;
1414
+ const residualDraw = {
1415
+ ...firstDraw,
1416
+ first: 3,
1417
+ materialSlot: 1,
1418
+ materialResourceClass: 'alpha-blend',
1419
+ prepared: { ...firstPrepared, first: 3 },
1420
+ };
1421
+ const mixed = {
1422
+ ...source,
1423
+ materials: [source.material, residualMaterial],
1424
+ gpuDrivenDraws: [firstDraw, residualDraw],
1425
+ } as RenderableSnapshot;
1426
+ const projection = new RenderScene();
1427
+ const delta = projection.apply([updateSnapshot(mixed)]);
1428
+ const availability = GpuScene.create(device, 1).unwrap();
1429
+ expect(availability.status).toBe('available');
1430
+ if (availability.status !== 'available') return;
1431
+ availability.scene.sync(delta).unwrap();
1432
+ const topology = new BatchTopology();
1433
+ topology.rebuild(projection.slotsSnapshot());
1434
+ expect(topology.plan()).toMatchObject({ candidateCount: 1 });
1435
+ const production = new GpuDrivenProduction(device, {
1436
+ createShaderModule: () => ok(shader),
1437
+ });
1438
+ const artifact = {
1439
+ material: 'forgeax::default-standard-pbr',
1440
+ pass: 'forward',
1441
+ wgsl: 'synthetic',
1442
+ layoutIdentity: STANDARD_PBR_RECEIPT.reflection.layoutIdentity,
1443
+ bindings: [],
1444
+ deps: [],
1445
+ vertexInputs: STANDARD_PBR_RECEIPT.vertexInputs as unknown as readonly Readonly<
1446
+ Record<string, unknown>
1447
+ >[],
1448
+ receipt: STANDARD_PBR_RECEIPT,
1449
+ };
1450
+ const programKey = standardPbrProgramKey(artifact.material);
1451
+ expect(programKey).toBeDefined();
1452
+ if (programKey === undefined) return;
1453
+ const entity = worldEntityKey(mixed.worldId, mixed.entityKey);
1454
+ const gpuKey = gpuDrivenShadowDrawKey(entity, -1, 0, 0);
1455
+ const residualKey = gpuDrivenShadowDrawKey(entity, 22, 1, 0);
1456
+ const allShadowKeys = new Set([gpuKey, residualKey]);
1457
+ const prepareFrame = () =>
1458
+ production
1459
+ .prepare({
1460
+ scene: {
1461
+ scene: availability.scene,
1462
+ plan: topology.plan(),
1463
+ slots: projection.slotsSnapshot(),
1464
+ },
1465
+ camera: makeZeroCameraFallbackSnapshot(),
1466
+ meshBySlot: new Map([[0, mesh(device)]]),
1467
+ viewBindGroupLayout: viewLayout,
1468
+ meshResidencyEpoch: 1,
1469
+ standardPbrArtifact: artifact,
1470
+ standardPbrPipelineState: {
1471
+ pbrPipelineLayout,
1472
+ meshBindGroupLayout: meshLayout,
1473
+ instancesBindGroupLayout: instancesLayout,
1474
+ gpuDrivenInstancesBindGroupLayout: instancesLayout,
1475
+ gpuDrivenPbrPipelineLayout: pbrPipelineLayout,
1476
+ standardPbrShaderModule: shader,
1477
+ gpuDrivenPbrPrograms: new Map([[programKey, { module: shader, artifact }]]),
1478
+ } as never,
1479
+ shadowCasterDrawKeys: allShadowKeys,
1480
+ })
1481
+ .unwrap();
1482
+ const prepared = prepareFrame();
1483
+ expect(prepared).toBeDefined();
1484
+ if (prepared === undefined) return;
1485
+ const claimed = new Set(prepared.shadowDrawKeys ?? []);
1486
+ expect([...claimed]).toEqual([gpuKey]);
1487
+ expect(prepared.ownsAllShadowCasters).toBe(false);
1488
+ const residual = [...allShadowKeys].filter((key) => !claimed.has(key));
1489
+ expect(residual).toEqual([residualKey]);
1490
+ expect([...new Set([...claimed, ...residual])].sort()).toEqual([...allShadowKeys].sort());
1491
+ const identity = { kind: 'directional' as const, index: 0 };
1492
+ const updated = prepared.updateShadowViews?.([
1493
+ { identity, planes: new Float32Array(24), cacheToken: 'mixed-candidates' },
1494
+ ]);
1495
+ expect(updated?.ok).toBe(true);
1496
+ expect(production.inspect().channels).toEqual(
1497
+ expect.arrayContaining([
1498
+ expect.objectContaining({
1499
+ viewPass: 'directional-shadow',
1500
+ lane: 'blocked',
1501
+ reason: 'shadow-ownership',
1502
+ drawCount: 1,
1503
+ failure: expect.objectContaining({
1504
+ code: 'shadow-ownership',
1505
+ detail: expect.objectContaining({
1506
+ owner: 'shadow',
1507
+ reason: 'shadow-ownership-missing',
1508
+ recovery: 'route-cpu-lane',
1509
+ }),
1510
+ }),
1511
+ }),
1512
+ ]),
1513
+ );
1514
+ const submission = prepared.shadowViewPool?.submission(identity);
1515
+ expect(submission?.plan.candidateCount).toBe(1);
1516
+ const resumed = prepareFrame();
1517
+ expect(
1518
+ resumed?.updateShadowViews?.([
1519
+ { identity, planes: new Float32Array(24), cacheToken: 'mixed-candidates' },
1520
+ ]).ok,
1521
+ ).toBe(true);
1522
+ expect(
1523
+ production.inspect().channels.some((channel) => channel.viewPass === 'directional-shadow'),
1524
+ ).toBe(true);
1525
+ production.dispose();
1526
+ availability.scene.dispose();
1527
+ });
1528
+
1529
+ it('claims exact ShadowCaster passes without swallowing residual or custom passes', async () => {
1530
+ const adapter = (await rhi.requestAdapter()).unwrap();
1531
+ const device = (await adapter.requestDevice()).unwrap() as RhiNullDevice;
1532
+ const shader = (await rhi.createShaderModule(device, { code: 'synthetic' })).unwrap();
1533
+ const viewLayout = device
1534
+ .createBindGroupLayout({
1535
+ entries: [{ binding: 0, visibility: 0x1, buffer: { type: 'uniform' } }],
1536
+ })
1537
+ .unwrap();
1538
+ const materialLayout = device.createBindGroupLayout({ entries: [] }).unwrap();
1539
+ const meshLayout = device
1540
+ .createBindGroupLayout({
1541
+ entries: [{ binding: 0, visibility: 0x1, buffer: { type: 'read-only-storage' } }],
1542
+ })
1543
+ .unwrap();
1544
+ const instancesLayout = device
1545
+ .createBindGroupLayout({
1546
+ entries: [
1547
+ { binding: 0, visibility: 0x1, buffer: { type: 'read-only-storage' } },
1548
+ { binding: 1, visibility: 0x1, buffer: { type: 'read-only-storage' } },
1549
+ ],
1550
+ })
1551
+ .unwrap();
1552
+ const pbrPipelineLayout = device
1553
+ .createPipelineLayout({
1554
+ bindGroupLayouts: [viewLayout, materialLayout, meshLayout, instancesLayout],
1555
+ })
1556
+ .unwrap();
1557
+ const source = preparedSnapshot(7, 3);
1558
+ const firstDraw = source.gpuDrivenDraws?.[0];
1559
+ expect(firstDraw?.prepared).toBeDefined();
1560
+ if (firstDraw === undefined || firstDraw.prepared === undefined) return;
1561
+ const materials = [0, 1, 2].map((handle) => ({
1562
+ ...source.material,
1563
+ materialHandle: handle + 10,
1564
+ })) as MaterialSnapshot[];
1565
+ const draws = [
1566
+ { ...firstDraw, drawItemIndex: 0, materialSlot: 0, first: 0, count: 3 },
1567
+ {
1568
+ ...withoutPrepared(firstDraw),
1569
+ drawItemIndex: 1,
1570
+ materialSlot: 1,
1571
+ first: 3,
1572
+ count: 3,
1573
+ },
1574
+ { ...firstDraw, drawItemIndex: 2, materialSlot: 2, first: 6, count: 3 },
1575
+ ];
1576
+ const mixed: RenderableSnapshot = {
1577
+ ...source,
1578
+ material: materials[0] as MaterialSnapshot,
1579
+ materials,
1580
+ gpuDrivenDraws: draws,
1581
+ };
1582
+ const projection = new RenderScene();
1583
+ const delta = projection.apply([updateSnapshot(mixed)]);
1584
+ const availability = GpuScene.create(device, 1).unwrap();
1585
+ expect(availability.status).toBe('available');
1586
+ if (availability.status !== 'available') return;
1587
+ availability.scene.sync(delta).unwrap();
1588
+ const topology = new BatchTopology();
1589
+ topology.rebuild(projection.slotsSnapshot());
1590
+ expect(topology.plan().candidateCount).toBe(3);
1591
+ const entity = worldEntityKey(mixed.worldId, mixed.entityKey);
1592
+ const membership: ShadowCasterMembership[] = [
1593
+ {
1594
+ worldEntity: entity,
1595
+ renderableIndex: 0,
1596
+ drawItemIndex: 0,
1597
+ materialHandle: 10,
1598
+ passIndex: 0,
1599
+ materialShaderId: 'forgeax::default-shadow-caster',
1600
+ },
1601
+ {
1602
+ worldEntity: entity,
1603
+ renderableIndex: 0,
1604
+ drawItemIndex: 1,
1605
+ materialHandle: 11,
1606
+ passIndex: 0,
1607
+ materialShaderId: 'forgeax::default-shadow-caster',
1608
+ },
1609
+ {
1610
+ worldEntity: entity,
1611
+ renderableIndex: 0,
1612
+ drawItemIndex: 2,
1613
+ materialHandle: 12,
1614
+ passIndex: 0,
1615
+ materialShaderId: 'forgeax::default-shadow-caster',
1616
+ },
1617
+ {
1618
+ worldEntity: entity,
1619
+ renderableIndex: 0,
1620
+ drawItemIndex: 2,
1621
+ materialHandle: 12,
1622
+ passIndex: 7,
1623
+ materialShaderId: 'custom-shadow-caster',
1624
+ },
1625
+ ];
1626
+ const allKeys = new Set(
1627
+ membership.map((entry) =>
1628
+ gpuDrivenShadowDrawKey(
1629
+ entry.worldEntity,
1630
+ entry.materialHandle,
1631
+ entry.drawItemIndex,
1632
+ entry.passIndex,
1633
+ ),
1634
+ ),
1635
+ );
1636
+ const production = new GpuDrivenProduction(device, {
1637
+ createShaderModule: () => ok(shader),
1638
+ });
1639
+ const artifact = {
1640
+ material: 'forgeax::default-standard-pbr',
1641
+ pass: 'forward',
1642
+ wgsl: 'synthetic',
1643
+ layoutIdentity: STANDARD_PBR_RECEIPT.reflection.layoutIdentity,
1644
+ bindings: [],
1645
+ deps: [],
1646
+ vertexInputs: STANDARD_PBR_RECEIPT.vertexInputs as unknown as readonly Readonly<
1647
+ Record<string, unknown>
1648
+ >[],
1649
+ receipt: STANDARD_PBR_RECEIPT,
1650
+ };
1651
+ const programKey = standardPbrProgramKey(artifact.material);
1652
+ expect(programKey).toBeDefined();
1653
+ if (programKey === undefined) return;
1654
+ const multiMesh = {
1655
+ ...mesh(device),
1656
+ submeshes: [0, 1, 2].map((materialSlot) => ({
1657
+ indexOffset: materialSlot * 3,
1658
+ indexCount: 3,
1659
+ vertexCount: 3,
1660
+ materialSlot,
1661
+ topology: 'triangle-list' as const,
1662
+ })),
1663
+ };
1664
+ const prepared = production
1665
+ .prepare({
1666
+ scene: {
1667
+ scene: availability.scene,
1668
+ plan: topology.plan(),
1669
+ slots: projection.slotsSnapshot(),
1670
+ },
1671
+ camera: makeZeroCameraFallbackSnapshot(),
1672
+ meshBySlot: new Map([[0, multiMesh]]),
1673
+ viewBindGroupLayout: viewLayout,
1674
+ meshResidencyEpoch: 1,
1675
+
1676
+ standardPbrArtifact: artifact,
1677
+ standardPbrPipelineState: {
1678
+ pbrPipelineLayout,
1679
+ meshBindGroupLayout: meshLayout,
1680
+ instancesBindGroupLayout: instancesLayout,
1681
+ gpuDrivenInstancesBindGroupLayout: instancesLayout,
1682
+ gpuDrivenPbrPipelineLayout: pbrPipelineLayout,
1683
+ standardPbrShaderModule: shader,
1684
+ gpuDrivenPbrPrograms: new Map([[programKey, { module: shader, artifact }]]),
1685
+ } as never,
1686
+ shadowCasterDrawKeys: allKeys,
1687
+ shadowCasterMembership: membership,
1688
+ })
1689
+ .unwrap();
1690
+ expect(prepared).toBeDefined();
1691
+ if (prepared === undefined) return;
1692
+ const gpuKey = gpuDrivenShadowDrawKey(entity, 10, 0, 0);
1693
+ expect([...(prepared.shadowDrawKeys ?? [])]).toEqual([gpuKey]);
1694
+ expect(prepared.ownsAllShadowCasters).toBe(false);
1695
+ const residual = [...allKeys].filter((key) => !prepared.shadowDrawKeys?.has(key));
1696
+ expect(residual).toHaveLength(3);
1697
+ expect(new Set([...(prepared.shadowDrawKeys ?? []), ...residual])).toEqual(allKeys);
1698
+ const identity = { kind: 'directional' as const, index: 0 };
1699
+ expect(
1700
+ prepared.updateShadowViews?.([
1701
+ { identity, planes: new Float32Array(24), cacheToken: 'exact-shadow-membership' },
1702
+ ]).ok,
1703
+ ).toBe(true);
1704
+ expect(prepared.shadowViewPool?.submission(identity)?.plan.candidateCount).toBe(1);
1705
+ expect(prepared.shadowDrawKeysByView?.get('directional:0:')).toEqual(new Set([gpuKey]));
1706
+ production.dispose();
1707
+ availability.scene.dispose();
1708
+ });
1709
+
1710
+ it.each([false, true])('prepares off-camera CPU shadow materials (main rows=%s)', (withMain) => {
1711
+ const material = { ...preparedSnapshot(1, 3).material, materialHandle: 42 };
1712
+ const shadow = {
1713
+ source: { ...preparedSnapshot(1, 3), material, materials: [material] },
1714
+ renderableIndex: 0,
1715
+ } as never;
1716
+ const main = { source: preparedSnapshot(2, 3), renderableIndex: 0 } as never;
1717
+ const plan = buildDispatchPlan({} as never, withMain ? [main] : [], [], [], [shadow]);
1718
+ expect(plan.shadowValidatedOrdered).toEqual([shadow]);
1719
+ const slot = plan.materialSlots.indexOf(material);
1720
+ expect(slot).toBeGreaterThanOrEqual(0);
1721
+ expect(plan.materialSlotOwners[slot]).toBe(shadow);
1722
+ const dispatch = {
1723
+ renderableIndex: 0,
1724
+ materialHandle: 42,
1725
+ materialShaderId: 'forgeax::default-standard-pbr',
1726
+ tags: { LightMode: 'ShadowCaster' },
1727
+ };
1728
+ const map = shadowShaderMap({ ...plan, shadowDispatch: [dispatch] } as never);
1729
+ expect(map.get(0)?.get(42)?.[0]?.materialSlot).toBe(slot);
1730
+ });
1731
+
1732
+ it.each([
1733
+ false,
1734
+ true,
1735
+ ])('records exact CPU residuals with authored state (skinned=%s)', async (skinned) => {
1736
+ const adapter = (await rhi.requestAdapter()).unwrap();
1737
+ const device = (await adapter.requestDevice()).unwrap() as RhiNullDevice;
1738
+ const source = preparedSnapshot(8, 3);
1739
+ const firstDraw = source.gpuDrivenDraws?.[0];
1740
+ expect(firstDraw?.prepared).toBeDefined();
1741
+ if (firstDraw === undefined || firstDraw.prepared === undefined) return;
1742
+ const materials = [0, 1, 2].map((handle) => ({
1743
+ ...source.material,
1744
+ materialHandle: handle + 20,
1745
+ })) as MaterialSnapshot[];
1746
+ const draws = [
1747
+ { ...firstDraw, drawItemIndex: 0, materialSlot: 0, first: 0, count: 3 },
1748
+ { ...firstDraw, drawItemIndex: 1, materialSlot: 1, first: 3, count: 3 },
1749
+ { ...firstDraw, drawItemIndex: 2, materialSlot: 2, first: 6, count: 3 },
1750
+ ];
1751
+ const sourceForRecord: RenderableSnapshot = {
1752
+ ...source,
1753
+ ...(skinned ? { skin: { buffer: {} as never, byteOffset: 0 } as never } : {}),
1754
+ material: materials[0] as MaterialSnapshot,
1755
+ materials,
1756
+ gpuDrivenDraws: draws,
1757
+ };
1758
+ const geometry = {
1759
+ ...mesh(device),
1760
+ submeshes: [0, 1, 2].map((materialSlot) => ({
1761
+ indexOffset: materialSlot * 3,
1762
+ indexCount: 3,
1763
+ vertexCount: 3,
1764
+ materialSlot,
1765
+ topology: 'triangle-list' as const,
1766
+ })),
1767
+ };
1768
+ const identityInstanceBuffer = device
1769
+ .createBuffer({ size: 64, usage: GPU_BUFFER_USAGE_UNIFORM | GPU_BUFFER_USAGE_COPY_DST })
1770
+ .unwrap();
1771
+ const instancesBindGroupLayout = device
1772
+ .createBindGroupLayout({
1773
+ entries: [
1774
+ { binding: 0, visibility: 0x1, buffer: { type: 'uniform' } },
1775
+ { binding: 1, visibility: 0x1, buffer: { type: 'uniform' } },
1776
+ ],
1777
+ })
1778
+ .unwrap();
1779
+ const worldEntity = worldEntityKey(sourceForRecord.worldId, sourceForRecord.entityKey);
1780
+ const claimedKey = gpuDrivenShadowDrawKey(worldEntity, 20, 0, 0);
1781
+ const residualKeys = new Set([
1782
+ gpuDrivenShadowDrawKey(worldEntity, 21, 1, 0),
1783
+ gpuDrivenShadowDrawKey(worldEntity, 22, 2, 0),
1784
+ gpuDrivenShadowDrawKey(worldEntity, 22, 2, 7),
1785
+ ]);
1786
+ const shadowDispatches = new Map([
1787
+ [
1788
+ 0,
1789
+ new Map([
1790
+ [
1791
+ 20,
1792
+ [
1793
+ {
1794
+ passIndex: 0,
1795
+ materialHandle: 20,
1796
+ vertexEntry: undefined,
1797
+ fragmentEntry: undefined,
1798
+ materialShaderId: 'forgeax::default-shadow-caster',
1799
+ renderState: undefined,
1800
+ materialSlot: 0,
1801
+ paramSnapshot: materials[0]?.paramSnapshot,
1802
+ },
1803
+ ],
1804
+ ],
1805
+ [
1806
+ 21,
1807
+ [
1808
+ {
1809
+ passIndex: 0,
1810
+ materialHandle: 21,
1811
+ vertexEntry: undefined,
1812
+ fragmentEntry: undefined,
1813
+ materialShaderId: 'forgeax::default-shadow-caster',
1814
+ renderState: undefined,
1815
+ materialSlot: 1,
1816
+ paramSnapshot: materials[1]?.paramSnapshot,
1817
+ },
1818
+ ],
1819
+ ],
1820
+ [
1821
+ 22,
1822
+ [
1823
+ {
1824
+ passIndex: 0,
1825
+ materialHandle: 22,
1826
+ vertexEntry: undefined,
1827
+ fragmentEntry: undefined,
1828
+ materialShaderId: 'forgeax::default-shadow-caster',
1829
+ renderState: undefined,
1830
+ materialSlot: 2,
1831
+ paramSnapshot: materials[2]?.paramSnapshot,
1832
+ },
1833
+ {
1834
+ passIndex: 7,
1835
+ materialHandle: 22,
1836
+ vertexEntry: undefined,
1837
+ fragmentEntry: undefined,
1838
+ materialShaderId: 'custom-shadow-caster',
1839
+ renderState: { cullMode: 'none' as const, frontFace: 'cw' as const },
1840
+ materialSlot: 2,
1841
+ paramSnapshot: materials[2]?.paramSnapshot,
1842
+ },
1843
+ ],
1844
+ ],
1845
+ ]),
1846
+ ],
1847
+ ]);
1848
+ const indexedDraws: number[][] = [];
1849
+ const pipelineRequests: unknown[][] = [];
1850
+ const pass = {
1851
+ setPipeline: () => undefined,
1852
+ setVertexBuffer: () => undefined,
1853
+ setIndexBuffer: () => undefined,
1854
+ setBindGroup: () => undefined,
1855
+ drawIndexed: (...args: number[]) => indexedDraws.push(args),
1856
+ draw: () => undefined,
1857
+ } as unknown as RhiRenderPassEncoder;
1858
+ const context = {
1859
+ runtime: {
1860
+ device,
1861
+ getMaterialShaderPipeline: (...args: unknown[]) => {
1862
+ pipelineRequests.push(args);
1863
+ return {} as never;
1864
+ },
1865
+ },
1866
+ pipelineState: {
1867
+ identityInstanceBuffer,
1868
+ pbrSkinMeshBindGroupLayout: {} as never,
1869
+ skinPaletteAllocator: { bindingWindowBytes: 256 },
1870
+ instancesBindGroupLayout,
1871
+ materialBindGroupLayout: {} as never,
1872
+ materialUniformBuffer: { buffer: {} as never, sizeInBytes: 256 },
1873
+ meshStorageBuffer: { buffer: {} as never, sizeInBytes: 256 },
1874
+ defaultSampler: {} as never,
1875
+ fallbackTextureView: {} as never,
1876
+ defaultNormalTextureView: {} as never,
1877
+ defaultWhiteTextureView: {} as never,
1878
+ skylightFallback: {
1879
+ irradianceView: {} as never,
1880
+ prefilterView: {} as never,
1881
+ brdfLutView: {} as never,
1882
+ sampler: {} as never,
1883
+ intensityBuffer: {} as never,
1884
+ },
1885
+ },
1886
+ validatedOrdered: [
1887
+ {
1888
+ source: sourceForRecord,
1889
+ mesh: geometry,
1890
+ renderableIndex: 0,
1891
+ },
1892
+ ],
1893
+ frameState: {
1894
+ instanceBuffers: new Map(),
1895
+ meshBindGroupCache: new Map(),
1896
+ instancesBgPerEntity: new Map(),
1897
+ shadowMaterialBindGroups: new WeakMap(),
1898
+ materialBgShared: new Map(),
1899
+ },
1900
+ materialBgAssemblyCache: new Map(),
1901
+ store: { materialResourceEpoch: 0 } as never,
1902
+ world: new World(),
1903
+ bindGroupCounts: { createBindGroup: 0, keys: [] },
1904
+ dispatch: [],
1905
+ } as never;
1906
+ const recorded = recordShadowCasterDraws(
1907
+ context,
1908
+ pass,
1909
+ {} as never,
1910
+ {} as never,
1911
+ new Set([0]),
1912
+ new Map([[0, new Set([20, 21, 22])]]),
1913
+ shadowDispatches,
1914
+ new Set([claimedKey]),
1915
+ );
1916
+ expect(indexedDraws).toHaveLength(3);
1917
+ expect(recorded).toEqual(residualKeys);
1918
+ expect(recorded.has(claimedKey)).toBe(false);
1919
+ const authored = pipelineRequests.find((args) => args[0] === 'custom-shadow-caster');
1920
+ expect(authored).toBeDefined();
1921
+ expect(authored?.[2]).toEqual({ cullMode: 'none', frontFace: 'cw' });
1922
+ expect(authored?.[15]).toBe(skinned ? 'pbr-skin' : 'pbr');
1923
+ if (skinned) expect(authored?.[5]).toContain('SKINNING_DISABLED=false');
1924
+ });
1925
+
1926
+ it('fails closed at every shadow front door when expected casters lack bind groups', () => {
1927
+ const membership: ShadowCasterMembership[] = [
1928
+ {
1929
+ worldEntity: 1,
1930
+ renderableIndex: 0,
1931
+ drawItemIndex: 0,
1932
+ materialHandle: 20,
1933
+ passIndex: 0,
1934
+ materialShaderId: 'forgeax::default-shadow-caster',
1935
+ cpuReason: 'unprepared',
1936
+ },
1937
+ ];
1938
+ const context = {
1939
+ runtime: {
1940
+ device: { caps: { storageBuffer: false } },
1941
+ getMaterialShaderPipeline: () => null,
1942
+ },
1943
+ pipelineState: { perPassResources: { shadowSampler: null } },
1944
+ frameState: {
1945
+ pointShadowSnapshots: [{ shadowAtlasLayer: 0 }],
1946
+ spotShadowSnapshots: [{ shadowAtlasTile: 0, lightViewProj: new Float32Array(16) }],
1947
+ },
1948
+ dispatch: [],
1949
+ shadowCasterMembership: membership,
1950
+ } as never;
1951
+ const pass = {} as RhiRenderPassEncoder;
1952
+ const calls = [
1953
+ () =>
1954
+ encodeDirectionalShadowPass(context, pass, 0, {
1955
+ x: 0,
1956
+ y: 0,
1957
+ w: 1,
1958
+ h: 1,
1959
+ }),
1960
+ () => encodePointShadowPass(context, pass, 0, 0),
1961
+ () => encodeSpotShadowPass(context, pass, 0),
1962
+ ];
1963
+ for (const call of calls) {
1964
+ expect(call).toThrowError(RhiError);
1965
+ try {
1966
+ call();
1967
+ } catch (error) {
1968
+ expect(error).toBeInstanceOf(RhiError);
1969
+ expect((error as RhiError).code).toBe('rhi-not-available');
1970
+ }
1971
+ }
1972
+ });
1973
+
1974
+ it('labels a prepared-contract admission rejection for the CPU residual', () => {
1975
+ const source = preparedSnapshot(9, 3);
1976
+ const draw = source.gpuDrivenDraws?.[0];
1977
+ expect(draw?.prepared).toBeDefined();
1978
+ if (draw === undefined || draw.prepared === undefined) return;
1979
+ const mismatched = {
1980
+ ...draw,
1981
+ prepared: {
1982
+ ...draw.prepared,
1983
+ identity: {
1984
+ ...draw.prepared.identity,
1985
+ material: 'forgeax::pbr-skin' as const,
1986
+ deformation: 'skin' as const,
1987
+ },
1988
+ },
1989
+ };
1990
+ const material = {
1991
+ ...source.material,
1992
+ materialShaderId: 'forgeax::default-standard-pbr',
1993
+ } as MaterialSnapshot;
1994
+ expect(
1995
+ shadowCasterCpuReason(
1996
+ material,
1997
+ mismatched,
1998
+ [
1999
+ {
2000
+ entityIndex: source.entityKey,
2001
+ materialHandle: material.materialHandle ?? -1,
2002
+ renderableIndex: 0,
2003
+ passIndex: 0,
2004
+ queue: 2000,
2005
+ layer: 0,
2006
+ tags: { LightMode: 'ShadowCaster' },
2007
+ renderState: undefined,
2008
+ defines: undefined,
2009
+ vertexEntry: undefined,
2010
+ fragmentEntry: undefined,
2011
+ materialShaderId: 'forgeax::default-shadow-caster',
2012
+ paramSnapshot: undefined,
2013
+ } as DispatchEntry,
2014
+ ],
2015
+ undefined,
2016
+ undefined,
2017
+ ),
2018
+ ).toBe('prepared-contract');
2019
+ });
1050
2020
  });