@forgeax/engine-render 0.1.27 → 0.1.29

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