@forgeax/engine-render 0.1.21 → 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 (426) hide show
  1. package/README.md +150 -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.map +1 -1
  5. package/dist/assembly/factory.d.ts +14 -6
  6. package/dist/assembly/factory.d.ts.map +1 -1
  7. package/dist/assembly/host-contract.d.ts +10 -0
  8. package/dist/assembly/host-contract.d.ts.map +1 -1
  9. package/dist/authoring.mjs +1 -1
  10. package/dist/{chunk-PCHBPYML.mjs → chunk-4L3P6DT5.mjs} +2 -2
  11. package/dist/chunk-4L3P6DT5.mjs.map +1 -0
  12. package/dist/{chunk-VFPLTKCU.mjs → chunk-4NPUASZR.mjs} +15 -4
  13. package/dist/chunk-4NPUASZR.mjs.map +1 -0
  14. package/dist/chunk-DIYW3M5D.mjs +5085 -0
  15. package/dist/chunk-DIYW3M5D.mjs.map +1 -0
  16. package/dist/chunk-FBC4R44J.mjs +1003 -0
  17. package/dist/chunk-FBC4R44J.mjs.map +1 -0
  18. package/dist/{chunk-FS2KNUQH.mjs → chunk-FF7K3FSO.mjs} +253 -489
  19. package/dist/chunk-FF7K3FSO.mjs.map +1 -0
  20. package/dist/chunk-HZUPSYW7.mjs +1104 -0
  21. package/dist/chunk-HZUPSYW7.mjs.map +1 -0
  22. package/dist/{chunk-6OPXDQ2E.mjs → chunk-QODA5UKU.mjs} +33 -294
  23. package/dist/chunk-QODA5UKU.mjs.map +1 -0
  24. package/dist/chunk-YALPS44A.mjs +296 -0
  25. package/dist/chunk-YALPS44A.mjs.map +1 -0
  26. package/dist/{chunk-5YADUCA7.mjs → chunk-ZUWKAZ2I.mjs} +585 -991
  27. package/dist/chunk-ZUWKAZ2I.mjs.map +1 -0
  28. package/dist/cluster-binner.d.ts +5 -0
  29. package/dist/cluster-binner.d.ts.map +1 -1
  30. package/dist/components/index.d.ts +3 -0
  31. package/dist/components/index.d.ts.map +1 -1
  32. package/dist/components/light-helpers.d.ts +4 -0
  33. package/dist/components/light-helpers.d.ts.map +1 -1
  34. package/dist/components/light-probe.d.ts +9 -0
  35. package/dist/components/light-probe.d.ts.map +1 -0
  36. package/dist/components/rect-area-light.d.ts +12 -0
  37. package/dist/components/rect-area-light.d.ts.map +1 -0
  38. package/dist/components/scene-instance.d.ts +4 -0
  39. package/dist/components/scene-instance.d.ts.map +1 -1
  40. package/dist/components/spot-light.d.ts +3 -0
  41. package/dist/components/spot-light.d.ts.map +1 -1
  42. package/dist/construct-renderer.mjs +10437 -10044
  43. package/dist/construct-renderer.mjs.map +1 -1
  44. package/dist/device/gpu-residency.d.ts +8 -1
  45. package/dist/device/gpu-residency.d.ts.map +1 -1
  46. package/dist/device/lifecycle-transaction.d.ts +2 -0
  47. package/dist/device/lifecycle-transaction.d.ts.map +1 -0
  48. package/dist/environment/lifecycle.d.ts.map +1 -1
  49. package/dist/errors/gpu-driven.d.ts +26 -0
  50. package/dist/errors/gpu-driven.d.ts.map +1 -0
  51. package/dist/errors/index.d.ts +1 -0
  52. package/dist/errors/index.d.ts.map +1 -1
  53. package/dist/errors/recover.d.ts +9 -1
  54. package/dist/errors/recover.d.ts.map +1 -1
  55. package/dist/errors/render.d.ts +26 -1
  56. package/dist/errors/render.d.ts.map +1 -1
  57. package/dist/extract/gpu-driven.d.ts +50 -0
  58. package/dist/extract/gpu-driven.d.ts.map +1 -0
  59. package/dist/extract/visibility.d.ts.map +1 -1
  60. package/dist/gpu-driven/batch-topology.d.ts +34 -0
  61. package/dist/gpu-driven/batch-topology.d.ts.map +1 -1
  62. package/dist/gpu-driven/prepared-draw.d.ts +63 -0
  63. package/dist/gpu-driven/prepared-draw.d.ts.map +1 -0
  64. package/dist/gpu-driven/production-raster.d.ts +98 -2
  65. package/dist/gpu-driven/production-raster.d.ts.map +1 -1
  66. package/dist/gpu-driven/view-gpu.d.ts +64 -1
  67. package/dist/gpu-driven/view-gpu.d.ts.map +1 -1
  68. package/dist/gpu-scene-schema.d.ts +14 -0
  69. package/dist/gpu-scene-schema.d.ts.map +1 -1
  70. package/dist/gpu-scene.d.ts +8 -1
  71. package/dist/gpu-scene.d.ts.map +1 -1
  72. package/dist/gpu-usage.d.ts +0 -1
  73. package/dist/gpu-usage.d.ts.map +1 -1
  74. package/dist/hdrp-buffers.d.ts +3 -3
  75. package/dist/hdrp-buffers.d.ts.map +1 -1
  76. package/dist/index.d.ts +12 -1
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.mjs +90 -5
  79. package/dist/index.mjs.map +1 -1
  80. package/dist/inspection-types.d.ts +163 -1
  81. package/dist/inspection-types.d.ts.map +1 -1
  82. package/dist/internal.d.ts +9 -0
  83. package/dist/internal.d.ts.map +1 -1
  84. package/dist/internal.mjs +23 -2
  85. package/dist/internal.mjs.map +1 -1
  86. package/dist/light-buffer-layout.d.ts +58 -149
  87. package/dist/light-buffer-layout.d.ts.map +1 -1
  88. package/dist/materials.d.ts.map +1 -1
  89. package/dist/pbr-pipeline.d.ts +26 -12
  90. package/dist/pbr-pipeline.d.ts.map +1 -1
  91. package/dist/pipeline/standard-forward-lane.d.ts.map +1 -1
  92. package/dist/pipeline/standard-lighting/graph.d.ts.map +1 -1
  93. package/dist/pipeline/standard-lighting/light-frame.d.ts +3 -3
  94. package/dist/pipeline/standard-lighting/light-frame.d.ts.map +1 -1
  95. package/dist/pipeline/standard-lighting/topology.d.ts +1 -1
  96. package/dist/pipeline/standard-lighting/topology.d.ts.map +1 -1
  97. package/dist/pipeline/standard-lighting/transport.d.ts.map +1 -1
  98. package/dist/pipeline/standard-pipeline.d.ts.map +1 -1
  99. package/dist/pipeline-builder.d.ts +2 -0
  100. package/dist/pipeline-builder.d.ts.map +1 -1
  101. package/dist/pipeline-spec.d.ts +1 -1
  102. package/dist/pipeline-spec.d.ts.map +1 -1
  103. package/dist/plugin.d.ts.map +1 -1
  104. package/dist/prepare/extended-lighting/ltc-resources.d.ts +31 -0
  105. package/dist/prepare/extended-lighting/ltc-resources.d.ts.map +1 -0
  106. package/dist/prepare/extended-lighting/resources.d.ts +72 -0
  107. package/dist/prepare/extended-lighting/resources.d.ts.map +1 -0
  108. package/dist/prepare/extended-lighting/spot-modifiers.d.ts +42 -0
  109. package/dist/prepare/extended-lighting/spot-modifiers.d.ts.map +1 -0
  110. package/dist/prepare/extended-lighting/state.d.ts +24 -0
  111. package/dist/prepare/extended-lighting/state.d.ts.map +1 -0
  112. package/dist/record/frame-lighting.d.ts +8 -6
  113. package/dist/record/frame-lighting.d.ts.map +1 -1
  114. package/dist/record/frame-snapshot.d.ts +50 -10
  115. package/dist/record/frame-snapshot.d.ts.map +1 -1
  116. package/dist/record/frame.d.ts +7 -3
  117. package/dist/record/frame.d.ts.map +1 -1
  118. package/dist/record/gpu-timing.d.ts +19 -7
  119. package/dist/record/gpu-timing.d.ts.map +1 -1
  120. package/dist/record/helpers.d.ts +0 -20
  121. package/dist/record/helpers.d.ts.map +1 -1
  122. package/dist/record/main-pass-geometry.d.ts +6 -1
  123. package/dist/record/main-pass-geometry.d.ts.map +1 -1
  124. package/dist/record/main-pass-material.d.ts +3 -1
  125. package/dist/record/main-pass-material.d.ts.map +1 -1
  126. package/dist/record/main-pass-sprite-draws.d.ts.map +1 -1
  127. package/dist/record/mesh-ssbo.d.ts.map +1 -1
  128. package/dist/record/render-context.d.ts +57 -6
  129. package/dist/record/render-context.d.ts.map +1 -1
  130. package/dist/record/shadow-pass.d.ts.map +1 -1
  131. package/dist/record/typed-frame-graph.d.ts +49 -2
  132. package/dist/record/typed-frame-graph.d.ts.map +1 -1
  133. package/dist/record/view-ubo.d.ts +1 -2
  134. package/dist/record/view-ubo.d.ts.map +1 -1
  135. package/dist/reflection/inspection.d.ts +10 -0
  136. package/dist/reflection/inspection.d.ts.map +1 -0
  137. package/dist/reflection/projection.d.ts +49 -0
  138. package/dist/reflection/projection.d.ts.map +1 -1
  139. package/dist/reflection/record-owner.d.ts +60 -0
  140. package/dist/reflection/record-owner.d.ts.map +1 -0
  141. package/dist/render-contract.d.ts +14 -8
  142. package/dist/render-contract.d.ts.map +1 -1
  143. package/dist/render-pipeline.d.ts +7 -0
  144. package/dist/render-pipeline.d.ts.map +1 -1
  145. package/dist/render-system-extract.d.ts +104 -16
  146. package/dist/render-system-extract.d.ts.map +1 -1
  147. package/dist/render-system.d.ts +12 -1
  148. package/dist/render-system.d.ts.map +1 -1
  149. package/dist/scene/probe-blend-record.d.ts +22 -0
  150. package/dist/scene/probe-blend-record.d.ts.map +1 -0
  151. package/dist/scene/probe-blend.d.ts +142 -0
  152. package/dist/scene/probe-blend.d.ts.map +1 -0
  153. package/dist/scene/render-scene-types.d.ts +0 -6
  154. package/dist/scene/render-scene-types.d.ts.map +1 -1
  155. package/dist/scene/render-scene.d.ts +103 -9
  156. package/dist/scene/render-scene.d.ts.map +1 -1
  157. package/dist/scene/visibility/bounds.d.ts +20 -0
  158. package/dist/scene/visibility/bounds.d.ts.map +1 -0
  159. package/dist/scene/visibility/budget.d.ts +16 -0
  160. package/dist/scene/visibility/budget.d.ts.map +1 -0
  161. package/dist/scene/visibility/facet.d.ts +73 -0
  162. package/dist/scene/visibility/facet.d.ts.map +1 -0
  163. package/dist/scene/visibility/gpu-frame-timing.d.ts +41 -0
  164. package/dist/scene/visibility/gpu-frame-timing.d.ts.map +1 -0
  165. package/dist/scene/visibility/gpu-lod.d.ts +46 -0
  166. package/dist/scene/visibility/gpu-lod.d.ts.map +1 -0
  167. package/dist/scene/visibility/inspection.d.ts +132 -0
  168. package/dist/scene/visibility/inspection.d.ts.map +1 -0
  169. package/dist/scene/visibility/lod-selector.d.ts +25 -0
  170. package/dist/scene/visibility/lod-selector.d.ts.map +1 -0
  171. package/dist/scene/visibility/occlusion-confidence.d.ts +81 -0
  172. package/dist/scene/visibility/occlusion-confidence.d.ts.map +1 -0
  173. package/dist/scene/visibility/occlusion-pass.d.ts +57 -0
  174. package/dist/scene/visibility/occlusion-pass.d.ts.map +1 -0
  175. package/dist/scene/visibility/occlusion-query-pool.d.ts +57 -0
  176. package/dist/scene/visibility/occlusion-query-pool.d.ts.map +1 -0
  177. package/dist/scene/visibility/occlusion-runtime.d.ts +96 -0
  178. package/dist/scene/visibility/occlusion-runtime.d.ts.map +1 -0
  179. package/dist/scene/visibility/types.d.ts +34 -0
  180. package/dist/scene/visibility/types.d.ts.map +1 -0
  181. package/dist/ssr/admission.d.ts +122 -0
  182. package/dist/ssr/admission.d.ts.map +1 -0
  183. package/dist/ssr/composition.d.ts +32 -0
  184. package/dist/ssr/composition.d.ts.map +1 -0
  185. package/dist/ssr/errors.d.ts +39 -0
  186. package/dist/ssr/errors.d.ts.map +1 -0
  187. package/dist/ssr/identity.d.ts +13 -0
  188. package/dist/ssr/identity.d.ts.map +1 -0
  189. package/dist/systems/skin-palette-allocator.d.ts +1 -1
  190. package/dist/temporal/frame-coordinator.d.ts +13 -0
  191. package/dist/temporal/frame-coordinator.d.ts.map +1 -1
  192. package/dist/temporal/frame.d.ts +2 -0
  193. package/dist/temporal/frame.d.ts.map +1 -1
  194. package/dist/temporal/index.mjs +6 -5
  195. package/dist/temporal/view.d.ts +7 -0
  196. package/dist/temporal/view.d.ts.map +1 -1
  197. package/dist/tilemap-chunk-extract-system.d.ts +1 -1
  198. package/dist/typed-render-graph-primitives.d.ts +4 -0
  199. package/dist/typed-render-graph-primitives.d.ts.map +1 -1
  200. package/dist/volume/passes.d.ts +8 -4
  201. package/dist/volume/passes.d.ts.map +1 -1
  202. package/package.json +20 -20
  203. package/src/__tests__/cluster-grid-default-surface.unit.test.ts +5 -0
  204. package/src/__tests__/detached-inspection.integration.test.ts +6 -0
  205. package/src/__tests__/device-feature-admission.unit.test.ts +13 -1
  206. package/src/__tests__/direct-light-snapshot.unit.test.ts +59 -3
  207. package/src/__tests__/environment-generation.integration.test.ts +16 -0
  208. package/src/__tests__/evidence-identity.ts +13 -0
  209. package/src/__tests__/evidence-identity.unit.test.ts +24 -0
  210. package/src/__tests__/extended-lighting-recovery.browser.test.ts +14 -0
  211. package/src/__tests__/extended-lighting-recovery.dawn.test.ts +14 -0
  212. package/src/__tests__/extended-lighting-recovery.integration.test.ts +81 -0
  213. package/src/__tests__/extended-lighting-resources.unit.test.ts +51 -0
  214. package/src/__tests__/extended-lighting-zero-feature.integration.test.ts +52 -0
  215. package/src/__tests__/factory-contract.integration.test.ts +33 -0
  216. package/src/__tests__/frame-observation.contract.test.ts +15 -0
  217. package/src/__tests__/gpu-driven-batch-topology-pbr.unit.test.ts +146 -0
  218. package/src/__tests__/gpu-driven-indirect-raster-evidence.ts +1 -0
  219. package/src/__tests__/gpu-driven-indirect-raster.dawn.test.ts +1 -0
  220. package/src/__tests__/gpu-driven-production.integration.test.ts +572 -9
  221. package/src/__tests__/gpu-driven-scaling.unit.test.ts +66 -2
  222. package/src/__tests__/gpu-driven-view-gpu-evidence.ts +26 -1
  223. package/src/__tests__/gpu-driven-view-graph.integration.test.ts +89 -2
  224. package/src/__tests__/gpu-driven-view.browser.test.ts +3 -3
  225. package/src/__tests__/gpu-driven-view.dawn.test.ts +3 -3
  226. package/src/__tests__/gpu-driven-view.unit.test.ts +50 -0
  227. package/src/__tests__/gpu-lod-evidence.ts +188 -0
  228. package/src/__tests__/gpu-lod-fallback.integration.test.ts +10 -0
  229. package/src/__tests__/gpu-lod-indirect.integration.test.ts +40 -0
  230. package/src/__tests__/gpu-lod-parity.dawn.test.ts +29 -0
  231. package/src/__tests__/gpu-lod-readiness.integration.test.ts +52 -0
  232. package/src/__tests__/gpu-lod-schema.unit.test.ts +66 -0
  233. package/src/__tests__/gpu-lod-selector.unit.test.ts +28 -0
  234. package/src/__tests__/gpu-lod-silhouette.browser.test.ts +13 -0
  235. package/src/__tests__/gpu-resource-store-stride.unit.test.ts +66 -1
  236. package/src/__tests__/gpu-scene.dawn.test.ts +8 -1
  237. package/src/__tests__/gpu-scene.unit.test.ts +49 -3
  238. package/src/__tests__/light-buffer-layout.unit.test.ts +113 -3
  239. package/src/__tests__/light-inspection.unit.test.ts +55 -0
  240. package/src/__tests__/light-resource-error.unit.test.ts +43 -0
  241. package/src/__tests__/lighting-scene-roundtrip.integration.test.ts +109 -0
  242. package/src/__tests__/lod-bounds.unit.test.ts +42 -0
  243. package/src/__tests__/lod-cpu-lane.integration.test.ts +55 -0
  244. package/src/__tests__/lod-cpu-no-change.perf.test.ts +19 -0
  245. package/src/__tests__/lod-gpu-frame-timing.unit.test.ts +49 -0
  246. package/src/__tests__/lod-multi-view.integration.test.ts +63 -0
  247. package/src/__tests__/lod-occlusion-device-loss.integration.test.ts +27 -0
  248. package/src/__tests__/lod-occlusion-final-decision.integration.test.ts +47 -0
  249. package/src/__tests__/lod-occlusion-inspection.integration.test.ts +217 -0
  250. package/src/__tests__/lod-occlusion-inspection.schema.test-d.ts +14 -0
  251. package/src/__tests__/lod-occlusion-inspection.unit.test.ts +72 -0
  252. package/src/__tests__/lod-occlusion-visual-evidence.ts +91 -0
  253. package/src/__tests__/lod-occlusion.browser.test.ts +15 -0
  254. package/src/__tests__/lod-occlusion.dawn.test.ts +20 -0
  255. package/src/__tests__/lod-selector.profile.test.ts +35 -0
  256. package/src/__tests__/lod-selector.unit.test.ts +68 -0
  257. package/src/__tests__/lod-visibility-identity.unit.test.ts +198 -0
  258. package/src/__tests__/lod-visibility-lifecycle.integration.test.ts +132 -0
  259. package/src/__tests__/ltc-resources.unit.test.ts +32 -0
  260. package/src/__tests__/material-contract-inventory.unit.test.ts +17 -0
  261. package/src/__tests__/occlusion-confidence.unit.test.ts +168 -0
  262. package/src/__tests__/occlusion-invalidation.integration.test.ts +37 -0
  263. package/src/__tests__/occlusion-pass-routing.unit.test.ts +16 -0
  264. package/src/__tests__/occlusion-pass.dawn.test.ts +20 -0
  265. package/src/__tests__/occlusion-pass.integration.test.ts +43 -0
  266. package/src/__tests__/occlusion-pass.rhi-null.test.ts +22 -0
  267. package/src/__tests__/occlusion-query-pool.rhi-null.test.ts +21 -0
  268. package/src/__tests__/occlusion-query-pool.unit.test.ts +95 -0
  269. package/src/__tests__/occlusion-query-ticket.integration.test.ts +47 -0
  270. package/src/__tests__/occlusion-query.dawn.test.ts +16 -0
  271. package/src/__tests__/occlusion-query.evidence.ts +178 -0
  272. package/src/__tests__/occlusion-query.rhi-null.test.ts +31 -0
  273. package/src/__tests__/occlusion-runtime.unit.test.ts +120 -0
  274. package/src/__tests__/prepared-gpu-driven-pbr.integration.test.ts +196 -0
  275. package/src/__tests__/probe-bindings.unit.test.ts +200 -0
  276. package/src/__tests__/probe-blend-numeric.unit.test.ts +67 -0
  277. package/src/__tests__/probe-blend-oracle.unit.test.ts +136 -0
  278. package/src/__tests__/probe-capacity.integration.test.ts +97 -0
  279. package/src/__tests__/probe-recovery.integration.test.ts +104 -0
  280. package/src/__tests__/probe-scene-dirty.integration.test.ts +211 -0
  281. package/src/__tests__/profiler-phase-catalog.test.ts +4 -1
  282. package/src/__tests__/profiler-phase-token.test.ts +1 -0
  283. package/src/__tests__/rect-area-boundary.unit.test.ts +24 -0
  284. package/src/__tests__/reflection-fallback-graph.unit.test.ts +67 -0
  285. package/src/__tests__/reflection-fallback-inspection.unit.test.ts +77 -0
  286. package/src/__tests__/reflection-fallback-lifecycle.integration.test.ts +21 -0
  287. package/src/__tests__/reflection-fallback-projection.unit.test.ts +90 -0
  288. package/src/__tests__/reflection-fallback-source.unit.test.ts +32 -0
  289. package/src/__tests__/reflection-probe-owner-placement.unit.test.ts +39 -0
  290. package/src/__tests__/reflection-probe-record-owner.characterization.unit.test.ts +120 -0
  291. package/src/__tests__/render-error-code-owner.test-d.ts +1 -0
  292. package/src/__tests__/render-feature-docs.unit.test.ts +12 -7
  293. package/src/__tests__/render-graph-fallback.integration.test.ts +24 -0
  294. package/src/__tests__/render-perf/extended-lighting.bench.test.ts +66 -0
  295. package/src/__tests__/render-scene-projection.unit.test.ts +447 -18
  296. package/src/__tests__/renderer-factory-material-contract.unit.test.ts +76 -0
  297. package/src/__tests__/skin-motion-regression.unit.test.ts +22 -0
  298. package/src/__tests__/skinned-shadow-caster.test.ts +2 -2
  299. package/src/__tests__/spot-modifier-projection.unit.test.ts +70 -0
  300. package/src/__tests__/ssr-admission-matrix.integration.test.ts +206 -0
  301. package/src/__tests__/ssr-composition.unit.test.ts +46 -0
  302. package/src/__tests__/ssr-fallback-zero-work.bench.test.ts +94 -0
  303. package/src/__tests__/ssr-recovery-errors.unit.test.ts +58 -0
  304. package/src/__tests__/standard-cluster-variant-resolution.unit.test.ts +67 -0
  305. package/src/__tests__/standard-clustered-pipeline.unit.test.ts +7 -0
  306. package/src/__tests__/standard-lighting-inspection.integration.test.ts +1 -1
  307. package/src/__tests__/standard-lighting-write.unit.test.ts +66 -2
  308. package/src/__tests__/temporal-fallback-generation.integration.test.ts +16 -0
  309. package/src/__tests__/temporal-view.unit.test.ts +3 -3
  310. package/src/__tests__/transmission-prewarm.unit.test.ts +69 -0
  311. package/src/__tests__/visibility-budget.integration.test.ts +140 -0
  312. package/src/__tests__/visibility-budget.unit.test.ts +70 -0
  313. package/src/__tests__/visibility-extract.integration.test.ts +11 -6
  314. package/src/__tests__/volumetric-fog-point-spot-accumulation.integration.test.ts +6 -3
  315. package/src/__tests__/volumetric-fog-stage-readback.dawn.test.ts +31 -28
  316. package/src/assembly/backend-contract.ts +13 -0
  317. package/src/assembly/device-feature-admission.ts +16 -9
  318. package/src/assembly/factory.ts +547 -93
  319. package/src/assembly/host-contract.ts +10 -0
  320. package/src/cluster-binner.ts +31 -0
  321. package/src/components/__tests__/lighting-authoring.unit.test.ts +39 -0
  322. package/src/components/__tests__/rect-area-light.unit.test.ts +33 -0
  323. package/src/components/index.ts +3 -0
  324. package/src/components/light-helpers.ts +52 -1
  325. package/src/components/light-probe.ts +13 -0
  326. package/src/components/rect-area-light.ts +15 -0
  327. package/src/components/scene-instance.ts +6 -0
  328. package/src/components/spot-light.ts +3 -0
  329. package/src/device/__tests__/extended-lighting-recovery.unit.test.ts +24 -0
  330. package/src/device/gpu-residency.ts +140 -9
  331. package/src/device/lifecycle-transaction.ts +12 -0
  332. package/src/environment/lifecycle.ts +8 -1
  333. package/src/errors/gpu-driven.ts +92 -0
  334. package/src/errors/index.ts +1 -0
  335. package/src/errors/recover.ts +32 -0
  336. package/src/errors/render.ts +55 -1
  337. package/src/extract/camera.ts +3 -3
  338. package/src/extract/gpu-driven.ts +108 -0
  339. package/src/extract/visibility.ts +0 -23
  340. package/src/gpu-driven/batch-topology.ts +206 -28
  341. package/src/gpu-driven/prepared-draw.ts +208 -0
  342. package/src/gpu-driven/production-raster.ts +953 -41
  343. package/src/gpu-driven/view-gpu.ts +411 -29
  344. package/src/gpu-scene-schema.ts +13 -0
  345. package/src/gpu-scene.ts +134 -65
  346. package/src/gpu-usage.ts +0 -1
  347. package/src/hdrp-buffers.ts +6 -6
  348. package/src/index.ts +74 -0
  349. package/src/inspection-types.ts +220 -1
  350. package/src/internal.ts +38 -0
  351. package/src/light-buffer-layout.ts +154 -324
  352. package/src/materials.ts +16 -2
  353. package/src/pbr-pipeline.ts +151 -45
  354. package/src/pipeline/standard-forward-lane.ts +47 -0
  355. package/src/pipeline/standard-lighting/graph.ts +2 -1
  356. package/src/pipeline/standard-lighting/light-frame.ts +3 -2
  357. package/src/pipeline/standard-lighting/topology.ts +1 -1
  358. package/src/pipeline/standard-lighting/transport.ts +2 -1
  359. package/src/pipeline/standard-pipeline.ts +2 -0
  360. package/src/pipeline-builder.ts +5 -2
  361. package/src/pipeline-spec.ts +12 -1
  362. package/src/plugin.ts +4 -0
  363. package/src/prepare/__tests__/extended-lighting-capability.unit.test.ts +56 -0
  364. package/src/prepare/extended-lighting/ltc-resources.ts +136 -0
  365. package/src/prepare/extended-lighting/resources.ts +389 -0
  366. package/src/prepare/extended-lighting/spot-modifiers.ts +194 -0
  367. package/src/prepare/extended-lighting/state.ts +92 -0
  368. package/src/record/__tests__/gpu-timing.unit.test.ts +177 -0
  369. package/src/record/__tests__/temporal-payload.unit.test.ts +23 -1
  370. package/src/record/__tests__/typed-frame-graph-plan.unit.test.ts +20 -1
  371. package/src/record/frame-lighting.ts +252 -53
  372. package/src/record/frame-snapshot.ts +57 -12
  373. package/src/record/frame.ts +410 -93
  374. package/src/record/gpu-timing.ts +108 -44
  375. package/src/record/helpers.ts +0 -58
  376. package/src/record/main-pass-geometry.ts +204 -39
  377. package/src/record/main-pass-material.ts +8 -1
  378. package/src/record/main-pass-sprite-draws.ts +52 -85
  379. package/src/record/mesh-ssbo.ts +47 -36
  380. package/src/record/render-context.ts +65 -4
  381. package/src/record/shadow-pass.ts +66 -40
  382. package/src/record/typed-frame-graph.ts +225 -83
  383. package/src/record/view-ubo.ts +14 -24
  384. package/src/reflection/inspection.ts +67 -0
  385. package/src/reflection/projection.ts +196 -1
  386. package/src/reflection/record-owner.ts +1283 -0
  387. package/src/render-contract.ts +17 -5
  388. package/src/render-pipeline.ts +5 -0
  389. package/src/render-system-extract.ts +493 -134
  390. package/src/render-system.ts +811 -615
  391. package/src/scene/__tests__/changed-block-summary.candidate.test.ts +36 -0
  392. package/src/scene/probe-blend-record.ts +54 -0
  393. package/src/scene/probe-blend.ts +813 -0
  394. package/src/scene/render-scene-types.ts +0 -7
  395. package/src/scene/render-scene.ts +848 -183
  396. package/src/scene/visibility/bounds.ts +57 -0
  397. package/src/scene/visibility/budget.ts +40 -0
  398. package/src/scene/visibility/facet.ts +400 -0
  399. package/src/scene/visibility/gpu-frame-timing.ts +101 -0
  400. package/src/scene/visibility/gpu-lod.ts +119 -0
  401. package/src/scene/visibility/inspection.ts +313 -0
  402. package/src/scene/visibility/lod-selector.ts +84 -0
  403. package/src/scene/visibility/occlusion-confidence.ts +478 -0
  404. package/src/scene/visibility/occlusion-pass.ts +165 -0
  405. package/src/scene/visibility/occlusion-query-pool.ts +178 -0
  406. package/src/scene/visibility/occlusion-runtime.ts +672 -0
  407. package/src/scene/visibility/types.ts +50 -0
  408. package/src/ssr/admission.ts +451 -0
  409. package/src/ssr/composition.ts +73 -0
  410. package/src/ssr/errors.ts +76 -0
  411. package/src/ssr/identity.ts +12 -0
  412. package/src/systems/skin-palette-allocator.ts +1 -1
  413. package/src/temporal/frame-coordinator.ts +26 -0
  414. package/src/temporal/frame.ts +14 -1
  415. package/src/temporal/view.ts +11 -0
  416. package/src/tilemap-chunk-extract-system.ts +4 -4
  417. package/src/typed-render-graph-primitives.ts +48 -0
  418. package/src/volume/passes.ts +46 -49
  419. package/dist/.tsbuildinfo +0 -1
  420. package/dist/chunk-5YADUCA7.mjs.map +0 -1
  421. package/dist/chunk-6OPXDQ2E.mjs.map +0 -1
  422. package/dist/chunk-BXBQQKFQ.mjs +0 -67
  423. package/dist/chunk-BXBQQKFQ.mjs.map +0 -1
  424. package/dist/chunk-FS2KNUQH.mjs.map +0 -1
  425. package/dist/chunk-PCHBPYML.mjs.map +0 -1
  426. 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,6 +147,7 @@ import {
148
147
  ASSET_ERROR_HINTS,
149
148
  AssetError,
150
149
  derive,
150
+ handleSlot,
151
151
  materialGuidText,
152
152
  materialValuesToLinearRuntime,
153
153
  toShared,
@@ -164,6 +164,7 @@ import {
164
164
  Fog,
165
165
  Instances,
166
166
  Layer,
167
+ LightProbe,
167
168
  Lines,
168
169
  MeshFilter,
169
170
  MeshRenderer,
@@ -173,6 +174,7 @@ import {
173
174
  Points,
174
175
  PostProcessParams,
175
176
  pointShapeFromU32,
177
+ RectAreaLight,
176
178
  ReflectionProbe,
177
179
  reflectionProbeUpdateIntentFromF32,
178
180
  SkyboxBackground,
@@ -194,6 +196,8 @@ import {
194
196
  degToCos,
195
197
  type LightValidationError,
196
198
  validateDirectionalLightData,
199
+ validateLightProbeData,
200
+ validateRectAreaLightData,
197
201
  } from './components/light-helpers';
198
202
  import { selectEnvironment } from './environment/frame';
199
203
  import {
@@ -212,12 +216,14 @@ import type {
212
216
  FogFrame,
213
217
  FogSelectionFailure,
214
218
  } from './extract/environment';
219
+ import { buildGpuDrivenDraws, resolvePreparedGpuDrivenDraw } from './extract/gpu-driven';
215
220
  import { resolveVisibility, type VisibilitySnapshot } from './extract/visibility';
216
221
  import type {
217
222
  RenderFeatureHiddenEntityReport,
218
223
  RenderFeatureWorldVisibilitySnapshot,
219
224
  } from './features/types';
220
225
  import { ensureGlyphMeshMaterialSlots } from './glyph-text-layout-system';
226
+ import type { GpuDrivenGeometryReceipt } from './gpu-driven/prepared-draw';
221
227
  import { deriveInstancesUnionBounds } from './instances-derived-bounds';
222
228
  import {
223
229
  type MeshMaterialBindingDiagnostic,
@@ -227,6 +233,8 @@ import {
227
233
  import { isStandardPbrMaterialShader } from './pbr-pipeline';
228
234
  import { expandPointsLinesBounds } from './points-lines/bounds';
229
235
  import type { PointsLinesRetainedSnapshot, PointsLinesStyle } from './points-lines/snapshot';
236
+ import { COOKIE_SLICE_CAPACITY, IES_SLICE_CAPACITY } from './prepare/extended-lighting/resources';
237
+ import { prepareCookieProjection } from './prepare/extended-lighting/spot-modifiers';
230
238
  import type { ReflectionProbeFact } from './reflection/projection';
231
239
  import {
232
240
  type CameraSnapshot,
@@ -289,18 +297,19 @@ export interface PointLightSnapshot {
289
297
  * plan-strategy §D-2 — record stage / shader skips shadow sampling when the
290
298
  * lane equals the sentinel. Joined with `pointShadow[]` by entity at the
291
299
  * end of extract; HDRP record stage threads `shadowAtlasLayer + shadowNear +
292
- * shadowFar` through `packLightSlot` as the §D-8 pad-lane payload.
300
+ * shadowFar` through the unified direct-light metadata payload.
293
301
  */
294
302
  readonly shadowAtlasLayer?: number;
295
303
  /**
296
304
  * Per-face perspective near plane (matches `PointLightShadow.nearPlane`).
297
- * Used by HDRP `evalPointShadowed` for depth-ref reconstruction; rides
298
- * `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.
299
307
  */
300
308
  readonly shadowNear?: number;
301
309
  /**
302
310
  * Per-face perspective far plane (matches `PointLightShadow.farPlane`).
303
- * 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.
304
313
  */
305
314
  readonly shadowFar?: number;
306
315
  }
@@ -324,8 +333,7 @@ export interface PointLightSnapshot {
324
333
  * cube atlas faces and feed shader the proj constants without re-querying ECS.
325
334
  * `shadowAtlasLayer` is assigned by extract in spawn order up to the renderer
326
335
  * atlas capacity; over-budget requests retain the sentinel `-1` so the
327
- * downstream LightSlot / packPointLight packers carry no invalid layer in
328
- * pointPadW (T-M1-8 will rename to shadowAtlasLayer).
336
+ * downstream DirectLightSlot packing carries no invalid layer identity.
329
337
  */
330
338
  export interface PointShadowSnapshot {
331
339
  /** Spawning entity index (joins PointLight + PointLightShadow + Transform). */
@@ -339,7 +347,7 @@ export interface PointShadowSnapshot {
339
347
  /**
340
348
  * Atlas layer assigned by extract in spawn order while capacity remains.
341
349
  * Sentinel -1 is used for a request that cannot be admitted and is
342
- * reserved by the shader-side LightSlot for no-shadow lights (T-M1-8).
350
+ * reserved by the shader-side DirectLightSlot metadata for no-shadow lights.
343
351
  */
344
352
  readonly shadowAtlasLayer: number;
345
353
  /**
@@ -378,6 +386,27 @@ export interface SpotLightSnapshot {
378
386
  readonly invRangeSquared: number;
379
387
  readonly cosInner: number;
380
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;
381
410
  // ── shadow fields (feat-20260625-spot-light-shadow-mapping M1) ──
382
411
  readonly castShadow: boolean;
383
412
  readonly lightViewProj: Float32Array | undefined;
@@ -398,20 +427,111 @@ export interface SpotLightSnapshot {
398
427
  readonly projectorRevision?: number;
399
428
  }
400
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
+
401
517
  /**
402
- * LightSnapshot — discriminated union of the three KHR_lights_punctual
518
+ * LightSnapshot — discriminated union of the directional and punctual
403
519
  * variants. AI users + the record stage perform an exhaustive switch on
404
520
  * `kind`; missing arms are caught at compile time (no `default`,
405
521
  * `assertNever`-style guards). Plan-strategy R-10.
406
522
  */
407
- export type LightSnapshot = DirectionalLightSnapshot | PointLightSnapshot | SpotLightSnapshot;
523
+ export type LightSnapshot =
524
+ | DirectionalLightSnapshot
525
+ | PointLightSnapshot
526
+ | SpotLightSnapshot
527
+ | RectAreaDirectLightSnapshot;
408
528
 
409
529
  /**
410
530
  * ExtractedLights — three-bucket output of the extractFrame three-query
411
531
  * union (M2 / w16 / AC-03). Plan-strategy section 3.1 EXT node:
412
532
  * - `directional` — at most one (record-stage N>1 fail-fast lands in M3)
413
- * - `point[]` first-slice cap of 4 enforced at record stage (M3)
414
- * - `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
415
535
  * `directionalCount` exposes the raw count of DirectionalLight entities the
416
536
  * extract observed (so the record stage can fire `render-system-multi-light`
417
537
  * when N>1 without re-running the query). M3 / w19 will widen this with
@@ -423,6 +543,7 @@ export interface ExtractedLights {
423
543
  readonly directionalCount: number;
424
544
  readonly point: readonly PointLightSnapshot[];
425
545
  readonly spot: readonly SpotLightSnapshot[];
546
+ readonly rect: readonly RectAreaDirectLightSnapshot[];
426
547
  /**
427
548
  * feat-20260613-csm-cascaded-shadow-maps M2 / w9: per-cascade light-view-
428
549
  * projection matrices (one per cascade, length 4 pre-allocated). Each matrix
@@ -482,7 +603,8 @@ export interface ExtractedLights {
482
603
  * shadow-casting point lights (PointLight + PointLightShadow + Transform
483
604
  * archetype join). Each entry carries the per-light 6-face VP matrices and
484
605
  * the assigned cube_array atlas layer (0..3 in spawn order; sentinel -1 is
485
- * 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).
486
608
  *
487
609
  * Empty array when no PointLightShadow components exist (zero-cost gate per
488
610
  * AC-09; record stage skips atlas allocation + shadow pass dispatch).
@@ -542,6 +664,15 @@ export interface SkylightSnapshot {
542
664
  readonly entityHandle: number;
543
665
  }
544
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
+
545
676
  /**
546
677
  * SkyboxSnapshot -- extract-stage view of one SkyboxBackground entity
547
678
  * (feat-20260531-skybox-env-background M2 / w5).
@@ -598,6 +729,9 @@ export interface ExtractedVolumetricFog {
598
729
  export interface RenderableSnapshot {
599
730
  readonly assetHandle: number;
600
731
  readonly transform: TransformSnapshot;
732
+ /** Producer-authored lower-detail references projected from MeshAsset. */
733
+ readonly lods?: readonly MeshLodLevel[];
734
+ readonly lodHysteresis?: number;
601
735
  /** Finite producer-owned local bounds retained for persistent CPU view work. */
602
736
  readonly localAabb?: Float32Array;
603
737
  /** Indexed single-submesh facts consumed by the first GPU-driven rigid lane. */
@@ -705,6 +839,8 @@ export interface RenderableSnapshot {
705
839
  readonly temporal?: RenderableTemporalSnapshot;
706
840
  /** Detached M2 authoring facts for the optional Points/Lines projection. */
707
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;
708
844
  }
709
845
 
710
846
  export type RenderablePreviousSource = 'last-submitted' | 'current-seed';
@@ -741,6 +877,16 @@ export interface GpuDrivenDrawSnapshot {
741
877
  readonly topology: PrimitiveTopology;
742
878
  readonly pipelineClass: string;
743
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
+ }[];
744
890
  }
745
891
 
746
892
  export interface InstancesSnapshot {
@@ -793,7 +939,7 @@ export interface SpriteInstancesSnapshot {
793
939
  /**
794
940
  * TransformSnapshot: extract-stage view of one entity's resolved world
795
941
  * transform (feat-20260601 D-3). Holds the single `world` mat4 (column-major
796
- * 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
797
943
  * propagateTransforms). The record stage copies these 16 floats straight into
798
944
  * the mesh SSBO slot (zero `mat4.compose`); position / scale consumers derive
799
945
  * from the mat4 via `mat4.getTranslation` / basis-column lengths.
@@ -981,20 +1127,6 @@ export interface MaterialSnapshotCacheEntry {
981
1127
  export type MaterialSnapshotCache = Map<number, MaterialSnapshotCacheEntry>;
982
1128
  export type MaterialSnapshotCachesByWorld = WeakMap<World, MaterialSnapshotCache>;
983
1129
 
984
- function gpuDrivenMaterialResourceClass(material: MaterialSnapshot): string {
985
- const textures = [...(material.textureHandles?.entries() ?? [])]
986
- .map(([name, handle]) => [name, Number(handle)] as const)
987
- .sort(([left], [right]) => left.localeCompare(right));
988
- const samplers = [...(material.samplerHandles?.entries() ?? [])]
989
- .map(([name, handle]) => [name, Number(handle)] as const)
990
- .sort(([left], [right]) => left.localeCompare(right));
991
- return JSON.stringify({
992
- textures,
993
- samplers,
994
- video: [...(material.videoTextureFields?.keys() ?? [])].sort(),
995
- });
996
- }
997
-
998
1130
  // === DispatchEntry — M3 / w26 single dispatch list (feat-20260526-material-asset-multipass-renderstate) ===
999
1131
  //
1000
1132
  // Plan-strategy D-3: single dispatch list sorted by queue value,
@@ -1115,6 +1247,8 @@ export interface ExtractedFrame {
1115
1247
  readonly dispatch: DispatchEntry[];
1116
1248
  readonly skylight: SkylightSnapshot | undefined;
1117
1249
  readonly skylightCount: number;
1250
+ /** World-local diffuse probe candidates; never a direct-light slot. */
1251
+ readonly lightProbes?: readonly LightProbeSnapshot[];
1118
1252
  readonly skybox: SkyboxSnapshot | undefined;
1119
1253
  readonly skyboxCount: number;
1120
1254
  /** Resource-owner Fog facts selected once at the frame boundary. */
@@ -2449,21 +2583,33 @@ export interface PreparedExtractContext {
2449
2583
  readonly resourceOwnerFog: FogFrame | null | undefined;
2450
2584
  readonly cull: 'self' | 'none' | 'external';
2451
2585
  readonly cullCameras: readonly CameraSnapshot[] | undefined;
2452
- readonly hierarchy: SceneHierarchySnapshot;
2586
+ readonly renderables: 'full' | 'none';
2453
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;
2454
2592
  }
2455
2593
 
2594
+ const ALL_VISIBLE: VisibilitySnapshot = {
2595
+ diagnostics: [],
2596
+ hasAnyIntent: false,
2597
+ hasAnyHiddenIntent: false,
2598
+ get: () => undefined,
2599
+ effective: () => 'visible',
2600
+ };
2601
+
2456
2602
  function collectCameraSnapshots(world: World): CameraSnapshot[] {
2457
2603
  const worldInternal = createWorldInternalView(world);
2458
2604
  const cameras: CameraSnapshot[] = [];
2459
2605
  const cameraQuery = world
2460
- .query({ read: [Camera], optional: [MotionBlur], with: [Transform] })
2606
+ .query({ read: [Camera], optional: [MotionBlur], with: [Transform, GlobalTransform] })
2461
2607
  .unwrap();
2462
2608
  for (const row of cameraQuery) {
2463
2609
  const cam = row.get(Camera);
2464
2610
  const motionBlur = row.get(MotionBlur);
2465
2611
  const entity = row.entity;
2466
- const view = worldInternal._getArrayView(entity, Transform, 'world');
2612
+ const view = worldInternal._getArrayView(entity, GlobalTransform, 'world');
2467
2613
  if (view === undefined) continue;
2468
2614
  const worldMat = new Float32Array(view);
2469
2615
  const target = resolveCameraTarget(world, cam.target);
@@ -2513,12 +2659,12 @@ function collectCameraSnapshots(world: World): CameraSnapshot[] {
2513
2659
  function collectCubeCameraSnapshots(world: World): CubeCameraSnapshot[] {
2514
2660
  const worldInternal = createWorldInternalView(world);
2515
2661
  const snapshots: CubeCameraSnapshot[] = [];
2516
- const query = world.query({ read: [CubeCamera], with: [Transform] }).unwrap();
2662
+ const query = world.query({ read: [CubeCamera], with: [Transform, GlobalTransform] }).unwrap();
2517
2663
  for (const row of query) {
2518
2664
  const camera = row.get(CubeCamera);
2519
2665
  const target = resolveCameraTarget(world, camera.target);
2520
2666
  if (target === undefined) continue;
2521
- const view = worldInternal._getArrayView(row.entity, Transform, 'world');
2667
+ const view = worldInternal._getArrayView(row.entity, GlobalTransform, 'world');
2522
2668
  if (view === undefined) continue;
2523
2669
  snapshots.push({
2524
2670
  entityKey: row.entity as number,
@@ -2537,10 +2683,12 @@ function collectCubeCameraSnapshots(world: World): CubeCameraSnapshot[] {
2537
2683
  function collectReflectionProbeFacts(world: World): ReflectionProbeFact[] {
2538
2684
  const worldInternal = createWorldInternalView(world);
2539
2685
  const facts: ReflectionProbeFact[] = [];
2540
- const query = world.query({ read: [ReflectionProbe], with: [Transform] }).unwrap();
2686
+ const query = world
2687
+ .query({ read: [ReflectionProbe], with: [Transform, GlobalTransform] })
2688
+ .unwrap();
2541
2689
  for (const row of query) {
2542
2690
  const probe = row.get(ReflectionProbe);
2543
- const view = worldInternal._getArrayView(row.entity, Transform, 'world');
2691
+ const view = worldInternal._getArrayView(row.entity, GlobalTransform, 'world');
2544
2692
  if (view === undefined) {
2545
2693
  continue;
2546
2694
  }
@@ -2627,9 +2775,13 @@ export function prepareExtractContext(
2627
2775
  readonly resourceOwnerFog?: FogFrame | null;
2628
2776
  readonly cull?: 'self' | 'none' | 'external';
2629
2777
  readonly cullCameras?: readonly CameraSnapshot[];
2778
+ readonly renderables?: 'full' | 'none';
2779
+ readonly getMaterialShaderArtifact?: (
2780
+ materialShaderId: string,
2781
+ ) => MaterialShaderArtifact | undefined;
2630
2782
  } = {},
2631
2783
  ): PreparedExtractContext {
2632
- const hierarchy = projectHierarchy(world);
2784
+ const renderables = options.renderables ?? 'full';
2633
2785
  return {
2634
2786
  assets: options.assets,
2635
2787
  pipelineState: options.pipelineState,
@@ -2637,8 +2789,9 @@ export function prepareExtractContext(
2637
2789
  resourceOwnerFog: options.resourceOwnerFog,
2638
2790
  cull: options.cull ?? 'self',
2639
2791
  cullCameras: options.cullCameras,
2640
- hierarchy,
2641
- visibility: resolveVisibility(world, hierarchy),
2792
+ renderables,
2793
+ visibility: renderables === 'none' ? ALL_VISIBLE : resolveVisibility(world),
2794
+ getMaterialShaderArtifact: options.getMaterialShaderArtifact,
2642
2795
  };
2643
2796
  }
2644
2797
 
@@ -2882,7 +3035,13 @@ export function extractFrames(
2882
3035
  assets?: AssetRegistry | null,
2883
3036
  pipelineState?: ExtractPipelineSurface | null,
2884
3037
  materialSnapshotCachesByWorld?: MaterialSnapshotCachesByWorld,
2885
- 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
+ } = {},
2886
3045
  ): ExtractedFrame {
2887
3046
  // w4: normalize the owner argument. A bare number is the legacy single-owner
2888
3047
  // form (cameraOwner === resourceOwner); an object carries the two split
@@ -2952,7 +3111,11 @@ export function extractFrames(
2952
3111
  })(),
2953
3112
  }),
2954
3113
  cull: options.cull === 'none' ? 'none' : isCameraOwner ? 'self' : 'external',
3114
+ ...(options.getMaterialShaderArtifact === undefined
3115
+ ? {}
3116
+ : { getMaterialShaderArtifact: options.getMaterialShaderArtifact }),
2955
3117
  ...(cameraOwnerFrame === undefined ? {} : { cullCameras: cameraOwnerFrame.cameras }),
3118
+ renderables: options.renderables ?? 'full',
2956
3119
  });
2957
3120
  const frame = extractFrame(world, prepared);
2958
3121
 
@@ -3023,6 +3186,7 @@ export function extractFrames(
3023
3186
  // frames); directionalCount sum.
3024
3187
  const point: PointLightSnapshot[] = [];
3025
3188
  const spot: SpotLightSnapshot[] = [];
3189
+ const rect: RectAreaDirectLightSnapshot[] = [];
3026
3190
  let directional: DirectionalLightSnapshot | undefined;
3027
3191
  let directionalCount = 0;
3028
3192
  let lightViewProj: readonly Float32Array[] | undefined;
@@ -3044,6 +3208,7 @@ export function extractFrames(
3044
3208
  for (const f of succeededFrames) {
3045
3209
  for (const p of f.lights.point) point.push(p);
3046
3210
  for (const s of f.lights.spot) spot.push(s);
3211
+ for (const r of f.lights.rect) rect.push(r);
3047
3212
  for (const ps of f.lights.pointShadow) pointShadow.push(ps);
3048
3213
  if (directional === undefined && f.lights.directional !== undefined) {
3049
3214
  directional = f.lights.directional;
@@ -3127,6 +3292,7 @@ export function extractFrames(
3127
3292
  directionalCount,
3128
3293
  point,
3129
3294
  spot,
3295
+ rect,
3130
3296
  lightViewProj,
3131
3297
  splitPlanes,
3132
3298
  cascadeCount,
@@ -3148,6 +3314,7 @@ export function extractFrames(
3148
3314
  const fog = resourceOwnerFrame?.fog;
3149
3315
  const fogFailure = resourceOwnerFrame?.fogFailure;
3150
3316
  const environment = resourceOwnerFrame?.environment;
3317
+ const lightProbes = succeededFrames.flatMap((frame) => frame.lightProbes ?? []);
3151
3318
  // User-authored PostProcessParams entities are singleton scene resources, so
3152
3319
  // they come from the resourceOwner world (holistic snapshot selection).
3153
3320
  const postProcessParams = new Map(resourceOwnerFrame?.postProcessParams);
@@ -3204,6 +3371,7 @@ export function extractFrames(
3204
3371
  dispatch: dispatchEntries,
3205
3372
  skylight,
3206
3373
  skylightCount,
3374
+ lightProbes,
3207
3375
  skybox,
3208
3376
  skyboxCount,
3209
3377
  fog,
@@ -3298,6 +3466,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3298
3466
  materialSnapshotCache: persistentMaterialSnapshotCache,
3299
3467
  cull: cullMode,
3300
3468
  resourceOwnerFog,
3469
+ getMaterialShaderArtifact,
3301
3470
  } = context;
3302
3471
  // Environment reads are owned by the retained resource projection and are
3303
3472
  // established at the frame boundary; record never revisits World state.
@@ -3310,7 +3479,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3310
3479
  const directionalLightQuery = world.query({ read: [DirectionalLight] }).unwrap();
3311
3480
 
3312
3481
  // feat-20260601 D-3: camera / point / spot light world transforms are read
3313
- // through the single resolved `Transform.world` mat4 (written by
3482
+ // through the single resolved `GlobalTransform.world` mat4 (written by
3314
3483
  // propagateTransforms), not the retired GlobalTransform-column-switch.
3315
3484
  //
3316
3485
  // Each segment routes through a World-owned Query with explicit read and
@@ -3411,22 +3580,22 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3411
3580
  const pointLightQuery = world
3412
3581
  .query({
3413
3582
  read: [PointLight],
3414
- optional: [Transform],
3583
+ optional: [Transform, GlobalTransform],
3415
3584
  })
3416
3585
  .unwrap();
3417
3586
  for (const row of pointLightQuery) {
3418
3587
  const p = row.get(PointLight);
3419
3588
  // K-2 scheme B: archetype-edge sniff -- `bundle.Transform` key is absent
3420
3589
  // when the archetype does not carry the Transform column.
3421
- const hasTransform = row.get(Transform) !== undefined;
3590
+ const hasTransform = row.get(GlobalTransform) !== undefined;
3422
3591
  const intensity = p.intensity;
3423
3592
  const range = p.range;
3424
3593
  const entityId = row.entity;
3425
- // Position = world-space translation extracted from Transform.world.
3594
+ // Position = world-space translation extracted from GlobalTransform.world.
3426
3595
  // A point light archetype without a Transform column sits at the origin.
3427
3596
  let worldMat: Float32Array | undefined;
3428
3597
  if (hasTransform) {
3429
- const view = worldInternal._getArrayView(entityId, Transform, 'world');
3598
+ const view = worldInternal._getArrayView(entityId, GlobalTransform, 'world');
3430
3599
  if (view !== undefined) worldMat = new Float32Array(view);
3431
3600
  }
3432
3601
  const position =
@@ -3450,16 +3619,19 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3450
3619
  const spotLightQuery = world
3451
3620
  .query({
3452
3621
  read: [SpotLight],
3453
- optional: [Transform],
3622
+ optional: [Transform, GlobalTransform],
3454
3623
  })
3455
3624
  .unwrap();
3456
3625
  // feat-20260625-spot-light-shadow-mapping M1 w5: tile allocation for castShadow spots.
3457
3626
  // Cap = 4 (OOS-5), sentinel -1 = unassigned (plan-strategy D-4).
3458
3627
  // Direction degeneration (near-zero) also skips shadow (requirements $112).
3459
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>();
3460
3632
  for (const row of spotLightQuery) {
3461
3633
  const s = row.get(SpotLight);
3462
- const hasTransform = row.get(Transform) !== undefined;
3634
+ const hasTransform = row.get(GlobalTransform) !== undefined;
3463
3635
  const intensity = s.intensity;
3464
3636
  const range = s.range;
3465
3637
  const innerConeDeg = s.innerConeDeg;
@@ -3467,7 +3639,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3467
3639
  let worldMat: Float32Array | undefined;
3468
3640
  if (hasTransform) {
3469
3641
  const entity = row.entity;
3470
- const view = worldInternal._getArrayView(entity, Transform, 'world');
3642
+ const view = worldInternal._getArrayView(entity, GlobalTransform, 'world');
3471
3643
  if (view !== undefined) worldMat = new Float32Array(view);
3472
3644
  }
3473
3645
  const position =
@@ -3505,13 +3677,93 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3505
3677
  const sMapSize = s.mapSize;
3506
3678
  const sNearPlane = s.nearPlane;
3507
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
+ }
3508
3762
  const shadowIntensity = s.shadowIntensity;
3509
3763
  // `shared<T>` columns are u32-backed and zero-filled when the optional
3510
3764
  // field is absent. Treat only a positive handle as an authored projector;
3511
3765
  // checking nullish values alone would turn the zero sentinel into a
3512
3766
  // phantom projector matrix and an avoidable asset-not-found diagnostic.
3513
- const hasProjector = Number(s.projector) > 0;
3514
-
3515
3767
  let lightViewProj: Float32Array | undefined;
3516
3768
  let shadowAtlasTile = -1;
3517
3769
 
@@ -3543,31 +3795,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3543
3795
  }
3544
3796
  }
3545
3797
 
3546
- let projectorHandle: Handle<'TextureAsset', 'shared'> | undefined;
3547
- let projectorAsset: TextureAsset | undefined;
3548
- let projectorGuid: string | undefined;
3549
- if (hasProjector) {
3550
- const candidate = toShared<'TextureAsset'>(Math.round(Number(s.projector)));
3551
- const resolvedProjector = resolveAssetHandle<TextureAsset>(world, candidate);
3552
- if (!resolvedProjector.ok || resolvedProjector.value.kind !== 'texture') {
3553
- worldInternal._routeError(
3554
- resolvedProjector.ok
3555
- ? new RhiError({
3556
- code: 'asset-not-registered',
3557
- expected: 'SpotLight.projector resolves to a TextureAsset',
3558
- hint: 'load the authored projector TextureAsset before assigning SpotLight.projector',
3559
- detail: { assetHandle: Number(candidate) },
3560
- })
3561
- : resolvedProjector.error,
3562
- { severity: Severity.Error, systemName: 'RenderSystem.extract (spot-projector)' },
3563
- );
3564
- } else {
3565
- projectorHandle = candidate;
3566
- projectorAsset = resolvedProjector.value;
3567
- projectorGuid = assets?.guidOf(projectorAsset) ?? `handle:${Number(candidate)}`;
3568
- }
3569
- }
3570
-
3571
3798
  spotSnapshots.push({
3572
3799
  kind: 'spot',
3573
3800
  entity: row.entity,
@@ -3584,6 +3811,25 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3584
3811
  invRangeSquared: computeInvRangeSquared(range),
3585
3812
  cosInner: degToCos(innerConeDeg),
3586
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,
3587
3833
  // ── shadow fields ──
3588
3834
  castShadow,
3589
3835
  lightViewProj,
@@ -3609,6 +3855,59 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3609
3855
  });
3610
3856
  }
3611
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
+ }
3612
3911
  // The SpotLight is the sole projector owner. Once all light snapshots have
3613
3912
  // been extracted, publish its accepted TextureAsset tuple on the volume
3614
3913
  // snapshot so surface and volume consume the same identity and revision.
@@ -3752,12 +4051,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3752
4051
  const pointShadowSnapshots: PointShadowSnapshot[] = [];
3753
4052
  {
3754
4053
  const pointShadowQuery = world
3755
- .query({ read: [Transform, PointLightShadow], with: [PointLight] })
4054
+ .query({ read: [Transform, GlobalTransform, PointLightShadow], with: [PointLight] })
3756
4055
  .unwrap();
3757
4056
  for (const row of pointShadowQuery) {
3758
- const t = row.get(Transform);
3759
4057
  const ps = row.get(PointLightShadow);
3760
- // Read world-space position from Transform.world (mat4 column-major;
4058
+ // Read world-space position from GlobalTransform.world (mat4 column-major;
3761
4059
  // translation lives at indices 12..14, mirroring CameraSnapshot.world
3762
4060
  // semantics in this file).
3763
4061
  // feat-20260614 M4 / w13: TypedArrayFor for `array<f32, 16>` now
@@ -3767,7 +4065,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3767
4065
  // `t.world?.[i]` form silently returned a single element under the
3768
4066
  // `never`-typed bundle path and `wRow[12]` widened to `undefined ?? 0`
3769
4067
  // so light positions clamped to the origin.
3770
- const wRow = t.world;
4068
+ const wRow = row.get(GlobalTransform).world;
3771
4069
  if (wRow === undefined) continue;
3772
4070
  const px = wRow[12] ?? 0;
3773
4071
  const py = wRow[13] ?? 0;
@@ -3808,7 +4106,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3808
4106
  // feat-20260612-point-light-shadows-urp-hdrp M4 / T-M4-4 (plan-strategy §D-8):
3809
4107
  // join pointShadow snapshots into the matching PointLightSnapshot so the
3810
4108
  // record stage threads `shadowAtlasLayer + shadowNear + shadowFar` through
3811
- // `packLightSlot` for the HDRP std430 LightSlot pad lanes (byte 52..64).
4109
+ // the unified direct-light metadata payload.
3812
4110
  // Mutates the freshly-built PointLightSnapshot in place; the snapshot is
3813
4111
  // not exposed elsewhere this frame yet (consumed only by lights.point[]).
3814
4112
  if (pointShadowSnapshots.length > 0) {
@@ -3833,6 +4131,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3833
4131
  directionalCount,
3834
4132
  point: pointSnapshots,
3835
4133
  spot: spotSnapshots,
4134
+ rect: rectSnapshots,
3836
4135
  lightViewProj,
3837
4136
  splitPlanes: splitPlanes !== undefined ? paddedSplitPlanes : undefined,
3838
4137
  cascadeCount,
@@ -3886,6 +4185,31 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
3886
4185
  }
3887
4186
  }
3888
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
+
3889
4213
  // feat-20260531-skybox-env-background M2 / w5: query SkyboxBackground entities.
3890
4214
  // First archetype hit wins (mirrors Skylight pattern); multi-entity
3891
4215
  // once-warn in record stage uses skyboxCount.
@@ -4080,24 +4404,28 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4080
4404
  // column id for the sprite-bucket per-entity region override read
4081
4405
  void SpriteRegionOverride;
4082
4406
 
4083
- const meshRendererQuery = world
4084
- .query({
4085
- read: [MeshRenderer],
4086
- optional: [
4087
- Transform,
4088
- MeshFilter,
4089
- Instances,
4090
- Skin,
4091
- Layer,
4092
- MorphWeights,
4093
- SpriteRegionOverride,
4094
- SpriteInstances,
4095
- Points,
4096
- Lines,
4097
- SortKey,
4098
- ],
4099
- })
4100
- .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();
4101
4429
  // QueryRow deliberately exposes component data, not an archetype version.
4102
4430
  // The optimized ECS owner keeps table storage private, so instance-buffer
4103
4431
  // invalidation uses a content fingerprint of the managed arrays instead.
@@ -4154,12 +4482,11 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4154
4482
  points !== undefined ? 'Points' : lines !== undefined ? 'Lines' : undefined;
4155
4483
  const isRenderable = hasTransform && hasMeshFilter;
4156
4484
 
4157
- // feat-20260601 D-3: the resolved world transform is read per-entity from
4158
- // the single `Transform.world` mat4 (propagateTransforms output) inside the
4159
- // row loop below. The retired GlobalTransform-column-switch + the
4160
- // ChildOf-without-GlobalTransform misconfig signal are gone: the world
4161
- // column always exists on a Transform-bearing entity, so the
4162
- // "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.
4163
4490
  const fAssetHandle = meshFilter?.assetHandle;
4164
4491
  // feat-20260520-2d-sprite-layer-mvp M-3 / w22: Layer column read here;
4165
4492
  // value folded into each DispatchEntry so the render-system sort can use
@@ -4251,6 +4578,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4251
4578
  let materialBindingDiagnostics: MeshMaterialBindingDiagnostic[] = [];
4252
4579
  let gpuDrivenSubmeshes: readonly MeshAsset['submeshes'][number][] = [];
4253
4580
  let gpuDrivenIndexed = false;
4581
+ let gpuDrivenGeometry: GpuDrivenGeometryReceipt | undefined;
4254
4582
 
4255
4583
  // Resolve instance overrides against mesh-owned slot defaults once.
4256
4584
  const fAssetHandleVal = fAssetHandle;
@@ -4264,8 +4592,20 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4264
4592
  const meshRes = resolveAssetHandle<Asset>(world, meshHandle);
4265
4593
  if (meshRes.ok && meshRes.value.kind === 'mesh') {
4266
4594
  const meshAsset = meshRes.value as MeshAsset;
4595
+ const guid = (meshRes.value as { guid?: string }).guid ?? '<no-guid>';
4267
4596
  gpuDrivenSubmeshes = meshAsset.submeshes;
4268
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
+ };
4269
4609
  // extractFrames is also a public read path and can run before the
4270
4610
  // Renderer.draw pre-render stage. GlyphText proves the derived mesh
4271
4611
  // owns exactly one Default slot; arbitrary meshes still fail closed
@@ -4273,7 +4613,6 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4273
4613
  if (!Array.isArray(meshAsset.materialSlots) && world.get(entity, GlyphText).ok) {
4274
4614
  ensureGlyphMeshMaterialSlots(world, meshHandle);
4275
4615
  }
4276
- const guid = (meshRes.value as { guid?: string }).guid ?? '<no-guid>';
4277
4616
  const resolvedBindings = resolveMeshMaterialBindings(meshAsset, materialsView ?? [], {
4278
4617
  isValidOverride(handle) {
4279
4618
  const resolved = resolveAssetHandle(world, toShared<'MaterialAsset'>(handle));
@@ -4999,7 +5338,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
4999
5338
  );
5000
5339
  continue;
5001
5340
  }
5002
- // (c) Resolve only each joint's Transform.world column. The
5341
+ // (c) Resolve only each joint's GlobalTransform.world column. The
5003
5342
  // transient view is consumed before a structural mutation and
5004
5343
  // therefore preserves the same dangling-joint behavior without
5005
5344
  // constructing every other Transform field.
@@ -5010,7 +5349,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
5010
5349
  for (let jIdx = 0; jIdx < skeleton.jointCount; jIdx++) {
5011
5350
  const jointEntityRaw = skinJoints[jIdx] ?? 0;
5012
5351
  const jointEntity = jointEntityRaw as EntityHandle;
5013
- const jointWorld = worldInternal._getArrayView(jointEntity, Transform, 'world');
5352
+ const jointWorld = worldInternal._getArrayView(jointEntity, GlobalTransform, 'world');
5014
5353
  if (jointWorld === undefined) {
5015
5354
  jointDangling = jIdx;
5016
5355
  break;
@@ -5062,13 +5401,13 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
5062
5401
  }
5063
5402
 
5064
5403
  // feat-20260601 D-3: read the resolved world mat4 (propagateTransforms
5065
- // output) straight from the Transform.world column array view. The
5404
+ // output) straight from the GlobalTransform.world column array view. The
5066
5405
  // record stage copies these 16 floats into the mesh SSBO with zero
5067
5406
  // per-snapshot `mat4.compose` (AC-07). A stale slot (generation gone)
5068
5407
  // skips the renderable, mirroring the Instances dangling-row sweep.
5069
5408
  // tweak-20260611 M1 / K-3: `_getArrayView` call survives untouched;
5070
5409
  // only the `entity` source switched to `bundle.Entity.self[i]`.
5071
- const worldView = worldInternal._getArrayView(entity, Transform, 'world');
5410
+ const worldView = worldInternal._getArrayView(entity, GlobalTransform, 'world');
5072
5411
  if (worldView === undefined) continue;
5073
5412
  const worldMat = new Float32Array(worldView);
5074
5413
  const transformSnap: TransformSnapshot = { world: worldMat };
@@ -5199,12 +5538,16 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
5199
5538
  }
5200
5539
 
5201
5540
  let localAabb: Float32Array | undefined;
5541
+ let lods: readonly MeshLodLevel[] | undefined;
5542
+ let lodHysteresis: number | undefined;
5202
5543
  let morph: MorphSnapshot | undefined;
5203
5544
  const assetHandleRaw = Math.round(fAssetHandle ?? 0);
5204
5545
  if (assetHandleRaw !== 0) {
5205
5546
  const meshRes = resolveAssetHandle(world, toShared<'MeshAsset'>(assetHandleRaw));
5206
5547
  if (meshRes.ok && meshRes.value.kind === 'mesh') {
5207
5548
  const meshAsset = meshRes.value as MeshAsset;
5549
+ lods = meshAsset.lods;
5550
+ lodHysteresis = meshAsset.lodHysteresis;
5208
5551
  morph = morphSnapshotFor(meshAsset, morphWeightsView);
5209
5552
  const meshAabb = meshAsset.aabb;
5210
5553
  // Morph targets can expand the authored bounds. Keep the
@@ -5287,34 +5630,16 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
5287
5630
  }
5288
5631
  }
5289
5632
 
5290
- let nonIndexedFirst = 0;
5291
- const gpuDrivenDraws = gpuDrivenSubmeshes.flatMap((submesh) => {
5292
- const drawMaterial = materialsArr[submesh.materialSlot] ?? materialSnap;
5293
- const first = gpuDrivenIndexed ? submesh.indexOffset : nonIndexedFirst;
5294
- nonIndexedFirst += submesh.vertexCount;
5295
- if (drawMaterial.transparent === true) return [];
5296
- return [
5297
- {
5298
- kind: gpuDrivenIndexed ? ('indexed' as const) : ('non-indexed' as const),
5299
- first,
5300
- count: gpuDrivenIndexed ? submesh.indexCount : submesh.vertexCount,
5301
- baseVertex: 0,
5302
- materialSlot: submesh.materialSlot,
5303
- topology: submesh.topology,
5304
- pipelineClass: `${drawMaterial.materialShaderId ?? 'forgeax::default-unlit'}|${submesh.topology}|${JSON.stringify(drawMaterial.renderState ?? null)}`,
5305
- materialResourceClass: gpuDrivenMaterialResourceClass(drawMaterial),
5306
- },
5307
- ];
5308
- });
5309
5633
  const baseRenderable: RenderableSnapshot = {
5310
5634
  assetHandle: Math.round(fAssetHandle ?? 0),
5311
5635
  transform: transformSnap,
5636
+ ...(lods === undefined ? {} : { lods }),
5637
+ ...(lodHysteresis === undefined ? {} : { lodHysteresis }),
5312
5638
  ...(localAabb !== undefined ? { localAabb: cullingLocalAabb } : {}),
5313
5639
  material: materialSnap,
5314
5640
  materials: materialsArr,
5315
5641
  materialBindingSources,
5316
5642
  materialBindingDiagnostics,
5317
- ...(morph === undefined && gpuDrivenDraws.length > 0 ? { gpuDrivenDraws } : {}),
5318
5643
  worldId: 0,
5319
5644
  entityKey: entity,
5320
5645
  ...(skinSlice !== undefined ? { skin: skinSlice } : {}),
@@ -5323,6 +5648,39 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
5323
5648
  ...(spriteInstancesSnap !== undefined ? { spriteInstances: spriteInstancesSnap } : {}),
5324
5649
  ...(pointsLines !== undefined ? { pointsLines } : {}),
5325
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;
5326
5684
 
5327
5685
  // feat-20260528-frustum-culling M3 / w10: frustum culling check.
5328
5686
  // Skip the entity if a valid AABB exists AND ALL cameras' frusta
@@ -5394,7 +5752,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
5394
5752
  }
5395
5753
 
5396
5754
  flushPendingDispatch(pendingDispatch);
5397
- renderables.push(baseRenderable);
5755
+ renderables.push(renderable);
5398
5756
  }
5399
5757
 
5400
5758
  // feat-20260520-2d-sprite-layer-mvp M-3 / w22 + w25: finalise the
@@ -5456,6 +5814,7 @@ export function extractFrame(world: World, context: PreparedExtractContext): Ext
5456
5814
  dispatch,
5457
5815
  skylight,
5458
5816
  skylightCount,
5817
+ lightProbes,
5459
5818
  skybox,
5460
5819
  skyboxCount,
5461
5820
  fog,