@forgeax/engine-render 0.1.20 → 0.1.23

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 (645) hide show
  1. package/README.md +365 -29
  2. package/dist/assembly/backend-contract.d.ts +13 -0
  3. package/dist/assembly/backend-contract.d.ts.map +1 -1
  4. package/dist/assembly/device-feature-admission.d.ts +18 -0
  5. package/dist/assembly/device-feature-admission.d.ts.map +1 -0
  6. package/dist/assembly/directional-shadow-inspection.d.ts +38 -0
  7. package/dist/assembly/directional-shadow-inspection.d.ts.map +1 -0
  8. package/dist/assembly/factory.d.ts +30 -19
  9. package/dist/assembly/factory.d.ts.map +1 -1
  10. package/dist/assembly/host-contract.d.ts +19 -0
  11. package/dist/assembly/host-contract.d.ts.map +1 -1
  12. package/dist/assembly/render-target-host.d.ts +46 -0
  13. package/dist/assembly/render-target-host.d.ts.map +1 -0
  14. package/dist/assets/render-asset-port.d.ts.map +1 -1
  15. package/dist/authoring.mjs +4 -1
  16. package/dist/authoring.mjs.map +1 -1
  17. package/dist/capture/cube-views.d.ts +21 -0
  18. package/dist/capture/cube-views.d.ts.map +1 -0
  19. package/dist/capture/scheduler.d.ts +54 -0
  20. package/dist/capture/scheduler.d.ts.map +1 -0
  21. package/dist/{chunk-PCHBPYML.mjs → chunk-4L3P6DT5.mjs} +2 -2
  22. package/dist/chunk-4L3P6DT5.mjs.map +1 -0
  23. package/dist/{chunk-VFPLTKCU.mjs → chunk-4NPUASZR.mjs} +15 -4
  24. package/dist/chunk-4NPUASZR.mjs.map +1 -0
  25. package/dist/chunk-DIYW3M5D.mjs +5085 -0
  26. package/dist/chunk-DIYW3M5D.mjs.map +1 -0
  27. package/dist/chunk-FBC4R44J.mjs +1003 -0
  28. package/dist/chunk-FBC4R44J.mjs.map +1 -0
  29. package/dist/{chunk-CDTWTHZR.mjs → chunk-FF7K3FSO.mjs} +474 -168
  30. package/dist/chunk-FF7K3FSO.mjs.map +1 -0
  31. package/dist/chunk-HZUPSYW7.mjs +1104 -0
  32. package/dist/chunk-HZUPSYW7.mjs.map +1 -0
  33. package/dist/{chunk-TBQJOK2L.mjs → chunk-QODA5UKU.mjs} +321 -269
  34. package/dist/chunk-QODA5UKU.mjs.map +1 -0
  35. package/dist/chunk-YALPS44A.mjs +296 -0
  36. package/dist/chunk-YALPS44A.mjs.map +1 -0
  37. package/dist/{chunk-4XINDLAY.mjs → chunk-ZUWKAZ2I.mjs} +1963 -2178
  38. package/dist/chunk-ZUWKAZ2I.mjs.map +1 -0
  39. package/dist/cluster-binner.d.ts +5 -0
  40. package/dist/cluster-binner.d.ts.map +1 -1
  41. package/dist/components/camera.d.ts +1 -0
  42. package/dist/components/camera.d.ts.map +1 -1
  43. package/dist/components/cube-camera.d.ts +20 -0
  44. package/dist/components/cube-camera.d.ts.map +1 -0
  45. package/dist/components/directional-light.d.ts +11 -2
  46. package/dist/components/directional-light.d.ts.map +1 -1
  47. package/dist/components/directional-shadow-filter.d.ts +25 -0
  48. package/dist/components/directional-shadow-filter.d.ts.map +1 -0
  49. package/dist/components/index.d.ts +5 -0
  50. package/dist/components/index.d.ts.map +1 -1
  51. package/dist/components/light-helpers.d.ts +4 -0
  52. package/dist/components/light-helpers.d.ts.map +1 -1
  53. package/dist/components/light-probe.d.ts +9 -0
  54. package/dist/components/light-probe.d.ts.map +1 -0
  55. package/dist/components/rect-area-light.d.ts +12 -0
  56. package/dist/components/rect-area-light.d.ts.map +1 -0
  57. package/dist/components/reflection-probe.d.ts +17 -0
  58. package/dist/components/reflection-probe.d.ts.map +1 -0
  59. package/dist/components/scene-instance.d.ts +4 -0
  60. package/dist/components/scene-instance.d.ts.map +1 -1
  61. package/dist/components/spot-light.d.ts +3 -0
  62. package/dist/components/spot-light.d.ts.map +1 -1
  63. package/dist/construct-renderer.mjs +14836 -9620
  64. package/dist/construct-renderer.mjs.map +1 -1
  65. package/dist/device/gpu-residency.d.ts +8 -1
  66. package/dist/device/gpu-residency.d.ts.map +1 -1
  67. package/dist/device/lifecycle-transaction.d.ts +2 -0
  68. package/dist/device/lifecycle-transaction.d.ts.map +1 -0
  69. package/dist/environment/frame.d.ts.map +1 -1
  70. package/dist/environment/lifecycle.d.ts +5 -0
  71. package/dist/environment/lifecycle.d.ts.map +1 -1
  72. package/dist/errors/gpu-driven.d.ts +26 -0
  73. package/dist/errors/gpu-driven.d.ts.map +1 -0
  74. package/dist/errors/index.d.ts +1 -0
  75. package/dist/errors/index.d.ts.map +1 -1
  76. package/dist/errors/recover.d.ts +9 -1
  77. package/dist/errors/recover.d.ts.map +1 -1
  78. package/dist/errors/render.d.ts +166 -93
  79. package/dist/errors/render.d.ts.map +1 -1
  80. package/dist/extract/directional-shadow-projection.d.ts +23 -0
  81. package/dist/extract/directional-shadow-projection.d.ts.map +1 -0
  82. package/dist/extract/environment.d.ts +14 -0
  83. package/dist/extract/environment.d.ts.map +1 -1
  84. package/dist/extract/gpu-driven.d.ts +50 -0
  85. package/dist/extract/gpu-driven.d.ts.map +1 -0
  86. package/dist/extract/visibility.d.ts.map +1 -1
  87. package/dist/features/fullscreen.d.ts.map +1 -1
  88. package/dist/features/targets.d.ts.map +1 -1
  89. package/dist/features/types.d.ts +10 -0
  90. package/dist/features/types.d.ts.map +1 -1
  91. package/dist/fullscreen-post-process-pass.d.ts +13 -2
  92. package/dist/fullscreen-post-process-pass.d.ts.map +1 -1
  93. package/dist/gpu-driven/batch-topology.d.ts +34 -0
  94. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  95. package/dist/gpu-driven/prepared-draw.d.ts +63 -0
  96. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -0
  97. package/dist/gpu-driven/production-raster.d.ts +97 -2
  98. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  99. package/dist/gpu-driven/view-gpu.d.ts +64 -1
  100. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  101. package/dist/gpu-scene-schema.d.ts +14 -0
  102. package/dist/gpu-scene-schema.d.ts.map +1 -1
  103. package/dist/gpu-scene.d.ts +8 -1
  104. package/dist/gpu-scene.d.ts.map +1 -1
  105. package/dist/gpu-usage.d.ts +0 -1
  106. package/dist/gpu-usage.d.ts.map +1 -1
  107. package/dist/hdrp-buffers.d.ts +21 -19
  108. package/dist/hdrp-buffers.d.ts.map +1 -1
  109. package/dist/ibl/IblPipelineCache.d.ts +4 -0
  110. package/dist/ibl/IblPipelineCache.d.ts.map +1 -1
  111. package/dist/ibl/kernel-cache.d.ts +36 -0
  112. package/dist/ibl/kernel-cache.d.ts.map +1 -0
  113. package/dist/index.d.ts +39 -3
  114. package/dist/index.d.ts.map +1 -1
  115. package/dist/index.mjs +91 -6
  116. package/dist/index.mjs.map +1 -1
  117. package/dist/inspection-types.d.ts +214 -1
  118. package/dist/inspection-types.d.ts.map +1 -1
  119. package/dist/instances-derived-bounds.d.ts +50 -0
  120. package/dist/instances-derived-bounds.d.ts.map +1 -0
  121. package/dist/internal.d.ts +9 -0
  122. package/dist/internal.d.ts.map +1 -1
  123. package/dist/internal.mjs +23 -2
  124. package/dist/internal.mjs.map +1 -1
  125. package/dist/light-buffer-layout.d.ts +58 -140
  126. package/dist/light-buffer-layout.d.ts.map +1 -1
  127. package/dist/materials.d.ts.map +1 -1
  128. package/dist/mesh-material-bindings.d.ts +54 -0
  129. package/dist/mesh-material-bindings.d.ts.map +1 -1
  130. package/dist/pbr-pipeline.d.ts +38 -14
  131. package/dist/pbr-pipeline.d.ts.map +1 -1
  132. package/dist/pipeline/standard-forward-lane.d.ts +2 -1
  133. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  134. package/dist/pipeline/standard-lighting/errors.d.ts +4 -0
  135. package/dist/pipeline/standard-lighting/errors.d.ts.map +1 -0
  136. package/dist/pipeline/standard-lighting/graph.d.ts +24 -0
  137. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -0
  138. package/dist/pipeline/standard-lighting/inspection.d.ts +38 -0
  139. package/dist/pipeline/standard-lighting/inspection.d.ts.map +1 -0
  140. package/dist/pipeline/standard-lighting/layout.d.ts +28 -0
  141. package/dist/pipeline/standard-lighting/layout.d.ts.map +1 -0
  142. package/dist/pipeline/standard-lighting/light-frame.d.ts +33 -0
  143. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -0
  144. package/dist/pipeline/standard-lighting/membership.d.ts +14 -0
  145. package/dist/pipeline/standard-lighting/membership.d.ts.map +1 -0
  146. package/dist/pipeline/standard-lighting/prepare.d.ts +16 -0
  147. package/dist/pipeline/standard-lighting/prepare.d.ts.map +1 -0
  148. package/dist/pipeline/standard-lighting/topology.d.ts +45 -0
  149. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -0
  150. package/dist/pipeline/standard-lighting/transport.d.ts +36 -0
  151. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -0
  152. package/dist/pipeline/standard-pipeline.d.ts +4 -13
  153. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  154. package/dist/pipeline/standard-post.d.ts.map +1 -1
  155. package/dist/pipeline/standard-profile.d.ts +5 -26
  156. package/dist/pipeline/standard-profile.d.ts.map +1 -1
  157. package/dist/pipeline-builder.d.ts +2 -0
  158. package/dist/pipeline-builder.d.ts.map +1 -1
  159. package/dist/pipeline-spec.d.ts +40 -7
  160. package/dist/pipeline-spec.d.ts.map +1 -1
  161. package/dist/plugin.d.ts.map +1 -1
  162. package/dist/point-shadow-inspection.d.ts +21 -0
  163. package/dist/point-shadow-inspection.d.ts.map +1 -0
  164. package/dist/prepare/extended-lighting/ltc-resources.d.ts +31 -0
  165. package/dist/prepare/extended-lighting/ltc-resources.d.ts.map +1 -0
  166. package/dist/prepare/extended-lighting/resources.d.ts +72 -0
  167. package/dist/prepare/extended-lighting/resources.d.ts.map +1 -0
  168. package/dist/prepare/extended-lighting/spot-modifiers.d.ts +42 -0
  169. package/dist/prepare/extended-lighting/spot-modifiers.d.ts.map +1 -0
  170. package/dist/prepare/extended-lighting/state.d.ts +24 -0
  171. package/dist/prepare/extended-lighting/state.d.ts.map +1 -0
  172. package/dist/record/frame-lighting.d.ts +33 -36
  173. package/dist/record/frame-lighting.d.ts.map +1 -1
  174. package/dist/record/frame-snapshot.d.ts +78 -24
  175. package/dist/record/frame-snapshot.d.ts.map +1 -1
  176. package/dist/record/frame-targets.d.ts +5 -0
  177. package/dist/record/frame-targets.d.ts.map +1 -1
  178. package/dist/record/frame.d.ts +16 -6
  179. package/dist/record/frame.d.ts.map +1 -1
  180. package/dist/record/gpu-pass-timing/contract.d.ts +96 -0
  181. package/dist/record/gpu-pass-timing/contract.d.ts.map +1 -0
  182. package/dist/record/gpu-pass-timing/errors.d.ts +18 -0
  183. package/dist/record/gpu-pass-timing/errors.d.ts.map +1 -0
  184. package/dist/record/gpu-pass-timing/index.d.ts +8 -0
  185. package/dist/record/gpu-pass-timing/index.d.ts.map +1 -0
  186. package/dist/record/gpu-pass-timing/parser.d.ts +11 -0
  187. package/dist/record/gpu-pass-timing/parser.d.ts.map +1 -0
  188. package/dist/record/gpu-pass-timing/session.d.ts +43 -0
  189. package/dist/record/gpu-pass-timing/session.d.ts.map +1 -0
  190. package/dist/record/gpu-timing.d.ts +20 -3
  191. package/dist/record/gpu-timing.d.ts.map +1 -1
  192. package/dist/record/helpers.d.ts +0 -20
  193. package/dist/record/helpers.d.ts.map +1 -1
  194. package/dist/record/main-pass-geometry.d.ts +6 -1
  195. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  196. package/dist/record/main-pass-material.d.ts +25 -2
  197. package/dist/record/main-pass-material.d.ts.map +1 -1
  198. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  199. package/dist/record/main-pass.d.ts +2 -0
  200. package/dist/record/main-pass.d.ts.map +1 -1
  201. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  202. package/dist/record/render-context.d.ts +98 -6
  203. package/dist/record/render-context.d.ts.map +1 -1
  204. package/dist/record/shadow-pass.d.ts.map +1 -1
  205. package/dist/record/skybox-post-pass.d.ts.map +1 -1
  206. package/dist/record/typed-frame-graph.d.ts +119 -4
  207. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  208. package/dist/record/view-ubo.d.ts +7 -5
  209. package/dist/record/view-ubo.d.ts.map +1 -1
  210. package/dist/reflection/filter.d.ts +22 -0
  211. package/dist/reflection/filter.d.ts.map +1 -0
  212. package/dist/reflection/gpu-table.d.ts +25 -0
  213. package/dist/reflection/gpu-table.d.ts.map +1 -0
  214. package/dist/reflection/inspection.d.ts +10 -0
  215. package/dist/reflection/inspection.d.ts.map +1 -0
  216. package/dist/reflection/projection.d.ts +140 -0
  217. package/dist/reflection/projection.d.ts.map +1 -0
  218. package/dist/reflection/record-owner.d.ts +60 -0
  219. package/dist/reflection/record-owner.d.ts.map +1 -0
  220. package/dist/render-contract.d.ts +62 -14
  221. package/dist/render-contract.d.ts.map +1 -1
  222. package/dist/render-graph-primitives.d.ts +1 -0
  223. package/dist/render-graph-primitives.d.ts.map +1 -1
  224. package/dist/render-pipeline.d.ts +49 -2
  225. package/dist/render-pipeline.d.ts.map +1 -1
  226. package/dist/render-system-extract.d.ts +160 -31
  227. package/dist/render-system-extract.d.ts.map +1 -1
  228. package/dist/render-system.d.ts +28 -2
  229. package/dist/render-system.d.ts.map +1 -1
  230. package/dist/renderer.d.ts +43 -0
  231. package/dist/renderer.d.ts.map +1 -0
  232. package/dist/scene/probe-blend-record.d.ts +22 -0
  233. package/dist/scene/probe-blend-record.d.ts.map +1 -0
  234. package/dist/scene/probe-blend.d.ts +142 -0
  235. package/dist/scene/probe-blend.d.ts.map +1 -0
  236. package/dist/scene/render-scene-types.d.ts +0 -6
  237. package/dist/scene/render-scene-types.d.ts.map +1 -1
  238. package/dist/scene/render-scene.d.ts +117 -9
  239. package/dist/scene/render-scene.d.ts.map +1 -1
  240. package/dist/scene/visibility/bounds.d.ts +20 -0
  241. package/dist/scene/visibility/bounds.d.ts.map +1 -0
  242. package/dist/scene/visibility/budget.d.ts +16 -0
  243. package/dist/scene/visibility/budget.d.ts.map +1 -0
  244. package/dist/scene/visibility/facet.d.ts +73 -0
  245. package/dist/scene/visibility/facet.d.ts.map +1 -0
  246. package/dist/scene/visibility/gpu-frame-timing.d.ts +41 -0
  247. package/dist/scene/visibility/gpu-frame-timing.d.ts.map +1 -0
  248. package/dist/scene/visibility/gpu-lod.d.ts +46 -0
  249. package/dist/scene/visibility/gpu-lod.d.ts.map +1 -0
  250. package/dist/scene/visibility/inspection.d.ts +132 -0
  251. package/dist/scene/visibility/inspection.d.ts.map +1 -0
  252. package/dist/scene/visibility/lod-selector.d.ts +25 -0
  253. package/dist/scene/visibility/lod-selector.d.ts.map +1 -0
  254. package/dist/scene/visibility/occlusion-confidence.d.ts +81 -0
  255. package/dist/scene/visibility/occlusion-confidence.d.ts.map +1 -0
  256. package/dist/scene/visibility/occlusion-pass.d.ts +57 -0
  257. package/dist/scene/visibility/occlusion-pass.d.ts.map +1 -0
  258. package/dist/scene/visibility/occlusion-query-pool.d.ts +57 -0
  259. package/dist/scene/visibility/occlusion-query-pool.d.ts.map +1 -0
  260. package/dist/scene/visibility/occlusion-runtime.d.ts +96 -0
  261. package/dist/scene/visibility/occlusion-runtime.d.ts.map +1 -0
  262. package/dist/scene/visibility/types.d.ts +34 -0
  263. package/dist/scene/visibility/types.d.ts.map +1 -0
  264. package/dist/shadow-atlas.d.ts +3 -3
  265. package/dist/shadow-atlas.d.ts.map +1 -1
  266. package/dist/ssao-buffers.d.ts +3 -0
  267. package/dist/ssao-buffers.d.ts.map +1 -1
  268. package/dist/ssr/admission.d.ts +122 -0
  269. package/dist/ssr/admission.d.ts.map +1 -0
  270. package/dist/ssr/composition.d.ts +32 -0
  271. package/dist/ssr/composition.d.ts.map +1 -0
  272. package/dist/ssr/errors.d.ts +39 -0
  273. package/dist/ssr/errors.d.ts.map +1 -0
  274. package/dist/ssr/identity.d.ts +13 -0
  275. package/dist/ssr/identity.d.ts.map +1 -0
  276. package/dist/systems/skin-palette-allocator.d.ts +1 -1
  277. package/dist/targets/contracts.d.ts +66 -0
  278. package/dist/targets/contracts.d.ts.map +1 -0
  279. package/dist/targets/graph-projection.d.ts +29 -0
  280. package/dist/targets/graph-projection.d.ts.map +1 -0
  281. package/dist/targets/material-source.d.ts +34 -0
  282. package/dist/targets/material-source.d.ts.map +1 -0
  283. package/dist/targets/owner.d.ts +32 -0
  284. package/dist/targets/owner.d.ts.map +1 -0
  285. package/dist/targets/physical.d.ts +18 -0
  286. package/dist/targets/physical.d.ts.map +1 -0
  287. package/dist/targets/readback.d.ts +39 -0
  288. package/dist/targets/readback.d.ts.map +1 -0
  289. package/dist/temporal/frame-coordinator.d.ts +13 -0
  290. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  291. package/dist/temporal/frame.d.ts +2 -0
  292. package/dist/temporal/frame.d.ts.map +1 -1
  293. package/dist/temporal/index.mjs +6 -5
  294. package/dist/temporal/view.d.ts +7 -0
  295. package/dist/temporal/view.d.ts.map +1 -1
  296. package/dist/tilemap-chunk-extract-system.d.ts +1 -1
  297. package/dist/typed-render-graph-primitives.d.ts +5 -1
  298. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  299. package/dist/typed-shadow-passes.d.ts +1 -1
  300. package/dist/typed-shadow-passes.d.ts.map +1 -1
  301. package/dist/volume/passes.d.ts +9 -5
  302. package/dist/volume/passes.d.ts.map +1 -1
  303. package/package.json +20 -20
  304. package/src/__tests__/bloom-exact-zero.unit.test.ts +48 -19
  305. package/src/__tests__/bloom-lifecycle.integration.test.ts +41 -13
  306. package/src/__tests__/camera-target-consumption.unit.test.ts +53 -0
  307. package/src/__tests__/cluster-binner.unit.test.ts +35 -0
  308. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +12 -2
  309. package/src/__tests__/cube-camera-scheduler.unit.test.ts +70 -0
  310. package/src/__tests__/cube-camera-single-submit.perf.test.ts +75 -0
  311. package/src/__tests__/cube-camera-view-matrices.unit.test.ts +41 -0
  312. package/src/__tests__/cube-camera-vocabulary.unit.test.ts +47 -0
  313. package/src/__tests__/cube-camera.integration.test.ts +42 -0
  314. package/src/__tests__/detached-inspection.integration.test.ts +6 -0
  315. package/src/__tests__/device-feature-admission.unit.test.ts +85 -0
  316. package/src/__tests__/direct-light-snapshot.unit.test.ts +62 -6
  317. package/src/__tests__/docs-gate-green.unit.test.ts +38 -0
  318. package/src/__tests__/environment-generation.integration.test.ts +16 -0
  319. package/src/__tests__/evidence-identity.ts +13 -0
  320. package/src/__tests__/evidence-identity.unit.test.ts +24 -0
  321. package/src/__tests__/extended-lighting-recovery.browser.test.ts +14 -0
  322. package/src/__tests__/extended-lighting-recovery.dawn.test.ts +14 -0
  323. package/src/__tests__/extended-lighting-recovery.integration.test.ts +81 -0
  324. package/src/__tests__/extended-lighting-resources.unit.test.ts +51 -0
  325. package/src/__tests__/extended-lighting-zero-feature.integration.test.ts +52 -0
  326. package/src/__tests__/factory-contract.integration.test.ts +288 -29
  327. package/src/__tests__/fog-component.unit.test.ts +67 -0
  328. package/src/__tests__/fog.integration.test.ts +86 -0
  329. package/src/__tests__/frame-observation.contract.test.ts +15 -0
  330. package/src/__tests__/fullscreen-feature-plan.unit.test.ts +37 -0
  331. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +146 -0
  332. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +10 -8
  333. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +18 -1
  334. package/src/__tests__/gpu-driven-production.integration.test.ts +579 -17
  335. package/src/__tests__/gpu-driven-scaling.unit.test.ts +66 -2
  336. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +26 -1
  337. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +101 -6
  338. package/src/__tests__/gpu-driven-view.browser.test.ts +3 -3
  339. package/src/__tests__/gpu-driven-view.dawn.test.ts +3 -3
  340. package/src/__tests__/gpu-driven-view.unit.test.ts +50 -0
  341. package/src/__tests__/gpu-lod-evidence.ts +188 -0
  342. package/src/__tests__/gpu-lod-fallback.integration.test.ts +10 -0
  343. package/src/__tests__/gpu-lod-indirect.integration.test.ts +40 -0
  344. package/src/__tests__/gpu-lod-parity.dawn.test.ts +29 -0
  345. package/src/__tests__/gpu-lod-readiness.integration.test.ts +52 -0
  346. package/src/__tests__/gpu-lod-schema.unit.test.ts +66 -0
  347. package/src/__tests__/gpu-lod-selector.unit.test.ts +28 -0
  348. package/src/__tests__/gpu-lod-silhouette.browser.test.ts +13 -0
  349. package/src/__tests__/gpu-pass-timing-browser-runner.ts +281 -0
  350. package/src/__tests__/gpu-pass-timing-coexistence.integration.test.ts +80 -0
  351. package/src/__tests__/gpu-pass-timing-contract.unit.test.ts +243 -0
  352. package/src/__tests__/gpu-pass-timing-exact-zero.unit.test.ts +131 -0
  353. package/src/__tests__/gpu-pass-timing-lifecycle.integration.test.ts +230 -0
  354. package/src/__tests__/gpu-pass-timing-observation.unit.test.ts +233 -0
  355. package/src/__tests__/gpu-pass-timing-session.unit.test.ts +438 -0
  356. package/src/__tests__/gpu-pass-timing.browser.test.ts +26 -0
  357. package/src/__tests__/gpu-pass-timing.rhi-null.unit.test.ts +125 -0
  358. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +66 -1
  359. package/src/__tests__/gpu-scene.dawn.test.ts +8 -1
  360. package/src/__tests__/gpu-scene.unit.test.ts +49 -3
  361. package/src/__tests__/hdrp-group2-visibility.unit.test.ts +24 -0
  362. package/src/__tests__/instances-culling.integration.test.ts +94 -0
  363. package/src/__tests__/instances-derived-bounds.unit.test.ts +118 -0
  364. package/src/__tests__/light-buffer-layout.unit.test.ts +113 -3
  365. package/src/__tests__/light-inspection.unit.test.ts +55 -0
  366. package/src/__tests__/light-resource-error.unit.test.ts +43 -0
  367. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +109 -0
  368. package/src/__tests__/lod-bounds.unit.test.ts +42 -0
  369. package/src/__tests__/lod-cpu-lane.integration.test.ts +55 -0
  370. package/src/__tests__/lod-cpu-no-change.perf.test.ts +19 -0
  371. package/src/__tests__/lod-gpu-frame-timing.unit.test.ts +49 -0
  372. package/src/__tests__/lod-multi-view.integration.test.ts +63 -0
  373. package/src/__tests__/lod-occlusion-device-loss.integration.test.ts +27 -0
  374. package/src/__tests__/lod-occlusion-final-decision.integration.test.ts +47 -0
  375. package/src/__tests__/lod-occlusion-inspection.integration.test.ts +217 -0
  376. package/src/__tests__/lod-occlusion-inspection.schema.test-d.ts +14 -0
  377. package/src/__tests__/lod-occlusion-inspection.unit.test.ts +72 -0
  378. package/src/__tests__/lod-occlusion-visual-evidence.ts +91 -0
  379. package/src/__tests__/lod-occlusion.browser.test.ts +15 -0
  380. package/src/__tests__/lod-occlusion.dawn.test.ts +20 -0
  381. package/src/__tests__/lod-selector.profile.test.ts +35 -0
  382. package/src/__tests__/lod-selector.unit.test.ts +68 -0
  383. package/src/__tests__/lod-visibility-identity.unit.test.ts +198 -0
  384. package/src/__tests__/lod-visibility-lifecycle.integration.test.ts +132 -0
  385. package/src/__tests__/ltc-resources.unit.test.ts +32 -0
  386. package/src/__tests__/material-contract-inventory.unit.test.ts +17 -0
  387. package/src/__tests__/material-observation.integration.test.ts +101 -0
  388. package/src/__tests__/occlusion-confidence.unit.test.ts +168 -0
  389. package/src/__tests__/occlusion-invalidation.integration.test.ts +37 -0
  390. package/src/__tests__/occlusion-pass-routing.unit.test.ts +16 -0
  391. package/src/__tests__/occlusion-pass.dawn.test.ts +20 -0
  392. package/src/__tests__/occlusion-pass.integration.test.ts +43 -0
  393. package/src/__tests__/occlusion-pass.rhi-null.test.ts +22 -0
  394. package/src/__tests__/occlusion-query-pool.rhi-null.test.ts +21 -0
  395. package/src/__tests__/occlusion-query-pool.unit.test.ts +95 -0
  396. package/src/__tests__/occlusion-query-ticket.integration.test.ts +47 -0
  397. package/src/__tests__/occlusion-query.dawn.test.ts +16 -0
  398. package/src/__tests__/occlusion-query.evidence.ts +178 -0
  399. package/src/__tests__/occlusion-query.rhi-null.test.ts +31 -0
  400. package/src/__tests__/occlusion-runtime.unit.test.ts +120 -0
  401. package/src/__tests__/pbr-view-layout.unit.test.ts +16 -0
  402. package/src/__tests__/point-shadow-inspection.unit.test.ts +60 -0
  403. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +196 -0
  404. package/src/__tests__/probe-bindings.unit.test.ts +200 -0
  405. package/src/__tests__/probe-blend-numeric.unit.test.ts +67 -0
  406. package/src/__tests__/probe-blend-oracle.unit.test.ts +136 -0
  407. package/src/__tests__/probe-capacity.integration.test.ts +97 -0
  408. package/src/__tests__/probe-recovery.integration.test.ts +104 -0
  409. package/src/__tests__/probe-scene-dirty.integration.test.ts +211 -0
  410. package/src/__tests__/profiler-phase-catalog.test.ts +4 -1
  411. package/src/__tests__/profiler-phase-token.test.ts +1 -0
  412. package/src/__tests__/public-surface.test-d.ts +10 -0
  413. package/src/__tests__/rect-area-boundary.unit.test.ts +24 -0
  414. package/src/__tests__/reflection-fallback-graph.unit.test.ts +67 -0
  415. package/src/__tests__/reflection-fallback-inspection.unit.test.ts +77 -0
  416. package/src/__tests__/reflection-fallback-lifecycle.integration.test.ts +21 -0
  417. package/src/__tests__/reflection-fallback-projection.unit.test.ts +90 -0
  418. package/src/__tests__/reflection-fallback-source.unit.test.ts +32 -0
  419. package/src/__tests__/reflection-probe-authoring.unit.test.ts +71 -0
  420. package/src/__tests__/reflection-probe-filter.unit.test.ts +54 -0
  421. package/src/__tests__/reflection-probe-ibl-topology.unit.test.ts +45 -0
  422. package/src/__tests__/reflection-probe-owner-placement.unit.test.ts +39 -0
  423. package/src/__tests__/reflection-probe-pass-encode.integration.test.ts +101 -0
  424. package/src/__tests__/reflection-probe-performance.perf.test.ts +60 -0
  425. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +120 -0
  426. package/src/__tests__/reflection-probe-recovery.integration.test.ts +55 -0
  427. package/src/__tests__/reflection-probe-selection.unit.test.ts +46 -0
  428. package/src/__tests__/render-error-code-owner.test-d.ts +19 -3
  429. package/src/__tests__/render-feature-docs.unit.test.ts +12 -7
  430. package/src/__tests__/render-graph-fallback.integration.test.ts +24 -0
  431. package/src/__tests__/render-perf/extended-lighting.bench.test.ts +66 -0
  432. package/src/__tests__/render-scene-projection.unit.test.ts +447 -18
  433. package/src/__tests__/render-target-admission.unit.test.ts +80 -0
  434. package/src/__tests__/render-target-graph-projection.unit.test.ts +102 -0
  435. package/src/__tests__/render-target-inspection.unit.test.ts +52 -0
  436. package/src/__tests__/render-target-material-source.test-d.ts +30 -0
  437. package/src/__tests__/render-target-material-source.unit.test.ts +97 -0
  438. package/src/__tests__/render-target-owner-placement.unit.test.ts +203 -0
  439. package/src/__tests__/render-target-owner.unit.test.ts +122 -0
  440. package/src/__tests__/render-target-probe-soak.perf.test.ts +94 -0
  441. package/src/__tests__/render-target-public-schema.json +107 -0
  442. package/src/__tests__/render-target-public-schema.unit.test.ts +91 -0
  443. package/src/__tests__/render-target-public-surface.test-d.ts +83 -0
  444. package/src/__tests__/render-target-readback.integration.test.ts +90 -0
  445. package/src/__tests__/render-target-recovery.integration.test.ts +85 -0
  446. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +124 -0
  447. package/src/__tests__/renderer-lifecycle.integration.test.ts +14 -0
  448. package/src/__tests__/shader-stage-surface.unit.test.ts +19 -4
  449. package/src/__tests__/skin-motion-regression.unit.test.ts +154 -0
  450. package/src/__tests__/skinned-shadow-caster.test.ts +13 -4
  451. package/src/__tests__/spot-modifier-projection.unit.test.ts +70 -0
  452. package/src/__tests__/ssr-admission-matrix.integration.test.ts +206 -0
  453. package/src/__tests__/ssr-composition.unit.test.ts +46 -0
  454. package/src/__tests__/ssr-fallback-zero-work.bench.test.ts +94 -0
  455. package/src/__tests__/ssr-recovery-errors.unit.test.ts +58 -0
  456. package/src/__tests__/standard-cluster-capability.integration.test.ts +63 -0
  457. package/src/__tests__/standard-cluster-transport.unit.test.ts +121 -0
  458. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +299 -0
  459. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +15 -5
  460. package/src/__tests__/standard-lighting-inspection.integration.test.ts +138 -0
  461. package/src/__tests__/standard-lighting-ssot.unit.test.ts +18 -0
  462. package/src/__tests__/standard-lighting-write.unit.test.ts +441 -0
  463. package/src/__tests__/standard-lighting.integration.test.ts +67 -0
  464. package/src/__tests__/standard-pipeline.dawn.test.ts +206 -5
  465. package/src/__tests__/standard-pipeline.integration.test.ts +181 -150
  466. package/src/__tests__/standard-probe-binding.unit.test.ts +25 -0
  467. package/src/__tests__/standard-profile-oracle.unit.test.ts +23 -38
  468. package/src/__tests__/temporal-fallback-generation.integration.test.ts +16 -0
  469. package/src/__tests__/temporal-view.unit.test.ts +3 -3
  470. package/src/__tests__/transmission-prewarm.unit.test.ts +69 -0
  471. package/src/__tests__/typed-pipeline-topology.unit.test.ts +395 -14
  472. package/src/__tests__/urp-pipeline-targets.unit.test.ts +2 -2
  473. package/src/__tests__/visibility-budget.integration.test.ts +140 -0
  474. package/src/__tests__/visibility-budget.unit.test.ts +70 -0
  475. package/src/__tests__/visibility-extract.integration.test.ts +11 -6
  476. package/src/__tests__/visibility-resolver.unit.test.ts +15 -0
  477. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +6 -3
  478. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +31 -28
  479. package/src/assembly/backend-contract.ts +13 -0
  480. package/src/assembly/device-feature-admission.ts +80 -0
  481. package/src/assembly/directional-shadow-inspection.ts +122 -0
  482. package/src/assembly/factory.ts +1411 -366
  483. package/src/assembly/host-contract.ts +35 -0
  484. package/src/assembly/render-target-host.ts +847 -0
  485. package/src/assets/asset-decoders.ts +1 -1
  486. package/src/assets/render-asset-port.ts +7 -1
  487. package/src/capture/cube-views.ts +69 -0
  488. package/src/capture/scheduler.ts +265 -0
  489. package/src/cluster-binner.ts +31 -0
  490. package/src/components/__tests__/lighting-authoring.unit.test.ts +39 -0
  491. package/src/components/__tests__/rect-area-light.unit.test.ts +33 -0
  492. package/src/components/__tests__/schema-contract.unit.test.ts +12 -0
  493. package/src/components/__tests__/schema-parity.unit.test.ts +9 -0
  494. package/src/components/camera.ts +4 -0
  495. package/src/components/cube-camera.ts +52 -0
  496. package/src/components/directional-light.ts +22 -2
  497. package/src/components/directional-shadow-filter.ts +57 -0
  498. package/src/components/index.ts +5 -0
  499. package/src/components/light-helpers.ts +133 -8
  500. package/src/components/light-probe.ts +13 -0
  501. package/src/components/rect-area-light.ts +15 -0
  502. package/src/components/reflection-probe.ts +49 -0
  503. package/src/components/scene-instance.ts +6 -0
  504. package/src/components/spot-light.ts +3 -0
  505. package/src/device/__tests__/extended-lighting-recovery.unit.test.ts +24 -0
  506. package/src/device/gpu-residency.ts +140 -9
  507. package/src/device/lifecycle-transaction.ts +12 -0
  508. package/src/environment/frame.ts +6 -24
  509. package/src/environment/lifecycle.ts +20 -1
  510. package/src/errors/gpu-driven.ts +92 -0
  511. package/src/errors/index.ts +1 -0
  512. package/src/errors/recover.ts +32 -0
  513. package/src/errors/render.ts +341 -143
  514. package/src/extract/camera.ts +3 -3
  515. package/src/extract/directional-shadow-projection.ts +61 -0
  516. package/src/extract/environment.ts +57 -0
  517. package/src/extract/gpu-driven.ts +108 -0
  518. package/src/extract/visibility.ts +2 -1
  519. package/src/features/fullscreen.ts +3 -0
  520. package/src/features/targets.ts +4 -1
  521. package/src/features/types.ts +10 -0
  522. package/src/fullscreen-post-process-pass.ts +43 -2
  523. package/src/gpu-driven/batch-topology.ts +207 -28
  524. package/src/gpu-driven/prepared-draw.ts +208 -0
  525. package/src/gpu-driven/production-raster.ts +953 -43
  526. package/src/gpu-driven/view-gpu.ts +411 -29
  527. package/src/gpu-scene-schema.ts +13 -0
  528. package/src/gpu-scene.ts +140 -64
  529. package/src/gpu-usage.ts +0 -1
  530. package/src/hdrp-buffers.ts +169 -63
  531. package/src/ibl/IblPipelineCache.ts +11 -0
  532. package/src/ibl/kernel-cache.ts +88 -0
  533. package/src/index.ts +222 -0
  534. package/src/inspection-types.ts +276 -1
  535. package/src/instances-derived-bounds.ts +171 -0
  536. package/src/internal.ts +38 -0
  537. package/src/light-buffer-layout.ts +155 -310
  538. package/src/materials.ts +16 -2
  539. package/src/mesh-material-bindings.ts +132 -0
  540. package/src/pbr-pipeline.ts +255 -60
  541. package/src/pipeline/standard-forward-lane.ts +93 -16
  542. package/src/pipeline/standard-lighting/errors.ts +6 -0
  543. package/src/pipeline/standard-lighting/graph.ts +158 -0
  544. package/src/pipeline/standard-lighting/inspection.ts +88 -0
  545. package/src/pipeline/standard-lighting/layout.ts +31 -0
  546. package/src/pipeline/standard-lighting/light-frame.ts +35 -0
  547. package/src/pipeline/standard-lighting/membership.ts +43 -0
  548. package/src/pipeline/standard-lighting/prepare.ts +37 -0
  549. package/src/pipeline/standard-lighting/topology.ts +120 -0
  550. package/src/pipeline/standard-lighting/transport.ts +78 -0
  551. package/src/pipeline/standard-pipeline.ts +124 -193
  552. package/src/pipeline/standard-post.ts +26 -7
  553. package/src/pipeline/standard-profile.ts +17 -47
  554. package/src/pipeline-builder.ts +5 -2
  555. package/src/pipeline-spec.ts +95 -21
  556. package/src/plugin.ts +4 -0
  557. package/src/point-shadow-inspection.ts +55 -0
  558. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +56 -0
  559. package/src/prepare/extended-lighting/ltc-resources.ts +136 -0
  560. package/src/prepare/extended-lighting/resources.ts +389 -0
  561. package/src/prepare/extended-lighting/spot-modifiers.ts +194 -0
  562. package/src/prepare/extended-lighting/state.ts +92 -0
  563. package/src/record/__tests__/gpu-timing.unit.test.ts +177 -0
  564. package/src/record/__tests__/temporal-payload.unit.test.ts +64 -1
  565. package/src/record/__tests__/transparent-group2-selection.unit.test.ts +73 -0
  566. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +20 -1
  567. package/src/record/__tests__/view-ubo-layout.unit.test.ts +40 -0
  568. package/src/record/frame-lighting.ts +591 -452
  569. package/src/record/frame-snapshot.ts +92 -39
  570. package/src/record/frame-targets.ts +17 -0
  571. package/src/record/frame.ts +662 -158
  572. package/src/record/gpu-pass-timing/contract.ts +180 -0
  573. package/src/record/gpu-pass-timing/errors.ts +34 -0
  574. package/src/record/gpu-pass-timing/index.ts +37 -0
  575. package/src/record/gpu-pass-timing/parser.ts +97 -0
  576. package/src/record/gpu-pass-timing/session.ts +938 -0
  577. package/src/record/gpu-timing.ts +112 -39
  578. package/src/record/helpers.ts +0 -58
  579. package/src/record/main-pass-geometry.ts +324 -119
  580. package/src/record/main-pass-material.ts +103 -10
  581. package/src/record/main-pass-sprite-draws.ts +99 -103
  582. package/src/record/main-pass.ts +71 -15
  583. package/src/record/mesh-ssbo.ts +47 -36
  584. package/src/record/render-context.ts +136 -3
  585. package/src/record/shadow-pass.ts +83 -68
  586. package/src/record/skybox-post-pass.ts +20 -4
  587. package/src/record/typed-frame-graph.ts +1034 -43
  588. package/src/record/view-ubo.ts +72 -47
  589. package/src/reflection/filter.ts +81 -0
  590. package/src/reflection/gpu-table.ts +44 -0
  591. package/src/reflection/inspection.ts +67 -0
  592. package/src/reflection/projection.ts +476 -0
  593. package/src/reflection/record-owner.ts +1283 -0
  594. package/src/render-contract.ts +93 -10
  595. package/src/render-graph-primitives.ts +9 -4
  596. package/src/render-pipeline.ts +122 -2
  597. package/src/render-system-extract.ts +1041 -225
  598. package/src/render-system.ts +1310 -85
  599. package/src/renderer.ts +222 -0
  600. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +36 -0
  601. package/src/scene/probe-blend-record.ts +54 -0
  602. package/src/scene/probe-blend.ts +813 -0
  603. package/src/scene/render-scene-types.ts +0 -7
  604. package/src/scene/render-scene.ts +942 -171
  605. package/src/scene/visibility/bounds.ts +57 -0
  606. package/src/scene/visibility/budget.ts +40 -0
  607. package/src/scene/visibility/facet.ts +400 -0
  608. package/src/scene/visibility/gpu-frame-timing.ts +101 -0
  609. package/src/scene/visibility/gpu-lod.ts +119 -0
  610. package/src/scene/visibility/inspection.ts +313 -0
  611. package/src/scene/visibility/lod-selector.ts +84 -0
  612. package/src/scene/visibility/occlusion-confidence.ts +478 -0
  613. package/src/scene/visibility/occlusion-pass.ts +165 -0
  614. package/src/scene/visibility/occlusion-query-pool.ts +178 -0
  615. package/src/scene/visibility/occlusion-runtime.ts +672 -0
  616. package/src/scene/visibility/types.ts +50 -0
  617. package/src/shadow-atlas.ts +5 -5
  618. package/src/ssao-buffers.ts +9 -2
  619. package/src/ssr/admission.ts +451 -0
  620. package/src/ssr/composition.ts +73 -0
  621. package/src/ssr/errors.ts +76 -0
  622. package/src/ssr/identity.ts +12 -0
  623. package/src/systems/skin-palette-allocator.ts +1 -1
  624. package/src/targets/contracts.ts +182 -0
  625. package/src/targets/graph-projection.ts +169 -0
  626. package/src/targets/material-source.ts +143 -0
  627. package/src/targets/owner.ts +214 -0
  628. package/src/targets/physical.ts +18 -0
  629. package/src/targets/readback.ts +153 -0
  630. package/src/temporal/frame-coordinator.ts +26 -0
  631. package/src/temporal/frame.ts +14 -1
  632. package/src/temporal/view.ts +11 -0
  633. package/src/tilemap-chunk-extract-system.ts +4 -4
  634. package/src/transmission/__tests__/standard-transmission.dawn.test.ts +59 -20
  635. package/src/typed-render-graph-primitives.ts +53 -3
  636. package/src/typed-shadow-passes.ts +46 -40
  637. package/src/volume/passes.ts +61 -58
  638. package/dist/.tsbuildinfo +0 -1
  639. package/dist/chunk-4XINDLAY.mjs.map +0 -1
  640. package/dist/chunk-CDTWTHZR.mjs.map +0 -1
  641. package/dist/chunk-M2MP4ZOU.mjs +0 -73
  642. package/dist/chunk-M2MP4ZOU.mjs.map +0 -1
  643. package/dist/chunk-PCHBPYML.mjs.map +0 -1
  644. package/dist/chunk-TBQJOK2L.mjs.map +0 -1
  645. package/dist/chunk-VFPLTKCU.mjs.map +0 -1
@@ -112,15 +112,12 @@ import {
112
112
  SpriteInstancesMutuallyExclusiveWithInstancesError,
113
113
  SpriteInstancesRequiresSpriteShaderError,
114
114
  } from '@forgeax/engine-ecs/projection';
115
+ import { deriveVertexLayoutProjection } from '@forgeax/engine-geometry';
115
116
  import { box3, frustum, type Mat4, mat4, type Vec3, vec3 } from '@forgeax/engine-math';
116
117
  import { AssetGuid, type AssetGuid as AssetGuidBytes } from '@forgeax/engine-pack/guid';
117
118
  import { RhiError } from '@forgeax/engine-rhi';
118
- import {
119
- MorphWeights,
120
- projectHierarchy,
121
- type SceneHierarchySnapshot,
122
- Transform,
123
- } from '@forgeax/engine-scene';
119
+ import { GlobalTransform, MorphWeights, Transform } from '@forgeax/engine-scene';
120
+ import type { MaterialShaderArtifact } from '@forgeax/engine-shader';
124
121
  import {
125
122
  JointCountMismatchError,
126
123
  JointEntityDanglingError,
@@ -131,6 +128,7 @@ import {
131
128
  import type {
132
129
  Asset,
133
130
  Handle,
131
+ IesProfileAsset,
134
132
  MaterialAsset,
135
133
  MaterialColorParameterSchema,
136
134
  MaterialParameter,
@@ -139,6 +137,7 @@ import type {
139
137
  MaterialTextureCoordinates,
140
138
  MaterialTextureValue,
141
139
  MeshAsset,
140
+ MeshLodLevel,
142
141
  ParamSchemaEntry,
143
142
  PrimitiveTopology,
144
143
  SkeletonAsset,
@@ -148,53 +147,84 @@ import {
148
147
  ASSET_ERROR_HINTS,
149
148
  AssetError,
150
149
  derive,
150
+ handleSlot,
151
151
  materialGuidText,
152
152
  materialValuesToLinearRuntime,
153
153
  toShared,
154
154
  } from '@forgeax/engine-types';
155
155
  import {
156
156
  Atmosphere,
157
+ antialiasFromF32,
158
+ bloomEnabledFromF32,
159
+ Camera,
160
+ CubeCamera,
161
+ cameraProjectionFromF32,
162
+ cubeCameraUpdateIntentFromF32,
157
163
  DirectionalLight,
158
164
  Fog,
159
165
  Instances,
160
166
  Layer,
167
+ LightProbe,
161
168
  Lines,
162
169
  MeshFilter,
163
170
  MeshRenderer,
171
+ MotionBlur,
164
172
  PointLight,
165
173
  PointLightShadow,
166
174
  Points,
167
175
  PostProcessParams,
168
176
  pointShapeFromU32,
177
+ RectAreaLight,
178
+ ReflectionProbe,
179
+ reflectionProbeUpdateIntentFromF32,
169
180
  SkyboxBackground,
170
181
  Skylight,
171
182
  SortKey,
172
183
  SpotLight,
173
184
  SpriteInstances,
174
185
  SpriteRegionOverride,
186
+ tonemapFromF32,
175
187
  tonemapToU32,
176
188
  } from './components';
189
+ import {
190
+ type DirectionalShadowQuality,
191
+ directionalShadowQualityFromF32,
192
+ } from './components/directional-shadow-filter';
177
193
  import { GlyphText } from './components/glyph-text';
178
- import { computeInvRangeSquared, degToCos } from './components/light-helpers';
194
+ import {
195
+ computeInvRangeSquared,
196
+ degToCos,
197
+ type LightValidationError,
198
+ validateDirectionalLightData,
199
+ validateLightProbeData,
200
+ validateRectAreaLightData,
201
+ } from './components/light-helpers';
179
202
  import { selectEnvironment } from './environment/frame';
180
203
  import {
181
204
  MaterialSkinAttrMissingError,
182
205
  ShadowInvalidConfigError,
183
206
  SkinMaterialMismatchError,
184
207
  } from './errors/render';
185
- import { extractCameraSnapshots } from './extract/camera';
208
+ import {
209
+ type DirectionalShadowCascadeFit,
210
+ projectDirectionalShadow,
211
+ } from './extract/directional-shadow-projection';
186
212
  import type {
187
213
  EnvironmentCandidate,
188
214
  EnvironmentFrame,
189
215
  FogCandidate,
190
216
  FogFrame,
217
+ FogSelectionFailure,
191
218
  } from './extract/environment';
219
+ import { buildGpuDrivenDraws, resolvePreparedGpuDrivenDraw } from './extract/gpu-driven';
192
220
  import { resolveVisibility, type VisibilitySnapshot } from './extract/visibility';
193
221
  import type {
194
222
  RenderFeatureHiddenEntityReport,
195
223
  RenderFeatureWorldVisibilitySnapshot,
196
224
  } from './features/types';
197
225
  import { ensureGlyphMeshMaterialSlots } from './glyph-text-layout-system';
226
+ import type { GpuDrivenGeometryReceipt } from './gpu-driven/prepared-draw';
227
+ import { deriveInstancesUnionBounds } from './instances-derived-bounds';
198
228
  import {
199
229
  type MeshMaterialBindingDiagnostic,
200
230
  type MeshMaterialBindingSource,
@@ -203,13 +233,22 @@ import {
203
233
  import { isStandardPbrMaterialShader } from './pbr-pipeline';
204
234
  import { expandPointsLinesBounds } from './points-lines/bounds';
205
235
  import type { PointsLinesRetainedSnapshot, PointsLinesStyle } from './points-lines/snapshot';
206
- import { type CameraSnapshot, STANDARD_OUTPUT_TRANSFORM_FEATURE_ID } from './render-contract';
236
+ import { COOKIE_SLICE_CAPACITY, IES_SLICE_CAPACITY } from './prepare/extended-lighting/resources';
237
+ import { prepareCookieProjection } from './prepare/extended-lighting/spot-modifiers';
238
+ import type { ReflectionProbeFact } from './reflection/projection';
239
+ import {
240
+ type CameraSnapshot,
241
+ type CubeCameraSnapshot,
242
+ STANDARD_OUTPUT_TRANSFORM_FEATURE_ID,
243
+ } from './render-contract';
244
+ import { SHADOW_ATLAS_DEFAULT_LAYERS } from './shadow-atlas';
245
+ import { getActiveCamera, selectActiveCameraIndex } from './systems/active-camera';
207
246
  import { selectPasses } from './systems/pass-selector';
208
247
 
209
- export { extractCameraSnapshots } from './extract/camera';
210
-
211
248
  import type { SkinPaletteAllocator } from './systems/skin-palette-allocator';
212
249
  import type { SkinPaletteSlice } from './systems/skin-palette-types';
250
+ import type { RenderTarget, RenderTargetTextureSource } from './targets/contracts';
251
+ import { resolveRenderTargetMaterialSource } from './targets/material-source';
213
252
  import { resolveSelectedVolumetricLight } from './volume/capability';
214
253
  import {
215
254
  type ValidatedVolumetricFog,
@@ -252,23 +291,25 @@ export interface PointLightSnapshot {
252
291
  readonly invRangeSquared: number;
253
292
  /**
254
293
  * feat-20260612-point-light-shadows-urp-hdrp M1 / T-M1-8 + M4 / T-M4-4:
255
- * cube_array atlas layer index for this light's shadow map (0..3 for shadow
256
- * casters; sentinel `-1` for non-shadow lights). Default `-1` per
294
+ * cube_array atlas layer index for this light's shadow map (0..capacity-1
295
+ * for admitted shadow casters; sentinel `-1` for non-admitted lights).
296
+ * Default `-1` per
257
297
  * plan-strategy §D-2 — record stage / shader skips shadow sampling when the
258
298
  * lane equals the sentinel. Joined with `pointShadow[]` by entity at the
259
299
  * end of extract; HDRP record stage threads `shadowAtlasLayer + shadowNear +
260
- * shadowFar` through `packLightSlot` as the §D-8 pad-lane payload.
300
+ * shadowFar` through the unified direct-light metadata payload.
261
301
  */
262
302
  readonly shadowAtlasLayer?: number;
263
303
  /**
264
304
  * Per-face perspective near plane (matches `PointLightShadow.nearPlane`).
265
- * Used by HDRP `evalPointShadowed` for depth-ref reconstruction; rides
266
- * `LightSlot.kind_and_pad.z` (byte 56..60) on the std430 std layout.
305
+ * Used by HDRP `evalPointShadowed` for depth-ref reconstruction; URP keeps
306
+ * the projection constants in its dedicated shadow-parameter buffer.
267
307
  */
268
308
  readonly shadowNear?: number;
269
309
  /**
270
310
  * Per-face perspective far plane (matches `PointLightShadow.farPlane`).
271
- * Rides `LightSlot.kind_and_pad.w` (byte 60..64).
311
+ * It is consumed with the shadow atlas layer through the shared
312
+ * shadow-parameter buffer; DirectLightSlot carries only the layer identity.
272
313
  */
273
314
  readonly shadowFar?: number;
274
315
  }
@@ -290,9 +331,9 @@ export interface PointLightSnapshot {
290
331
  *
291
332
  * `mapSize` / `nearPlane` / `farPlane` ride here so record stage can size the
292
333
  * cube atlas faces and feed shader the proj constants without re-querying ECS.
293
- * `shadowAtlasLayer` is assigned by extract in spawn order (0 .. cap-1) so the
294
- * downstream LightSlot / packPointLight packers carry the layer index in
295
- * pointPadW (T-M1-8 will rename to shadowAtlasLayer).
334
+ * `shadowAtlasLayer` is assigned by extract in spawn order up to the renderer
335
+ * atlas capacity; over-budget requests retain the sentinel `-1` so the
336
+ * downstream DirectLightSlot packing carries no invalid layer identity.
296
337
  */
297
338
  export interface PointShadowSnapshot {
298
339
  /** Spawning entity index (joins PointLight + PointLightShadow + Transform). */
@@ -304,9 +345,9 @@ export interface PointShadowSnapshot {
304
345
  readonly nearPlane: number;
305
346
  readonly farPlane: number;
306
347
  /**
307
- * Atlas layer assigned by extract in spawn order (0..cap-1, where cap=4
308
- * matches PointLightShadow cardinality). Sentinel -1 is reserved by the
309
- * shader-side LightSlot for no-shadow lights (T-M1-8).
348
+ * Atlas layer assigned by extract in spawn order while capacity remains.
349
+ * Sentinel -1 is used for a request that cannot be admitted and is
350
+ * reserved by the shader-side DirectLightSlot metadata for no-shadow lights.
310
351
  */
311
352
  readonly shadowAtlasLayer: number;
312
353
  /**
@@ -345,6 +386,27 @@ export interface SpotLightSnapshot {
345
386
  readonly invRangeSquared: number;
346
387
  readonly cosInner: number;
347
388
  readonly cosOuter: number;
389
+ /** Shared asset identities projected from the authored modifier fields. */
390
+ readonly iesProfileHandle?: number;
391
+ readonly cookieHandle?: number;
392
+ /** Resource-array slice identities; absent means the modifier falls back. */
393
+ readonly iesProfileSlice?: number;
394
+ readonly cookieSlice?: number;
395
+ /** Resolved authored payloads; absent means the renderer keeps identity LKG. */
396
+ readonly iesProfileData?: Uint8Array;
397
+ readonly cookieData?: Uint8Array;
398
+ /** Aspect-only UV matrix paired with `cookieSlice`; upload is dirty-ranged. */
399
+ readonly cookieMatrix?: Float32Array;
400
+ /**
401
+ * Legacy projector payload projected into the shared Cookie array lane when
402
+ * the adapter exposes the extended-lighting topology. The projector keeps
403
+ * its lightViewProj sampling semantics; only the texture transport is
404
+ * shared with authored Cookie resources.
405
+ */
406
+ readonly projectorSlice?: number;
407
+ readonly projectorData?: Uint8Array;
408
+ readonly projectorMatrix?: Float32Array;
409
+ readonly rollDeg?: number;
348
410
  // ── shadow fields (feat-20260625-spot-light-shadow-mapping M1) ──
349
411
  readonly castShadow: boolean;
350
412
  readonly lightViewProj: Float32Array | undefined;
@@ -365,20 +427,111 @@ export interface SpotLightSnapshot {
365
427
  readonly projectorRevision?: number;
366
428
  }
367
429
 
430
+ /** RectArea direct-light snapshot carried by the unified slot contract. */
431
+ export interface RectAreaDirectLightSnapshot {
432
+ readonly kind: 'rect-area';
433
+ readonly position: Vec3;
434
+ readonly color: Vec3;
435
+ readonly intensity: number;
436
+ readonly invRangeSquared: number;
437
+ readonly halfWidth: number;
438
+ readonly halfHeight: number;
439
+ readonly axisX: Vec3;
440
+ readonly axisY: Vec3;
441
+ readonly shadowAtlasTile?: number;
442
+ }
443
+
444
+ /** Normalized world-space frame used by Rect extraction, culling, and BRDF. */
445
+ export interface RectAreaWorldFrame {
446
+ readonly center: Vec3;
447
+ readonly axisX: Vec3;
448
+ readonly axisY: Vec3;
449
+ readonly normal: Vec3;
450
+ readonly halfWidth: number;
451
+ readonly halfHeight: number;
452
+ }
453
+
454
+ /** Build the single normalized Rect frame shared by direct and cluster paths. */
455
+ export function buildRectAreaWorldFrame(input: {
456
+ readonly center: ArrayLike<number>;
457
+ readonly axisX: ArrayLike<number>;
458
+ readonly axisY: ArrayLike<number>;
459
+ readonly width: number;
460
+ readonly height: number;
461
+ }): RectAreaWorldFrame {
462
+ const center = vec3.create(input.center[0] ?? 0, input.center[1] ?? 0, input.center[2] ?? 0);
463
+ const axisX = vec3.normalize(
464
+ vec3.create(),
465
+ vec3.create(input.axisX[0] ?? 0, input.axisX[1] ?? 0, input.axisX[2] ?? 0),
466
+ );
467
+ const axisY = vec3.normalize(
468
+ vec3.create(),
469
+ vec3.create(input.axisY[0] ?? 0, input.axisY[1] ?? 0, input.axisY[2] ?? 0),
470
+ );
471
+ const normal = vec3.normalize(vec3.create(), vec3.cross(vec3.create(), axisX, axisY));
472
+ normal[0] = normal[0] === 0 ? 0 : (normal[0] ?? 0);
473
+ normal[1] = normal[1] === 0 ? 0 : (normal[1] ?? 0);
474
+ normal[2] = normal[2] === 0 ? 0 : (normal[2] ?? 0);
475
+ return {
476
+ center,
477
+ axisX,
478
+ axisY,
479
+ normal,
480
+ halfWidth: Math.max(0, input.width * 0.5),
481
+ halfHeight: Math.max(0, input.height * 0.5),
482
+ };
483
+ }
484
+
485
+ /** Return true only on the authored front side of a one-sided Rect. */
486
+ export function rectAreaFacesPoint(frame: RectAreaWorldFrame, point: ArrayLike<number>): boolean {
487
+ const toPoint = vec3.create(
488
+ (point[0] ?? 0) - (frame.center[0] ?? 0),
489
+ (point[1] ?? 0) - (frame.center[1] ?? 0),
490
+ (point[2] ?? 0) - (frame.center[2] ?? 0),
491
+ );
492
+ return vec3.dot(frame.normal, toPoint) > 0;
493
+ }
494
+
495
+ /** Project a world point onto the finite authored Rect boundary. */
496
+ export function closestPointOnRectArea(frame: RectAreaWorldFrame, point: ArrayLike<number>): Vec3 {
497
+ const toPoint = vec3.create(
498
+ (point[0] ?? 0) - (frame.center[0] ?? 0),
499
+ (point[1] ?? 0) - (frame.center[1] ?? 0),
500
+ (point[2] ?? 0) - (frame.center[2] ?? 0),
501
+ );
502
+ const localX = Math.min(
503
+ frame.halfWidth,
504
+ Math.max(-frame.halfWidth, vec3.dot(toPoint, frame.axisX)),
505
+ );
506
+ const localY = Math.min(
507
+ frame.halfHeight,
508
+ Math.max(-frame.halfHeight, vec3.dot(toPoint, frame.axisY)),
509
+ );
510
+ return vec3.create(
511
+ (frame.center[0] ?? 0) + (frame.axisX[0] ?? 0) * localX + (frame.axisY[0] ?? 0) * localY,
512
+ (frame.center[1] ?? 0) + (frame.axisX[1] ?? 0) * localX + (frame.axisY[1] ?? 0) * localY,
513
+ (frame.center[2] ?? 0) + (frame.axisX[2] ?? 0) * localX + (frame.axisY[2] ?? 0) * localY,
514
+ );
515
+ }
516
+
368
517
  /**
369
- * LightSnapshot — discriminated union of the three KHR_lights_punctual
518
+ * LightSnapshot — discriminated union of the directional and punctual
370
519
  * variants. AI users + the record stage perform an exhaustive switch on
371
520
  * `kind`; missing arms are caught at compile time (no `default`,
372
521
  * `assertNever`-style guards). Plan-strategy R-10.
373
522
  */
374
- export type LightSnapshot = DirectionalLightSnapshot | PointLightSnapshot | SpotLightSnapshot;
523
+ export type LightSnapshot =
524
+ | DirectionalLightSnapshot
525
+ | PointLightSnapshot
526
+ | SpotLightSnapshot
527
+ | RectAreaDirectLightSnapshot;
375
528
 
376
529
  /**
377
530
  * ExtractedLights — three-bucket output of the extractFrame three-query
378
531
  * union (M2 / w16 / AC-03). Plan-strategy section 3.1 EXT node:
379
532
  * - `directional` — at most one (record-stage N>1 fail-fast lands in M3)
380
- * - `point[]` first-slice cap of 4 enforced at record stage (M3)
381
- * - `spot[]` — first-slice cap of 4 enforced at record stage (M3)
533
+ * - `point[]` / `spot[]` / `rect[]` authored local-light snapshots; the
534
+ * record stage preserves this order in one bounded Cluster corpus
382
535
  * `directionalCount` exposes the raw count of DirectionalLight entities the
383
536
  * extract observed (so the record stage can fire `render-system-multi-light`
384
537
  * when N>1 without re-running the query). M3 / w19 will widen this with
@@ -390,6 +543,7 @@ export interface ExtractedLights {
390
543
  readonly directionalCount: number;
391
544
  readonly point: readonly PointLightSnapshot[];
392
545
  readonly spot: readonly SpotLightSnapshot[];
546
+ readonly rect: readonly RectAreaDirectLightSnapshot[];
393
547
  /**
394
548
  * feat-20260613-csm-cascaded-shadow-maps M2 / w9: per-cascade light-view-
395
549
  * projection matrices (one per cascade, length 4 pre-allocated). Each matrix
@@ -404,6 +558,7 @@ export interface ExtractedLights {
404
558
  * unused slots are 0.0f. Undefined when castShadow=false or no directional
405
559
  * light.
406
560
  */
561
+ /** Four vec4 lanes: split, worldUnitsPerTexel, lightDepthWorldSpan, reserved. */
407
562
  readonly splitPlanes: Float32Array | undefined;
408
563
  /**
409
564
  * feat-20260613-csm-cascaded-shadow-maps M2 / w9: effective cascade count
@@ -439,19 +594,17 @@ export interface ExtractedLights {
439
594
  * undefined otherwise.
440
595
  */
441
596
  readonly normalBias: number | undefined;
442
- /**
443
- * feat-20260621-merge-directionallightshadow-into-directionallight M2:
444
- * pcfKernelSize from the merged DirectionalLight (PCF kernel width, odd>=1).
445
- * Populated when castShadow=true on the first-hit directional light;
446
- * undefined otherwise.
447
- */
448
- readonly pcfKernelSize: number | undefined;
597
+ /** Accepted closed Directional filter projected from the first query result. */
598
+ readonly directionalShadowQuality: DirectionalShadowQuality | undefined;
599
+ /** Structured author-config failure; the candidate is withheld from record. */
600
+ readonly directionalShadowError: LightValidationError | undefined;
449
601
  /**
450
602
  * feat-20260612-point-light-shadows-urp-hdrp M1 / T-M1-7:
451
603
  * shadow-casting point lights (PointLight + PointLightShadow + Transform
452
604
  * archetype join). Each entry carries the per-light 6-face VP matrices and
453
605
  * the assigned cube_array atlas layer (0..3 in spawn order; sentinel -1 is
454
- * reserved by the shader-side LightSlot for no-shadow point lights).
606
+ * reserved by the shader-side DirectLightSlot metadata for no-shadow point
607
+ * lights).
455
608
  *
456
609
  * Empty array when no PointLightShadow components exist (zero-cost gate per
457
610
  * AC-09; record stage skips atlas allocation + shadow pass dispatch).
@@ -511,6 +664,15 @@ export interface SkylightSnapshot {
511
664
  readonly entityHandle: number;
512
665
  }
513
666
 
667
+ /** Local diffuse probe facts; specular remains owned by Skylight/ReflectionProbe. */
668
+ export interface LightProbeSnapshot {
669
+ readonly identity: string;
670
+ readonly position: readonly [number, number, number];
671
+ readonly radius: number;
672
+ readonly irradiance: Float32Array;
673
+ readonly admitted: boolean;
674
+ }
675
+
514
676
  /**
515
677
  * SkyboxSnapshot -- extract-stage view of one SkyboxBackground entity
516
678
  * (feat-20260531-skybox-env-background M2 / w5).
@@ -567,6 +729,9 @@ export interface ExtractedVolumetricFog {
567
729
  export interface RenderableSnapshot {
568
730
  readonly assetHandle: number;
569
731
  readonly transform: TransformSnapshot;
732
+ /** Producer-authored lower-detail references projected from MeshAsset. */
733
+ readonly lods?: readonly MeshLodLevel[];
734
+ readonly lodHysteresis?: number;
570
735
  /** Finite producer-owned local bounds retained for persistent CPU view work. */
571
736
  readonly localAabb?: Float32Array;
572
737
  /** Indexed single-submesh facts consumed by the first GPU-driven rigid lane. */
@@ -674,6 +839,8 @@ export interface RenderableSnapshot {
674
839
  readonly temporal?: RenderableTemporalSnapshot;
675
840
  /** Detached M2 authoring facts for the optional Points/Lines projection. */
676
841
  readonly pointsLines?: PointsLinesRetainedSnapshot;
842
+ /** Renderer-owned per-object probe record uploaded at group(3) binding(1). */
843
+ readonly probeBlendRecord?: import('./scene/probe-blend-record').ProbeBlendRecord;
677
844
  }
678
845
 
679
846
  export type RenderablePreviousSource = 'last-submitted' | 'current-seed';
@@ -710,6 +877,16 @@ export interface GpuDrivenDrawSnapshot {
710
877
  readonly topology: PrimitiveTopology;
711
878
  readonly pipelineClass: string;
712
879
  readonly materialResourceClass: string;
880
+ /** Producer-owned prepared ABI facts used by the Standard PBR GPU lane. */
881
+ readonly prepared?: import('./gpu-driven/prepared-draw').PreparedGpuDrivenDraw;
882
+ /** Optional projected height supplied by direct GPU-view evidence/tests. */
883
+ readonly projectedHeight?: number;
884
+ /** Root plus lower-detail index/vertex ranges, in LOD order. */
885
+ readonly lodRanges?: readonly {
886
+ readonly first: number;
887
+ readonly count: number;
888
+ readonly baseVertex: number;
889
+ }[];
713
890
  }
714
891
 
715
892
  export interface InstancesSnapshot {
@@ -762,7 +939,7 @@ export interface SpriteInstancesSnapshot {
762
939
  /**
763
940
  * TransformSnapshot: extract-stage view of one entity's resolved world
764
941
  * transform (feat-20260601 D-3). Holds the single `world` mat4 (column-major
765
- * 16 floats, copied from the entity's `Transform.world` view written by
942
+ * 16 floats, copied from the entity's `GlobalTransform.world` view written by
766
943
  * propagateTransforms). The record stage copies these 16 floats straight into
767
944
  * the mesh SSBO slot (zero `mat4.compose`); position / scale consumers derive
768
945
  * from the mat4 via `mat4.getTranslation` / basis-column lengths.
@@ -851,6 +1028,8 @@ export interface MaterialSnapshot {
851
1028
  * them in a lightmap injection region.
852
1029
  */
853
1030
  readonly textureHandles?: ReadonlyMap<string, Handle<'TextureAsset', 'shared'>> | undefined;
1031
+ /** Renderer-local target sources; never projected as TextureAsset handles. */
1032
+ readonly textureSources?: ReadonlyMap<string, RenderTargetTextureSource> | undefined;
854
1033
  /**
855
1034
  * User-region texture field names whose paramValue resolved to a VideoAsset
856
1035
  * (kind `'video'`) rather than a static TextureAsset
@@ -948,20 +1127,6 @@ export interface MaterialSnapshotCacheEntry {
948
1127
  export type MaterialSnapshotCache = Map<number, MaterialSnapshotCacheEntry>;
949
1128
  export type MaterialSnapshotCachesByWorld = WeakMap<World, MaterialSnapshotCache>;
950
1129
 
951
- function gpuDrivenMaterialResourceClass(material: MaterialSnapshot): string {
952
- const textures = [...(material.textureHandles?.entries() ?? [])]
953
- .map(([name, handle]) => [name, Number(handle)] as const)
954
- .sort(([left], [right]) => left.localeCompare(right));
955
- const samplers = [...(material.samplerHandles?.entries() ?? [])]
956
- .map(([name, handle]) => [name, Number(handle)] as const)
957
- .sort(([left], [right]) => left.localeCompare(right));
958
- return JSON.stringify({
959
- textures,
960
- samplers,
961
- video: [...(material.videoTextureFields?.keys() ?? [])].sort(),
962
- });
963
- }
964
-
965
1130
  // === DispatchEntry — M3 / w26 single dispatch list (feat-20260526-material-asset-multipass-renderstate) ===
966
1131
  //
967
1132
  // Plan-strategy D-3: single dispatch list sorted by queue value,
@@ -1060,6 +1225,12 @@ function appendMaterialDispatchEntries(
1060
1225
 
1061
1226
  export interface ExtractedFrame {
1062
1227
  readonly cameras: CameraSnapshot[];
1228
+ /** Auxiliary target cameras selected after the display camera is fixed. */
1229
+ readonly auxiliaryCameras: readonly CameraSnapshot[];
1230
+ /** Cube capture intents extracted beside the display camera in one pass. */
1231
+ readonly cubeCameras: readonly CubeCameraSnapshot[];
1232
+ /** Reflection probe facts projected from the same World extraction. */
1233
+ readonly reflectionProbes?: readonly ReflectionProbeFact[];
1063
1234
  readonly lights: ExtractedLights;
1064
1235
  /** Resource-owner environment facts selected once at the extract boundary. */
1065
1236
  readonly environment: EnvironmentFrame | undefined;
@@ -1076,10 +1247,14 @@ export interface ExtractedFrame {
1076
1247
  readonly dispatch: DispatchEntry[];
1077
1248
  readonly skylight: SkylightSnapshot | undefined;
1078
1249
  readonly skylightCount: number;
1250
+ /** World-local diffuse probe candidates; never a direct-light slot. */
1251
+ readonly lightProbes?: readonly LightProbeSnapshot[];
1079
1252
  readonly skybox: SkyboxSnapshot | undefined;
1080
1253
  readonly skyboxCount: number;
1081
1254
  /** Resource-owner Fog facts selected once at the frame boundary. */
1082
1255
  readonly fog: FogFrame | undefined;
1256
+ /** Last invalid Fog update retained alongside the current valid/LKG frame. */
1257
+ readonly fogFailure?: FogSelectionFailure;
1083
1258
  /**
1084
1259
  * feat-20260528-frustum-culling M3 / w11: frustum culling statistics
1085
1260
  * collected during the extract phase. `total` is the count of entities
@@ -1105,6 +1280,65 @@ export interface ExtractedFrame {
1105
1280
  readonly hiddenEntityReports: readonly RenderFeatureHiddenEntityReport[];
1106
1281
  }
1107
1282
 
1283
+ export type CameraTargetUpdate = 'once' | 'on-demand' | 'continuous';
1284
+
1285
+ export interface CameraTargetCandidate {
1286
+ readonly worldId: number;
1287
+ readonly entityKey: number;
1288
+ readonly target?: RenderTarget;
1289
+ readonly requestVersion: number;
1290
+ readonly update: CameraTargetUpdate;
1291
+ }
1292
+
1293
+ export interface CameraTargetRejection {
1294
+ readonly entityKey: number;
1295
+ readonly reason: 'budget' | 'display-target' | 'duplicate-target';
1296
+ }
1297
+
1298
+ export interface CameraTargetSelection {
1299
+ readonly display: CameraTargetCandidate | undefined;
1300
+ readonly auxiliary: readonly CameraTargetCandidate[];
1301
+ readonly rejected: readonly CameraTargetRejection[];
1302
+ }
1303
+
1304
+ export function selectCameraTargetViews(
1305
+ candidates: readonly CameraTargetCandidate[],
1306
+ options: { readonly displayEntityKey?: number; readonly budget: number },
1307
+ ): CameraTargetSelection {
1308
+ const display = candidates.find(
1309
+ (candidate) =>
1310
+ candidate.entityKey === options.displayEntityKey && candidate.target === undefined,
1311
+ );
1312
+ const rejected: CameraTargetRejection[] = [];
1313
+ const ordered = candidates
1314
+ .filter((candidate) => candidate.target !== undefined)
1315
+ .sort((left, right) => left.worldId - right.worldId || left.entityKey - right.entityKey);
1316
+ const auxiliary: CameraTargetCandidate[] = [];
1317
+ const seenTargets = new Set<RenderTarget>();
1318
+ for (const candidate of ordered) {
1319
+ const duplicate = candidate.target !== undefined && seenTargets.has(candidate.target);
1320
+ if (candidate.target !== undefined) seenTargets.add(candidate.target);
1321
+ if (candidate.entityKey === options.displayEntityKey) {
1322
+ rejected.push({ entityKey: candidate.entityKey, reason: 'display-target' });
1323
+ continue;
1324
+ }
1325
+ if (candidate.target === undefined || duplicate) {
1326
+ rejected.push({ entityKey: candidate.entityKey, reason: 'duplicate-target' });
1327
+ continue;
1328
+ }
1329
+ if (auxiliary.length >= Math.max(0, options.budget)) {
1330
+ rejected.push({ entityKey: candidate.entityKey, reason: 'budget' });
1331
+ continue;
1332
+ }
1333
+ auxiliary.push(candidate);
1334
+ }
1335
+ return {
1336
+ display,
1337
+ auxiliary: Object.freeze(auxiliary),
1338
+ rejected: Object.freeze(rejected),
1339
+ };
1340
+ }
1341
+
1108
1342
  /**
1109
1343
  * Internal world surface used by extract for archetype-graph traversal and
1110
1344
  * error routing. The packed `Entity` handle for a row is read directly from
@@ -1147,6 +1381,14 @@ function createWorldInternalView(world: World): WorldInternalView {
1147
1381
  };
1148
1382
  }
1149
1383
 
1384
+ function resolveCameraTarget(world: World, raw: number | undefined): RenderTarget | undefined {
1385
+ if (raw === undefined || raw <= 0) return undefined;
1386
+ const resolved = world.sharedRefs.resolve<'RenderTarget', RenderTarget>(
1387
+ toShared<'RenderTarget'>(Math.round(raw)),
1388
+ );
1389
+ return resolved.ok ? resolved.value : undefined;
1390
+ }
1391
+
1150
1392
  /**
1151
1393
  * The user-region texture fields the built-in standard-PBR material declares.
1152
1394
  * Used as the fallback texture-field set when the shader id is not registered
@@ -1427,6 +1669,24 @@ function collectMaterialTextureCoordinates(
1427
1669
  return out;
1428
1670
  }
1429
1671
 
1672
+ function collectMaterialTextureSources(
1673
+ values: Readonly<Record<string, unknown>>,
1674
+ world: World,
1675
+ ): Map<string, RenderTargetTextureSource> {
1676
+ const out = new Map<string, RenderTargetTextureSource>();
1677
+ for (const [field, value] of Object.entries(values)) {
1678
+ const reference = materialTextureRef(value);
1679
+ if (typeof reference !== 'number') continue;
1680
+ const resolved = world.sharedRefs.resolve<
1681
+ 'RenderTargetTextureSource',
1682
+ RenderTargetTextureSource
1683
+ >(toShared<'RenderTargetTextureSource'>(reference));
1684
+ if (!resolved.ok || resolveRenderTargetMaterialSource(resolved.value) === undefined) continue;
1685
+ out.set(field, resolved.value);
1686
+ }
1687
+ return out;
1688
+ }
1689
+
1430
1690
  function collectMaterialTextureSamplers(
1431
1691
  values: Readonly<Record<string, unknown>>,
1432
1692
  resolveSampler: (value: unknown) => Handle<'SamplerAsset', 'shared'> | undefined,
@@ -1517,9 +1777,14 @@ function materialParametersToParamSchema(
1517
1777
  return parameters.flatMap((parameter): ParamSchemaEntry[] => {
1518
1778
  // Boolean material values are runtime-only and have no UBO representation.
1519
1779
  if (parameter.type === 'bool') return [];
1520
- if (parameter.type === 'texture') {
1780
+ if (parameter.type === 'texture' || parameter.type === 'texture_cube') {
1521
1781
  if (engineInjectedTextureFields?.has(parameter.name) === true) return [];
1522
- return [{ name: parameter.name, type: 'texture2d' }];
1782
+ return [
1783
+ {
1784
+ name: parameter.name,
1785
+ type: parameter.type === 'texture_cube' ? 'texture_cube' : 'texture2d',
1786
+ },
1787
+ ];
1523
1788
  }
1524
1789
  return [
1525
1790
  {
@@ -1779,6 +2044,7 @@ export function resolveMaterialSnapshot(
1779
2044
  const samplerHandles = collectMaterialTextureSamplers(pv, (value) =>
1780
2045
  resolveTexLike(materialTextureRef(value), 'SamplerAsset'),
1781
2046
  );
2047
+ const textureSources = collectMaterialTextureSources(pv, world);
1782
2048
  const emissiveTextureHandle = resolveTexLike(
1783
2049
  materialTextureRef(pv.emissiveTexture),
1784
2050
  'TextureAsset',
@@ -1814,6 +2080,7 @@ export function resolveMaterialSnapshot(
1814
2080
  ...(materialParamSchema.length > 0 && { materialParamSchema }),
1815
2081
  ...(textureCoordinates.size > 0 && { textureCoordinates }),
1816
2082
  ...(textureHandles.size > 0 && { textureHandles }),
2083
+ ...(textureSources.size > 0 && { textureSources }),
1817
2084
  ...(videoTextureFields.size > 0 && { videoTextureFields }),
1818
2085
  ...(samplerHandles.size > 0 && { samplerHandles }),
1819
2086
  ...(baseColorTextureHandle !== undefined && { baseColorTexture: baseColorTextureHandle }),
@@ -2117,6 +2384,9 @@ export interface DirectionalCsmConfig {
2117
2384
  readonly cascadeBlend: number;
2118
2385
  readonly mapSize: number;
2119
2386
  readonly shadowDistance: number;
2387
+ readonly shadowFilter: number;
2388
+ readonly shadowAngularRadius: number;
2389
+ readonly maxPenumbraTexels: number;
2120
2390
  }
2121
2391
 
2122
2392
  /**
@@ -2131,6 +2401,7 @@ export interface DirectionalCsmResult {
2131
2401
  readonly cascadeCount: number;
2132
2402
  readonly cascadeBlend: number;
2133
2403
  readonly shadowMapSize: number;
2404
+ readonly directionalShadowQuality: DirectionalShadowQuality;
2134
2405
  }
2135
2406
 
2136
2407
  /**
@@ -2163,9 +2434,6 @@ export function computeDirectionalCsm(
2163
2434
 
2164
2435
  // PSSM split planes: [camera near, shadowDistance], not the camera far.
2165
2436
  const splits = pssmSplit(sNear, sFar, cascadeCount, config.splitLambda);
2166
- const paddedSplits = new Float32Array(4);
2167
- for (let i = 0; i < splits.length; i++) paddedSplits[i] = splits[i] ?? 0;
2168
-
2169
2437
  // Light view matrix: camera at origin, looking along the light direction.
2170
2438
  const lightDirN = vec3.normalize(vec3.create(), direction);
2171
2439
  const lightTarget = vec3.create(lightDirN[0] ?? 0, lightDirN[1] ?? 0, lightDirN[2] ?? 0);
@@ -2196,6 +2464,7 @@ export function computeDirectionalCsm(
2196
2464
  mat4.multiply(cameraVP, camProj, camView);
2197
2465
 
2198
2466
  const resultLightViewProjs: Float32Array[] = [];
2467
+ const cascadeFits: DirectionalShadowCascadeFit[] = [];
2199
2468
 
2200
2469
  // bug-20260619 RC-2 (AC-05): toward-light Z reach. Extend the near (toward-
2201
2470
  // light) bound of EVERY cascade to the toward-light extreme of the WHOLE
@@ -2259,16 +2528,34 @@ export function computeDirectionalCsm(
2259
2528
  // lightViewProj = orthoProj * lightView — pure clip-space [-1,1]. Atlas
2260
2529
  // tile placement is handled by the per-cascade viewport + fragment-side UV
2261
2530
  // math (evalDirectional), so shadow_caster.gl_Position stays clip-space.
2262
- void cIdx;
2531
+ cascadeFits.push({
2532
+ split: splits[cIdx] ?? sFar,
2533
+ minX,
2534
+ maxX,
2535
+ minY,
2536
+ maxY,
2537
+ minZ,
2538
+ maxZ,
2539
+ });
2263
2540
  resultLightViewProjs.push(new Float32Array(mat4.multiply(mat4.create(), orthoProj, lightView)));
2264
2541
  }
2265
2542
 
2543
+ const projection = projectDirectionalShadow(
2544
+ cascadeFits,
2545
+ config.mapSize,
2546
+ config.shadowFilter ?? 2,
2547
+ config.shadowAngularRadius ?? 0.00465,
2548
+ config.maxPenumbraTexels ?? 32,
2549
+ );
2550
+ if (projection === undefined) return null;
2551
+
2266
2552
  return {
2267
2553
  lightViewProj: resultLightViewProjs,
2268
- splitPlanes: paddedSplits,
2554
+ splitPlanes: projection.splitPlanes,
2269
2555
  cascadeCount,
2270
2556
  cascadeBlend: config.cascadeBlend,
2271
2557
  shadowMapSize: config.mapSize,
2558
+ directionalShadowQuality: projection.directionalShadowQuality,
2272
2559
  };
2273
2560
  }
2274
2561
 
@@ -2296,8 +2583,177 @@ export interface PreparedExtractContext {
2296
2583
  readonly resourceOwnerFog: FogFrame | null | undefined;
2297
2584
  readonly cull: 'self' | 'none' | 'external';
2298
2585
  readonly cullCameras: readonly CameraSnapshot[] | undefined;
2299
- readonly hierarchy: SceneHierarchySnapshot;
2586
+ readonly renderables: 'full' | 'none';
2300
2587
  readonly visibility: VisibilitySnapshot;
2588
+ /** Producer-owned artifact lookup; extraction only, never record-time asset reads. */
2589
+ readonly getMaterialShaderArtifact:
2590
+ | ((materialShaderId: string) => MaterialShaderArtifact | undefined)
2591
+ | undefined;
2592
+ }
2593
+
2594
+ const ALL_VISIBLE: VisibilitySnapshot = {
2595
+ diagnostics: [],
2596
+ hasAnyIntent: false,
2597
+ hasAnyHiddenIntent: false,
2598
+ get: () => undefined,
2599
+ effective: () => 'visible',
2600
+ };
2601
+
2602
+ function collectCameraSnapshots(world: World): CameraSnapshot[] {
2603
+ const worldInternal = createWorldInternalView(world);
2604
+ const cameras: CameraSnapshot[] = [];
2605
+ const cameraQuery = world
2606
+ .query({ read: [Camera], optional: [MotionBlur], with: [Transform, GlobalTransform] })
2607
+ .unwrap();
2608
+ for (const row of cameraQuery) {
2609
+ const cam = row.get(Camera);
2610
+ const motionBlur = row.get(MotionBlur);
2611
+ const entity = row.entity;
2612
+ const view = worldInternal._getArrayView(entity, GlobalTransform, 'world');
2613
+ if (view === undefined) continue;
2614
+ const worldMat = new Float32Array(view);
2615
+ const target = resolveCameraTarget(world, cam.target);
2616
+ cameras.push({
2617
+ entityKey: entity as number,
2618
+ ...(target === undefined ? {} : { target }),
2619
+ historyVersion: cam.historyVersion,
2620
+ position: mat4.getTranslation(vec3.create(), worldMat),
2621
+ world: worldMat,
2622
+ fov: cam.fov,
2623
+ aspect: cam.aspect,
2624
+ near: cam.near,
2625
+ far: cam.far,
2626
+ projection: cameraProjectionFromF32(cam.projection),
2627
+ orthoLeft: cam.left,
2628
+ orthoRight: cam.right,
2629
+ orthoBottom: cam.bottom,
2630
+ orthoTop: cam.top,
2631
+ tonemap: tonemapFromF32(cam.tonemap),
2632
+ exposure: cam.exposure,
2633
+ whitePoint: cam.whitePoint,
2634
+ antialias: antialiasFromF32(cam.antialias),
2635
+ bloom: bloomEnabledFromF32(cam.bloom),
2636
+ bloomThreshold: cam.bloomThreshold,
2637
+ bloomIntensity: cam.bloomIntensity,
2638
+ bloomBlurRadius: cam.bloomBlurRadius,
2639
+ clearColor: [
2640
+ cam.clearColor[0] ?? 0,
2641
+ cam.clearColor[1] ?? 0,
2642
+ cam.clearColor[2] ?? 0,
2643
+ cam.clearColor[3] ?? 1,
2644
+ ],
2645
+ ...(motionBlur === undefined
2646
+ ? {}
2647
+ : {
2648
+ motionBlur: {
2649
+ shutterAngle: motionBlur.shutterAngle,
2650
+ maxRadiusPixels: motionBlur.maxRadiusPixels,
2651
+ sampleCount: motionBlur.sampleCount,
2652
+ },
2653
+ }),
2654
+ });
2655
+ }
2656
+ return cameras;
2657
+ }
2658
+
2659
+ function collectCubeCameraSnapshots(world: World): CubeCameraSnapshot[] {
2660
+ const worldInternal = createWorldInternalView(world);
2661
+ const snapshots: CubeCameraSnapshot[] = [];
2662
+ const query = world.query({ read: [CubeCamera], with: [Transform, GlobalTransform] }).unwrap();
2663
+ for (const row of query) {
2664
+ const camera = row.get(CubeCamera);
2665
+ const target = resolveCameraTarget(world, camera.target);
2666
+ if (target === undefined) continue;
2667
+ const view = worldInternal._getArrayView(row.entity, GlobalTransform, 'world');
2668
+ if (view === undefined) continue;
2669
+ snapshots.push({
2670
+ entityKey: row.entity as number,
2671
+ target,
2672
+ position: [view[12] ?? 0, view[13] ?? 0, view[14] ?? 0],
2673
+ near: camera.near,
2674
+ far: camera.far,
2675
+ updateIntent: cubeCameraUpdateIntentFromF32(camera.updateIntent),
2676
+ requestVersion: camera.requestVersion,
2677
+ faceBudget: camera.faceBudget,
2678
+ });
2679
+ }
2680
+ return snapshots;
2681
+ }
2682
+
2683
+ function collectReflectionProbeFacts(world: World): ReflectionProbeFact[] {
2684
+ const worldInternal = createWorldInternalView(world);
2685
+ const facts: ReflectionProbeFact[] = [];
2686
+ const query = world
2687
+ .query({ read: [ReflectionProbe], with: [Transform, GlobalTransform] })
2688
+ .unwrap();
2689
+ for (const row of query) {
2690
+ const probe = row.get(ReflectionProbe);
2691
+ const view = worldInternal._getArrayView(row.entity, GlobalTransform, 'world');
2692
+ if (view === undefined) {
2693
+ continue;
2694
+ }
2695
+ facts.push({
2696
+ worldId: 0,
2697
+ entityKey: row.entity as number,
2698
+ center: [view[12] ?? 0, view[13] ?? 0, view[14] ?? 0],
2699
+ halfExtents: [
2700
+ probe.halfExtents[0] ?? 0,
2701
+ probe.halfExtents[1] ?? 0,
2702
+ probe.halfExtents[2] ?? 0,
2703
+ ],
2704
+ priority: probe.priority,
2705
+ intensity: probe.intensity,
2706
+ resolution: probe.resolution,
2707
+ revision: probe.invalidationVersion,
2708
+ updateIntent: reflectionProbeUpdateIntentFromF32(probe.updateIntent),
2709
+ invalidationVersion: probe.invalidationVersion,
2710
+ });
2711
+ }
2712
+ return facts;
2713
+ }
2714
+
2715
+ interface CameraRoleSelection {
2716
+ readonly display: readonly CameraSnapshot[];
2717
+ readonly auxiliary: readonly CameraSnapshot[];
2718
+ }
2719
+
2720
+ function selectCameraRoles(world: World): CameraRoleSelection {
2721
+ const cameras = collectCameraSnapshots(world);
2722
+ const displayEntities = cameras
2723
+ .filter((camera) => camera.target === undefined)
2724
+ .map((camera) => camera.entityKey ?? 0);
2725
+ const activeCameraIndex = selectActiveCameraIndex(
2726
+ displayEntities,
2727
+ getActiveCamera(world)?.entity,
2728
+ );
2729
+ const displayEntityKey = displayEntities[activeCameraIndex] ?? displayEntities[0];
2730
+ const display =
2731
+ displayEntityKey === undefined
2732
+ ? []
2733
+ : cameras.filter((camera) => camera.entityKey === displayEntityKey);
2734
+ const candidates: CameraTargetCandidate[] = cameras.map((camera) => ({
2735
+ worldId: 0,
2736
+ entityKey: camera.entityKey ?? 0,
2737
+ ...(camera.target === undefined ? {} : { target: camera.target }),
2738
+ requestVersion: 0,
2739
+ update: 'continuous',
2740
+ }));
2741
+ const selected = selectCameraTargetViews(candidates, {
2742
+ ...(displayEntityKey === undefined ? {} : { displayEntityKey }),
2743
+ budget: 1,
2744
+ });
2745
+ const auxiliary = selected.auxiliary.flatMap((candidate) =>
2746
+ cameras.filter((camera) => camera.entityKey === candidate.entityKey),
2747
+ );
2748
+ return { display, auxiliary };
2749
+ }
2750
+
2751
+ export function extractCameraSnapshots(world: World): CameraSnapshot[] {
2752
+ return [...selectCameraRoles(world).display];
2753
+ }
2754
+
2755
+ export function extractCameraTargetSnapshots(world: World): readonly CameraSnapshot[] {
2756
+ return selectCameraRoles(world).auxiliary;
2301
2757
  }
2302
2758
 
2303
2759
  function tonemapParams(camera: CameraSnapshot): Uint8Array {
@@ -2319,9 +2775,13 @@ export function prepareExtractContext(
2319
2775
  readonly resourceOwnerFog?: FogFrame | null;
2320
2776
  readonly cull?: 'self' | 'none' | 'external';
2321
2777
  readonly cullCameras?: readonly CameraSnapshot[];
2778
+ readonly renderables?: 'full' | 'none';
2779
+ readonly getMaterialShaderArtifact?: (
2780
+ materialShaderId: string,
2781
+ ) => MaterialShaderArtifact | undefined;
2322
2782
  } = {},
2323
2783
  ): PreparedExtractContext {
2324
- const hierarchy = projectHierarchy(world);
2784
+ const renderables = options.renderables ?? 'full';
2325
2785
  return {
2326
2786
  assets: options.assets,
2327
2787
  pipelineState: options.pipelineState,
@@ -2329,12 +2789,28 @@ export function prepareExtractContext(
2329
2789
  resourceOwnerFog: options.resourceOwnerFog,
2330
2790
  cull: options.cull ?? 'self',
2331
2791
  cullCameras: options.cullCameras,
2332
- hierarchy,
2333
- visibility: resolveVisibility(world, hierarchy),
2792
+ renderables,
2793
+ visibility: renderables === 'none' ? ALL_VISIBLE : resolveVisibility(world),
2794
+ getMaterialShaderArtifact: options.getMaterialShaderArtifact,
2334
2795
  };
2335
2796
  }
2336
2797
 
2337
2798
  const fogQueries = new WeakMap<World, Query<readonly [typeof Fog]>>();
2799
+ const fogLkgStates = new WeakMap<
2800
+ World,
2801
+ { lkg: FogFrame | undefined; failure: FogSelectionFailure | undefined }
2802
+ >();
2803
+
2804
+ function fogState(world: World): {
2805
+ lkg: FogFrame | undefined;
2806
+ failure: FogSelectionFailure | undefined;
2807
+ } {
2808
+ const existing = fogLkgStates.get(world);
2809
+ if (existing !== undefined) return existing;
2810
+ const created = { lkg: undefined, failure: undefined };
2811
+ fogLkgStates.set(world, created);
2812
+ return created;
2813
+ }
2338
2814
 
2339
2815
  function selectFogFrame(world: World): FogFrame | undefined {
2340
2816
  // Query compilation is owned once per World; Query refreshes its matched
@@ -2362,10 +2838,22 @@ function selectFogFrame(world: World): FogFrame | undefined {
2362
2838
  suns: [],
2363
2839
  lane: 'direct',
2364
2840
  });
2365
- if (fogSelection.ok) return fogSelection.value.fog;
2841
+ const state = fogState(world);
2842
+ if (fogSelection.ok) {
2843
+ state.lkg = fogSelection.value.fog;
2844
+ state.failure = undefined;
2845
+ return fogSelection.value.fog;
2846
+ }
2847
+ state.failure = {
2848
+ code: fogSelection.error.code,
2849
+ expected: fogSelection.error.expected,
2850
+ hint: fogSelection.error.hint,
2851
+ detail: Object.freeze({ ...fogSelection.error.detail }),
2852
+ };
2366
2853
  routeWorldError(world, fogSelection.error, {
2367
2854
  systemName: 'RenderSystem.extract (fog-selection)',
2368
2855
  });
2856
+ if (state.lkg !== undefined) return state.lkg;
2369
2857
  throw fogSelection.error;
2370
2858
  }
2371
2859
 
@@ -2547,7 +3035,13 @@ export function extractFrames(
2547
3035
  assets?: AssetRegistry | null,
2548
3036
  pipelineState?: ExtractPipelineSurface | null,
2549
3037
  materialSnapshotCachesByWorld?: MaterialSnapshotCachesByWorld,
2550
- options: { readonly cull?: 'normal' | 'none' } = {},
3038
+ options: {
3039
+ readonly cull?: 'normal' | 'none';
3040
+ readonly renderables?: 'full' | 'none';
3041
+ readonly getMaterialShaderArtifact?: (
3042
+ materialShaderId: string,
3043
+ ) => MaterialShaderArtifact | undefined;
3044
+ } = {},
2551
3045
  ): ExtractedFrame {
2552
3046
  // w4: normalize the owner argument. A bare number is the legacy single-owner
2553
3047
  // form (cameraOwner === resourceOwner); an object carries the two split
@@ -2617,7 +3111,11 @@ export function extractFrames(
2617
3111
  })(),
2618
3112
  }),
2619
3113
  cull: options.cull === 'none' ? 'none' : isCameraOwner ? 'self' : 'external',
3114
+ ...(options.getMaterialShaderArtifact === undefined
3115
+ ? {}
3116
+ : { getMaterialShaderArtifact: options.getMaterialShaderArtifact }),
2620
3117
  ...(cameraOwnerFrame === undefined ? {} : { cullCameras: cameraOwnerFrame.cameras }),
3118
+ renderables: options.renderables ?? 'full',
2621
3119
  });
2622
3120
  const frame = extractFrame(world, prepared);
2623
3121
 
@@ -2688,6 +3186,7 @@ export function extractFrames(
2688
3186
  // frames); directionalCount sum.
2689
3187
  const point: PointLightSnapshot[] = [];
2690
3188
  const spot: SpotLightSnapshot[] = [];
3189
+ const rect: RectAreaDirectLightSnapshot[] = [];
2691
3190
  let directional: DirectionalLightSnapshot | undefined;
2692
3191
  let directionalCount = 0;
2693
3192
  let lightViewProj: readonly Float32Array[] | undefined;
@@ -2697,7 +3196,8 @@ export function extractFrames(
2697
3196
  let shadowMapSize: number | undefined;
2698
3197
  let depthBias: number | undefined;
2699
3198
  let normalBias: number | undefined;
2700
- let pcfKernelSize: number | undefined;
3199
+ let directionalShadowQuality: DirectionalShadowQuality | undefined;
3200
+ let directionalShadowError: LightValidationError | undefined;
2701
3201
  const pointShadow: PointShadowSnapshot[] = [];
2702
3202
  // bug-20260710-editor-cross-world-shadow: carry the raw CSM config +
2703
3203
  // direction of the first-hit directional so the merge layer can recompute
@@ -2708,6 +3208,7 @@ export function extractFrames(
2708
3208
  for (const f of succeededFrames) {
2709
3209
  for (const p of f.lights.point) point.push(p);
2710
3210
  for (const s of f.lights.spot) spot.push(s);
3211
+ for (const r of f.lights.rect) rect.push(r);
2711
3212
  for (const ps of f.lights.pointShadow) pointShadow.push(ps);
2712
3213
  if (directional === undefined && f.lights.directional !== undefined) {
2713
3214
  directional = f.lights.directional;
@@ -2719,7 +3220,8 @@ export function extractFrames(
2719
3220
  shadowMapSize = f.lights.shadowMapSize;
2720
3221
  depthBias = f.lights.depthBias;
2721
3222
  normalBias = f.lights.normalBias;
2722
- pcfKernelSize = f.lights.pcfKernelSize;
3223
+ directionalShadowQuality = f.lights.directionalShadowQuality;
3224
+ directionalShadowError = f.lights.directionalShadowError;
2723
3225
  directionalCsmConfig = f.lights.directionalCsmConfig;
2724
3226
  directionalCsmDirection = f.lights.directionalCsmDirection;
2725
3227
  }
@@ -2738,6 +3240,9 @@ export function extractFrames(
2738
3240
  if (succeededIndices[fi] === resourceOwner) resourceOwnerFrame = succeededFrames[fi];
2739
3241
  }
2740
3242
  const cameras = cameraOwnerFrame !== undefined ? [...cameraOwnerFrame.cameras] : [];
3243
+ const auxiliaryCameras =
3244
+ cameraOwnerFrame !== undefined ? [...cameraOwnerFrame.auxiliaryCameras] : [];
3245
+ const cubeCameras = cameraOwnerFrame !== undefined ? [...cameraOwnerFrame.cubeCameras] : [];
2741
3246
 
2742
3247
  // bug-20260710-editor-cross-world-shadow: RECOMPUTE directional CSM matrices
2743
3248
  // against the surfaced camera. In a single-world app the per-world extract
@@ -2776,6 +3281,7 @@ export function extractFrames(
2776
3281
  cascadeCount = csm.cascadeCount;
2777
3282
  cascadeBlend = csm.cascadeBlend;
2778
3283
  shadowMapSize = csm.shadowMapSize;
3284
+ directionalShadowQuality = csm.directionalShadowQuality;
2779
3285
  }
2780
3286
  // csm === null (no surfaced camera at all): keep the per-world carry —
2781
3287
  // there is no better data, and a cameraless frame renders nothing anyway.
@@ -2786,6 +3292,7 @@ export function extractFrames(
2786
3292
  directionalCount,
2787
3293
  point,
2788
3294
  spot,
3295
+ rect,
2789
3296
  lightViewProj,
2790
3297
  splitPlanes,
2791
3298
  cascadeCount,
@@ -2793,7 +3300,8 @@ export function extractFrames(
2793
3300
  shadowMapSize,
2794
3301
  depthBias,
2795
3302
  normalBias,
2796
- pcfKernelSize,
3303
+ directionalShadowQuality,
3304
+ directionalShadowError,
2797
3305
  pointShadow,
2798
3306
  directionalCsmConfig,
2799
3307
  directionalCsmDirection,
@@ -2804,7 +3312,9 @@ export function extractFrames(
2804
3312
  const skybox = resourceOwnerFrame?.skybox;
2805
3313
  const skyboxCount = resourceOwnerFrame?.skyboxCount ?? 0;
2806
3314
  const fog = resourceOwnerFrame?.fog;
3315
+ const fogFailure = resourceOwnerFrame?.fogFailure;
2807
3316
  const environment = resourceOwnerFrame?.environment;
3317
+ const lightProbes = succeededFrames.flatMap((frame) => frame.lightProbes ?? []);
2808
3318
  // User-authored PostProcessParams entities are singleton scene resources, so
2809
3319
  // they come from the resourceOwner world (holistic snapshot selection).
2810
3320
  const postProcessParams = new Map(resourceOwnerFrame?.postProcessParams);
@@ -2843,9 +3353,16 @@ export function extractFrames(
2843
3353
  const visibilityStats = {
2844
3354
  explicitlyHidden: succeededFrames.reduce((s, f) => s + f.visibilityStats.explicitlyHidden, 0),
2845
3355
  };
3356
+ const reflectionProbes = succeededFrames.flatMap((f, index) => {
3357
+ const worldId = succeededIndices[index] ?? 0;
3358
+ return (f.reflectionProbes ?? []).map((probe) => ({ ...probe, worldId }));
3359
+ });
2846
3360
 
2847
3361
  return {
2848
3362
  cameras,
3363
+ auxiliaryCameras,
3364
+ cubeCameras,
3365
+ reflectionProbes,
2849
3366
  lights,
2850
3367
  ...(volumetricFog === undefined
2851
3368
  ? {}
@@ -2854,9 +3371,11 @@ export function extractFrames(
2854
3371
  dispatch: dispatchEntries,
2855
3372
  skylight,
2856
3373
  skylightCount,
3374
+ lightProbes,
2857
3375
  skybox,
2858
3376
  skyboxCount,
2859
3377
  fog,
3378
+ ...(fogFailure === undefined ? {} : { fogFailure }),
2860
3379
  environment,
2861
3380
  environmentReady: resourceOwnerFrame !== undefined && environment !== undefined,
2862
3381
  frustumStats,
@@ -2947,18 +3466,20 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2947
3466
  materialSnapshotCache: persistentMaterialSnapshotCache,
2948
3467
  cull: cullMode,
2949
3468
  resourceOwnerFog,
3469
+ getMaterialShaderArtifact,
2950
3470
  } = context;
2951
3471
  // Environment reads are owned by the retained resource projection and are
2952
3472
  // established at the frame boundary; record never revisits World state.
2953
3473
  const fog =
2954
3474
  resourceOwnerFog === undefined ? selectFogFrame(world) : (resourceOwnerFog ?? undefined);
3475
+ const fogFailure = fogState(world).failure;
2955
3476
  const visibility = context.visibility.hasAnyHiddenIntent ? context.visibility : undefined;
2956
3477
  const skinPaletteAllocator = pipelineState?.skinPaletteAllocator ?? null;
2957
3478
 
2958
3479
  const directionalLightQuery = world.query({ read: [DirectionalLight] }).unwrap();
2959
3480
 
2960
3481
  // feat-20260601 D-3: camera / point / spot light world transforms are read
2961
- // through the single resolved `Transform.world` mat4 (written by
3482
+ // through the single resolved `GlobalTransform.world` mat4 (written by
2962
3483
  // propagateTransforms), not the retired GlobalTransform-column-switch.
2963
3484
  //
2964
3485
  // Each segment routes through a World-owned Query with explicit read and
@@ -2971,7 +3492,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2971
3492
  const worldInternal = createWorldInternalView(world);
2972
3493
  let volumetricFog = extractVolumeSnapshot(world, assets, worldInternal);
2973
3494
 
2974
- const cameras = extractCameraSnapshots(world);
3495
+ const cameraRoles = selectCameraRoles(world);
3496
+ const cameras = [...cameraRoles.display];
3497
+ const auxiliaryCameras = cameraRoles.auxiliary;
3498
+ const cubeCameras = collectCubeCameraSnapshots(world);
3499
+ const reflectionProbes = collectReflectionProbeFacts(world);
2975
3500
 
2976
3501
  // Three-query union (M2 / w16 / AC-03): directional has no Transform
2977
3502
  // dependency (sun-like infinite-source semantics); point + spot pull
@@ -2992,7 +3517,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
2992
3517
  depthBias: number;
2993
3518
  normalBias: number;
2994
3519
  shadowDistance: number;
2995
- pcfKernelSize: number;
3520
+ shadowFilter: number;
3521
+ shadowAngularRadius: number;
3522
+ maxPenumbraTexels: number;
2996
3523
  }
2997
3524
  | undefined;
2998
3525
  for (const row of directionalLightQuery) {
@@ -3022,11 +3549,29 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3022
3549
  depthBias: l.depthBias,
3023
3550
  normalBias: l.normalBias,
3024
3551
  shadowDistance: l.shadowDistance,
3025
- pcfKernelSize: l.pcfKernelSize,
3552
+ shadowFilter: l.shadowFilter,
3553
+ shadowAngularRadius: l.shadowAngularRadius,
3554
+ maxPenumbraTexels: l.maxPenumbraTexels,
3026
3555
  };
3027
3556
  }
3028
3557
  }
3029
3558
 
3559
+ let directionalShadowError: LightValidationError | undefined;
3560
+ if (directional !== undefined && firstHitShadowFields !== undefined) {
3561
+ const validation = validateDirectionalLightData({
3562
+ direction: directional.direction,
3563
+ castShadow: firstHitCastShadow,
3564
+ ...firstHitShadowFields,
3565
+ });
3566
+ if (!validation.ok) {
3567
+ directionalShadowError = validation.error;
3568
+ worldInternal._routeError(directionalShadowError, {
3569
+ severity: Severity.Error,
3570
+ systemName: 'RenderSystem.extract (directional-shadow)',
3571
+ });
3572
+ }
3573
+ }
3574
+
3030
3575
  const pointSnapshots: PointLightSnapshot[] = [];
3031
3576
  // feat-20260612-point-light-shadows-urp-hdrp M4 / T-M4-4: track entity per
3032
3577
  // pointSnapshots index so the post-extract pointShadow join can stamp
@@ -3035,22 +3580,22 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3035
3580
  const pointLightQuery = world
3036
3581
  .query({
3037
3582
  read: [PointLight],
3038
- optional: [Transform],
3583
+ optional: [Transform, GlobalTransform],
3039
3584
  })
3040
3585
  .unwrap();
3041
3586
  for (const row of pointLightQuery) {
3042
3587
  const p = row.get(PointLight);
3043
3588
  // K-2 scheme B: archetype-edge sniff -- `bundle.Transform` key is absent
3044
3589
  // when the archetype does not carry the Transform column.
3045
- const hasTransform = row.get(Transform) !== undefined;
3590
+ const hasTransform = row.get(GlobalTransform) !== undefined;
3046
3591
  const intensity = p.intensity;
3047
3592
  const range = p.range;
3048
3593
  const entityId = row.entity;
3049
- // Position = world-space translation extracted from Transform.world.
3594
+ // Position = world-space translation extracted from GlobalTransform.world.
3050
3595
  // A point light archetype without a Transform column sits at the origin.
3051
3596
  let worldMat: Float32Array | undefined;
3052
3597
  if (hasTransform) {
3053
- const view = worldInternal._getArrayView(entityId, Transform, 'world');
3598
+ const view = worldInternal._getArrayView(entityId, GlobalTransform, 'world');
3054
3599
  if (view !== undefined) worldMat = new Float32Array(view);
3055
3600
  }
3056
3601
  const position =
@@ -3074,16 +3619,19 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3074
3619
  const spotLightQuery = world
3075
3620
  .query({
3076
3621
  read: [SpotLight],
3077
- optional: [Transform],
3622
+ optional: [Transform, GlobalTransform],
3078
3623
  })
3079
3624
  .unwrap();
3080
3625
  // feat-20260625-spot-light-shadow-mapping M1 w5: tile allocation for castShadow spots.
3081
3626
  // Cap = 4 (OOS-5), sentinel -1 = unassigned (plan-strategy D-4).
3082
3627
  // Direction degeneration (near-zero) also skips shadow (requirements $112).
3083
3628
  let spotTileNext = 0;
3629
+ const iesSlices = new Map<number, number>();
3630
+ const cookieSlices = new Map<number, number>();
3631
+ const projectorSlices = new Map<number, number>();
3084
3632
  for (const row of spotLightQuery) {
3085
3633
  const s = row.get(SpotLight);
3086
- const hasTransform = row.get(Transform) !== undefined;
3634
+ const hasTransform = row.get(GlobalTransform) !== undefined;
3087
3635
  const intensity = s.intensity;
3088
3636
  const range = s.range;
3089
3637
  const innerConeDeg = s.innerConeDeg;
@@ -3091,7 +3639,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3091
3639
  let worldMat: Float32Array | undefined;
3092
3640
  if (hasTransform) {
3093
3641
  const entity = row.entity;
3094
- const view = worldInternal._getArrayView(entity, Transform, 'world');
3642
+ const view = worldInternal._getArrayView(entity, GlobalTransform, 'world');
3095
3643
  if (view !== undefined) worldMat = new Float32Array(view);
3096
3644
  }
3097
3645
  const position =
@@ -3129,13 +3677,93 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3129
3677
  const sMapSize = s.mapSize;
3130
3678
  const sNearPlane = s.nearPlane;
3131
3679
  const sFarPlane = s.farPlane;
3680
+ const iesProfileAsset =
3681
+ s.iesProfile === undefined
3682
+ ? undefined
3683
+ : resolveAssetHandle<IesProfileAsset>(world, s.iesProfile);
3684
+ const cookieAsset =
3685
+ s.cookie === undefined ? undefined : resolveAssetHandle<TextureAsset>(world, s.cookie);
3686
+ // Resolve the projector before assigning array slices so the
3687
+ // extended-lighting path can reuse its texture transport while retaining
3688
+ // the authored lightViewProj projection semantics.
3689
+ const hasProjector = Number(s.projector) > 0;
3690
+ let projectorHandle: Handle<'TextureAsset', 'shared'> | undefined;
3691
+ let projectorAsset: TextureAsset | undefined;
3692
+ let projectorGuid: string | undefined;
3693
+ if (hasProjector) {
3694
+ const candidate = toShared<'TextureAsset'>(Math.round(Number(s.projector)));
3695
+ const resolvedProjector = resolveAssetHandle<TextureAsset>(world, candidate);
3696
+ if (!resolvedProjector.ok || resolvedProjector.value.kind !== 'texture') {
3697
+ worldInternal._routeError(
3698
+ resolvedProjector.ok
3699
+ ? new RhiError({
3700
+ code: 'asset-not-registered',
3701
+ expected: 'SpotLight.projector resolves to a TextureAsset',
3702
+ hint: 'load the authored projector TextureAsset before assigning SpotLight.projector',
3703
+ detail: { assetHandle: Number(candidate) },
3704
+ })
3705
+ : resolvedProjector.error,
3706
+ { severity: Severity.Error, systemName: 'RenderSystem.extract (spot-projector)' },
3707
+ );
3708
+ } else {
3709
+ projectorHandle = candidate;
3710
+ projectorAsset = resolvedProjector.value;
3711
+ projectorGuid = assets?.guidOf(projectorAsset) ?? `handle:${Number(candidate)}`;
3712
+ }
3713
+ }
3714
+ const iesProfileData =
3715
+ iesProfileAsset?.ok && iesProfileAsset.value.kind === 'ies-profile'
3716
+ ? new Uint8Array(iesProfileAsset.value.data)
3717
+ : undefined;
3718
+ const cookieProjection =
3719
+ cookieAsset?.ok && cookieAsset.value.kind === 'texture'
3720
+ ? prepareCookieProjection(cookieAsset.value)
3721
+ : undefined;
3722
+ const cookieData = cookieProjection?.data;
3723
+ const projectorProjection =
3724
+ projectorAsset === undefined ? undefined : prepareCookieProjection(projectorAsset);
3725
+ const projectorData = projectorProjection?.data;
3726
+ const iesProfileHandle = s.iesProfile === undefined ? undefined : handleSlot(s.iesProfile);
3727
+ const cookieHandle = s.cookie === undefined ? undefined : handleSlot(s.cookie);
3728
+ let iesProfileSlice =
3729
+ iesProfileHandle === undefined ? undefined : iesSlices.get(iesProfileHandle);
3730
+ if (
3731
+ iesProfileSlice === undefined &&
3732
+ iesProfileHandle !== undefined &&
3733
+ iesProfileData !== undefined &&
3734
+ iesSlices.size < IES_SLICE_CAPACITY
3735
+ ) {
3736
+ iesProfileSlice = iesSlices.size;
3737
+ iesSlices.set(iesProfileHandle, iesProfileSlice);
3738
+ }
3739
+ let cookieSlice = cookieHandle === undefined ? undefined : cookieSlices.get(cookieHandle);
3740
+ if (
3741
+ cookieSlice === undefined &&
3742
+ cookieHandle !== undefined &&
3743
+ cookieData !== undefined &&
3744
+ cookieSlices.size < COOKIE_SLICE_CAPACITY
3745
+ ) {
3746
+ cookieSlice = cookieSlices.size;
3747
+ cookieSlices.set(cookieHandle, cookieSlice);
3748
+ }
3749
+ let projectorSlice =
3750
+ projectorHandle === undefined
3751
+ ? undefined
3752
+ : (projectorSlices.get(projectorHandle) ?? cookieSlices.get(projectorHandle));
3753
+ if (
3754
+ projectorSlice === undefined &&
3755
+ projectorHandle !== undefined &&
3756
+ projectorData !== undefined &&
3757
+ cookieSlices.size + projectorSlices.size < COOKIE_SLICE_CAPACITY
3758
+ ) {
3759
+ projectorSlice = cookieSlices.size + projectorSlices.size;
3760
+ projectorSlices.set(projectorHandle, projectorSlice);
3761
+ }
3132
3762
  const shadowIntensity = s.shadowIntensity;
3133
3763
  // `shared<T>` columns are u32-backed and zero-filled when the optional
3134
3764
  // field is absent. Treat only a positive handle as an authored projector;
3135
3765
  // checking nullish values alone would turn the zero sentinel into a
3136
3766
  // phantom projector matrix and an avoidable asset-not-found diagnostic.
3137
- const hasProjector = Number(s.projector) > 0;
3138
-
3139
3767
  let lightViewProj: Float32Array | undefined;
3140
3768
  let shadowAtlasTile = -1;
3141
3769
 
@@ -3167,31 +3795,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3167
3795
  }
3168
3796
  }
3169
3797
 
3170
- let projectorHandle: Handle<'TextureAsset', 'shared'> | undefined;
3171
- let projectorAsset: TextureAsset | undefined;
3172
- let projectorGuid: string | undefined;
3173
- if (hasProjector) {
3174
- const candidate = toShared<'TextureAsset'>(Math.round(Number(s.projector)));
3175
- const resolvedProjector = resolveAssetHandle<TextureAsset>(world, candidate);
3176
- if (!resolvedProjector.ok || resolvedProjector.value.kind !== 'texture') {
3177
- worldInternal._routeError(
3178
- resolvedProjector.ok
3179
- ? new RhiError({
3180
- code: 'asset-not-registered',
3181
- expected: 'SpotLight.projector resolves to a TextureAsset',
3182
- hint: 'load the authored projector TextureAsset before assigning SpotLight.projector',
3183
- detail: { assetHandle: Number(candidate) },
3184
- })
3185
- : resolvedProjector.error,
3186
- { severity: Severity.Error, systemName: 'RenderSystem.extract (spot-projector)' },
3187
- );
3188
- } else {
3189
- projectorHandle = candidate;
3190
- projectorAsset = resolvedProjector.value;
3191
- projectorGuid = assets?.guidOf(projectorAsset) ?? `handle:${Number(candidate)}`;
3192
- }
3193
- }
3194
-
3195
3798
  spotSnapshots.push({
3196
3799
  kind: 'spot',
3197
3800
  entity: row.entity,
@@ -3208,6 +3811,25 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3208
3811
  invRangeSquared: computeInvRangeSquared(range),
3209
3812
  cosInner: degToCos(innerConeDeg),
3210
3813
  cosOuter: degToCos(outerConeDeg),
3814
+ ...(s.iesProfile === undefined
3815
+ ? {}
3816
+ : {
3817
+ iesProfileHandle: handleSlot(s.iesProfile),
3818
+ ...(iesProfileSlice === undefined ? {} : { iesProfileSlice }),
3819
+ }),
3820
+ ...(s.cookie === undefined
3821
+ ? {}
3822
+ : {
3823
+ cookieHandle: handleSlot(s.cookie),
3824
+ ...(cookieSlice === undefined ? {} : { cookieSlice }),
3825
+ }),
3826
+ ...(iesProfileData === undefined ? {} : { iesProfileData }),
3827
+ ...(cookieData === undefined ? {} : { cookieData }),
3828
+ ...(cookieProjection === undefined ? {} : { cookieMatrix: cookieProjection.matrix }),
3829
+ ...(projectorSlice === undefined ? {} : { projectorSlice }),
3830
+ ...(projectorData === undefined ? {} : { projectorData }),
3831
+ ...(projectorProjection === undefined ? {} : { projectorMatrix: projectorProjection.matrix }),
3832
+ rollDeg: s.rollDeg,
3211
3833
  // ── shadow fields ──
3212
3834
  castShadow,
3213
3835
  lightViewProj,
@@ -3233,6 +3855,59 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3233
3855
  });
3234
3856
  }
3235
3857
 
3858
+ const rectSnapshots: RectAreaDirectLightSnapshot[] = [];
3859
+ const rectLightQuery = world
3860
+ .query({
3861
+ read: [RectAreaLight],
3862
+ optional: [Transform, GlobalTransform],
3863
+ })
3864
+ .unwrap();
3865
+ for (const row of rectLightQuery) {
3866
+ const light = row.get(RectAreaLight);
3867
+ const validation = validateRectAreaLightData({
3868
+ intensity: light.intensity,
3869
+ color: light.color,
3870
+ width: light.width,
3871
+ height: light.height,
3872
+ range: light.range,
3873
+ });
3874
+ if (!validation.ok) {
3875
+ worldInternal._routeError(validation.error, {
3876
+ severity: Severity.Error,
3877
+ systemName: 'RenderSystem.extract (rect-area-light)',
3878
+ });
3879
+ continue;
3880
+ }
3881
+ const worldView = row.get(GlobalTransform);
3882
+ const worldMat =
3883
+ worldView === undefined
3884
+ ? undefined
3885
+ : new Float32Array(worldInternal._getArrayView(row.entity, GlobalTransform, 'world') ?? []);
3886
+ const position =
3887
+ worldMat !== undefined ? mat4.getTranslation(vec3.create(), worldMat) : vec3.create(0, 0, 0);
3888
+ const frame = buildRectAreaWorldFrame({
3889
+ center: position,
3890
+ axisX: [worldMat?.[0] ?? 1, worldMat?.[1] ?? 0, worldMat?.[2] ?? 0],
3891
+ axisY: [worldMat?.[4] ?? 0, worldMat?.[5] ?? 1, worldMat?.[6] ?? 0],
3892
+ width: light.width,
3893
+ height: light.height,
3894
+ });
3895
+ rectSnapshots.push({
3896
+ kind: 'rect-area',
3897
+ position,
3898
+ color: vec3.create(
3899
+ (light.color[0] ?? 1) * light.intensity,
3900
+ (light.color[1] ?? 1) * light.intensity,
3901
+ (light.color[2] ?? 1) * light.intensity,
3902
+ ),
3903
+ intensity: light.intensity,
3904
+ invRangeSquared: computeInvRangeSquared(light.range),
3905
+ halfWidth: frame.halfWidth,
3906
+ halfHeight: frame.halfHeight,
3907
+ axisX: frame.axisX,
3908
+ axisY: frame.axisY,
3909
+ });
3910
+ }
3236
3911
  // The SpotLight is the sole projector owner. Once all light snapshots have
3237
3912
  // been extracted, publish its accepted TextureAsset tuple on the volume
3238
3913
  // snapshot so surface and volume consume the same identity and revision.
@@ -3282,6 +3957,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3282
3957
  let cascadeCount: number | undefined;
3283
3958
  let cascadeBlend: number | undefined;
3284
3959
  let shadowMapSize: number | undefined;
3960
+ let directionalShadowQuality: DirectionalShadowQuality | undefined;
3285
3961
  let directionalCsmConfig: DirectionalCsmConfig | undefined;
3286
3962
  let directionalCsmDirection: Vec3 | undefined;
3287
3963
 
@@ -3307,7 +3983,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3307
3983
  // feat-20260621 M2: CSM computation gated on castShadow from the
3308
3984
  // merged DirectionalLight. castShadow defaults to true (first-hit-wins
3309
3985
  // semantics, D-6 no cardinality cap).
3310
- if (directional !== undefined && firstHitCastShadow !== false) {
3986
+ if (
3987
+ directional !== undefined &&
3988
+ firstHitCastShadow !== false &&
3989
+ directionalShadowError === undefined
3990
+ ) {
3311
3991
  const dirSnapshot = directional;
3312
3992
  const sf = firstHitShadowFields;
3313
3993
  if (sf !== undefined) {
@@ -3317,6 +3997,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3317
3997
  cascadeBlend: sf.cascadeBlend,
3318
3998
  mapSize: sf.mapSize,
3319
3999
  shadowDistance: sf.shadowDistance,
4000
+ shadowFilter: sf.shadowFilter,
4001
+ shadowAngularRadius: sf.shadowAngularRadius,
4002
+ maxPenumbraTexels: sf.maxPenumbraTexels,
3320
4003
  };
3321
4004
  directionalCsmDirection = dirSnapshot.direction;
3322
4005
  const csm = computeDirectionalCsm(dirSnapshot.direction, directionalCsmConfig, cameraData);
@@ -3331,13 +4014,19 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3331
4014
  if (csm !== null) {
3332
4015
  lightViewProj = csm.lightViewProj;
3333
4016
  splitPlanes = csm.splitPlanes;
4017
+ directionalShadowQuality = csm.directionalShadowQuality;
3334
4018
  } else {
3335
4019
  const sNear = cameraData?.near ?? 0.1;
3336
4020
  const splits = pssmSplit(sNear, sf.shadowDistance, cascadeCount, sf.splitLambda);
3337
- const padded = new Float32Array(4);
3338
- for (let i = 0; i < splits.length; i++) padded[i] = splits[i] ?? 0;
4021
+ const padded = new Float32Array(16);
4022
+ for (let i = 0; i < splits.length; i++) padded[i * 4] = splits[i] ?? 0;
3339
4023
  splitPlanes = padded;
3340
4024
  }
4025
+ directionalShadowQuality = directionalShadowQualityFromF32(
4026
+ sf.shadowFilter,
4027
+ sf.shadowAngularRadius,
4028
+ sf.maxPenumbraTexels,
4029
+ );
3341
4030
  }
3342
4031
  }
3343
4032
 
@@ -3346,7 +4035,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3346
4035
  // the View UBO tail keeps a stable layout regardless of the runtime
3347
4036
  // cascadeCount. Host-side correctness invariant: only the first
3348
4037
  // cascadeCount slots are ever read by the WGSL kernel.
3349
- const paddedSplitPlanes = new Float32Array(4);
4038
+ const paddedSplitPlanes = new Float32Array(16);
3350
4039
  if (splitPlanes !== undefined) {
3351
4040
  for (let i = 0; i < splitPlanes.length; i++) {
3352
4041
  paddedSplitPlanes[i] = splitPlanes[i] ?? 0;
@@ -3356,18 +4045,17 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3356
4045
  // feat-20260612-point-light-shadows-urp-hdrp M1 / T-M1-7 (plan-strategy §D-3,
3357
4046
  // requirements §5.3): query (PointLight + PointLightShadow + Transform)
3358
4047
  // archetype join. For each shadow caster, build 6 face VP matrices and pack
3359
- // into Float32Array(96). Atlas layer assigned in spawn order (0..3); the
3360
- // sentinel -1 is shader-side and applies to non-shadow PointLights only.
3361
- // Cap of 4 is enforced by ECS cardinality on PointLightShadow.
4048
+ // into Float32Array(96). Atlas layer is assigned in spawn order while the
4049
+ // renderer-owned atlas has capacity; the sentinel -1 is shader-side and
4050
+ // applies to non-admitted PointLightShadow requests as well.
3362
4051
  const pointShadowSnapshots: PointShadowSnapshot[] = [];
3363
4052
  {
3364
4053
  const pointShadowQuery = world
3365
- .query({ read: [Transform, PointLightShadow], with: [PointLight] })
4054
+ .query({ read: [Transform, GlobalTransform, PointLightShadow], with: [PointLight] })
3366
4055
  .unwrap();
3367
4056
  for (const row of pointShadowQuery) {
3368
- const t = row.get(Transform);
3369
4057
  const ps = row.get(PointLightShadow);
3370
- // Read world-space position from Transform.world (mat4 column-major;
4058
+ // Read world-space position from GlobalTransform.world (mat4 column-major;
3371
4059
  // translation lives at indices 12..14, mirroring CameraSnapshot.world
3372
4060
  // semantics in this file).
3373
4061
  // feat-20260614 M4 / w13: TypedArrayFor for `array<f32, 16>` now
@@ -3377,7 +4065,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3377
4065
  // `t.world?.[i]` form silently returned a single element under the
3378
4066
  // `never`-typed bundle path and `wRow[12]` widened to `undefined ?? 0`
3379
4067
  // so light positions clamped to the origin.
3380
- const wRow = t.world;
4068
+ const wRow = row.get(GlobalTransform).world;
3381
4069
  if (wRow === undefined) continue;
3382
4070
  const px = wRow[12] ?? 0;
3383
4071
  const py = wRow[13] ?? 0;
@@ -3386,7 +4074,13 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3386
4074
  const mapSize = ps.mapSize;
3387
4075
  const nearPlane = ps.nearPlane;
3388
4076
  const farPlane = ps.farPlane;
3389
- const layer = pointShadowSnapshots.length; // 0, 1, 2, 3 in spawn order
4077
+ // The renderer, not ECS, owns the atlas budget. Preserve every request
4078
+ // for inspection, but emit the shader sentinel for entries that cannot
4079
+ // acquire a layer so record never calls faceView outside the atlas.
4080
+ const layer =
4081
+ pointShadowSnapshots.length < SHADOW_ATLAS_DEFAULT_LAYERS
4082
+ ? pointShadowSnapshots.length
4083
+ : -1;
3390
4084
 
3391
4085
  const matrices = buildPointShadowMatrices(lightPos, nearPlane, farPlane);
3392
4086
  const packed = new Float32Array(96);
@@ -3412,7 +4106,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3412
4106
  // feat-20260612-point-light-shadows-urp-hdrp M4 / T-M4-4 (plan-strategy §D-8):
3413
4107
  // join pointShadow snapshots into the matching PointLightSnapshot so the
3414
4108
  // record stage threads `shadowAtlasLayer + shadowNear + shadowFar` through
3415
- // `packLightSlot` for the HDRP std430 LightSlot pad lanes (byte 52..64).
4109
+ // the unified direct-light metadata payload.
3416
4110
  // Mutates the freshly-built PointLightSnapshot in place; the snapshot is
3417
4111
  // not exposed elsewhere this frame yet (consumed only by lights.point[]).
3418
4112
  if (pointShadowSnapshots.length > 0) {
@@ -3437,6 +4131,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3437
4131
  directionalCount,
3438
4132
  point: pointSnapshots,
3439
4133
  spot: spotSnapshots,
4134
+ rect: rectSnapshots,
3440
4135
  lightViewProj,
3441
4136
  splitPlanes: splitPlanes !== undefined ? paddedSplitPlanes : undefined,
3442
4137
  cascadeCount,
@@ -3444,7 +4139,8 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3444
4139
  shadowMapSize,
3445
4140
  depthBias: firstHitCastShadow !== false ? firstHitShadowFields?.depthBias : undefined,
3446
4141
  normalBias: firstHitCastShadow !== false ? firstHitShadowFields?.normalBias : undefined,
3447
- pcfKernelSize: firstHitCastShadow !== false ? firstHitShadowFields?.pcfKernelSize : undefined,
4142
+ directionalShadowQuality,
4143
+ directionalShadowError,
3448
4144
  pointShadow: pointShadowSnapshots,
3449
4145
  // bug-20260710-editor-cross-world-shadow: raw CSM config + light direction
3450
4146
  // so the merge layer can recompute matrices against the surfaced camera.
@@ -3489,6 +4185,31 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3489
4185
  }
3490
4186
  }
3491
4187
 
4188
+ const lightProbeQuery = world.query({ read: [LightProbe, GlobalTransform] }).unwrap();
4189
+ const lightProbes: LightProbeSnapshot[] = [];
4190
+ for (const row of lightProbeQuery) {
4191
+ const probe = row.get(LightProbe);
4192
+ const validation = validateLightProbeData({
4193
+ irradiance: probe.irradiance,
4194
+ radius: probe.radius,
4195
+ });
4196
+ if (!validation.ok) {
4197
+ worldInternal._routeError(validation.error, {
4198
+ severity: Severity.Error,
4199
+ systemName: 'RenderSystem.extract (light-probe)',
4200
+ });
4201
+ continue;
4202
+ }
4203
+ const transform = row.get(GlobalTransform);
4204
+ lightProbes.push({
4205
+ identity: String(row.entity),
4206
+ position: [transform.world[12] ?? 0, transform.world[13] ?? 0, transform.world[14] ?? 0],
4207
+ radius: probe.radius,
4208
+ irradiance: new Float32Array(probe.irradiance),
4209
+ admitted: true,
4210
+ });
4211
+ }
4212
+
3492
4213
  // feat-20260531-skybox-env-background M2 / w5: query SkyboxBackground entities.
3493
4214
  // First archetype hit wins (mirrors Skylight pattern); multi-entity
3494
4215
  // once-warn in record stage uses skyboxCount.
@@ -3683,24 +4404,28 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3683
4404
  // column id for the sprite-bucket per-entity region override read
3684
4405
  void SpriteRegionOverride;
3685
4406
 
3686
- const meshRendererQuery = world
3687
- .query({
3688
- read: [MeshRenderer],
3689
- optional: [
3690
- Transform,
3691
- MeshFilter,
3692
- Instances,
3693
- Skin,
3694
- Layer,
3695
- MorphWeights,
3696
- SpriteRegionOverride,
3697
- SpriteInstances,
3698
- Points,
3699
- Lines,
3700
- SortKey,
3701
- ],
3702
- })
3703
- .unwrap();
4407
+ const meshRendererQuery =
4408
+ context.renderables === 'none'
4409
+ ? []
4410
+ : world
4411
+ .query({
4412
+ read: [MeshRenderer],
4413
+ optional: [
4414
+ Transform,
4415
+ GlobalTransform,
4416
+ MeshFilter,
4417
+ Instances,
4418
+ Skin,
4419
+ Layer,
4420
+ MorphWeights,
4421
+ SpriteRegionOverride,
4422
+ SpriteInstances,
4423
+ Points,
4424
+ Lines,
4425
+ SortKey,
4426
+ ],
4427
+ })
4428
+ .unwrap();
3704
4429
  // QueryRow deliberately exposes component data, not an archetype version.
3705
4430
  // The optimized ECS owner keeps table storage private, so instance-buffer
3706
4431
  // invalidation uses a content fingerprint of the managed arrays instead.
@@ -3757,12 +4482,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3757
4482
  points !== undefined ? 'Points' : lines !== undefined ? 'Lines' : undefined;
3758
4483
  const isRenderable = hasTransform && hasMeshFilter;
3759
4484
 
3760
- // feat-20260601 D-3: the resolved world transform is read per-entity from
3761
- // the single `Transform.world` mat4 (propagateTransforms output) inside the
3762
- // row loop below. The retired GlobalTransform-column-switch + the
3763
- // ChildOf-without-GlobalTransform misconfig signal are gone: the world
3764
- // column always exists on a Transform-bearing entity, so the
3765
- // "ChildOf but forgot GlobalTransform" misconfiguration cannot occur.
4485
+ // The resolved world transform is read per-entity from the single
4486
+ // `GlobalTransform.world` mat4 (propagateTransforms output) inside the row
4487
+ // loop below. Normal structural paths materialize this carrier through
4488
+ // Transform's generic ECS requirement; keep the defensive row check because
4489
+ // an owner can explicitly remove a required component during recovery.
3766
4490
  const fAssetHandle = meshFilter?.assetHandle;
3767
4491
  // feat-20260520-2d-sprite-layer-mvp M-3 / w22: Layer column read here;
3768
4492
  // value folded into each DispatchEntry so the render-system sort can use
@@ -3854,6 +4578,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3854
4578
  let materialBindingDiagnostics: MeshMaterialBindingDiagnostic[] = [];
3855
4579
  let gpuDrivenSubmeshes: readonly MeshAsset['submeshes'][number][] = [];
3856
4580
  let gpuDrivenIndexed = false;
4581
+ let gpuDrivenGeometry: GpuDrivenGeometryReceipt | undefined;
3857
4582
 
3858
4583
  // Resolve instance overrides against mesh-owned slot defaults once.
3859
4584
  const fAssetHandleVal = fAssetHandle;
@@ -3867,8 +4592,20 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3867
4592
  const meshRes = resolveAssetHandle<Asset>(world, meshHandle);
3868
4593
  if (meshRes.ok && meshRes.value.kind === 'mesh') {
3869
4594
  const meshAsset = meshRes.value as MeshAsset;
4595
+ const guid = (meshRes.value as { guid?: string }).guid ?? '<no-guid>';
3870
4596
  gpuDrivenSubmeshes = meshAsset.submeshes;
3871
4597
  gpuDrivenIndexed = meshAsset.indices !== undefined;
4598
+ const layoutProjection = deriveVertexLayoutProjection(meshAsset.attributes);
4599
+ gpuDrivenGeometry = {
4600
+ identity: `${guid}:${layoutProjection.digest}`,
4601
+ vertexInputs: layoutProjection.attributes.map((attribute) => ({
4602
+ semantic: attribute.key,
4603
+ location: attribute.shaderLocation,
4604
+ format: attribute.format,
4605
+ })),
4606
+ topology: meshAsset.submeshes[0]?.topology ?? 'triangle-list',
4607
+ indexed: gpuDrivenIndexed,
4608
+ };
3872
4609
  // extractFrames is also a public read path and can run before the
3873
4610
  // Renderer.draw pre-render stage. GlyphText proves the derived mesh
3874
4611
  // owns exactly one Default slot; arbitrary meshes still fail closed
@@ -3876,7 +4613,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3876
4613
  if (!Array.isArray(meshAsset.materialSlots) && world.get(entity, GlyphText).ok) {
3877
4614
  ensureGlyphMeshMaterialSlots(world, meshHandle);
3878
4615
  }
3879
- const guid = (meshRes.value as { guid?: string }).guid ?? '<no-guid>';
3880
4616
  const resolvedBindings = resolveMeshMaterialBindings(meshAsset, materialsView ?? [], {
3881
4617
  isValidOverride(handle) {
3882
4618
  const resolved = resolveAssetHandle(world, toShared<'MaterialAsset'>(handle));
@@ -4336,6 +5072,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4336
5072
  pv.occlusionTexture,
4337
5073
  );
4338
5074
  const textureCoordinates = collectMaterialTextureCoordinates(pv);
5075
+ const textureSources = collectMaterialTextureSources(pv, world);
4339
5076
  const emissivePv = pv.emissive as readonly number[] | undefined;
4340
5077
  // feat-20260625 M2 / w6: first-pass transparency flag folds into
4341
5078
  // MaterialSnapshot.transparent so the record stage can drive the
@@ -4435,6 +5172,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4435
5172
  ...(textureCoordinates.size > 0 && { textureCoordinates }),
4436
5173
  ...(samplerHandles.size > 0 && { samplerHandles }),
4437
5174
  ...(textureHandles.size > 0 && { textureHandles }),
5175
+ ...(textureSources.size > 0 && { textureSources }),
4438
5176
  ...(videoTextureFields.size > 0 && { videoTextureFields }),
4439
5177
  ...(baseColorTextureHandle !== undefined && {
4440
5178
  baseColorTexture: baseColorTextureHandle,
@@ -4600,7 +5338,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4600
5338
  );
4601
5339
  continue;
4602
5340
  }
4603
- // (c) Resolve only each joint's Transform.world column. The
5341
+ // (c) Resolve only each joint's GlobalTransform.world column. The
4604
5342
  // transient view is consumed before a structural mutation and
4605
5343
  // therefore preserves the same dangling-joint behavior without
4606
5344
  // constructing every other Transform field.
@@ -4611,7 +5349,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4611
5349
  for (let jIdx = 0; jIdx < skeleton.jointCount; jIdx++) {
4612
5350
  const jointEntityRaw = skinJoints[jIdx] ?? 0;
4613
5351
  const jointEntity = jointEntityRaw as EntityHandle;
4614
- const jointWorld = worldInternal._getArrayView(jointEntity, Transform, 'world');
5352
+ const jointWorld = worldInternal._getArrayView(jointEntity, GlobalTransform, 'world');
4615
5353
  if (jointWorld === undefined) {
4616
5354
  jointDangling = jIdx;
4617
5355
  break;
@@ -4653,14 +5391,23 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4653
5391
  }
4654
5392
  }
4655
5393
 
5394
+ // A pbr-skin PSO requires the complete three-binding group(2) skin
5395
+ // projection. Do not publish a renderable or its staged dispatch when
5396
+ // palette extraction was unavailable; the record stage must never
5397
+ // reinterpret a skinned vertex stream through an ordinary mesh group.
5398
+ if (materialSnap.materialShaderId === 'forgeax::pbr-skin' && skinSlice === undefined) {
5399
+ pendingDispatch.length = 0;
5400
+ continue;
5401
+ }
5402
+
4656
5403
  // feat-20260601 D-3: read the resolved world mat4 (propagateTransforms
4657
- // output) straight from the Transform.world column array view. The
5404
+ // output) straight from the GlobalTransform.world column array view. The
4658
5405
  // record stage copies these 16 floats into the mesh SSBO with zero
4659
5406
  // per-snapshot `mat4.compose` (AC-07). A stale slot (generation gone)
4660
5407
  // skips the renderable, mirroring the Instances dangling-row sweep.
4661
5408
  // tweak-20260611 M1 / K-3: `_getArrayView` call survives untouched;
4662
5409
  // only the `entity` source switched to `bundle.Entity.self[i]`.
4663
- const worldView = worldInternal._getArrayView(entity, Transform, 'world');
5410
+ const worldView = worldInternal._getArrayView(entity, GlobalTransform, 'world');
4664
5411
  if (worldView === undefined) continue;
4665
5412
  const worldMat = new Float32Array(worldView);
4666
5413
  const transformSnap: TransformSnapshot = { world: worldMat };
@@ -4791,12 +5538,16 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4791
5538
  }
4792
5539
 
4793
5540
  let localAabb: Float32Array | undefined;
5541
+ let lods: readonly MeshLodLevel[] | undefined;
5542
+ let lodHysteresis: number | undefined;
4794
5543
  let morph: MorphSnapshot | undefined;
4795
5544
  const assetHandleRaw = Math.round(fAssetHandle ?? 0);
4796
5545
  if (assetHandleRaw !== 0) {
4797
5546
  const meshRes = resolveAssetHandle(world, toShared<'MeshAsset'>(assetHandleRaw));
4798
5547
  if (meshRes.ok && meshRes.value.kind === 'mesh') {
4799
5548
  const meshAsset = meshRes.value as MeshAsset;
5549
+ lods = meshAsset.lods;
5550
+ lodHysteresis = meshAsset.lodHysteresis;
4800
5551
  morph = morphSnapshotFor(meshAsset, morphWeightsView);
4801
5552
  const meshAabb = meshAsset.aabb;
4802
5553
  // Morph targets can expand the authored bounds. Keep the
@@ -4841,41 +5592,95 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4841
5592
  projection: identityProjection(),
4842
5593
  } satisfies PointsLinesRetainedSnapshot);
4843
5594
 
4844
- let nonIndexedFirst = 0;
4845
- const gpuDrivenDraws = gpuDrivenSubmeshes.flatMap((submesh) => {
4846
- const drawMaterial = materialsArr[submesh.materialSlot] ?? materialSnap;
4847
- const first = gpuDrivenIndexed ? submesh.indexOffset : nonIndexedFirst;
4848
- nonIndexedFirst += submesh.vertexCount;
4849
- if (drawMaterial.transparent === true) return [];
4850
- return [
4851
- {
4852
- kind: gpuDrivenIndexed ? ('indexed' as const) : ('non-indexed' as const),
4853
- first,
4854
- count: gpuDrivenIndexed ? submesh.indexCount : submesh.vertexCount,
4855
- baseVertex: 0,
4856
- materialSlot: submesh.materialSlot,
4857
- topology: submesh.topology,
4858
- pipelineClass: `${drawMaterial.materialShaderId ?? 'forgeax::default-unlit'}|${submesh.topology}|${JSON.stringify(drawMaterial.renderState ?? null)}`,
4859
- materialResourceClass: gpuDrivenMaterialResourceClass(drawMaterial),
4860
- },
4861
- ];
4862
- });
5595
+ // Instances are snapshotted before the CPU visibility decision so the
5596
+ // culling owner can derive the union of every instance's transformed
5597
+ // mesh bounds. The same detached snapshot is then carried to record;
5598
+ // do not re-read the ECS array after culling (a structural write must
5599
+ // not make the cull and draw disagree within one frame).
5600
+ let instancesSnap: InstancesSnapshot | undefined;
5601
+ if (hasInstances) {
5602
+ const transforms = worldInternal._getArrayView(entity, Instances, 'transforms');
5603
+ if (transforms === undefined) {
5604
+ // A present component with an unavailable array is an empty
5605
+ // instance contribution, not an absent component. Preserve the
5606
+ // zero-draw contract so the record stage cannot synthesize one
5607
+ // identity instance from an incomplete row.
5608
+ instancesSnap = {
5609
+ transforms: new Float32Array(),
5610
+ instanceCount: 0,
5611
+ cacheKey: entity,
5612
+ archVersion,
5613
+ };
5614
+ } else {
5615
+ const actualLength = transforms.length;
5616
+ if (actualLength % 16 !== 0) {
5617
+ worldInternal._routeError(new InstanceTransformsStrideMismatchError(actualLength), {
5618
+ severity: Severity.Error,
5619
+ systemName: 'RenderSystem.extract (Instances stride)',
5620
+ });
5621
+ continue;
5622
+ }
5623
+ const snapshotCopy = new Float32Array(transforms);
5624
+ instancesSnap = {
5625
+ transforms: snapshotCopy,
5626
+ instanceCount: actualLength / 16,
5627
+ cacheKey: entity,
5628
+ archVersion,
5629
+ };
5630
+ }
5631
+ }
5632
+
4863
5633
  const baseRenderable: RenderableSnapshot = {
4864
5634
  assetHandle: Math.round(fAssetHandle ?? 0),
4865
5635
  transform: transformSnap,
5636
+ ...(lods === undefined ? {} : { lods }),
5637
+ ...(lodHysteresis === undefined ? {} : { lodHysteresis }),
4866
5638
  ...(localAabb !== undefined ? { localAabb: cullingLocalAabb } : {}),
4867
5639
  material: materialSnap,
4868
5640
  materials: materialsArr,
4869
5641
  materialBindingSources,
4870
5642
  materialBindingDiagnostics,
4871
- ...(morph === undefined && gpuDrivenDraws.length > 0 ? { gpuDrivenDraws } : {}),
4872
5643
  worldId: 0,
4873
5644
  entityKey: entity,
4874
5645
  ...(skinSlice !== undefined ? { skin: skinSlice } : {}),
4875
5646
  ...(morph !== undefined ? { morph } : {}),
5647
+ ...(instancesSnap !== undefined ? { instances: instancesSnap } : {}),
4876
5648
  ...(spriteInstancesSnap !== undefined ? { spriteInstances: spriteInstancesSnap } : {}),
4877
5649
  ...(pointsLines !== undefined ? { pointsLines } : {}),
4878
5650
  };
5651
+ const gpuDrivenDraws = buildGpuDrivenDraws({
5652
+ submeshes: gpuDrivenSubmeshes,
5653
+ indexed: gpuDrivenIndexed,
5654
+ materials: materialsArr,
5655
+ fallbackMaterial: materialSnap,
5656
+ ...(gpuDrivenGeometry === undefined || getMaterialShaderArtifact === undefined
5657
+ ? {}
5658
+ : {
5659
+ prepare(draw, drawMaterial) {
5660
+ const materialShaderId = drawMaterial.materialShaderId;
5661
+ if (materialShaderId === undefined) return undefined;
5662
+ const artifact = getMaterialShaderArtifact(materialShaderId);
5663
+ if (artifact?.receipt === undefined) return undefined;
5664
+ const geometry: GpuDrivenGeometryReceipt = {
5665
+ ...gpuDrivenGeometry,
5666
+ topology: draw.topology,
5667
+ indexed: draw.kind === 'indexed',
5668
+ };
5669
+ const result = resolvePreparedGpuDrivenDraw({
5670
+ snapshot: { ...baseRenderable, material: drawMaterial },
5671
+ artifact,
5672
+ geometry,
5673
+ generation: artifact.receipt.generation,
5674
+ draw,
5675
+ });
5676
+ return result.ok ? result.value : undefined;
5677
+ },
5678
+ }),
5679
+ });
5680
+ const renderable =
5681
+ morph === undefined && gpuDrivenDraws.length > 0
5682
+ ? { ...baseRenderable, gpuDrivenDraws }
5683
+ : baseRenderable;
4879
5684
 
4880
5685
  // feat-20260528-frustum-culling M3 / w10: frustum culling check.
4881
5686
  // Skip the entity if a valid AABB exists AND ALL cameras' frusta
@@ -4884,66 +5689,70 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4884
5689
  // producer-owned finite local-space AABBs. Culling is unconditional
4885
5690
  // engine behavior; there is no per-entity opt-out.
4886
5691
  if (localAabb !== undefined) {
4887
- // feat-20260601 D-3: cull AABB uses the resolved world mat4
4888
- // directly (no compose) -- same source the record stage feeds
4889
- // the mesh SSBO, so cull stays same-source with render (AC-05).
4890
- const worldAabb = box3.create();
4891
- box3.transformBox3(worldAabb, cullingLocalAabb, transformSnap.world);
4892
-
4893
- // Test against all cameras. Entity is visible if one camera
4894
- // frustum intersects the world-space AABB (or planes are empty
4895
- // from degenerate projection).
4896
- frustumTotal += 1;
4897
- let visible = frustumPlanes.length === 0;
4898
- for (let ci = 0; ci < frustumPlanes.length; ci++) {
4899
- const planes = frustumPlanes[ci] as Float32Array;
4900
- if (planes.length === 0) {
4901
- visible = true;
4902
- break;
4903
- }
4904
- if (frustum.intersectsBox(planes as frustum.Frustum, worldAabb as box3.Box3Like)) {
4905
- visible = true;
4906
- break;
4907
- }
4908
- }
4909
- if (!visible) {
4910
- frustumCulled += 1;
4911
- continue;
4912
- }
4913
- }
4914
-
4915
- if (hasInstances) {
4916
- const transforms = worldInternal._getArrayView(entity, Instances, 'transforms');
4917
- if (transforms === undefined) {
4918
- flushPendingDispatch(pendingDispatch);
4919
- renderables.push(baseRenderable);
5692
+ // An empty or malformed Instances payload has no meaningful bounds.
5693
+ // Keep it conservative (the draw path later emits zero instances)
5694
+ // instead of manufacturing the entity's identity bounds. For a
5695
+ // valid payload, derive one world-space union for CPU culling while
5696
+ // leaving the mesh-local AABB untouched for GPU per-instance cull.
5697
+ const derivedInstancesBounds =
5698
+ instancesSnap === undefined || instancesSnap.instanceCount === 0
5699
+ ? undefined
5700
+ : deriveInstancesUnionBounds({
5701
+ meshAabb: cullingLocalAabb,
5702
+ entityWorld: transformSnap.world,
5703
+ transforms: instancesSnap.transforms,
5704
+ });
5705
+ if (instancesSnap?.instanceCount === 0) {
5706
+ // No instance can contribute a visible bound. The retained
5707
+ // snapshot still records instanceCount=0 for the record stage.
5708
+ } else if (
5709
+ hasInstances &&
5710
+ (instancesSnap === undefined || derivedInstancesBounds === undefined)
5711
+ ) {
5712
+ // Missing/malformed derived facts are a conservative no-cull
5713
+ // result. The stride validator above already rejects malformed
5714
+ // lengths; this branch covers non-finite producer matrices/AABB.
4920
5715
  } else {
4921
- const actualLength = transforms.length;
4922
- if (actualLength % 16 !== 0) {
4923
- worldInternal._routeError(new InstanceTransformsStrideMismatchError(actualLength), {
4924
- severity: Severity.Error,
4925
- systemName: 'RenderSystem.extract (Instances stride)',
4926
- });
5716
+ // feat-20260601 D-3: cull AABB uses the resolved world mat4
5717
+ // directly (no compose) -- same source the record stage feeds
5718
+ // the mesh SSBO, so cull stays same-source with render (AC-05).
5719
+ const worldAabb = box3.create();
5720
+ if (derivedInstancesBounds !== undefined) {
5721
+ worldAabb[0] = derivedInstancesBounds[0] ?? 0;
5722
+ worldAabb[1] = derivedInstancesBounds[1] ?? 0;
5723
+ worldAabb[2] = derivedInstancesBounds[2] ?? 0;
5724
+ worldAabb[3] = derivedInstancesBounds[3] ?? 0;
5725
+ worldAabb[4] = derivedInstancesBounds[4] ?? 0;
5726
+ worldAabb[5] = derivedInstancesBounds[5] ?? 0;
5727
+ } else {
5728
+ box3.transformBox3(worldAabb, cullingLocalAabb, transformSnap.world);
5729
+ }
4927
5730
 
5731
+ // Test against all cameras. Entity is visible if one camera
5732
+ // frustum intersects the world-space AABB (or planes are empty
5733
+ // from degenerate projection).
5734
+ frustumTotal += 1;
5735
+ let visible = frustumPlanes.length === 0;
5736
+ for (let ci = 0; ci < frustumPlanes.length; ci++) {
5737
+ const planes = frustumPlanes[ci] as Float32Array;
5738
+ if (planes.length === 0) {
5739
+ visible = true;
5740
+ break;
5741
+ }
5742
+ if (frustum.intersectsBox(planes as frustum.Frustum, worldAabb as box3.Box3Like)) {
5743
+ visible = true;
5744
+ break;
5745
+ }
5746
+ }
5747
+ if (!visible) {
5748
+ frustumCulled += 1;
4928
5749
  continue;
4929
5750
  }
4930
- const snapshotCopy = new Float32Array(transforms);
4931
- const instanceCount = Math.max(1, Math.floor(actualLength / 16));
4932
- flushPendingDispatch(pendingDispatch);
4933
- renderables.push({
4934
- ...baseRenderable,
4935
- instances: {
4936
- transforms: snapshotCopy,
4937
- instanceCount,
4938
- cacheKey: entity,
4939
- archVersion,
4940
- },
4941
- });
4942
5751
  }
4943
- } else {
4944
- flushPendingDispatch(pendingDispatch);
4945
- renderables.push(baseRenderable);
4946
5752
  }
5753
+
5754
+ flushPendingDispatch(pendingDispatch);
5755
+ renderables.push(renderable);
4947
5756
  }
4948
5757
 
4949
5758
  // feat-20260520-2d-sprite-layer-mvp M-3 / w22 + w25: finalise the
@@ -4978,10 +5787,12 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4978
5787
  // whitePoint / tonemap` onto the SAME unified params channel custom
4979
5788
  // post-processes use — `Camera.exposure` stays the AI-user-facing SSOT (D-5),
4980
5789
  // the engine itself acts as the provider for the Standard tonemap shader
4981
- // id. The 16B layout is byte-identical to the prior recordTonemapPass packing
4982
- // (render-system-record.ts pre-w14): Float32 [exposure, whitePoint, _, pad]
4983
- // with the mode u32 occupying the third 4-byte slot via tonemapToU32 (SSOT in
4984
- // camera.ts). Run AFTER the user-entity collection above so the engine's
5790
+ // id. The 16B layout keeps the prior recordTonemapPass packing
5791
+ // (render-system-record.ts pre-w14): Float32 [exposure, whitePoint, _,
5792
+ // ditherEnabled], with the mode u32 occupying the third 4-byte slot via
5793
+ // tonemapToU32 (SSOT in camera.ts). The final Output Transform makes a
5794
+ // per-pass copy and writes ditherEnabled at byte offset 12; the extracted
5795
+ // camera snapshot remains the SSOT. Run AFTER the user-entity collection above so the engine's
4985
5796
  // built-in provider is authoritative for its own reserved key (a user entity
4986
5797
  // can never shadow the Standard tonemap identity. The single active camera mirrors
4987
5798
  // recordFrame's `activeCameras[0]` selection.
@@ -4992,6 +5803,9 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4992
5803
 
4993
5804
  return {
4994
5805
  cameras,
5806
+ auxiliaryCameras,
5807
+ cubeCameras,
5808
+ reflectionProbes,
4995
5809
  lights,
4996
5810
  environment,
4997
5811
  environmentReady: true,
@@ -5000,9 +5814,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
5000
5814
  dispatch,
5001
5815
  skylight,
5002
5816
  skylightCount,
5817
+ lightProbes,
5003
5818
  skybox,
5004
5819
  skyboxCount,
5005
5820
  fog,
5821
+ ...(fogFailure === undefined ? {} : { fogFailure }),
5006
5822
  frustumStats: { culled: frustumCulled, total: frustumTotal },
5007
5823
  visibilityStats: { explicitlyHidden: explicitlyHidden.size },
5008
5824
  postProcessParams,