@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
@@ -29,11 +29,12 @@ import {
29
29
  type ObservationUnavailableReason,
30
30
  RendererOperationError,
31
31
  } from '../errors/render';
32
- import type { FogFrame } from '../extract/environment';
33
32
  import type { GpuDrivenProduction, PreparedGpuDrivenFrame } from '../gpu-driven/production-raster';
34
33
  import { GpuBuffer } from '../gpu-resource';
34
+ import { GPU_TEXTURE_USAGE_COPY_SRC } from '../gpu-texture-usage';
35
35
  import {
36
36
  GPU_BUFFER_USAGE_COPY_DST,
37
+ GPU_BUFFER_USAGE_MAP_READ,
37
38
  GPU_BUFFER_USAGE_UNIFORM,
38
39
  GPU_BUFFER_USAGE_VERTEX,
39
40
  } from '../gpu-usage';
@@ -58,6 +59,7 @@ import type {
58
59
  SkylightSnapshot,
59
60
  } from '../render-system-extract';
60
61
  import type { PersistentGpuDrivenState } from '../scene/render-scene';
62
+ import type { OcclusionFrameProjection } from '../scene/visibility/occlusion-runtime';
61
63
  import { SHADOW_ATLAS_DEFAULT_LAYERS } from '../shadow-atlas';
62
64
  import {
63
65
  getTransparentSortConfig,
@@ -75,6 +77,7 @@ import {
75
77
  warnZeroLightStandard,
76
78
  writeHdrpClusterAndSsaoBuffers,
77
79
  writeShadowParamsBuffer,
80
+ writeSpotModifierTextures,
78
81
  } from './frame-lighting';
79
82
  import {
80
83
  type BindGroupCounts,
@@ -82,9 +85,12 @@ import {
82
85
  type DirectionalShadowWorldState,
83
86
  type DispatchCounts,
84
87
  type FrameObservationSource,
88
+ getTextureIdentity,
85
89
  makeZeroCameraFallbackSnapshot,
90
+ type ReflectionFallbackReadbackRequest,
86
91
  type RenderFrameState,
87
92
  type ValidatedRenderable,
93
+ validateGraphTargetCaptureReadback,
88
94
  worldEntityKey,
89
95
  } from './frame-snapshot';
90
96
  // feat-20260622-chunk-gpu-instancing-sprite-tilemap M1 / w4 (D-1 record-stage
@@ -246,6 +252,75 @@ import {
246
252
  writeViewUbo,
247
253
  } from './view-ubo';
248
254
 
255
+ /**
256
+ * Resolve the GPU mesh view used by the optional production raster before its
257
+ * filtering pass runs. User meshes are pull-resident during record; relying
258
+ * only on the boot-time builtin map would make the first frame classify every
259
+ * imported mesh as a CPU fallback and would hide its LOD ranges from the GPU
260
+ * cull. Keep the map frame-local so the pipeline state remains the builtin
261
+ * compatibility owner while the residency cache remains the user-mesh SSOT.
262
+ */
263
+ function gpuDrivenMeshesForFrame(
264
+ internals: RenderSystemInternals,
265
+ pipelineState: PipelineState,
266
+ worlds: readonly World[],
267
+ scene: PersistentGpuDrivenState | undefined,
268
+ ): ReadonlyMap<number, MeshGpuHandles> {
269
+ const meshes = new Map(pipelineState.meshes);
270
+ if (scene === undefined) return meshes;
271
+ const cached = gpuDrivenMeshCache.get(scene.scene);
272
+ if (
273
+ cached !== undefined &&
274
+ cached.meshResidencyEpoch === internals.gpuStore.meshResidencyEpoch &&
275
+ cached.pipelineMeshes === pipelineState.meshes
276
+ ) {
277
+ return cached.value;
278
+ }
279
+ for (const slot of scene.slots) {
280
+ const world = worlds[slot.snapshot.worldId];
281
+ if (world === undefined) continue;
282
+ const assetHandle = slot.snapshot.assetHandle;
283
+ const sharedHandle = toShared<'MeshAsset'>(assetHandle);
284
+ let gpu = internals.gpuStore.getMeshGpuHandles(sharedHandle, world);
285
+ if (gpu === undefined && !meshes.has(assetHandle)) {
286
+ const asset = resolveAssetHandle<MeshAsset>(world, sharedHandle);
287
+ if (asset.ok) {
288
+ const lodMeshes = slot.snapshot.lods?.map((lod) =>
289
+ internals.assets.lookup<MeshAsset>(lod.mesh),
290
+ );
291
+ const resolvedLodMeshes =
292
+ lodMeshes?.every((lod): lod is MeshAsset => lod?.kind === 'mesh') === true
293
+ ? lodMeshes
294
+ : undefined;
295
+ const resident = internals.gpuStore.ensureResident(
296
+ sharedHandle,
297
+ asset.value,
298
+ world,
299
+ resolvedLodMeshes,
300
+ );
301
+ if (resident.ok) gpu = resident.value;
302
+ }
303
+ }
304
+ if (gpu !== undefined) meshes.set(assetHandle, gpu);
305
+ }
306
+ const value = meshes;
307
+ gpuDrivenMeshCache.set(scene.scene, {
308
+ meshResidencyEpoch: internals.gpuStore.meshResidencyEpoch,
309
+ pipelineMeshes: pipelineState.meshes,
310
+ value,
311
+ });
312
+ return value;
313
+ }
314
+
315
+ const gpuDrivenMeshCache = new WeakMap<
316
+ object,
317
+ {
318
+ readonly meshResidencyEpoch: number;
319
+ readonly pipelineMeshes: ReadonlyMap<number, MeshGpuHandles>;
320
+ readonly value: ReadonlyMap<number, MeshGpuHandles>;
321
+ }
322
+ >();
323
+
249
324
  /**
250
325
  * A frame-local material table. Repeated snapshot objects share one UBO slot;
251
326
  * each renderable keeps only the slot index used by each authored material.
@@ -388,6 +463,135 @@ function runRecordProfilePhase<T>(
388
463
  return runner === undefined ? action() : runner(phase, action);
389
464
  }
390
465
 
466
+ function halfFloatToNumber(bits: number): number {
467
+ const sign = (bits & 0x8000) === 0 ? 1 : -1;
468
+ const exponent = (bits >>> 10) & 0x1f;
469
+ const fraction = bits & 0x03ff;
470
+ if (exponent === 0) return sign * 2 ** -14 * (fraction / 1024);
471
+ if (exponent === 0x1f) return fraction === 0 ? sign * Infinity : Number.NaN;
472
+ return sign * 2 ** (exponent - 15) * (1 + fraction / 1024);
473
+ }
474
+
475
+ function createReflectionFallbackReadbackRequest(
476
+ internals: RenderSystemInternals,
477
+ frameState: RenderFrameState,
478
+ ): ReflectionFallbackReadbackRequest | undefined {
479
+ if (!frameState.reflectionFallbackDemand || frameState.reflectionFallbackReadback !== undefined) {
480
+ return undefined;
481
+ }
482
+ const graph = frameState.perFrameGraph;
483
+ if (graph === undefined || graph === null) return undefined;
484
+ const descriptor = graph.getColorTargetDescriptor('reflection-fallback-linear-hdr');
485
+ const texture = graph.getColorTargetTexture('reflection-fallback-linear-hdr');
486
+ if (
487
+ descriptor === undefined ||
488
+ texture === undefined ||
489
+ descriptor.format !== 'rgba16float' ||
490
+ descriptor.sample !== 1
491
+ ) {
492
+ return undefined;
493
+ }
494
+ const bytesPerRow = Math.ceil((descriptor.size.width * 8) / 256) * 256;
495
+ const created = internals.device.createBuffer({
496
+ label: 'reflection-fallback-linear-hdr.readback',
497
+ size: bytesPerRow * descriptor.size.height,
498
+ usage: GPU_BUFFER_USAGE_COPY_DST | GPU_BUFFER_USAGE_MAP_READ,
499
+ mappedAtCreation: false,
500
+ });
501
+ if (!created.ok) {
502
+ internals.errorRegistry.fire(created.error);
503
+ return undefined;
504
+ }
505
+ return {
506
+ name: 'reflection-fallback-linear-hdr',
507
+ buffer: created.value,
508
+ bytesPerRow,
509
+ width: descriptor.size.width,
510
+ height: descriptor.size.height,
511
+ expected: {
512
+ format: descriptor.format,
513
+ width: descriptor.size.width,
514
+ height: descriptor.size.height,
515
+ usage: GPU_TEXTURE_USAGE_COPY_SRC,
516
+ graphGeneration: graph.graphGeneration,
517
+ frameId: frameState.frameNumber,
518
+ textureIdentity: getTextureIdentity(texture),
519
+ },
520
+ encoded: false,
521
+ };
522
+ }
523
+
524
+ async function mapReflectionFallbackReadback(
525
+ internals: RenderSystemInternals,
526
+ request: ReflectionFallbackReadbackRequest,
527
+ allowZero: boolean,
528
+ ): Promise<{
529
+ readonly linearHdr: readonly [number, number, number, number];
530
+ readonly hash: string;
531
+ readonly graphGeneration: number;
532
+ readonly textureIdentity: number;
533
+ }> {
534
+ try {
535
+ if (!request.encoded) throw new Error('reflection fallback copy was not encoded');
536
+ const mapped = await request.buffer.mapAsync(GPU_BUFFER_USAGE_MAP_READ);
537
+ if (!mapped.ok) throw mapped.error;
538
+ const range = mapped.value.getMappedRange();
539
+ if (!range.ok) throw range.error;
540
+ const bytes = new Uint8Array(range.value.slice(0));
541
+ mapped.value.unmap();
542
+ const validation = validateGraphTargetCaptureReadback({
543
+ bytes,
544
+ expectedByteLength: request.bytesPerRow * request.height,
545
+ allowZero,
546
+ });
547
+ if (!validation.ok) throw new Error(validation.code);
548
+ let linearHdr: readonly [number, number, number, number] = [0, 0, 0, 0];
549
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
550
+ for (let y = 0; y < request.height; y += 1) {
551
+ for (let x = 0; x < request.width; x += 1) {
552
+ const offset = y * request.bytesPerRow + x * 8;
553
+ const pixel = [0, 1, 2, 3].map((channel) =>
554
+ halfFloatToNumber(view.getUint16(offset + channel * 2, true)),
555
+ ) as [number, number, number, number];
556
+ // The clear attachment carries alpha=1, so alpha alone is not
557
+ // evidence that the Standard BRDF wrote a fallback lobe. Decode the
558
+ // RGB channels from the mapped MRT and admit the first finite
559
+ // non-zero color sample only.
560
+ if (pixel.slice(0, 3).some((value) => Number.isFinite(value) && value !== 0)) {
561
+ linearHdr = pixel;
562
+ y = request.height;
563
+ break;
564
+ }
565
+ }
566
+ }
567
+ // A cleared attachment has a non-zero alpha lane, so byte-level
568
+ // validation alone can incorrectly admit an otherwise empty MRT. When
569
+ // the selected producer is non-neutral, require an actual finite RGB
570
+ // sample before publishing a complete receipt; neutral rows explicitly
571
+ // retain the zero quartet through allowZero=true.
572
+ if (
573
+ !allowZero &&
574
+ !linearHdr.slice(0, 3).some((value) => Number.isFinite(value) && value !== 0)
575
+ ) {
576
+ throw new Error('capture-readback-empty');
577
+ }
578
+ const subtle = globalThis.crypto?.subtle;
579
+ if (subtle === undefined) throw new Error('Web Crypto SHA-256 is unavailable');
580
+ const digest = await subtle.digest('SHA-256', bytes);
581
+ const hash = Array.from(new Uint8Array(digest), (value) =>
582
+ value.toString(16).padStart(2, '0'),
583
+ ).join('');
584
+ return {
585
+ linearHdr,
586
+ hash: `sha256:${hash}`,
587
+ graphGeneration: request.expected.graphGeneration,
588
+ textureIdentity: request.expected.textureIdentity,
589
+ };
590
+ } finally {
591
+ internals.device.destroyBuffer(request.buffer);
592
+ }
593
+ }
594
+
391
595
  const VOLUMETRIC_FOG_PASS_COUNT = 4;
392
596
 
393
597
  type VolumetricFogParamsSlot = 0 | 1;
@@ -696,16 +900,21 @@ function prepareVolumetricFogFrame(
696
900
  // sub-froxel jitter. The temporal resolver consumes it without growing the
697
901
  // public fog component or allocating another uniform buffer.
698
902
  params[23] = frameState.frameNumber % 1024;
903
+ const selectedPointIndex =
904
+ selectedPoint === undefined
905
+ ? -1
906
+ : lights.point.findIndex((light) => light.entity === selectedPoint.entity);
699
907
  const selectedSpotIndex =
700
908
  selectedSpot === undefined
701
909
  ? -1
702
910
  : lights.spot.findIndex((light) => light.entity === selectedSpot.entity);
911
+ // Cluster light_data is one point/spot/rect array. Point slots are emitted
912
+ // first, so volume stores the global slot rather than a per-kind array index.
913
+ const selectedSpotClusterIndex =
914
+ selectedSpotIndex < 0 ? -1 : lights.point.length + selectedSpotIndex;
703
915
  params.set(lightColor, 24);
704
- params[19] =
705
- selectedPoint === undefined
706
- ? -1
707
- : lights.point.findIndex((light) => light.entity === selectedPoint.entity);
708
- params[27] = selectedSpotIndex;
916
+ params[19] = selectedPointIndex;
917
+ params[27] = selectedSpotClusterIndex;
709
918
  params[28] = fog.fog.maxDistance;
710
919
  params[29] = fog.fog.anisotropy;
711
920
  params[30] =
@@ -1102,7 +1311,6 @@ export function recordFrame(
1102
1311
  skybox: SkyboxSnapshot | undefined,
1103
1312
  skyboxCount: number,
1104
1313
  postProcessParams: ReadonlyMap<string, Uint8Array>,
1105
- fog: FogFrame | undefined,
1106
1314
  // feat-20260709-editor-world-partition ENGINE-fix-round2 (defect 2): the
1107
1315
  // full worlds[] list passed to `renderer.draw`, indexed by
1108
1316
  // RenderableSnapshot.worldId so the record stage resolves each renderable's
@@ -1115,7 +1323,11 @@ export function recordFrame(
1115
1323
  gpuDriven?: {
1116
1324
  readonly owner: GpuDrivenProduction;
1117
1325
  readonly scene: PersistentGpuDrivenState | undefined;
1118
- readonly onCoverage: (ownsAll: boolean, clusteredLighting: boolean) => void;
1326
+ readonly onSceneDemand: (sceneRowsRequired: boolean) => void;
1327
+ readonly activeEntityKeys?: ReadonlySet<number>;
1328
+ readonly activeEntityRevision?: number;
1329
+ readonly telemetryCandidateCount?: number;
1330
+ readonly occlusion?: OcclusionFrameProjection;
1119
1331
  },
1120
1332
  renderReadLeases?: readonly RenderReadLease[],
1121
1333
  featureGraphCandidate?: RenderFeatureGraphCandidate,
@@ -1128,6 +1340,8 @@ export function recordFrame(
1128
1340
  volumetricFog?: ExtractedVolumetricFog,
1129
1341
  timingCapture?: GpuTimingCapture,
1130
1342
  ): boolean {
1343
+ frameState.reflectionFallbackObservationSource = undefined;
1344
+ frameState.reflectionFallbackCompletion = undefined;
1131
1345
  // The `try / finally` wrapper advances `frameState.frameNumber` exactly
1132
1346
  // once per `recordFrame` invocation regardless of which early-return
1133
1347
  // branch is taken (camera missing, cap exceeded, pipeline pending,
@@ -1195,6 +1409,10 @@ export function recordFrame(
1195
1409
  return false;
1196
1410
  }
1197
1411
  frameState.currentFrameObservationSource = undefined;
1412
+ if (frameState.reflectionFallbackReadback !== undefined) {
1413
+ internals.device.destroyBuffer(frameState.reflectionFallbackReadback.buffer);
1414
+ frameState.reflectionFallbackReadback = undefined;
1415
+ }
1198
1416
  frameState.currentDirectionalShadowView = null;
1199
1417
  frameState.currentSpotShadowView = null;
1200
1418
 
@@ -1210,7 +1428,7 @@ export function recordFrame(
1210
1428
 
1211
1429
  // Multi-light warnings + point/spot shadow-snapshot pin + point-shadow atlas
1212
1430
  // ensure + ExtractedLights three-arm destructure (directional fallback,
1213
- // point/spot arrays, totalLightCount). Extracted to prepareFrameLighting
1431
+ // point/spot/Rect arrays, totalLightCount). Extracted to prepareFrameLighting
1214
1432
  // (M3/w18) so recordFrame stays a skeleton.
1215
1433
  const lighting = runRecordProfilePhase(profilePhase, 'record/scene-state/lighting-prep', () =>
1216
1434
  prepareFrameLighting(internals, frameState, lights, camera, pipelineState),
@@ -1359,12 +1577,32 @@ export function recordFrame(
1359
1577
 
1360
1578
  let preparedGpuDriven: PreparedGpuDrivenFrame | undefined;
1361
1579
  if (gpuDriven !== undefined) {
1580
+ const standardPbrArtifact = internals.getMaterialShaderArtifact?.(
1581
+ 'forgeax::default-standard-pbr',
1582
+ );
1362
1583
  const prepared = gpuDriven.owner.prepare({
1363
1584
  scene: gpuDriven.scene,
1364
1585
  camera,
1365
- meshes: pipelineState.meshes,
1586
+ meshes: gpuDrivenMeshesForFrame(internals, pipelineState, worlds, gpuDriven.scene),
1366
1587
  viewBindGroupLayout: pipelineState.viewBindGroupLayout,
1367
1588
  meshResidencyEpoch: internals.gpuStore.meshResidencyEpoch,
1589
+ // The built-in Standard graph (including its clustered transport) is
1590
+ // eligible for the GPU-driven LOD lane. The old HDRP boolean no
1591
+ // longer exists in the unified pipeline state, so it must not be
1592
+ // inferred from `standard.kind`.
1593
+ hdrp: false,
1594
+ ...(gpuDriven.activeEntityKeys === undefined
1595
+ ? {}
1596
+ : { activeEntityKeys: gpuDriven.activeEntityKeys }),
1597
+ ...(gpuDriven.activeEntityRevision === undefined
1598
+ ? {}
1599
+ : { activeEntityRevision: gpuDriven.activeEntityRevision }),
1600
+ ...(gpuDriven.telemetryCandidateCount === undefined
1601
+ ? {}
1602
+ : { telemetryCandidateCount: gpuDriven.telemetryCandidateCount }),
1603
+ ...(gpuDriven.occlusion === undefined ? {} : { occlusion: gpuDriven.occlusion }),
1604
+ ...(standardPbrArtifact === undefined ? {} : { standardPbrArtifact }),
1605
+ standardPbrPipelineState: pipelineState,
1368
1606
  });
1369
1607
  if (prepared.ok) preparedGpuDriven = prepared.value;
1370
1608
  else if (prepared.error.code !== 'rhi-not-available')
@@ -1378,18 +1616,17 @@ export function recordFrame(
1378
1616
  lights.pointShadow.length > 0 ||
1379
1617
  lights.spot.some((spot) => spot.castShadow);
1380
1618
  const gpuOwnsAllPrimaryRaster = preparedGpuDriven?.ownsAllRenderables === true;
1381
- gpuDriven?.onCoverage(
1382
- gpuOwnsAllPrimaryRaster && !cpuShadowCastersActive,
1383
- standard.kind === 'clustered',
1384
- );
1619
+ gpuDriven?.onSceneDemand(gpuDriven.owner.requiresSceneRows());
1385
1620
 
1386
1621
  const cubeCaptureWork = prepareCubeCapture(internals, cubeCapture, camera);
1622
+ const reflectionFallbackCandidate = cubeCapture?.reflectionProbes?.fallbackDemand === true;
1387
1623
 
1388
1624
  // Validate renderables before graph admission. Besides producing the draw
1389
1625
  // plan, this is the owner of per-frame cache eviction; running it before a
1390
1626
  // graph candidate can fail guarantees despawned instance buffers are
1391
1627
  // released even when the frame cannot be submitted.
1392
1628
  const gpuOwnedEntityKeys = preparedGpuDriven?.entityKeys ?? EMPTY_ENTITY_KEYS;
1629
+ const gpuDrivenWorldKeys = gpuDriven?.scene?.worldKeys;
1393
1630
  const validated = runRecordProfilePhase(profilePhase, 'record/validation', () =>
1394
1631
  gpuOwnsAllPrimaryRaster && !cpuShadowCastersActive && cubeCaptureWork.length === 0
1395
1632
  ? []
@@ -1404,11 +1641,24 @@ export function recordFrame(
1404
1641
  cpuShadowCastersActive || cubeCaptureWork.length > 0
1405
1642
  ? EMPTY_ENTITY_KEYS
1406
1643
  : gpuOwnedEntityKeys,
1644
+ gpuDrivenWorldKeys,
1407
1645
  ),
1408
1646
  );
1409
- gpuDriven?.owner.recordCpuValidation(validated, gpuOwnedEntityKeys);
1647
+ gpuDriven?.owner.recordCpuValidation(validated, gpuOwnedEntityKeys, gpuDrivenWorldKeys);
1410
1648
  cleanPerFrameCaches(internals, frameState, validated, gpuOwnedEntityKeys);
1411
1649
 
1650
+ // S_fallback is the second output of the built-in Standard PBR BRDF. A
1651
+ // graph may still carry the detached owner tickets for inspection, but
1652
+ // the MRT is admitted only when every CPU-rendered primary row in this
1653
+ // frame uses that exact output contract. Unsupported compositions remain
1654
+ // fail-closed for SSR instead of submitting a device-invalid command.
1655
+ frameState.reflectionFallbackDemand =
1656
+ reflectionFallbackCandidate &&
1657
+ validated.length > 0 &&
1658
+ validated.every(
1659
+ (entry) => entry.source.material.materialShaderId === 'forgeax::default-standard-pbr',
1660
+ );
1661
+
1412
1662
  const preflightGraph = runRecordProfilePhase(profilePhase, 'record/render-graph', () =>
1413
1663
  ensureCompiledFrameGraph(
1414
1664
  internals,
@@ -1424,6 +1674,7 @@ export function recordFrame(
1424
1674
  cubeCapture?.state,
1425
1675
  cameras.length === 0,
1426
1676
  transmissionDemand,
1677
+ gpuDriven?.occlusion,
1427
1678
  graphVolumetricFog,
1428
1679
  volumeTopologyCandidate,
1429
1680
  standard,
@@ -1453,6 +1704,12 @@ export function recordFrame(
1453
1704
  return false;
1454
1705
  }
1455
1706
  if (standard.kind === 'clustered') {
1707
+ // The clustered HDRP payload owns the direct-light slot upload, so it
1708
+ // must also drive the shared Spot IES/Cookie resource upload from the
1709
+ // authoritative extracted-light corpus. The prepared local carrier can
1710
+ // omit the source payload on this path, leaving the shader bound to its
1711
+ // white Cookie fallback even though the authored handle is valid.
1712
+ writeSpotModifierTextures(internals, pipelineState, lights.spot);
1456
1713
  const clusterWrite = runRecordProfilePhase(
1457
1714
  profilePhase,
1458
1715
  'record/scene-state/hdrp-cluster',
@@ -1467,6 +1724,7 @@ export function recordFrame(
1467
1724
  pipelineState.hdrpClusterMembershipPipeline !== null,
1468
1725
  pipelineState.hdrpClusterMembershipBindGroupLayout,
1469
1726
  preparedSpotLightProjector?.lightSlotIndex,
1727
+ pipelineState,
1470
1728
  ),
1471
1729
  );
1472
1730
  if (!clusterWrite.ok) {
@@ -1489,78 +1747,58 @@ export function recordFrame(
1489
1747
  const targetW = swapTarget.targetW;
1490
1748
  const targetH = swapTarget.targetH;
1491
1749
 
1492
- const temporalView =
1750
+ const previousTemporalView = frameState.lastSuccessfulTemporalView;
1751
+ const currentUnjitteredViewProjection = mat4.multiply(
1752
+ mat4.create(),
1753
+ computeProjectionMatrix(camera),
1754
+ computeViewMatrix(camera),
1755
+ );
1756
+ const currentCameraPosition = [
1757
+ camera.position[0] ?? 0,
1758
+ camera.position[1] ?? 0,
1759
+ camera.position[2] ?? 0,
1760
+ ] as const;
1761
+ const temporalCandidate = createTemporalView({
1762
+ antialias: camera.antialias,
1763
+ width: Math.max(1, targetW),
1764
+ height: Math.max(1, targetH),
1765
+ frameIndex: frameState.successfulTemporalFrameIndex ?? 0,
1766
+ viewIdentity: `camera:${camera.entityKey ?? 0}`,
1767
+ environmentSignature,
1768
+ fogSignature,
1769
+ deviceGeneration: internals.deviceScope?.generation ?? 0,
1770
+ ...(camera.historyVersion === undefined ? {} : { historyVersion: camera.historyVersion }),
1771
+ historyValid: false,
1772
+ currentUnjitteredViewProjection,
1773
+ currentCameraPosition,
1774
+ });
1775
+ const resetReason =
1493
1776
  camera.antialias === 'taa' && environmentReady
1494
- ? (() => {
1495
- const created = createTemporalView({
1496
- antialias: camera.antialias,
1497
- width: targetW,
1498
- height: targetH,
1499
- frameIndex: frameState.successfulTemporalFrameIndex ?? 0,
1500
- viewIdentity: `camera:${camera.entityKey ?? 0}`,
1501
- environmentSignature,
1502
- fogSignature,
1503
- deviceGeneration: internals.deviceScope.generation,
1504
- ...(camera.historyVersion === undefined
1505
- ? {}
1506
- : { historyVersion: camera.historyVersion }),
1507
- });
1508
- const previous = frameState.lastSuccessfulTemporalView;
1509
- const currentUnjitteredViewProjection = mat4.multiply(
1510
- mat4.create(),
1511
- computeProjectionMatrix(camera),
1512
- computeViewMatrix(camera),
1513
- );
1514
- const resetCandidate = createTemporalView({
1515
- antialias: created.input.antialias,
1516
- width: created.input.width,
1517
- height: created.input.height,
1518
- frameIndex: created.input.frameIndex,
1519
- historyVersion: created.input.historyVersion,
1520
- viewIdentity: created.input.viewIdentity,
1521
- environmentSignature,
1522
- fogSignature,
1523
- deviceGeneration: internals.deviceScope.generation,
1524
- cameraCut: created.input.cameraCut,
1525
- historyValid: false,
1526
- currentUnjitteredViewProjection,
1527
- });
1528
- const resetReason = resolveTemporalReset(previous, resetCandidate);
1529
- return createTemporalView({
1530
- ...resetCandidate.input,
1531
- frameIndex: resetReason === undefined ? resetCandidate.input.frameIndex : 0,
1532
- historyValid: previous !== undefined && resetReason === undefined,
1533
- currentUnjitteredViewProjection,
1534
- resetReason,
1535
- ...(resetReason !== undefined ||
1536
- previous?.currentUnjitteredViewProjection === undefined
1537
- ? {}
1538
- : {
1539
- previousUnjitteredViewProjection: previous.currentUnjitteredViewProjection,
1540
- }),
1541
- });
1542
- })()
1777
+ ? resolveTemporalReset(previousTemporalView, temporalCandidate)
1543
1778
  : undefined;
1779
+ const canReusePrevious = previousTemporalView !== undefined && resetReason === undefined;
1780
+ const temporalView = createTemporalView({
1781
+ ...temporalCandidate.input,
1782
+ frameIndex: resetReason === undefined ? temporalCandidate.input.frameIndex : 0,
1783
+ historyValid: camera.antialias === 'taa' && environmentReady && canReusePrevious,
1784
+ resetReason,
1785
+ ...(canReusePrevious && previousTemporalView.currentUnjitteredViewProjection !== undefined
1786
+ ? {
1787
+ previousUnjitteredViewProjection: previousTemporalView.currentUnjitteredViewProjection,
1788
+ }
1789
+ : {}),
1790
+ ...(canReusePrevious && previousTemporalView.currentCameraPosition !== undefined
1791
+ ? { previousCameraPosition: previousTemporalView.currentCameraPosition }
1792
+ : {}),
1793
+ });
1544
1794
  frameState.pendingTemporalCommit =
1545
1795
  cameras.length === 0 || !environmentReady || internals.deviceScope === undefined
1546
1796
  ? { kind: 'none' }
1547
- : temporalView === undefined
1548
- ? {
1549
- kind: 'off',
1550
- view: createTemporalView({
1551
- antialias: camera.antialias,
1552
- width: targetW,
1553
- height: targetH,
1554
- frameIndex: frameState.successfulTemporalFrameIndex ?? 0,
1555
- viewIdentity: `camera:${camera.entityKey ?? 0}`,
1556
- environmentSignature,
1557
- fogSignature,
1558
- deviceGeneration: internals.deviceScope.generation,
1559
- }),
1560
- }
1561
- : { kind: 'taa', view: temporalView };
1797
+ : temporalView.mode === 'taa'
1798
+ ? { kind: 'taa', view: temporalView }
1799
+ : { kind: 'off', view: temporalView };
1562
1800
  const recordCamera: CameraSnapshot =
1563
- temporalView === undefined ? camera : { ...camera, temporal: temporalView };
1801
+ temporalView.mode === 'taa' ? { ...camera, temporal: temporalView } : camera;
1564
1802
  let activeVolumetricFogContext: VolumetricFogFrameContext | undefined;
1565
1803
  let activeVolumetricFogSignature: VolumeTemporalSignature | undefined;
1566
1804
  if (graphVolumetricFog?.status === 'available' && baseVolumetricFogContext !== undefined) {
@@ -1727,7 +1965,6 @@ export function recordFrame(
1727
1965
  light,
1728
1966
  lights,
1729
1967
  frameState.spotShadowSnapshots,
1730
- fog,
1731
1968
  temporalView,
1732
1969
  preparedSpotLightProjector?.spotIndex,
1733
1970
  );
@@ -1808,7 +2045,6 @@ export function recordFrame(
1808
2045
  lights,
1809
2046
  frameState.spotShadowSnapshots,
1810
2047
  capture.viewBindGroupDynamicOffset,
1811
- undefined,
1812
2048
  preparedSpotLightProjector?.spotIndex,
1813
2049
  );
1814
2050
  }
@@ -1823,7 +2059,6 @@ export function recordFrame(
1823
2059
  lights,
1824
2060
  frameState.spotShadowSnapshots,
1825
2061
  probe.viewBindGroupDynamicOffset,
1826
- undefined,
1827
2062
  preparedSpotLightProjector?.spotIndex,
1828
2063
  );
1829
2064
  }
@@ -1859,6 +2094,7 @@ export function recordFrame(
1859
2094
  assets: internals.assets,
1860
2095
  world,
1861
2096
  gpuDrivenEntityKeys: gpuOwnedEntityKeys,
2097
+ ...(gpuDrivenWorldKeys === undefined ? {} : { gpuDrivenWorldKeys }),
1862
2098
  store: internals.gpuStore,
1863
2099
  pipelineState,
1864
2100
  bloomResources: pipelineState.perPassResources.getBloomResources?.() ?? null,
@@ -1903,6 +2139,7 @@ export function recordFrame(
1903
2139
  dispatch: transparentDispatch,
1904
2140
  hdrpClusterBindGroup,
1905
2141
  hdrpClusterMembershipBindGroup,
2142
+ ...(gpuDriven?.occlusion === undefined ? {} : { occlusion: gpuDriven.occlusion }),
1906
2143
  standardLighting: standard,
1907
2144
  foldDispatchPlan,
1908
2145
  materialSlotIndices,
@@ -1917,7 +2154,19 @@ export function recordFrame(
1917
2154
  directionalShadowCacheReuse: directionalShadowCache.reuse,
1918
2155
  ...(profilePhase !== undefined ? { profilePhase } : {}),
1919
2156
  };
2157
+ const fallbackReadbackRequest = createReflectionFallbackReadbackRequest(internals, frameState);
2158
+ if (fallbackReadbackRequest !== undefined) {
2159
+ frameState.reflectionFallbackReadback = fallbackReadbackRequest;
2160
+ }
1920
2161
  let completion: Promise<unknown> | undefined;
2162
+ let fallbackReadback:
2163
+ | Promise<{
2164
+ readonly linearHdr: readonly [number, number, number, number];
2165
+ readonly hash: string;
2166
+ readonly graphGeneration: number;
2167
+ readonly textureIdentity: number;
2168
+ }>
2169
+ | undefined;
1921
2170
  const stagedTemporalFrame = {
1922
2171
  frameId: frameState.frameNumber,
1923
2172
  currentViewProjection: new Float32Array(currentViewProjection),
@@ -1941,9 +2190,34 @@ export function recordFrame(
1941
2190
  profilePhase === undefined
1942
2191
  ? undefined
1943
2192
  : (pass, encode) => profilePhase(graphExecutionPhase(pass.name), encode),
2193
+ gpuDriven === undefined
2194
+ ? undefined
2195
+ : {
2196
+ afterGraphExecute: () => gpuDriven.occlusion?.recordResolve(encoder) ?? ok(undefined),
2197
+ onSubmitted: () => {
2198
+ gpuDriven.occlusion?.commit(true, frameState.frameNumber, profilePhase);
2199
+ },
2200
+ onAborted: () => {
2201
+ gpuDriven.occlusion?.commit(false, frameState.frameNumber);
2202
+ },
2203
+ },
1944
2204
  cubeCaptureWork.length === 0 ? undefined : cubeCaptureWork.map((work) => work.faceIndex),
1945
2205
  (done) => {
1946
2206
  completion = done;
2207
+ if (fallbackReadbackRequest !== undefined) {
2208
+ fallbackReadback = done.then(
2209
+ () =>
2210
+ mapReflectionFallbackReadback(
2211
+ internals,
2212
+ fallbackReadbackRequest,
2213
+ cubeCapture?.reflectionProbes?.fallbackHasNonNeutral !== true,
2214
+ ),
2215
+ (cause) => {
2216
+ internals.device.destroyBuffer(fallbackReadbackRequest.buffer);
2217
+ throw cause;
2218
+ },
2219
+ );
2220
+ }
1947
2221
  },
1948
2222
  timingCapture,
1949
2223
  ),
@@ -1952,7 +2226,29 @@ export function recordFrame(
1952
2226
  const completed = cubeCapture.scheduler.completeSubmission(submitted, completion);
1953
2227
  if (!completed.ok) internals.errorRegistry.fire(completed.error);
1954
2228
  }
1955
- cubeCapture?.reflectionProbes?.completeSubmission(submitted, completion);
2229
+ if (!submitted && fallbackReadbackRequest !== undefined) {
2230
+ internals.device.destroyBuffer(fallbackReadbackRequest.buffer);
2231
+ }
2232
+ const reflectionFallbackCompletion = cubeCapture?.reflectionProbes?.completeSubmission(
2233
+ submitted,
2234
+ completion,
2235
+ fallbackReadbackRequest === undefined || fallbackReadback === undefined
2236
+ ? undefined
2237
+ : {
2238
+ format: fallbackReadbackRequest.expected.format,
2239
+ size: {
2240
+ width: fallbackReadbackRequest.expected.width,
2241
+ height: fallbackReadbackRequest.expected.height,
2242
+ },
2243
+ frameId: fallbackReadbackRequest.expected.frameId,
2244
+ graphGeneration: fallbackReadbackRequest.expected.graphGeneration,
2245
+ textureIdentity: fallbackReadbackRequest.expected.textureIdentity,
2246
+ readback: fallbackReadback,
2247
+ },
2248
+ frameState.reflectionFallbackDemand,
2249
+ );
2250
+ frameState.reflectionFallbackCompletion = reflectionFallbackCompletion;
2251
+ frameState.reflectionFallbackObservationSource = undefined;
1956
2252
  if (submitted && !directionalShadowCache.reuse) {
1957
2253
  frameState.directionalShadowCache = frameState.directionalShadowCacheRecorded
1958
2254
  ? directionalShadowCache.next
@@ -2038,7 +2334,7 @@ interface DirectionalShadowCacheDecision {
2038
2334
  function readWorldState(lease: RenderReadLease): DirectionalShadowWorldState {
2039
2335
  return {
2040
2336
  worldIdentity: lease.worldIdentity,
2041
- changeCursor: lease.inspectCursor(),
2337
+ version: lease.captureVersion(),
2042
2338
  };
2043
2339
  }
2044
2340
 
@@ -2060,16 +2356,14 @@ function sameWorldState(
2060
2356
  if (cachedState.worldIdentity !== lease.worldIdentity) return false;
2061
2357
  let changes: ReturnType<RenderReadLease['readChanges']>;
2062
2358
  try {
2063
- changes = lease.readChanges(cachedState.changeCursor);
2359
+ changes = lease.readChanges(cachedState.version);
2064
2360
  } catch {
2065
- // A lease may have evicted an old cursor after another owner consumed
2066
- // its bounded cursor window. Treat that as stale render evidence and
2067
- // rebuild the atlas from the current lease rather than reaching into
2068
- // World for a second mutation clock.
2361
+ // Treat malformed or foreign version evidence as stale and rebuild the
2362
+ // atlas rather than reaching through World for a second mutation clock.
2069
2363
  return false;
2070
2364
  }
2071
2365
  if (changes.status !== 'ok') return false;
2072
- if (changes.world.records.length !== 0 || changes.sharedRefs.records.length !== 0) {
2366
+ if (changes.world.changedComponentIds.length !== 0 || changes.sharedRefs.records.length !== 0) {
2073
2367
  return false;
2074
2368
  }
2075
2369
  }
@@ -2205,6 +2499,7 @@ function validateRenderables(
2205
2499
  renderables: readonly RenderableSnapshot[],
2206
2500
  transparentDispatch: readonly DispatchEntry[],
2207
2501
  excludedEntityKeys: ReadonlySet<number> = new Set<number>(),
2502
+ worldKeys?: readonly number[],
2208
2503
  ): ValidatedRenderable[] {
2209
2504
  // bug-20260527-renderstate-pipeline-dispatch-gap D-4:
2210
2505
  // build a renderableIndex -> renderState map from dispatch entries
@@ -2230,7 +2525,8 @@ function validateRenderables(
2230
2525
  for (let rIdx = 0; rIdx < renderables.length; rIdx++) {
2231
2526
  const r = renderables[rIdx];
2232
2527
  if (r === undefined) continue;
2233
- if (excludedEntityKeys.has(worldEntityKey(r.worldId, r.entityKey))) continue;
2528
+ if (excludedEntityKeys.has(worldEntityKey(worldKeys?.[r.worldId] ?? r.worldId, r.entityKey)))
2529
+ continue;
2234
2530
  // feat-20260709-editor-world-partition ENGINE-fix-round2 (defect 2): resolve
2235
2531
  // this renderable's mesh against the world it was EXTRACTED from
2236
2532
  // (worlds[r.worldId]) — NOT the single resource-owner `world`. Builtin mesh
@@ -2265,11 +2561,17 @@ function validateRenderables(
2265
2561
  // frames hit the O(1) cache.
2266
2562
  const meshAssetHandle = toShared<'MeshAsset'>(r.assetHandle);
2267
2563
  let meshHandles = internals.gpuStore.getMeshGpuHandles(meshAssetHandle, renderableWorld);
2564
+ const lodMeshes = r.lods?.map((lod) => internals.assets.lookup<MeshAsset>(lod.mesh));
2565
+ const resolvedLodMeshes =
2566
+ lodMeshes?.every((lod): lod is MeshAsset => lod?.kind === 'mesh') === true
2567
+ ? lodMeshes
2568
+ : undefined;
2268
2569
  if (meshHandles === undefined && r.assetHandle > handleSlot(HANDLE_NINESLICE_QUAD)) {
2269
2570
  const residentRes = internals.gpuStore.ensureResident(
2270
2571
  meshAssetHandle,
2271
2572
  assetRes.value,
2272
2573
  renderableWorld,
2574
+ resolvedLodMeshes,
2273
2575
  );
2274
2576
  if (residentRes.ok) {
2275
2577
  meshHandles = residentRes.value;
@@ -2451,6 +2753,21 @@ function buildDispatchPlan(
2451
2753
  materialSlotOwners: readonly number[];
2452
2754
  materialSlotCount: number;
2453
2755
  } {
2756
+ // A production GPU-owned frame has no CPU validated rows by design. The
2757
+ // dispatch list can still contain one entry per authored entity, but no
2758
+ // CPU plan, material table, or fold bucket is consumed in that lane. Return
2759
+ // the empty plan without walking that list so a steady 100k scene remains
2760
+ // bounded by the persistent GPU batches rather than per-entity record work.
2761
+ if (validated.length === 0) {
2762
+ return {
2763
+ validatedOrdered: [],
2764
+ foldDispatchPlan: null,
2765
+ materialSlotIndices: [],
2766
+ materialSlots: [],
2767
+ materialSlotOwners: [],
2768
+ materialSlotCount: 0,
2769
+ };
2770
+ }
2454
2771
  // M3 / w26: dispatch-ordered render. The dispatch list is pre-sorted
2455
2772
  // by queue (ascending, stable) by the extract stage per plan-strategy D-3.
2456
2773
  // Reorder validated renderables to follow the dispatch order, falling